mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-12 19:53:24 +02:00
Implement handling of icon names on Linux
This commit is contained in:
@@ -188,6 +188,20 @@ except KeyError:
|
||||
# https://github.com/ansible/ansible/issues/11536#issuecomment-153030743
|
||||
ssh_control_master_template = 'kssh-{kitty_pid}-{ssh_placeholder}'
|
||||
|
||||
# See https://specifications.freedesktop.org/icon-naming-spec/latest/ar01s04.html
|
||||
standard_icon_names = {
|
||||
'error': 'dialog-error',
|
||||
'warning': 'dialog-warning',
|
||||
'warn': 'dialog-warning',
|
||||
'info': 'dialog-information',
|
||||
'question': 'dialog-question',
|
||||
|
||||
'help': 'system-help',
|
||||
'file-manager': 'system-file-manager',
|
||||
'system-monitor': 'utilities-system-monitor',
|
||||
'text-editor': 'utilities-text-editor',
|
||||
}
|
||||
|
||||
|
||||
def glfw_path(module: str) -> str:
|
||||
prefix = 'kitty.' if getattr(sys, 'frozen', False) else ''
|
||||
|
||||
@@ -11,7 +11,7 @@ from itertools import count
|
||||
from typing import Any, Callable, Dict, FrozenSet, Iterator, List, NamedTuple, Optional, Sequence, Set, Tuple, Union
|
||||
from weakref import ReferenceType, ref
|
||||
|
||||
from .constants import cache_dir, config_dir, is_macos, logo_png_file
|
||||
from .constants import cache_dir, config_dir, is_macos, logo_png_file, standard_icon_names
|
||||
from .fast_data_types import ESC_OSC, StreamingBase64Decoder, add_timer, base64_decode, current_focused_os_window_id, get_boss, get_options
|
||||
from .types import run_once
|
||||
from .typing import WindowType
|
||||
@@ -207,7 +207,7 @@ class NotificationCommand:
|
||||
only_when: OnlyWhen = OnlyWhen.unset
|
||||
urgency: Optional[Urgency] = None
|
||||
icon_data_key: str = ''
|
||||
icon_name: str = ''
|
||||
icon_names: Tuple[str, ...] = ()
|
||||
application_name: str = ''
|
||||
notification_type: str = ''
|
||||
timeout: int = -2
|
||||
@@ -296,7 +296,10 @@ class NotificationCommand:
|
||||
elif k == 'g':
|
||||
self.icon_data_key = sanitize_id(v)
|
||||
elif k == 'n':
|
||||
self.icon_name = v
|
||||
try:
|
||||
self.icon_names += (base64_decode(v).decode('utf-8', 'replace'),)
|
||||
except Exception:
|
||||
self.log('Ignoring invalid icon name in notification: {v!r}')
|
||||
elif k == 'f':
|
||||
try:
|
||||
self.application_name = base64_decode(v).decode('utf-8', 'replace')
|
||||
@@ -328,8 +331,8 @@ class NotificationCommand:
|
||||
self.close_response_requested = prev.close_response_requested
|
||||
if not self.icon_data_key:
|
||||
self.icon_data_key = prev.icon_data_key
|
||||
if not self.icon_name:
|
||||
self.icon_name = prev.icon_name
|
||||
if prev.icon_names:
|
||||
self.icon_names += prev.icon_names
|
||||
if not self.application_name:
|
||||
self.application_name = prev.application_name
|
||||
if not self.notification_type:
|
||||
@@ -574,7 +577,23 @@ class FreeDesktopIntegration(DesktopIntegration):
|
||||
|
||||
def notify(self, nc: NotificationCommand, existing_desktop_notification_id: Optional[int]) -> int:
|
||||
from .fast_data_types import dbus_send_notification
|
||||
app_icon = nc.icon_name or nc.icon_path or get_custom_window_icon()[1] or logo_png_file
|
||||
from .xdg import icon_exists, icon_for_appname
|
||||
app_icon = ''
|
||||
if nc.icon_names:
|
||||
for name in nc.icon_names:
|
||||
if sn := standard_icon_names.get(name):
|
||||
app_icon = sn
|
||||
break
|
||||
if icon_exists(name):
|
||||
app_icon = name
|
||||
break
|
||||
if not app_icon:
|
||||
app_icon = nc.icon_path or nc.icon_names[0]
|
||||
else:
|
||||
app_icon = nc.icon_path or icon_for_appname(nc.application_name)
|
||||
if not app_icon:
|
||||
app_icon = get_custom_window_icon()[1] or logo_png_file
|
||||
|
||||
body = nc.body.replace('<', '<\u200c').replace('&', '&\u200c') # prevent HTML markup from being recognized
|
||||
assert nc.urgency is not None
|
||||
replaces_dbus_id = 0
|
||||
|
||||
134
kitty/xdg.py
Normal file
134
kitty/xdg.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import re
|
||||
from contextlib import suppress
|
||||
|
||||
from kitty.types import run_once
|
||||
|
||||
|
||||
@run_once
|
||||
def xdg_data_dirs() -> tuple[str, ...]:
|
||||
return tuple(os.environ.get('XDG_DATA_DIRS', '/usr/local/share/:/usr/share/').split(os.pathsep))
|
||||
|
||||
|
||||
@run_once
|
||||
def icon_dirs() -> list[str]:
|
||||
ans = []
|
||||
def a(x: str) -> None:
|
||||
if os.path.isdir(x):
|
||||
ans.append(x)
|
||||
|
||||
a(os.path.expanduser('~/.icons'))
|
||||
for x in xdg_data_dirs():
|
||||
a(os.path.join(x, 'icons'))
|
||||
return ans
|
||||
|
||||
|
||||
class XDGIconCache:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.existing_icon_names: set[str] = set()
|
||||
self.scanned = False
|
||||
self.themes_to_search: set[str] = set()
|
||||
|
||||
def find_inherited_themes(self, basedir: str) -> bool:
|
||||
with suppress(OSError), open(os.path.join(basedir, 'index.theme')) as f:
|
||||
raw = f.read()
|
||||
if m := re.search(r'^Inherits\s*=\s*(.+?)$', raw, re.MULTILINE):
|
||||
for x in m.group(1).split(','):
|
||||
self.themes_to_search.add(x.strip())
|
||||
return True
|
||||
return False
|
||||
|
||||
def scan(self) -> None:
|
||||
self.scanned = True
|
||||
for icdir in icon_dirs():
|
||||
if self.find_inherited_themes(os.path.join(icdir, 'default')):
|
||||
break
|
||||
self.themes_to_search.add('hicolor')
|
||||
while True:
|
||||
before = len(self.themes_to_search)
|
||||
for icdir in icon_dirs():
|
||||
for theme in tuple(self.themes_to_search):
|
||||
self.find_inherited_themes(os.path.join(icdir, theme))
|
||||
if len(self.themes_to_search) == before:
|
||||
break
|
||||
for icdir in icon_dirs():
|
||||
for theme in self.themes_to_search:
|
||||
self.scan_theme_dir(os.path.join(icdir, theme))
|
||||
self.scan_theme_dir('/usr/share/pixmaps')
|
||||
|
||||
def scan_theme_dir(self, base: str) -> None:
|
||||
with suppress(OSError):
|
||||
for (dirpath, dirnames, filenames) in os.walk(base):
|
||||
for q in filenames:
|
||||
icon_name, sep, ext = q.lower().rpartition('.')
|
||||
if sep == '.' and ext in ('svg', 'png', 'xpm'):
|
||||
self.existing_icon_names.add(icon_name)
|
||||
|
||||
def icon_exists(self, name: str) -> bool:
|
||||
if not self.scanned:
|
||||
self.scan()
|
||||
return name.lower() in self.existing_icon_names
|
||||
|
||||
|
||||
xdg_icon_cache = XDGIconCache()
|
||||
icon_exists = xdg_icon_cache.icon_exists
|
||||
|
||||
|
||||
class AppIconCache:
|
||||
def __init__(self) -> None:
|
||||
self.scanned = False
|
||||
self.lcase_app_name_to_path: dict[str, str] = {}
|
||||
self.lcase_full_name_to_path: dict[str, str] = {}
|
||||
self.icon_name_cache: dict[str, str] = {}
|
||||
|
||||
def scan(self) -> None:
|
||||
self.scanned = True
|
||||
for d in xdg_data_dirs():
|
||||
d = os.path.join(d, 'applications')
|
||||
with suppress(OSError):
|
||||
for (dirpath, dirnames, filenames) in os.walk(d):
|
||||
for fname in filenames:
|
||||
if fname.endswith('.desktop'):
|
||||
path = os.path.join(dirpath, fname)
|
||||
self.process_desktop_file(path, os.path.relpath(path, d))
|
||||
|
||||
def process_desktop_file(self, path: str, relpath: str) -> None:
|
||||
# file_id = relpath.replace('/', '-')
|
||||
bname = os.path.basename(relpath)
|
||||
parts = bname.split('.')[:-1]
|
||||
appname = parts[-1]
|
||||
self.lcase_app_name_to_path[appname.lower()] = path
|
||||
self.lcase_full_name_to_path['.'.join(parts).lower()] = path
|
||||
|
||||
def icon_for_appname(self, appname: str) -> str:
|
||||
if not self.scanned:
|
||||
self.scan()
|
||||
q = appname.lower()
|
||||
if not appname or q in ('kitty', 'kitten', 'kitten-notify'):
|
||||
return ''
|
||||
path = self.lcase_full_name_to_path.get(q) or self.lcase_app_name_to_path.get(q)
|
||||
if not path:
|
||||
return ''
|
||||
ans = self.icon_name_cache.get(path)
|
||||
if ans is None:
|
||||
try:
|
||||
ans = self.icon_name_cache[path] = self.icon_name_from_desktop_file(path)
|
||||
except OSError:
|
||||
ans = self.icon_name_cache[path] = ''
|
||||
|
||||
return ans
|
||||
|
||||
def icon_name_from_desktop_file(self, path: str) -> str:
|
||||
with open(path) as f:
|
||||
raw = f.read()
|
||||
if m := re.search(r'^Icon\s*=\s*(.+?)\s*?$', raw, re.MULTILINE):
|
||||
return m.group(1)
|
||||
return ''
|
||||
|
||||
|
||||
app_icon_cache = AppIconCache()
|
||||
icon_for_appname = app_icon_cache.icon_for_appname
|
||||
Reference in New Issue
Block a user