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.
This commit is contained in:
Kovid Goyal
2025-08-16 11:58:30 +05:30
parent 0fb1835af1
commit 337cbf1435
6 changed files with 35 additions and 2 deletions

View File

@@ -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 <launch>`
action.
.. envvar:: ZDOTDIR
Set when enabling :ref:`shell_integration` with :program:`zsh`, allowing

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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"

View File

@@ -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() {