From 244f4597dcc9037e85c8571093639ff5c16a7d73 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 7 Feb 2025 13:40:44 +0530 Subject: [PATCH] launch: Decouple source and dest windows from active window Fixes #8295 --- docs/changelog.rst | 4 ++ kitty/boss.py | 5 +- kitty/launch.py | 129 +++++++++++++++++++++++++---------------- kitty/layout/base.py | 10 ++-- kitty/layout/splits.py | 3 +- kitty/rc/launch.py | 2 + kitty/tabs.py | 7 ++- kitty/window.py | 1 - 8 files changed, 101 insertions(+), 60 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1c6432ed4..08e61991b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -106,6 +106,10 @@ Detailed list of changes terminal ecosystem for decades by allowing terminal programs to specify how many cells to render a piece of text in (:iss:`8226`) +- launch: Add options :option:`launch --source-window` and :option:`launch --next-to` to allow + specifying which window is used as the data source and destination location independently of the + currently active window (:iss:`8295`) + - Linux: Add support for `COLRv1 `__ fonts. These are typically emoji fonts that use vector images for emoji - Add support for the octant box-drawing characters diff --git a/kitty/boss.py b/kitty/boss.py index 183893d17..da2fa99b7 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -2637,7 +2637,10 @@ class Boss: def launch(self, *args: str) -> None: from kitty.launch import launch, parse_launch_args opts, args_ = parse_launch_args(args) - launch(self, opts, args_, active=self.window_for_dispatch) + if self.window_for_dispatch: + opts.source_window = opts.source_window or f'id:{self.window_for_dispatch.id}' + opts.next_to = opts.next_to or f'id:{self.window_for_dispatch.id}' + launch(self, opts, args_) @ac('tab', 'Move the active tab forward') def move_tab_forward(self) -> None: diff --git a/kitty/launch.py b/kitty/launch.py index 3201183be..3250868ef 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -58,17 +58,24 @@ This option was added to kitty in version 0.26.0 @run_once def options_spec() -> str: - return f''' + return f""" +--source-window +A match expression to select the window from which data such as title, colors, env vars, +screen contents, etc. are copied. When not specified the currently active window is used. +See :ref:`search_syntax` for the syntax used for specifying windows. + + --window-title --title The title to set for the new window. By default, title is controlled by the child process. The special value :code:`current` will copy the title from the -currently active window. +:option:`source window `. --tab-title The title for the new tab if launching in a new tab. By default, the title of the active window in the tab is used as the tab title. The special value -:code:`current` will copy the title from the title of the currently active tab. +:code:`current` will copy the title from the tab containing the +:option:`source window `. --type @@ -119,7 +126,7 @@ opened window. --cwd completion=type:directory kwds:current,oldest,last_reported,root The working directory for the newly launched child. Use the special value -:code:`current` to use the working directory of the currently active window. +:code:`current` to use the working directory of the :option:`source window ` The special value :code:`last_reported` uses the last working directory reported by the shell (needs :ref:`shell_integration` to work). The special value :code:`oldest` works like :code:`current` but uses the working directory of the @@ -147,18 +154,18 @@ Keep the window open even after the command being executed exits, at a shell pro --copy-colors type=bool-set Set the colors of the newly created window to be the same as the colors in the -currently active window. +:option:`source window `. --copy-cmdline type=bool-set Ignore any specified command line and instead use the command line from the -currently active window. +:option:`source window `. --copy-env type=bool-set -Copy the environment variables from the currently active window into the newly +Copy the environment variables from the :option:`source window ` into the newly launched child process. Note that this only copies the environment when the window was first created, as it is not possible to get updated environment variables from arbitrary processes. To copy that environment, use either the :ref:`clone-in-kitty @@ -178,7 +185,15 @@ instead of at the end. The values of :code:`vsplit`, :code:`hsplit` and :code:`split` are only used by the :code:`splits` layout and control if the new window is placed in a vertical, horizontal or automatic split with the currently active window. The default is to place the window in a layout dependent manner, -typically, after the currently active window. +typically, after the currently active window. See :option:`--next-to ` +to use a window other than the currently active window. + + +--next-to +A match expression to select the window next to which the new window is created. +See :ref:`search_syntax` for the syntax for specifying windows. If not specified +defaults to the active window. When used via remote control and a target tab is +specified this option is ignored unless the matched window is in the specified tab. --bias @@ -241,16 +256,16 @@ choices=none,@selection,@screen,@screen_scrollback,@alternate,@alternate_scrollb Pass the screen contents as :file:`STDIN` to the child process. :code:`@selection` - is the currently selected text. + is the currently selected text in the :option:`source window `. :code:`@screen` - is the contents of the currently active window. + is the contents of the :option:`source window `. :code:`@screen_scrollback` is the same as :code:`@screen`, but includes the scrollback buffer as well. :code:`@alternate` - is the secondary screen of the current active window. For example if you run + is the secondary screen of the :option:`source window `. For example if you run a full screen terminal application, the secondary screen will be the screen you return to when quitting the application. @@ -302,7 +317,8 @@ using :option:`--type=os-window `. Defaults to --os-window-title Set the title for the newly created OS window. This title will override any titles set by programs running in kitty. The special value :code:`current` will -use the title of the current OS window, if any. +copy the title from the OS Window containing the +:option:`source window `. --os-window-state @@ -357,7 +373,7 @@ the section on watchers in the launch command documentation: :ref:`watchers`. Relative paths are resolved relative to the :ref:`kitty config directory `. Global watchers for all windows can be specified with :opt:`watcher` in :file:`kitty.conf`. -''' +""" def parse_launch_args(args: Sequence[str] | None = None) -> LaunchSpec: @@ -536,25 +552,36 @@ def _launch( args: list[str], target_tab: Tab | None = None, force_target_tab: bool = False, - active: Window | None = None, is_clone_launch: str = '', rc_from_window: Window | None = None, base_env: dict[str, str] | None = None, ) -> Window | None: - active = active or boss.active_window_for_cwd - if active: - active_child = active.child - else: - active_child = None + source_window = boss.active_window_for_cwd + if opts.source_window: + for qw in boss.match_windows(opts.source_window, rc_from_window): + source_window = qw + break + source_child = source_window.child if source_window else None + next_to = boss.active_window + if opts.next_to: + for qw in boss.match_windows(opts.next_to, rc_from_window): + next_to = qw + break + if target_tab and next_to and next_to not in target_tab: + next_to = None if opts.window_title == 'current': - opts.window_title = active.title if active else None + opts.window_title = source_window.title if source_window else None if opts.tab_title == 'current': atab = boss.active_tab + if source_window and (qt := source_window.tabref()): + atab = qt opts.tab_title = atab.effective_title if atab else None if opts.os_window_title == 'current': tm = boss.active_tab_manager + if source_window and (qt := source_window.tabref()) and (qr := qt.tab_manager_ref()): + tm = qr opts.os_window_title = get_os_window_title(tm.os_window_id) if tm else None - env = get_env(opts, active_child, base_env) + env = get_env(opts, source_child, base_env) kw: LaunchKwds = { 'allow_remote_control': opts.allow_remote_control, 'remote_control_passwords': parse_remote_control_passwords(opts.allow_remote_control, opts.remote_control_password), @@ -578,25 +605,23 @@ def _launch( kw['bias'] = max(-100, min(opts.bias, 100)) if opts.cwd: if opts.cwd == 'current': - if active: - kw['cwd_from'] = CwdRequest(active) + if source_window: + kw['cwd_from'] = CwdRequest(source_window) elif opts.cwd == 'last_reported': - if active: - kw['cwd_from'] = CwdRequest(active, CwdRequestType.last_reported) + if source_window: + kw['cwd_from'] = CwdRequest(source_window, CwdRequestType.last_reported) elif opts.cwd == 'oldest': - if active: - kw['cwd_from'] = CwdRequest(active, CwdRequestType.oldest) + if source_window: + kw['cwd_from'] = CwdRequest(source_window, CwdRequestType.oldest) elif opts.cwd == 'root': - if active: - kw['cwd_from'] = CwdRequest(active, CwdRequestType.root) + if source_window: + kw['cwd_from'] = CwdRequest(source_window, CwdRequestType.root) else: kw['cwd'] = opts.cwd - if kw['cwd_from'] is not None and rc_from_window is not None: - kw['cwd_from'].rc_from_window_id = rc_from_window.id if opts.location != 'default': kw['location'] = opts.location - if opts.copy_colors and active: - kw['copy_colors_from'] = active + if opts.copy_colors and source_window: + kw['copy_colors_from'] = source_window pipe_data: dict[str, Any] = {} if opts.stdin_source != 'none': q = str(opts.stdin_source) @@ -606,25 +631,27 @@ def _launch( q = f'@ansi_{q[1:]}' if opts.stdin_add_line_wrap_markers: q += '_wrap' - penv, stdin = boss.process_stdin_source(window=active, stdin=q, copy_pipe_data=pipe_data) + penv, stdin = boss.process_stdin_source(window=source_window, stdin=q, copy_pipe_data=pipe_data) if stdin: kw['stdin'] = stdin if penv: env.update(penv) cmd = args or None - if opts.copy_cmdline and active_child: - cmd = active_child.foreground_cmdline + if opts.copy_cmdline and source_child: + cmd = source_child.foreground_cmdline + active = boss.active_window if cmd: final_cmd: list[str] = [] for x in cmd: - if active and not opts.copy_cmdline: + if source_window and not opts.copy_cmdline: if x == '@selection': - s = boss.data_for_at(which=x, window=active) + s = boss.data_for_at(which=x, window=source_window) if s: x = s elif x == '@active-kitty-window-id': - x = str(active.id) + if active: + x = str(active.id) elif x == '@input-line-number': if 'input_line_number' in pipe_data: x = str(pipe_data['input_line_number']) @@ -632,8 +659,8 @@ def _launch( if 'lines' in pipe_data: x = str(pipe_data['lines']) elif x in ('@cursor-x', '@cursor-y', '@scrolled-by', '@first-line-on-screen', '@last-line-on-screen'): - if active is not None: - screen = active.screen + if source_window is not None: + screen = source_window.screen if x == '@scrolled-by': x = str(screen.scrolled_by) elif x == '@cursor-x': @@ -652,8 +679,8 @@ def _launch( kw['cmd'] = final_cmd if force_window_launch and opts.type not in non_window_launch_types: opts.type = 'window' - base_for_overlay = active - if target_tab: + base_for_overlay = next_to + if target_tab and (not base_for_overlay or base_for_overlay not in target_tab): base_for_overlay = target_tab.active_window if opts.type in ('overlay', 'overlay-main') and base_for_overlay: kw['overlay_for'] = base_for_overlay.id @@ -682,7 +709,7 @@ def _launch( watchers = load_watch_modules(opts.watcher) with Window.set_ignore_focus_changes_for_new_windows(opts.keep_focus): new_window: Window = tab.new_window( - env=env or None, watchers=watchers or None, is_clone_launch=is_clone_launch, **kw) + env=env or None, watchers=watchers or None, is_clone_launch=is_clone_launch, next_to=next_to, **kw) if spacing: patch_window_edges(new_window, spacing) tab.relayout() @@ -710,16 +737,15 @@ def launch( args: list[str], target_tab: Tab | None = None, force_target_tab: bool = False, - active: Window | None = None, is_clone_launch: str = '', rc_from_window: Window | None = None, base_env: dict[str, str] | None = None, ) -> Window | None: - active = active or boss.active_window_for_cwd + active = boss.active_window if opts.keep_focus and active: orig, active.ignore_focus_changes = active.ignore_focus_changes, True try: - return _launch(boss, opts, args, target_tab, force_target_tab, active, is_clone_launch, rc_from_window, base_env) + return _launch(boss, opts, args, target_tab, force_target_tab, is_clone_launch, rc_from_window, base_env) finally: if opts.keep_focus and active: active.ignore_focus_changes = orig @@ -729,7 +755,7 @@ def clone_safe_opts() -> frozenset[str]: return frozenset(( 'window_title', 'tab_title', 'type', 'keep_focus', 'cwd', 'env', 'var', 'hold', 'location', 'os_window_class', 'os_window_name', 'os_window_title', 'os_window_state', - 'logo', 'logo_position', 'logo_alpha', 'color', 'spacing', + 'logo', 'logo_position', 'logo_alpha', 'color', 'spacing', 'next_to', )) @@ -953,7 +979,8 @@ def remote_edit(msg: str, window: Window) -> None: q.abort_signaled = c.abort_signaled return cmdline = get_editor(path_to_edit=c.file_localpath, line_number=c.line_number) - w = launch(get_boss(), c.opts, cmdline, active=window) + c.opts.source_window = c.opts.next_to = f'id:{window.id}' + w = launch(get_boss(), c.opts, cmdline) if w is not None: c.source_window_id = window.id c.editor_window_id = w.id @@ -1014,4 +1041,6 @@ def clone_and_launch(msg: str, window: Window) -> None: cmdline[0] = window.child.final_exe if cmdline and cmdline == [window.child.final_exe] + window.child.argv[1:]: cmdline = window.child.unmodified_argv - launch(get_boss(), c.opts, cmdline, active=window, is_clone_launch=is_clone_launch) + c.opts.source_window = f'id:{window.id}' + c.opts.next_to = c.opts.next_to or c.opts.source_window + launch(get_boss(), c.opts, cmdline, is_clone_launch=is_clone_launch) diff --git a/kitty/layout/base.py b/kitty/layout/base.py index 64e7fa8ac..36743c02c 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -295,6 +295,7 @@ class Layout: def add_window( self, all_windows: WindowList, window: WindowType, location: str | None = None, overlay_for: int | None = None, put_overlay_behind: bool = False, bias: float | None = None, + next_to: WindowType | None = None, ) -> WindowType | None: if overlay_for is not None: underlay = all_windows.id_map.get(overlay_for) @@ -304,13 +305,14 @@ class Layout: return underlay if location == 'neighbor': location = 'after' - self.add_non_overlay_window(all_windows, window, location, bias) + self.add_non_overlay_window(all_windows, window, location, bias, next_to) return None - def add_non_overlay_window(self, all_windows: WindowList, window: WindowType, location: str | None, bias: float | None = None) -> None: - next_to: WindowType | None = None + def add_non_overlay_window( + self, all_windows: WindowList, window: WindowType, location: str | None, bias: float | None = None, next_to: WindowType | None = None + ) -> None: before = False - next_to = all_windows.active_window + next_to = next_to or all_windows.active_window if location is not None: if location in ('after', 'vsplit', 'hsplit'): pass diff --git a/kitty/layout/splits.py b/kitty/layout/splits.py index ba6e71003..ba6949546 100644 --- a/kitty/layout/splits.py +++ b/kitty/layout/splits.py @@ -502,6 +502,7 @@ class Splits(Layout): window: WindowType, location: str | None, bias: float | None = None, + next_to: WindowType | None = None, ) -> None: horizontal = self.default_axis_is_horizontal after = True @@ -511,7 +512,7 @@ class Splits(Layout): horizontal = False elif location in ('before', 'first'): after = False - aw = all_windows.active_window + aw = next_to or all_windows.active_window if bias: bias = max(0, min(abs(bias), 100)) / 100 if aw is not None: diff --git a/kitty/rc/launch.py b/kitty/rc/launch.py index af2691698..6f6097cf9 100644 --- a/kitty/rc/launch.py +++ b/kitty/rc/launch.py @@ -20,6 +20,8 @@ class Launch(RemoteCommand): protocol_spec = __doc__ = ''' args+/list.str: The command line to run in the new window, as a list, use an empty list to run the default shell match/str: The tab to open the new window in + next_to/str: The window next to which to create the new window or empty string to use active window + source_window/str: The window to use as source for data or empty string to use active window window_title/str: Title for the new window cwd/str: Working directory for the new window env/list.str: List of environment variables of the form NAME=VALUE diff --git a/kitty/tabs.py b/kitty/tabs.py index daf2c3642..685c8fe5b 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -536,9 +536,9 @@ class Tab: # {{{ def _add_window( self, window: Window, location: str | None = None, overlay_for: int | None = None, - overlay_behind: bool = False, bias: float | None = None + overlay_behind: bool = False, bias: float | None = None, next_to: Window | None = None, ) -> None: - self.current_layout.add_window(self.windows, window, location, overlay_for, put_overlay_behind=overlay_behind, bias=bias) + self.current_layout.add_window(self.windows, window, location, overlay_for, put_overlay_behind=overlay_behind, bias=bias, next_to=next_to) if overlay_behind and (w := self.active_window): set_redirect_keys_to_overlay(self.os_window_id, self.id, w.id, window.id) buffer_keys_in_window(self.os_window_id, self.id, window.id, True) @@ -568,6 +568,7 @@ class Tab: # {{{ bias: float | None = None, pass_fds: tuple[int, ...] = (), remote_control_fd: int = -1, + next_to: Window | None = None, ) -> Window: child = self.launch_child( use_shell=use_shell, cmd=cmd, stdin=stdin, cwd_from=cwd_from, cwd=cwd, env=env, @@ -581,7 +582,7 @@ class Tab: # {{{ ) # Must add child before laying out so that resize_pty succeeds get_boss().add_child(window) - self._add_window(window, location=location, overlay_for=overlay_for, overlay_behind=overlay_behind, bias=bias) + self._add_window(window, location=location, overlay_for=overlay_for, overlay_behind=overlay_behind, bias=bias, next_to=next_to) if marker: try: window.set_marker(marker) diff --git a/kitty/window.py b/kitty/window.py index ea126c786..f0e520470 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -134,7 +134,6 @@ class CwdRequest: def __init__(self, window: Optional['Window'] = None, request_type: CwdRequestType = CwdRequestType.current) -> None: self.window_id = -1 if window is None else window.id self.request_type = request_type - self.rc_from_window_id = 0 def __bool__(self) -> bool: return self.window_id > -1