It's easier to type, and cuter. Also, most, if not all of the TUI parts of kitty's kittens will eventually be re-written into kitten. The only downside I can see is that we cant tab complete kitty anymore, but hopefully there will be less reason to run kitty from the shell as command line tools migrate to kitten. Meowrrrr!!!
299 lines
9.7 KiB
Python
299 lines
9.7 KiB
Python
#!/usr/bin/env python3
|
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
import errno
|
|
import os
|
|
import pwd
|
|
import sys
|
|
from contextlib import suppress
|
|
from typing import TYPE_CHECKING, Any, Iterator, NamedTuple, Optional, Set
|
|
|
|
from .types import run_once
|
|
|
|
if TYPE_CHECKING:
|
|
from .options.types import Options
|
|
|
|
|
|
class Version(NamedTuple):
|
|
major: int
|
|
minor: int
|
|
patch: int
|
|
|
|
|
|
appname: str = 'kitty'
|
|
kitty_face = '🐱'
|
|
version: Version = Version(0, 26, 5)
|
|
str_version: str = '.'.join(map(str, version))
|
|
_plat = sys.platform.lower()
|
|
is_macos: bool = 'darwin' in _plat
|
|
is_freebsd: bool = 'freebsd' in _plat
|
|
is_running_from_develop: bool = False
|
|
RC_ENCRYPTION_PROTOCOL_VERSION = '1'
|
|
website_base_url = 'https://sw.kovidgoyal.net/kitty/'
|
|
default_pager_for_help = ('less', '-iRXF')
|
|
if getattr(sys, 'frozen', False):
|
|
extensions_dir: str = getattr(sys, 'kitty_run_data')['extensions_dir']
|
|
|
|
def get_frozen_base() -> str:
|
|
global is_running_from_develop
|
|
try:
|
|
from bypy_importer import running_in_develop_mode # type: ignore
|
|
except ImportError:
|
|
pass
|
|
else:
|
|
is_running_from_develop = running_in_develop_mode()
|
|
|
|
if is_running_from_develop:
|
|
q = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
try:
|
|
if os.path.isdir(q):
|
|
return q
|
|
except OSError:
|
|
pass
|
|
ans = os.path.dirname(extensions_dir)
|
|
if is_macos:
|
|
ans = os.path.dirname(os.path.dirname(ans))
|
|
ans = os.path.join(ans, 'kitty')
|
|
return ans
|
|
kitty_base_dir = get_frozen_base()
|
|
del get_frozen_base
|
|
else:
|
|
kitty_base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
extensions_dir = os.path.join(kitty_base_dir, 'kitty')
|
|
|
|
|
|
@run_once
|
|
def kitty_exe() -> str:
|
|
rpath = getattr(sys, 'kitty_run_data').get('bundle_exe_dir')
|
|
if not rpath:
|
|
items = os.environ.get('PATH', '').split(os.pathsep) + [os.path.join(kitty_base_dir, 'kitty', 'launcher')]
|
|
seen: Set[str] = set()
|
|
for candidate in filter(None, items):
|
|
if candidate not in seen:
|
|
seen.add(candidate)
|
|
if os.access(os.path.join(candidate, 'kitty'), os.X_OK):
|
|
rpath = candidate
|
|
break
|
|
else:
|
|
raise RuntimeError('kitty binary not found')
|
|
return os.path.join(rpath, 'kitty')
|
|
|
|
|
|
def kitten_exe() -> str:
|
|
return os.path.join(os.path.dirname(kitty_exe()), 'kitten')
|
|
|
|
|
|
def _get_config_dir() -> str:
|
|
if 'KITTY_CONFIG_DIRECTORY' in os.environ:
|
|
return os.path.abspath(os.path.expanduser(os.environ['KITTY_CONFIG_DIRECTORY']))
|
|
|
|
locations = []
|
|
if 'XDG_CONFIG_HOME' in os.environ:
|
|
locations.append(os.path.abspath(os.path.expanduser(os.environ['XDG_CONFIG_HOME'])))
|
|
locations.append(os.path.expanduser('~/.config'))
|
|
if is_macos:
|
|
locations.append(os.path.expanduser('~/Library/Preferences'))
|
|
for loc in filter(None, os.environ.get('XDG_CONFIG_DIRS', '').split(os.pathsep)):
|
|
locations.append(os.path.abspath(os.path.expanduser(loc)))
|
|
for loc in locations:
|
|
if loc:
|
|
q = os.path.join(loc, appname)
|
|
if os.access(q, os.W_OK) and os.path.exists(os.path.join(q, 'kitty.conf')):
|
|
return q
|
|
|
|
def make_tmp_conf() -> None:
|
|
import atexit
|
|
import tempfile
|
|
ans = tempfile.mkdtemp(prefix='kitty-conf-')
|
|
|
|
def cleanup() -> None:
|
|
import shutil
|
|
with suppress(Exception):
|
|
shutil.rmtree(ans)
|
|
atexit.register(cleanup)
|
|
|
|
candidate = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME') or '~/.config'))
|
|
ans = os.path.join(candidate, appname)
|
|
try:
|
|
os.makedirs(ans, exist_ok=True)
|
|
except FileExistsError:
|
|
raise SystemExit(f'A file {ans} already exists. It must be a directory, not a file.')
|
|
except PermissionError:
|
|
make_tmp_conf()
|
|
except OSError as err:
|
|
if err.errno != errno.EROFS: # Error other than read-only file system
|
|
raise
|
|
make_tmp_conf()
|
|
return ans
|
|
|
|
|
|
config_dir = _get_config_dir()
|
|
del _get_config_dir
|
|
defconf = os.path.join(config_dir, 'kitty.conf')
|
|
|
|
|
|
@run_once
|
|
def cache_dir() -> str:
|
|
if 'KITTY_CACHE_DIRECTORY' in os.environ:
|
|
candidate = os.path.abspath(os.environ['KITTY_CACHE_DIRECTORY'])
|
|
elif is_macos:
|
|
candidate = os.path.join(os.path.expanduser('~/Library/Caches'), appname)
|
|
else:
|
|
candidate = os.environ.get('XDG_CACHE_HOME', '~/.cache')
|
|
candidate = os.path.join(os.path.expanduser(candidate), appname)
|
|
os.makedirs(candidate, exist_ok=True)
|
|
return candidate
|
|
|
|
|
|
@run_once
|
|
def runtime_dir() -> str:
|
|
if 'KITTY_RUNTIME_DIRECTORY' in os.environ:
|
|
candidate = os.path.abspath(os.environ['KITTY_RUNTIME_DIRECTORY'])
|
|
elif is_macos:
|
|
from .fast_data_types import user_cache_dir
|
|
candidate = user_cache_dir()
|
|
elif 'XDG_RUNTIME_DIR' in os.environ:
|
|
candidate = os.path.abspath(os.environ['XDG_RUNTIME_DIR'])
|
|
else:
|
|
candidate = f'/run/user/{os.geteuid()}'
|
|
if not os.path.isdir(candidate) or not os.access(candidate, os.X_OK | os.W_OK | os.R_OK):
|
|
candidate = os.path.join(cache_dir(), 'run')
|
|
os.makedirs(candidate, exist_ok=True)
|
|
import stat
|
|
if stat.S_IMODE(os.stat(candidate).st_mode) != 0o700:
|
|
os.chmod(candidate, 0o700)
|
|
return candidate
|
|
|
|
|
|
def wakeup_io_loop() -> None:
|
|
from .fast_data_types import get_boss
|
|
b = get_boss()
|
|
if b is not None:
|
|
b.child_monitor.wakeup()
|
|
|
|
|
|
terminfo_dir = os.path.join(kitty_base_dir, 'terminfo')
|
|
logo_png_file = os.path.join(kitty_base_dir, 'logo', 'kitty.png')
|
|
beam_cursor_data_file = os.path.join(kitty_base_dir, 'logo', 'beam-cursor.png')
|
|
shell_integration_dir = os.path.join(kitty_base_dir, 'shell-integration')
|
|
try:
|
|
shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'
|
|
except KeyError:
|
|
with suppress(Exception):
|
|
print('Failed to read login shell via getpwuid() for current user, falling back to /bin/sh', file=sys.stderr)
|
|
shell_path = '/bin/sh'
|
|
# Keep this short as it is limited to 103 bytes on macOS
|
|
# https://github.com/ansible/ansible/issues/11536#issuecomment-153030743
|
|
ssh_control_master_template = 'kssh-{kitty_pid}-{ssh_placeholder}'
|
|
|
|
|
|
def glfw_path(module: str) -> str:
|
|
prefix = 'kitty.' if getattr(sys, 'frozen', False) else ''
|
|
return os.path.join(extensions_dir, f'{prefix}glfw-{module}.so')
|
|
|
|
|
|
def detect_if_wayland_ok() -> bool:
|
|
if 'WAYLAND_DISPLAY' not in os.environ:
|
|
return False
|
|
if 'KITTY_DISABLE_WAYLAND' in os.environ:
|
|
return False
|
|
wayland = glfw_path('wayland')
|
|
if not os.path.exists(wayland):
|
|
return False
|
|
return True
|
|
|
|
|
|
def is_wayland(opts: Optional['Options'] = None) -> bool:
|
|
if is_macos:
|
|
return False
|
|
if opts is None:
|
|
return bool(getattr(is_wayland, 'ans'))
|
|
if opts.linux_display_server == 'auto':
|
|
ans = detect_if_wayland_ok()
|
|
else:
|
|
ans = opts.linux_display_server == 'wayland'
|
|
setattr(is_wayland, 'ans', ans)
|
|
return ans
|
|
|
|
|
|
supports_primary_selection = not is_macos
|
|
|
|
|
|
def running_in_kitty(set_val: Optional[bool] = None) -> bool:
|
|
if set_val is not None:
|
|
setattr(running_in_kitty, 'ans', set_val)
|
|
return bool(getattr(running_in_kitty, 'ans', False))
|
|
|
|
|
|
def list_kitty_resources(package: str = 'kitty') -> Iterator[str]:
|
|
try:
|
|
if sys.version_info[:2] < (3, 10):
|
|
raise ImportError('importlib.resources.files() doesnt work with frozen builds on python 3.9')
|
|
from importlib.resources import files
|
|
except ImportError:
|
|
from importlib.resources import contents
|
|
return iter(contents(package))
|
|
else:
|
|
return (path.name for path in files(package).iterdir())
|
|
|
|
|
|
def read_kitty_resource(name: str, package_name: str = 'kitty') -> bytes:
|
|
try:
|
|
if sys.version_info[:2] < (3, 10):
|
|
raise ImportError('importlib.resources.files() doesnt work with frozen builds on python 3.9')
|
|
from importlib.resources import files
|
|
except ImportError:
|
|
from importlib.resources import read_binary
|
|
return read_binary(package_name, name)
|
|
else:
|
|
return (files(package_name) / name).read_bytes()
|
|
|
|
|
|
def website_url(doc_name: str = '', website: str = website_base_url) -> str:
|
|
if doc_name:
|
|
base, _, frag = doc_name.partition('#')
|
|
base = base.rstrip('/')
|
|
if base:
|
|
base += '/'
|
|
doc_name = base + (f'#{frag}' if frag else '')
|
|
return website + doc_name.lstrip('/')
|
|
|
|
|
|
handled_signals: Set[int] = set()
|
|
|
|
|
|
def clear_handled_signals(*a: Any) -> None:
|
|
if not handled_signals:
|
|
return
|
|
import signal
|
|
if hasattr(signal, 'pthread_sigmask'):
|
|
signal.pthread_sigmask(signal.SIG_UNBLOCK, handled_signals)
|
|
for s in handled_signals:
|
|
signal.signal(s, signal.SIG_DFL)
|
|
|
|
|
|
@run_once
|
|
def local_docs() -> str:
|
|
d = os.path.dirname
|
|
base = d(d(kitty_exe()))
|
|
from_source = getattr(sys, 'kitty_run_data').get('from_source')
|
|
if is_macos and from_source and '/kitty.app/Contents/' in kitty_exe():
|
|
base = d(d(d(base)))
|
|
subdir = os.path.join('doc', 'kitty', 'html')
|
|
linux_ans = os.path.join(base, 'share', subdir)
|
|
if getattr(sys, 'frozen', False):
|
|
if is_macos:
|
|
return os.path.join(d(d(d(extensions_dir))), subdir)
|
|
return linux_ans
|
|
if os.path.isdir(linux_ans):
|
|
return linux_ans
|
|
if from_source:
|
|
sq = os.path.join(d(base), 'docs', '_build', 'html')
|
|
if os.path.isdir(sq):
|
|
return sq
|
|
for candidate in ('/usr', '/usr/local', '/opt/homebrew'):
|
|
q = os.path.join(candidate, 'share', subdir)
|
|
if os.path.isdir(q):
|
|
return q
|
|
return ''
|