diff --git a/docs/changelog.rst b/docs/changelog.rst index 27ec4c3b7..fcc82c75f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -137,6 +137,10 @@ Detailed list of changes 0.43.2 [future] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- Allow kitty to read a specified set of environment variables from your + login shell at startup using the :opt:`env` directive in kitty.conf + (:iss:`9042`) + - Fix a regression in 0.43.0 that caused a black flicker when closing a tab in the presence of a background image (:iss:`9060`) diff --git a/docs/faq.rst b/docs/faq.rst index fb86d8a91..8e4a43be7 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -480,6 +480,16 @@ related to localization, such as :envvar:`LANG`, ``LC_*`` and loading of configuration files such as ``XDG_*``, :envvar:`KITTY_CONFIG_DIRECTORY` and, most importantly, ``PATH`` to locate binaries. +The simplest way to fix this is to have kitty load the environment variables +from your shell configuration at startup using the :opt:`env` directive, +adding the following to :file:`kitty.conf`:: + + env read_from_login_shell=PATH LANG LC_* XDG_* EDITOR VISUAL + +This works for POSIX compliant shells and the fish shell. Note that it +does add significantly to kitty startup time, so use only if really necessary. +This feature was added in version ``0.43.2``. + To see the environment variables that kitty sees, you can add the following mapping to :file:`kitty.conf`:: diff --git a/kitty/main.py b/kitty/main.py index 784c83d37..2e390723c 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -67,6 +67,7 @@ from .utils import ( get_custom_window_icon, log_error, parse_os_window_state, + read_shell_environment, safe_mtime, startup_notification_handler, ) @@ -478,6 +479,17 @@ def setup_environment(opts: Options, cli_opts: CLIOptions) -> None: from_config_file = True if cli_opts.listen_on: cli_opts.listen_on = expand_listen_on(cli_opts.listen_on, from_config_file) + if vars := opts.env.pop('read_from_login_shell', ''): + import fnmatch + import re + senv = read_shell_environment(opts) + patterns = tuple(re.compile(fnmatch.translate(x.strip())) for x in vars.split() if x.strip()) + if patterns: + for k, v in senv.items(): + for pat in patterns: + if pat.match(k) is not None: + opts.env[k] = v + break env = opts.env.copy() ensure_kitty_in_path() ensure_kitten_in_path() diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 124df26b0..09ffde89c 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -3254,7 +3254,9 @@ Changing this option by reloading the config is not supported. ''' ) -opt('+env', '', +opt( + '+env', + '', option_type='env', add_to_default=False, long_text=''' @@ -3268,8 +3270,17 @@ recursively, for example:: env VAR2=${HOME}/${VAR1}/b The value of :code:`VAR2` will be :code:`/a/b`. -''' - ) + +Use the special +value :code:`read_from_login_shell` to have kitty read the specified variables from +your :opt:`login shell ` configuration. +Useful if your shell startup files setup a bunch of environment variables that you want available to kitty and +in kitty session files. Each variable name is treated as a glob pattern to match. For example: +:code:`env read_from_login_shell=PATH LANG LC_* XDG_* EDITOR VISUAL`. Note that these variables are only +read after the configuration is fully processed, thus they are not available for recursive expansion and +they will override any variables set by other :opt:`env` directives. +''', +) opt('+filter_notification', '', option_type='filter_notification', add_to_default=False, long_text=''' Specify rules to filter out notifications sent by applications running in kitty. diff --git a/kitty/utils.py b/kitty/utils.py index 1cd7b44ca..6b5b4b3e9 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -748,12 +748,13 @@ def which(name: str, only_system: bool = False) -> str | None: def read_resolved_shell_environment(shell: tuple[str, ...]) -> MappingProxyType[str, str]: import subprocess cmdline = list(shell) - if '-l' not in cmdline and '--login' not in shell: + if '-l' not in cmdline and '--login' not in cmdline: cmdline += ['-l'] - if '-i' not in shell and '--interactive' not in shell: + if '-i' not in cmdline and '--interactive' not in cmdline: cmdline += ['-i'] - is_fish = 'fish' in os.path.basename(cmdline[0]).lower() - cmd = 'command env -0' if is_fish else 'builtin command env -0' + q = os.path.basename(cmdline[0]).lower() + has_builtin = q in ('bash', 'zsh') + cmd = 'builtin command env -0' if has_builtin else 'command env -0' ans: MappingProxyType[str, str] = MappingProxyType({}) from .child import openpty @@ -764,7 +765,7 @@ def read_resolved_shell_environment(shell: tuple[str, ...]) -> MappingProxyType[ cmdline + ['-c', cmd], stdout=slave, stdin=slave, stderr=slave, start_new_session=True, close_fds=True, preexec_fn=clear_handled_signals) except FileNotFoundError: - log_error('Could not find shell to read environment') + log_error(f'Could not find shell {cmdline[0]} to read environment') return ans with os.fdopen(master, 'rb') as stdout, os.fdopen(slave, 'wb'): raw = b'' @@ -781,7 +782,7 @@ def read_resolved_shell_environment(shell: tuple[str, ...]) -> MappingProxyType[ if ret is not None: break if ret is None: - log_error('Timed out waiting for shell to quit while reading shell environment') + log_error(f'Timed out waiting for shell {cmdline} to quit while reading shell environment') p.kill() elif ret == 0: while True: @@ -800,7 +801,7 @@ def read_resolved_shell_environment(shell: tuple[str, ...]) -> MappingProxyType[ env[k] = v ans = MappingProxyType(env) else: - log_error('Failed to run shell to read its environment') + log_error(f'Failed to run shell {cmdline} to read its environment') return ans