1#!/usr/local/bin/python3.8
2# vim:fileencoding=utf-8
3# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
4
5import errno
6import os
7import pwd
8import sys
9from contextlib import suppress
10from typing import NamedTuple, Optional, Set, TYPE_CHECKING
11
12from .types import run_once
13
14if TYPE_CHECKING:
15    from .options.types import Options
16
17
18class Version(NamedTuple):
19    major: int
20    minor: int
21    patch: int
22
23
24appname: str = 'kitty'
25kitty_face = '��'
26version: Version = Version(0, 23, 1)
27str_version: str = '.'.join(map(str, version))
28_plat = sys.platform.lower()
29is_macos: bool = 'darwin' in _plat
30if getattr(sys, 'frozen', False):
31    extensions_dir: str = getattr(sys, 'kitty_extensions_dir')
32    kitty_base_dir = os.path.dirname(extensions_dir)
33    if is_macos:
34        kitty_base_dir = os.path.dirname(os.path.dirname(kitty_base_dir))
35    kitty_base_dir = os.path.join(kitty_base_dir, 'kitty')
36else:
37    kitty_base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
38    extensions_dir = os.path.join(kitty_base_dir, 'kitty')
39
40
41@run_once
42def kitty_exe() -> str:
43    rpath = sys._xoptions.get('bundle_exe_dir')
44    if not rpath:
45        items = os.environ.get('PATH', '').split(os.pathsep) + [os.path.join(kitty_base_dir, 'kitty', 'launcher')]
46        seen: Set[str] = set()
47        for candidate in filter(None, items):
48            if candidate not in seen:
49                seen.add(candidate)
50                if os.access(os.path.join(candidate, 'kitty'), os.X_OK):
51                    rpath = candidate
52                    break
53        else:
54            raise RuntimeError('kitty binary not found')
55    return os.path.join(rpath, 'kitty')
56
57
58def _get_config_dir() -> str:
59    if 'KITTY_CONFIG_DIRECTORY' in os.environ:
60        return os.path.abspath(os.path.expanduser(os.environ['KITTY_CONFIG_DIRECTORY']))
61
62    locations = []
63    if 'XDG_CONFIG_HOME' in os.environ:
64        locations.append(os.path.abspath(os.path.expanduser(os.environ['XDG_CONFIG_HOME'])))
65    locations.append(os.path.expanduser('~/.config'))
66    if is_macos:
67        locations.append(os.path.expanduser('~/Library/Preferences'))
68    for loc in filter(None, os.environ.get('XDG_CONFIG_DIRS', '').split(os.pathsep)):
69        locations.append(os.path.abspath(os.path.expanduser(loc)))
70    for loc in locations:
71        if loc:
72            q = os.path.join(loc, appname)
73            if os.access(q, os.W_OK) and os.path.exists(os.path.join(q, 'kitty.conf')):
74                return q
75
76    def make_tmp_conf() -> None:
77        import tempfile
78        import atexit
79        ans = tempfile.mkdtemp(prefix='kitty-conf-')
80
81        def cleanup() -> None:
82            import shutil
83            with suppress(Exception):
84                shutil.rmtree(ans)
85        atexit.register(cleanup)
86
87    candidate = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME') or '~/.config'))
88    ans = os.path.join(candidate, appname)
89    try:
90        os.makedirs(ans, exist_ok=True)
91    except FileExistsError:
92        raise SystemExit('A file {} already exists. It must be a directory, not a file.'.format(ans))
93    except PermissionError:
94        make_tmp_conf()
95    except OSError as err:
96        if err.errno != errno.EROFS:  # Error other than read-only file system
97            raise
98        make_tmp_conf()
99    return ans
100
101
102config_dir = _get_config_dir()
103del _get_config_dir
104defconf = os.path.join(config_dir, 'kitty.conf')
105
106
107@run_once
108def cache_dir() -> str:
109    if 'KITTY_CACHE_DIRECTORY' in os.environ:
110        candidate = os.path.abspath(os.environ['KITTY_CACHE_DIRECTORY'])
111    elif is_macos:
112        candidate = os.path.join(os.path.expanduser('~/Library/Caches'), appname)
113    else:
114        candidate = os.environ.get('XDG_CACHE_HOME', '~/.cache')
115        candidate = os.path.join(os.path.expanduser(candidate), appname)
116    os.makedirs(candidate, exist_ok=True)
117    return candidate
118
119
120def wakeup() -> None:
121    from .fast_data_types import get_boss
122    b = get_boss()
123    if b is not None:
124        b.child_monitor.wakeup()
125
126
127terminfo_dir = os.path.join(kitty_base_dir, 'terminfo')
128logo_png_file = os.path.join(kitty_base_dir, 'logo', 'kitty.png')
129beam_cursor_data_file = os.path.join(kitty_base_dir, 'logo', 'beam-cursor.png')
130try:
131    shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'
132except KeyError:
133    with suppress(Exception):
134        print('Failed to read login shell via getpwuid() for current user, falling back to /bin/sh', file=sys.stderr)
135    shell_path = '/bin/sh'
136
137
138def glfw_path(module: str) -> str:
139    prefix = 'kitty.' if getattr(sys, 'frozen', False) else ''
140    return os.path.join(extensions_dir, f'{prefix}glfw-{module}.so')
141
142
143def detect_if_wayland_ok() -> bool:
144    if 'WAYLAND_DISPLAY' not in os.environ:
145        return False
146    if 'KITTY_DISABLE_WAYLAND' in os.environ:
147        return False
148    wayland = glfw_path('wayland')
149    if not os.path.exists(wayland):
150        return False
151    return True
152
153
154def is_wayland(opts: Optional['Options'] = None) -> bool:
155    if is_macos:
156        return False
157    if opts is None:
158        return bool(getattr(is_wayland, 'ans'))
159    if opts.linux_display_server == 'auto':
160        ans = detect_if_wayland_ok()
161    else:
162        ans = opts.linux_display_server == 'wayland'
163    setattr(is_wayland, 'ans', ans)
164    return ans
165
166
167supports_primary_selection = not is_macos
168
169
170def running_in_kitty(set_val: Optional[bool] = None) -> bool:
171    if set_val is not None:
172        setattr(running_in_kitty, 'ans', set_val)
173    return bool(getattr(running_in_kitty, 'ans', False))
174
175
176def resolve_custom_file(path: str) -> str:
177    path = os.path.expandvars(os.path.expanduser(path))
178    if not os.path.isabs(path):
179        path = os.path.join(config_dir, path)
180    return path
181
182
183def read_kitty_resource(name: str) -> bytes:
184    try:
185        from importlib.resources import read_binary
186    except ImportError:
187        from importlib_resources import read_binary  # type: ignore
188    return read_binary('kitty', name)
189
190
191def website_url(doc_name: str = '') -> str:
192    if doc_name:
193        doc_name = doc_name.rstrip('/')
194        if doc_name:
195            doc_name += '/'
196    return f'https://sw.kovidgoyal.net/kitty/{doc_name}'
197