From 8451ad44b595581f90f12129cf5dd3909c10b2fb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 19 Aug 2025 10:37:05 +0530 Subject: [PATCH] Implement running foreground process even on unknown shells This is not as nice because the environment wont have been setup by the shell before running the process, but it's the best we can do for an unknown shell. --- kitty/child.py | 24 ++++++++++++++++++------ kitty/launch.py | 20 +++----------------- kitty/tabs.py | 13 +++++++++---- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/kitty/child.py b/kitty/child.py index 2f5a8f6ac..1e10b1f8b 100644 --- a/kitty/child.py +++ b/kitty/child.py @@ -13,7 +13,7 @@ from .constants import handled_signals, is_freebsd, is_macos, kitten_exe, kitty_base_dir, shell_path, terminfo_dir from .types import run_once -from .utils import cmdline_for_hold, log_error, which +from .utils import cmdline_for_hold, log_error, resolved_shell, which if TYPE_CHECKING: from .window import CwdRequest @@ -229,7 +229,8 @@ def __init__( hold: bool = False, pass_fds: tuple[int, ...] = (), remote_control_fd: int = -1, - hold_after_ssh: bool = False + hold_after_ssh: bool = False, + startup_command_via_shell_integration: Sequence[str] = (), ): self.is_clone_launch = is_clone_launch self.id = next(child_counter) @@ -247,12 +248,13 @@ def __init__( self.cwd = os.path.abspath(cwd) self.stdin = stdin self.env = env or {} + self.startup_command_via_shell_integration = startup_command_via_shell_integration self.final_env:dict[str, str] = {} self.is_default_shell = bool(self.argv and self.argv[0] == shell_path) self.should_run_via_run_shell_kitten = is_macos and self.is_default_shell self.hold = hold - def get_final_env(self) -> dict[str, str]: + def get_final_env(self) -> tuple[dict[str, str], bool]: from kitty.options.utils import DELETE_ENV_VAR env = default_env().copy() opts = fast_data_types.get_options() @@ -294,7 +296,15 @@ def get_final_env(self) -> dict[str, str]: self.is_clone_launch = '1' # free memory else: env.pop('KITTY_IS_CLONE_LAUNCH', None) - return env + must_run_startup_command_via_kitten = False + if self.startup_command_via_shell_integration: + from .shell_integration import join + scmd = self.argv or resolved_shell(fast_data_types.get_options()) + try: + env['KITTY_SI_RUN_COMMAND_AT_STARTUP'] = join(scmd[0], self.startup_command_via_shell_integration) + except Exception: + must_run_startup_command_via_kitten = True # unknown shell + return env, must_run_startup_command_via_kitten def fork(self) -> int | None: if self.forked: @@ -312,13 +322,13 @@ def fork(self) -> int | None: os.set_inheritable(stdin_read_fd, True) else: stdin_read_fd = stdin_write_fd = -1 - self.final_env = self.get_final_env() + self.final_env, must_run_startup_command_via_kitten = self.get_final_env() argv = list(self.argv) cwd = self.cwd pass_fds = self.pass_fds if self.remote_control_fd > -1: pass_fds += self.remote_control_fd, - if self.should_run_via_run_shell_kitten: + if self.should_run_via_run_shell_kitten or must_run_startup_command_via_kitten: # bash will only source ~/.bash_profile if it detects it is a login # shell (see the invocation section of the bash man page), which it # does if argv[0] is prefixed by a hyphen see @@ -338,6 +348,8 @@ def fork(self) -> int | None: if ksi == 'invalid': ksi = 'enabled' argv = [kitten_exe(), 'run-shell', '--shell', shlex.join(argv), '--shell-integration', ksi] + if must_run_startup_command_via_kitten: + argv.extend(self.startup_command_via_shell_integration) if is_macos and not pass_fds and not opts.forward_stdio: # In addition for getlogin() to work we need to run the shell # via the /usr/bin/login wrapper, sigh. diff --git a/kitty/launch.py b/kitty/launch.py index e925a3051..86daeb9b9 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -17,7 +17,7 @@ from .options.utils import env as parse_env from .tabs import Tab, TabManager from .types import LayerShellConfig, OverlayType, run_once -from .utils import get_editor, log_error, resolve_custom_file, resolved_shell, which +from .utils import get_editor, log_error, resolve_custom_file, which from .window import CwdRequest, CwdRequestType, Watchers, Window @@ -776,19 +776,9 @@ def _launch( tab = tab_for_window(boss, opts, target_tab, next_to) watchers = load_watch_modules(opts.watcher) with Window.set_ignore_focus_changes_for_new_windows(opts.keep_focus): - startup_command_env_added = False - if startup_command_via_shell_integration: - from .shell_integration import join - try: - scmd = kw.get('cmd') or resolved_shell(get_options()) - env = env or {} - env['KITTY_SI_RUN_COMMAND_AT_STARTUP'] = join(scmd[0], startup_command_via_shell_integration) - startup_command_env_added = True - except Exception: - pass # shell is not a known shell - new_window: Window = tab.new_window( - env=env or None, watchers=watchers or None, is_clone_launch=is_clone_launch, next_to=next_to, **kw) + env=env or None, watchers=watchers or None, is_clone_launch=is_clone_launch, next_to=next_to, + startup_command_via_shell_integration=startup_command_via_shell_integration, **kw) if child_death_callback is not None: boss.monitor_pid(new_window.child.pid or 0, child_death_callback) if new_window.creation_spec: @@ -798,10 +788,6 @@ def _launch( new_window.creation_spec = new_window.creation_spec._replace(spacing=tuple(opts.spacing)) if opts.color: new_window.creation_spec = new_window.creation_spec._replace(colors=tuple(opts.color)) - if startup_command_env_added and new_window.creation_spec.env: - def is_not_scmd(x: tuple[str, str]) -> bool: - return x[0] != 'KITTY_SI_RUN_COMMAND_AT_STARTUP' - new_window.creation_spec = new_window.creation_spec._replace(env=tuple(filter(is_not_scmd, new_window.creation_spec.env))) if spacing: patch_window_edges(new_window, spacing) tab.relayout() diff --git a/kitty/tabs.py b/kitty/tabs.py index 9be92fe0e..55c385a14 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -534,7 +534,8 @@ def launch_child( hold: bool = False, pass_fds: tuple[int, ...] = (), remote_control_fd: int = -1, - hold_after_ssh: bool = False + hold_after_ssh: bool = False, + startup_command_via_shell_integration: Sequence[str] = (), ) -> Child: check_for_suitability = True if cmd is None: @@ -584,7 +585,9 @@ def launch_child( fenv['WINDOWID'] = str(pwid) ans = Child( cmd, cwd or self.cwd, stdin, fenv, cwd_from, is_clone_launch=is_clone_launch, - add_listen_on_env_var=add_listen_on_env_var, hold=hold, pass_fds=pass_fds, remote_control_fd=remote_control_fd, hold_after_ssh=hold_after_ssh) + add_listen_on_env_var=add_listen_on_env_var, hold=hold, pass_fds=pass_fds, + remote_control_fd=remote_control_fd, hold_after_ssh=hold_after_ssh, + startup_command_via_shell_integration=startup_command_via_shell_integration) ans.fork() return ans @@ -623,7 +626,8 @@ def new_window( pass_fds: tuple[int, ...] = (), remote_control_fd: int = -1, next_to: Window | None = None, - hold_after_ssh: bool = False + hold_after_ssh: bool = False, + startup_command_via_shell_integration: Sequence[str] = (), ) -> Window: cs = WindowCreationSpec( use_shell=use_shell, cmd=cmd, has_stdin=bool(stdin), override_title=override_title, cwd_from=cwd_from, @@ -637,7 +641,8 @@ def new_window( child = self.launch_child( use_shell=use_shell, cmd=cmd, stdin=stdin, cwd_from=cwd_from, cwd=cwd, env=env, is_clone_launch=is_clone_launch, add_listen_on_env_var=False if allow_remote_control and remote_control_passwords else True, - hold=hold, pass_fds=pass_fds, remote_control_fd=remote_control_fd, hold_after_ssh=hold_after_ssh + hold=hold, pass_fds=pass_fds, remote_control_fd=remote_control_fd, hold_after_ssh=hold_after_ssh, + startup_command_via_shell_integration=startup_command_via_shell_integration, ) window = Window( self, child, self.args, override_title=override_title,