mirror of
https://github.com/kovidgoyal/kitty.git
synced 2025-12-13 20:36:22 +01:00
Useful if user builds up session to save by running programs via the shell. Note that the serialization format for session files has changed slightly, becoming more robust and allowing us to add more types of saved data in the future, without overloading user_vars and thus risking name conflicts.
256 lines
8.4 KiB
Python
256 lines
8.4 KiB
Python
#!/usr/bin/env python
|
|
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
from collections.abc import Callable
|
|
from contextlib import suppress
|
|
from typing import Iterable
|
|
|
|
from .constants import shell_integration_dir
|
|
from .fast_data_types import get_options
|
|
from .options.types import Options, defaults
|
|
from .types import run_once
|
|
from .utils import log_error, which
|
|
|
|
|
|
def setup_fish_env(env: dict[str, str], argv: list[str]) -> None:
|
|
val = env.get('XDG_DATA_DIRS')
|
|
env['KITTY_FISH_XDG_DATA_DIR'] = shell_integration_dir
|
|
if not val:
|
|
env['XDG_DATA_DIRS'] = shell_integration_dir
|
|
else:
|
|
dirs = list(filter(None, val.split(os.pathsep)))
|
|
dirs.insert(0, shell_integration_dir)
|
|
env['XDG_DATA_DIRS'] = os.pathsep.join(dirs)
|
|
|
|
|
|
def is_new_zsh_install(env: dict[str, str], zdotdir: str | None) -> bool:
|
|
# if ZDOTDIR is empty, zsh will read user rc files from /
|
|
# if there aren't any, it'll run zsh-newuser-install
|
|
# the latter will bail if there are rc files in $HOME
|
|
if not zdotdir:
|
|
zdotdir = env.get('HOME', os.path.expanduser('~'))
|
|
assert isinstance(zdotdir, str)
|
|
if zdotdir == '~':
|
|
return True
|
|
for q in ('.zshrc', '.zshenv', '.zprofile', '.zlogin'):
|
|
if os.path.exists(os.path.join(zdotdir, q)):
|
|
return False
|
|
return True
|
|
|
|
|
|
def get_zsh_zdotdir_from_global_zshenv(env: dict[str, str], argv: list[str]) -> str | None:
|
|
exe = which(argv[0], only_system=True) or 'zsh'
|
|
with suppress(Exception):
|
|
return subprocess.check_output([exe, '--norcs', '--interactive', '-c', 'echo -n $ZDOTDIR'], env=env).decode('utf-8')
|
|
return None
|
|
|
|
|
|
def setup_zsh_env(env: dict[str, str], argv: list[str]) -> None:
|
|
zdotdir = env.get('ZDOTDIR')
|
|
if is_new_zsh_install(env, zdotdir):
|
|
if zdotdir is None:
|
|
# Try to get ZDOTDIR from /etc/zshenv, when all startup files are not present
|
|
zdotdir = get_zsh_zdotdir_from_global_zshenv(env, argv)
|
|
if zdotdir is None or is_new_zsh_install(env, zdotdir):
|
|
return
|
|
else:
|
|
# dont prevent zsh-newuser-install from running
|
|
# zsh-newuser-install never runs as root but we assume that it does
|
|
return
|
|
if zdotdir is not None:
|
|
env['KITTY_ORIG_ZDOTDIR'] = zdotdir
|
|
else:
|
|
# KITTY_ORIG_ZDOTDIR can be set at this point if, for example, the global
|
|
# zshenv overrides ZDOTDIR; we try to limit the damage in this case
|
|
env.pop('KITTY_ORIG_ZDOTDIR', None)
|
|
env['ZDOTDIR'] = os.path.join(shell_integration_dir, 'zsh')
|
|
|
|
|
|
def setup_bash_env(env: dict[str, str], argv: list[str]) -> None:
|
|
inject = {'1'}
|
|
posix_env = rcfile = ''
|
|
remove_args = set()
|
|
|
|
expecting_multi_chars_opt = True
|
|
expecting_option_arg = False
|
|
interactive_opt = False
|
|
expecting_file_arg = False
|
|
file_arg_set = False
|
|
|
|
for i in range(1, len(argv)):
|
|
arg = argv[i]
|
|
if expecting_file_arg:
|
|
file_arg_set = True
|
|
break
|
|
if expecting_option_arg:
|
|
expecting_option_arg = False
|
|
continue
|
|
if arg in ('-', '--'):
|
|
if not expecting_file_arg:
|
|
expecting_file_arg = True
|
|
continue
|
|
elif len(arg) > 1 and arg[1] != '-' and (arg[0] == '-' or arg.startswith('+O')):
|
|
expecting_multi_chars_opt = False
|
|
options = arg.lstrip('-+')
|
|
# shopt option
|
|
if 'O' in options:
|
|
t = options.split('O', maxsplit=1)
|
|
if not t[1]:
|
|
expecting_option_arg = True
|
|
options = t[0]
|
|
# command string
|
|
if 'c' in options:
|
|
# non-interactive shell
|
|
# also skip `bash -ic` interactive mode with command string
|
|
return
|
|
# read from stdin and follow with args
|
|
if 's' in options:
|
|
break
|
|
# interactive option
|
|
if 'i' in options:
|
|
interactive_opt = True
|
|
elif arg.startswith('--') and expecting_multi_chars_opt:
|
|
if arg == '--posix':
|
|
inject.add('posix')
|
|
posix_env = env.get('ENV', '')
|
|
remove_args.add(i)
|
|
elif arg == '--norc':
|
|
inject.add('no-rc')
|
|
remove_args.add(i)
|
|
elif arg == '--noprofile':
|
|
inject.add('no-profile')
|
|
remove_args.add(i)
|
|
elif arg in ('--rcfile', '--init-file') and i + 1 < len(argv):
|
|
expecting_option_arg = True
|
|
rcfile = argv[i+1]
|
|
remove_args |= {i, i+1}
|
|
else:
|
|
file_arg_set = True
|
|
break
|
|
if file_arg_set and not interactive_opt:
|
|
# non-interactive shell
|
|
return
|
|
env['ENV'] = os.path.join(shell_integration_dir, 'bash', 'kitty.bash')
|
|
env['KITTY_BASH_INJECT'] = ' '.join(inject)
|
|
if posix_env:
|
|
env['KITTY_BASH_POSIX_ENV'] = posix_env
|
|
if rcfile:
|
|
env['KITTY_BASH_RCFILE'] = rcfile
|
|
for i in sorted(remove_args, reverse=True):
|
|
del argv[i]
|
|
if 'HISTFILE' not in env and 'posix' not in inject:
|
|
# In POSIX mode the default history file is ~/.sh_history instead of ~/.bash_history
|
|
env['HISTFILE'] = os.path.expanduser('~/.bash_history')
|
|
env['KITTY_BASH_UNEXPORT_HISTFILE'] = '1'
|
|
argv.insert(1, '--posix')
|
|
|
|
|
|
def as_str_literal(x: str) -> str:
|
|
parts = x.split("'")
|
|
return '"\'"'.join(f"'{x}'" for x in parts)
|
|
|
|
|
|
def as_fish_str_literal(x: str) -> str:
|
|
x = x.replace('\\', '\\\\').replace("'", "\\'")
|
|
return f"'{x}'"
|
|
|
|
|
|
def posix_serialize_env(env: dict[str, str], prefix: str = 'builtin export', sep: str = '=') -> str:
|
|
ans = []
|
|
for k, v in env.items():
|
|
ans.append(f'{prefix} {as_str_literal(k)}{sep}{as_str_literal(v)}')
|
|
return '\n'.join(ans)
|
|
|
|
|
|
def fish_serialize_env(env: dict[str, str]) -> str:
|
|
ans = []
|
|
for k, v in env.items():
|
|
ans.append(f'set -gx {as_fish_str_literal(k)} {as_fish_str_literal(v)}')
|
|
return '\n'.join(ans)
|
|
|
|
|
|
ENV_MODIFIERS = {
|
|
'fish': setup_fish_env,
|
|
'zsh': setup_zsh_env,
|
|
'bash': setup_bash_env,
|
|
}
|
|
|
|
ENV_SERIALIZERS: dict[str, Callable[[dict[str, str]], str]] = {
|
|
'zsh': posix_serialize_env,
|
|
'bash': posix_serialize_env,
|
|
'fish': fish_serialize_env,
|
|
}
|
|
|
|
QUOTERES = {
|
|
'fish': as_fish_str_literal
|
|
}
|
|
|
|
def get_supported_shell_name(path: str) -> str | None:
|
|
name = os.path.basename(path)
|
|
if name.lower().endswith('.exe'):
|
|
name = name.rpartition('.')[0]
|
|
if name.startswith('-'):
|
|
name = name[1:]
|
|
return name if name in ENV_MODIFIERS else None
|
|
|
|
|
|
def shell_integration_allows_rc_modification(opts: Options) -> bool:
|
|
return not (opts.shell_integration & {'disabled', 'no-rc'})
|
|
|
|
|
|
def serialize_env(path: str, env: dict[str, str]) -> str:
|
|
if not env:
|
|
return ''
|
|
name = get_supported_shell_name(path)
|
|
if not name:
|
|
raise ValueError(f'{path} is not a supported shell')
|
|
return ENV_SERIALIZERS[name](env)
|
|
|
|
|
|
@run_once
|
|
def unsafe_pat() -> re.Pattern[str]:
|
|
return re.compile(r'[^\w@%+=:,./-]', re.ASCII)
|
|
|
|
|
|
def join(path: str, cmd: Iterable[str]) -> str:
|
|
name = get_supported_shell_name(path)
|
|
_find_unsafe = unsafe_pat().search
|
|
if not name:
|
|
raise ValueError(f'{path} is not a supported shell')
|
|
q = QUOTERES.get(name, as_str_literal)
|
|
def quote(x: str) -> str:
|
|
return x if _find_unsafe(x) is None else q(x)
|
|
return ' '.join(map(quote, cmd))
|
|
|
|
|
|
def get_effective_ksi_env_var(opts: Options | None = None) -> str:
|
|
opts = opts or get_options()
|
|
if 'disabled' in opts.shell_integration:
|
|
return ''
|
|
# Use the default when shell_integration is empty due to misconfiguration
|
|
if 'invalid' in opts.shell_integration:
|
|
return ' '.join(defaults.shell_integration)
|
|
return ' '.join(opts.shell_integration)
|
|
|
|
|
|
def modify_shell_environ(opts: Options, env: dict[str, str], argv: list[str]) -> None:
|
|
shell = get_supported_shell_name(argv[0])
|
|
ksi = get_effective_ksi_env_var(opts)
|
|
if shell is None or not ksi:
|
|
return
|
|
env['KITTY_SHELL_INTEGRATION'] = ksi
|
|
if not shell_integration_allows_rc_modification(opts):
|
|
return
|
|
f = ENV_MODIFIERS.get(shell)
|
|
if f is not None:
|
|
try:
|
|
f(env, argv)
|
|
except Exception:
|
|
import traceback
|
|
traceback.print_exc()
|
|
log_error(f'Failed to setup shell integration for: {shell}')
|