File: xdg.py

package info (click to toggle)
kitty 0.45.0-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 27,476 kB
  • sloc: ansic: 84,285; python: 57,992; objc: 5,432; sh: 1,333; xml: 364; makefile: 144; javascript: 78
file content (142 lines) | stat: -rw-r--r-- 4,968 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#!/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 xdg_data_home() -> str:
    return os.environ.get('XDG_DATA_HOME', os.path.expanduser('~/.local/share/'))


@run_once
def icon_dirs() -> list[str]:
    ans = []
    def a(x: str) -> None:
        if os.path.isdir(x):
            ans.append(x)

    a(os.path.join(xdg_data_home(), 'icons'))
    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

    def find_inherited_themes(self, basedir: str, seen_indexes: set[str], themes_to_search: set[str]) -> bool:
        if basedir not in seen_indexes:
            seen_indexes.add(basedir)
            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(','):
                        themes_to_search.add(x.strip())
                return True
        return False

    def scan(self) -> None:
        themes_to_search: set[str] = set()
        self.scanned = True
        seen_indexes: set[str] = set()
        for icdir in icon_dirs():
            if self.find_inherited_themes(os.path.join(icdir, 'default'), seen_indexes, themes_to_search):
                break
        themes_to_search.add('hicolor')
        while True:
            before = len(themes_to_search)
            for icdir in icon_dirs():
                for theme in tuple(themes_to_search):
                    self.find_inherited_themes(os.path.join(icdir, theme), seen_indexes, themes_to_search)
            if len(themes_to_search) == before:
                break
        for icdir in icon_dirs():
            for theme in 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