diff --git a/kitty/launch.py b/kitty/launch.py index f4917b06d..df7fe8190 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -10,7 +10,7 @@ from .cli import parse_args from .cli_stub import LaunchCLIOptions from .constants import kitty_exe, shell_path from .fast_data_types import ( - get_os_window_title, patch_color_profiles, set_clipboard_string + get_boss, get_os_window_title, patch_color_profiles, set_clipboard_string ) from .options.utils import env as parse_env from .tabs import Tab, TabManager @@ -224,7 +224,7 @@ def parse_launch_args(args: Optional[Sequence[str]] = None) -> LaunchSpec: return LaunchSpec(opts, args) -def get_env(opts: LaunchCLIOptions, active_child: Optional[Child]) -> Dict[str, str]: +def get_env(opts: LaunchCLIOptions, active_child: Optional[Child] = None) -> Dict[str, str]: env: Dict[str, str] = {} if opts.copy_env and active_child: env.update(active_child.foreground_environ) @@ -342,7 +342,8 @@ def launch( opts: LaunchCLIOptions, args: List[str], target_tab: Optional[Tab] = None, - force_target_tab: bool = False + force_target_tab: bool = False, + base_env: Optional[Dict[str, str]] = None ) -> Optional[Window]: active = boss.active_window_for_cwd if active: @@ -357,7 +358,11 @@ def launch( if opts.os_window_title == 'current': tm = boss.active_tab_manager opts.os_window_title = get_os_window_title(tm.os_window_id) if tm else None - env = get_env(opts, active_child) + if base_env: + env = base_env.copy() + env.update(get_env(opts)) + else: + env = get_env(opts, active_child) kw: LaunchKwds = { 'allow_remote_control': opts.allow_remote_control, 'cwd_from': None, @@ -470,3 +475,55 @@ def launch( new_window.set_logo(opts.logo, opts.logo_position or '', opts.logo_alpha) return new_window return None + + +def parse_opts_for_clone(args: List[str]) -> LaunchCLIOptions: + unsafe, unsafe_args = parse_launch_args(args) + default_opts, default_args = parse_launch_args() + for x in ( + 'window_title', 'tab_title', 'type', 'keep_focus', 'cwd', 'env', 'hold', + 'location', 'os_window_class', 'os_window_name', 'os_window_title', + 'logo', 'logo_position', 'logo_alpha', 'color' + ): + setattr(default_opts, x, getattr(unsafe, x)) + return default_opts + + +def clone_and_launch(msg: str, window: Window) -> None: + import base64 + + from .child import cmdline_of_process + args = [] + env: Dict[str, str] = {} + cwd = '' + pid = -1 + + for x in msg.split(','): + k, v = x.split('=', 1) + if k == 'pid': + pid = int(v) + continue + v = base64.standard_b64decode(v).decode('utf-8', 'replace') + if k == 'a': + args.append(v) + elif k == 'env': + for line in v.split('\0'): + if line: + try: + k, v = line.split('=', 1) + except ValueError: + continue + env[k] = v + elif k == 'cwd': + cwd = v.rstrip() + opts = parse_opts_for_clone(args) + if cwd: + opts.cwd = cwd + opts.copy_colors = True + try: + cmdline = cmdline_of_process(pid) + except Exception: + cmdline = [] + if not cmdline: + cmdline = list(window.child.argv) + launch(get_boss(), opts, cmdline, base_env=env) diff --git a/kitty/parser.c b/kitty/parser.c index 36b34dabb..c0eab050e 100644 --- a/kitty/parser.c +++ b/kitty/parser.c @@ -1086,6 +1086,7 @@ dispatch_dcs(Screen *screen, PyObject DUMP_UNUSED *dump_callback) { } else IF_SIMPLE_PREFIX("echo|", handle_remote_echo) } else IF_SIMPLE_PREFIX("ssh|", handle_remote_ssh) } else IF_SIMPLE_PREFIX("ask|", handle_remote_askpass) + } else IF_SIMPLE_PREFIX("clone|", handle_remote_clone) #undef IF_SIMPLE_PREFIX } else { REPORT_ERROR("Unrecognized DCS @ code: 0x%x", screen->parser_buf[1]); diff --git a/kitty/window.py b/kitty/window.py index 6283b8002..09276dc29 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -473,6 +473,7 @@ class Window: self.watchers = global_watchers().copy() self.last_focused_at = 0. self.started_at = monotonic() + self.current_clone_data = '' self.current_mouse_event_button = 0 self.current_clipboard_read_ask: Optional[bool] = None self.prev_osc99_cmd = NotificationCommand() @@ -1009,6 +1010,19 @@ class Window: if tab is not None: tab.move_window_to_top_of_group(self) + def handle_remote_clone(self, msg: str) -> None: + if not msg: + if self.current_clone_data: + cdata, self.current_clone_data = self.current_clone_data, '' + from .launch import clone_and_launch + clone_and_launch(cdata, self) + self.current_clone_data = '' + return + num, rest = msg.split(':', 1) + if num == '0' or len(self.current_clone_data) > 1024 * 1024: + self.current_clone_data = '' + self.current_clone_data += msg + def handle_remote_askpass(self, msg: str) -> None: from .shm import SharedMemory with SharedMemory(name=msg, readonly=True) as shm: diff --git a/shell-integration/zsh/kitty-integration b/shell-integration/zsh/kitty-integration index 188d99cd0..3798627d3 100644 --- a/shell-integration/zsh/kitty-integration +++ b/shell-integration/zsh/kitty-integration @@ -354,3 +354,24 @@ _ksi_deferred_init() { # to unfunction themselves when invoked. Unfunctioning is done by calling code. builtin unfunction _ksi_deferred_init } + +clone-in-kitty() { + builtin local data="pid=$$,cwd=$(builtin pwd -P | builtin command base64),env=$(builtin command env -0 | builtin command base64)" + while :; do + case "$1" in + "") break;; + *) data="$data,a=$(builtin printf "%s" "$1" | builtin command base64)";; + esac + shift + done + data="${data//[[:space:]]}" + builtin local pos=0 + builtin local chunk_num=0 + while [ $pos -lt ${#data} ]; do + builtin local chunk="${data:$pos:2048}" + pos=$(($pos+2048)) + builtin print -nu "$_ksi_fd" '\eP@kitty-clone|'"${chunk}:"'\e\\' + chunk_num=$(($chunk_num+1)) + done + builtin print -nu "$_ksi_fd" '\eP@kitty-clone|\e\\' +}