From 337cbf14358b0336786aa0e58687b8cf8f7e312f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 16 Aug 2025 11:58:30 +0530 Subject: [PATCH] Add an env var that can be used to eval an expression at startup of shell This will come in handy to implement serialization as session with running of current foreground command. --- docs/glossary.rst | 8 ++++++++ kitty/child.py | 1 + kitty_tests/shell_integration.py | 15 +++++++++++++-- shell-integration/bash/kitty.bash | 3 +++ .../vendor_conf.d/kitty-shell-integration.fish | 5 +++++ shell-integration/zsh/kitty-integration | 5 +++++ 6 files changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 9fec5a5ae..520ed02e0 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -224,6 +224,14 @@ Variables that kitty sets when running child programs Set when enabling :ref:`shell_integration`. It is automatically removed by the shell integration scripts. +.. envvar:: KITTY_SI_RUN_COMMAND_AT_STARTUP + + Set this to an expression that the kitty shell integration scripts will + ``eval`` after the shell is started. Note that this environment variable + is ignored when present in the environment in which kitty itself is launched + in. It is most useful with the ``--env`` flag for the :doc:`launch ` + action. + .. envvar:: ZDOTDIR Set when enabling :ref:`shell_integration` with :program:`zsh`, allowing diff --git a/kitty/child.py b/kitty/child.py index 40f7667a9..69ada6cf0 100644 --- a/kitty/child.py +++ b/kitty/child.py @@ -152,6 +152,7 @@ def process_env(env: Mapping[str, str] | None = None) -> dict[str, str]: ans.pop(ssl_env_var, None) ans.pop('XDG_ACTIVATION_TOKEN', None) ans.pop('VTE_VERSION', None) # Used by the stupid VTE shell integration script that is installed system wide, sigh + ans.pop('KITTY_SI_RUN_COMMAND_AT_STARTUP', None) return ans diff --git a/kitty_tests/shell_integration.py b/kitty_tests/shell_integration.py index 59a298007..829d9c4bd 100644 --- a/kitty_tests/shell_integration.py +++ b/kitty_tests/shell_integration.py @@ -86,12 +86,14 @@ class ShellIntegration(BaseTest): with_kitten = False @contextmanager - def run_shell(self, shell='zsh', rc='', cmd='', setup_env=None): + def run_shell(self, shell='zsh', rc='', cmd='', setup_env=None, extra_env=None): home_dir = self.home_dir = os.path.realpath(tempfile.mkdtemp()) cmd = cmd or shell cmd = shlex.split(cmd.format(**locals())) env = (setup_env or safe_env_for_running_shell)(cmd, home_dir, rc=rc, shell=shell, with_kitten=self.with_kitten) env['KITTY_RUNNING_SHELL_INTEGRATION_TEST'] = '1' + if extra_env: + env.update(extra_env) try: if self.with_kitten: cmd = [kitten_exe(), 'run-shell', '--shell', shlex.join(cmd)] @@ -183,6 +185,10 @@ RPS1="{rps1}" self.assert_command(pty) env = pty.callbacks.clone_cmds[0].env self.ae(env.get('ES'), 'a\n b c\nd') + with self.run_shell(rc='PS1=XXX', extra_env={'KITTY_SI_RUN_COMMAND_AT_STARTUP': 'echo pre-start'}) as pty: + pty.wait_till(lambda: 'XXX' in pty.screen_contents()) + self.assertIn('pre-start', pty.screen_contents()) + self.assertTrue(pty.screen_contents().startswith('pre-start')) @unittest.skipUnless(shutil.which('fish'), 'fish not installed') def test_fish_integration(self): @@ -190,6 +196,7 @@ RPS1="{rps1}" completions_dir = os.path.join(kitty_base_dir, 'shell-integration', 'fish', 'vendor_completions.d') with self.run_shell( shell='fish', + extra_env={'KITTY_SI_RUN_COMMAND_AT_STARTUP': 'echo XXX'}, rc=f''' set -g fish_greeting function fish_prompt; echo -n "{fish_prompt}"; end @@ -198,7 +205,7 @@ function _test_comp_path; contains "{completions_dir}" $fish_complete_path; and function _set_key; set -g fish_key_bindings fish_$argv[1]_key_bindings; end function _set_status_prompt; function fish_prompt; echo -n "$pipestatus $status {fish_prompt}"; end; end ''') as pty: - q = fish_prompt + ' ' * (pty.screen.columns - len(fish_prompt) - len(right_prompt)) + right_prompt + q = 'XXX\n' + fish_prompt + ' ' * (pty.screen.columns - len(fish_prompt) - len(right_prompt)) + right_prompt pty.wait_till(lambda: pty.screen_contents().count(right_prompt) == 1) self.ae(pty.screen_contents(), q) @@ -366,6 +373,10 @@ PS1="{ps1}" self.ae(ps1.splitlines()[-1] + 'echo $COLUMNS', str(pty.screen.line(pty.screen.cursor.y - 1 - len(ps1.splitlines())))) self.assert_command(pty, 'echo $COLUMNS') + with self.run_shell(shell='bash', rc='PS1=XXX', extra_env={'KITTY_SI_RUN_COMMAND_AT_STARTUP': 'echo pre-start'}) as pty: + pty.wait_till(lambda: 'XXX' in pty.screen_contents()) + self.assertIn('pre-start', pty.screen_contents()) + self.assertTrue(pty.screen_contents().startswith('pre-start')) # test startup file sourcing def setup_env(excluded, argv, home_dir, rc='', shell='bash', with_kitten=self.with_kitten): diff --git a/shell-integration/bash/kitty.bash b/shell-integration/bash/kitty.bash index 9b2072ee2..dd2760c98 100644 --- a/shell-integration/bash/kitty.bash +++ b/shell-integration/bash/kitty.bash @@ -121,6 +121,8 @@ _ksi_main() { fi builtin unset SSH_KITTEN_KITTY_DIR fi + builtin local krcs="$KITTY_SI_RUN_COMMAND_AT_STARTUP" + builtin unset KITTY_SI_RUN_COMMAND_AT_STARTUP _ksi_debug_print() { # print a line to STDERR of parent kitty process @@ -351,6 +353,7 @@ _ksi_main() { fi fi builtin unset KITTY_IS_CLONE_LAUNCH KITTY_CLONE_SOURCE_STRATEGIES + if [[ -n "$krcs" ]]; then builtin eval "$krcs"; fi } _ksi_main builtin unset -f _ksi_main diff --git a/shell-integration/fish/vendor_conf.d/kitty-shell-integration.fish b/shell-integration/fish/vendor_conf.d/kitty-shell-integration.fish index b0116ad24..f88a07386 100644 --- a/shell-integration/fish/vendor_conf.d/kitty-shell-integration.fish +++ b/shell-integration/fish/vendor_conf.d/kitty-shell-integration.fish @@ -37,6 +37,8 @@ function __ksi_schedule --on-event fish_prompt -d "Setup kitty integration after end set --erase SSH_KITTEN_KITTY_DIR end + set --local krcs "$KITTY_SI_RUN_COMMAND_AT_STARTUP" + set --erase KITTY_SI_RUN_COMMAND_AT_STARTUP # Enable cursor shape changes for default mode and vi mode if not contains "no-cursor" $_ksi @@ -201,6 +203,9 @@ function __ksi_schedule --on-event fish_prompt -d "Setup kitty integration after test (count $new_path) -eq (count $PATH) or set --global --export --path PATH $new_path end + if test -n "$krcs" + eval "$krcs" + end end function edit-in-kitty --wraps "kitten edit-in-kitty" -d "Edit the specified file in a kitty overlay window with your locally installed editor" diff --git a/shell-integration/zsh/kitty-integration b/shell-integration/zsh/kitty-integration index f289cda33..eefcdf6e0 100644 --- a/shell-integration/zsh/kitty-integration +++ b/shell-integration/zsh/kitty-integration @@ -88,6 +88,8 @@ _ksi_deferred_init() { builtin local -a opt opt=(${(s: :)KITTY_SHELL_INTEGRATION}) builtin unset KITTY_SHELL_INTEGRATION + builtin local krcs="$KITTY_SI_RUN_COMMAND_AT_STARTUP" + builtin unset KITTY_SI_RUN_COMMAND_AT_STARTUP if [[ -n "$SSH_KITTEN_KITTY_DIR" ]]; then if [[ ! "$PATH" =~ (^|:)${SSH_KITTEN_KITTY_DIR}(:|$) ]] && [[ -z "$(builtin command -v kitten)" ]]; then @@ -424,6 +426,9 @@ _ksi_deferred_init() { # kitty-integration though because decent public functions aren't supposed to # to unfunction themselves when invoked. Unfunctioning is done by calling code. builtin unfunction _ksi_deferred_init + + # run startup command + if [[ -n "$krcs" ]]; then builtin eval "$krcs"; fi } _ksi_transmit_data() {