From c9645378f7f07d7c9df0679f86b012262db77c2d Mon Sep 17 00:00:00 2001 From: Jackie Li Date: Sat, 25 Oct 2025 18:10:01 +0100 Subject: [PATCH 1/3] add focus-tab command to session file --- kitty/session.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/kitty/session.py b/kitty/session.py index b69583d44..0c4b32374 100644 --- a/kitty/session.py +++ b/kitty/session.py @@ -177,6 +177,9 @@ def focus(self) -> None: self.active_tab_idx = max(0, len(self.tabs) - 1) self.tabs[-1].active_window_idx = max(0, len(self.tabs[-1].windows) - 1) + def focus_tab(self, idx: int) -> None: + self.active_tab_idx = max(0, min(idx, len(self.tabs) - 1)) + def set_enabled_layouts(self, raw: str) -> None: self.tabs[-1].enabled_layouts = to_layout_names(raw) if self.tabs[-1].layout not in self.tabs[-1].enabled_layouts: @@ -250,6 +253,11 @@ def finalize_session(ans: Session) -> Session: ans.add_window(rest, expand) elif cmd == 'focus': ans.focus() + elif cmd == 'focus_tab': + try: + ans.focus_tab(int(rest)) + except ValueError: + raise ValueError(f'focus_tab requires an integer tab index, got: {rest}') elif cmd == 'focus_os_window': ans.focus_os_window = True elif cmd == 'enabled_layouts': @@ -372,8 +380,9 @@ def skey(w: WindowType) -> float: seen_session_paths: dict[str, str] = {} -def create_session(boss: BossType, path: str) -> str: +def create_session(boss: BossType, path: str) -> tuple[str, bool]: session_name = '' + created_new_os_window = False for i, s in enumerate(create_sessions(get_options(), default_session=path)): if i == 0: session_name = s.session_name @@ -382,14 +391,16 @@ def create_session(boss: BossType, path: str) -> str: tm = boss.active_tab_manager if tm is None: os_window_id = boss.add_os_window(s) + created_new_os_window = True else: os_window_id = tm.os_window_id tm.add_tabs_from_session(s, session_name) else: os_window_id = boss.add_os_window(s) + created_new_os_window = True if s.focus_os_window: boss.focus_os_window(os_window_id) - return session_name + return session_name, created_new_os_window goto_session_history: list[str] = [] @@ -569,14 +580,15 @@ def goto_session(boss: BossType, cmdline: Sequence[str]) -> None: if switch_to_session(boss, session_name): return try: - session_name = create_session(boss, path) + session_name, created_new_os_window = create_session(boss, path) except Exception: import traceback tb = traceback.format_exc() boss.show_error(_('Failed to create session'), _('Could not create session from {0} with error:\n{1}').format(path, tb)) else: # Ensure newly created session is focused needed when it doesn't create its own OS Windows. - switch_to_session(boss, session_name) + if not created_new_os_window: + switch_to_session(boss, session_name) save_as_session_message = '''\ From e0bea0cac4ac7dd3ffd98977dfa1f572be10993e Mon Sep 17 00:00:00 2001 From: Jackie Li Date: Sat, 25 Oct 2025 18:11:57 +0100 Subject: [PATCH 2/3] add doc + changelog --- docs/changelog.rst | 5 +++++ docs/sessions.rst | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index aac698a98..345d147b2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -184,6 +184,11 @@ Detailed list of changes - Fix a regression in 0.43.0 that caused :opt:`tab_bar_margin_width` to be doubled on the right edge of the tab bar (:iss:`9154`) +- Session files: Add a new ``focus_tab`` command to specify which tab should be + active when a session is loaded. Accepts either a plain number (0-based index) + or a match expression for flexible tab selection, allowing sessions to preserve + the active tab state (:doc:`sessions`) + 0.43.1 [2025-10-01] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/sessions.rst b/docs/sessions.rst index 5fc62d5b2..d0719f99a 100644 --- a/docs/sessions.rst +++ b/docs/sessions.rst @@ -154,6 +154,13 @@ all the major keywords you can use in kitty session files: focus_os_window launch emacs + # Create another tab + new_tab logs + launch tail -f /var/log/syslog + + # Focus the first tab (index 0) when the session loads + focus_tab 0 + # Create a complex layout using multiple splits. Creates two columns of # windows with two windows in each column. The windows in the first column are # split 50:50. In the second column the windows are not evenly split. @@ -272,6 +279,13 @@ documentation for them. otherwise the window manager might block changing focus to prevent *focus stealing*. +``focus_tab [tab index]`` + Set which tab should be active (focused) in the current OS Window. The tab + index is 0-based, so ``focus_tab 0`` will focus the first tab, ``focus_tab 1`` + the second tab, and so on. This is useful for session files that create + multiple tabs and want to ensure a specific tab is active when the session + is loaded. + ``enabled_layouts comma separated list of layout names`` Set the layouts allowed in the current tab. Same syntax as :opt:`enabled_layouts`. From 75537187a86e3fa3637375ec2dee3149ff465eba Mon Sep 17 00:00:00 2001 From: Jackie Li Date: Sun, 26 Oct 2025 09:29:12 +0000 Subject: [PATCH 3/3] support search_syntax in focus_tab --- docs/sessions.rst | 13 ++++++++----- kitty/session.py | 10 ++++------ kitty/tabs.py | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/docs/sessions.rst b/docs/sessions.rst index d0719f99a..8b379c35a 100644 --- a/docs/sessions.rst +++ b/docs/sessions.rst @@ -159,6 +159,7 @@ all the major keywords you can use in kitty session files: launch tail -f /var/log/syslog # Focus the first tab (index 0) when the session loads + # You can also use a match expression like: focus_tab title:logs focus_tab 0 # Create a complex layout using multiple splits. Creates two columns of @@ -279,12 +280,14 @@ documentation for them. otherwise the window manager might block changing focus to prevent *focus stealing*. -``focus_tab [tab index]`` +``focus_tab [tab specifier]`` Set which tab should be active (focused) in the current OS Window. The tab - index is 0-based, so ``focus_tab 0`` will focus the first tab, ``focus_tab 1`` - the second tab, and so on. This is useful for session files that create - multiple tabs and want to ensure a specific tab is active when the session - is loaded. + specifier can be either a plain number (treated as a 0-based index) or a + match expression. For example, ``focus_tab 0`` will focus the first tab, + ``focus_tab 1`` the second tab, and ``focus_tab title:logs`` will focus the + tab whose title matches "logs". See :ref:`search_syntax` for the full syntax + of match expressions. This is useful for session files that create multiple + tabs and want to ensure a specific tab is active when the session is loaded. ``enabled_layouts comma separated list of layout names`` Set the layouts allowed in the current tab. Same syntax as diff --git a/kitty/session.py b/kitty/session.py index 0c4b32374..e6e175dd4 100644 --- a/kitty/session.py +++ b/kitty/session.py @@ -88,6 +88,7 @@ class Session: def __init__(self, default_title: str | None = None): self.tabs: list[Tab] = [] self.active_tab_idx = 0 + self.focus_tab_spec: str | None = None self.default_title = default_title self.os_window_size: WindowSizes | None = None self.os_window_class: str | None = None @@ -177,8 +178,8 @@ def focus(self) -> None: self.active_tab_idx = max(0, len(self.tabs) - 1) self.tabs[-1].active_window_idx = max(0, len(self.tabs[-1].windows) - 1) - def focus_tab(self, idx: int) -> None: - self.active_tab_idx = max(0, min(idx, len(self.tabs) - 1)) + def focus_tab(self, spec: str) -> None: + self.focus_tab_spec = spec def set_enabled_layouts(self, raw: str) -> None: self.tabs[-1].enabled_layouts = to_layout_names(raw) @@ -254,10 +255,7 @@ def finalize_session(ans: Session) -> Session: elif cmd == 'focus': ans.focus() elif cmd == 'focus_tab': - try: - ans.focus_tab(int(rest)) - except ValueError: - raise ValueError(f'focus_tab requires an integer tab index, got: {rest}') + ans.focus_tab(rest) elif cmd == 'focus_os_window': ans.focus_os_window = True elif cmd == 'enabled_layouts': diff --git a/kitty/tabs.py b/kitty/tabs.py index 011dd5871..d2eafa2f3 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -1064,6 +1064,24 @@ def add_tabs_from_session(self, session: SessionType, session_name: str = '') -> self._add_tab(tab) if i == session.active_tab_idx: active_tab = tab + + # Handle focus_tab_spec if specified + if session.focus_tab_spec is not None: + spec = session.focus_tab_spec.strip() + # Try to parse as a plain number (index) + try: + idx = int(spec) + # Clamp to valid range + idx = max(0, min(idx, len(self.tabs) - 1)) + active_tab = self.tabs[idx] + except ValueError: + # Not a plain number, treat as match expression + from .fast_data_types import get_boss + boss = get_boss() + matched_tabs = list(boss.match_tabs(spec, self.tabs)) + if matched_tabs: + active_tab = matched_tabs[0] + if active_tab is not None: idx = self.tabs.index(active_tab) self._set_active_tab(idx)