launch: Decouple source and dest windows from active window

Fixes #8295
This commit is contained in:
Kovid Goyal 2025-02-07 13:40:44 +05:30
parent 77416d0fc8
commit 244f4597dc
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
8 changed files with 101 additions and 60 deletions

View file

@ -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 <https://nabla.typearture.com/whatisCOLRV1.html>`__ fonts. These are typically emoji fonts that use vector images for emoji
- Add support for the octant box-drawing characters

View file

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

View file

@ -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 <launch --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 <launch --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 <launch --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 <launch --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 <launch --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 <launch --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 <launch --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 <launch --source-window>`.
:code:`@screen`
is the contents of the currently active window.
is the contents of the :option:`source window <launch --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 <launch --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 <launch --type>`. 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 <launch --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
<confloc>`. 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)

View file

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

View file

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

View file

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

View file

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

View file

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