From c2389a1adcfdbae4b82ae431829731fa58000cc1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 17 Aug 2025 11:56:32 +0530 Subject: [PATCH] Option to make saved session files relocatable --- kitty/boss.py | 4 ++-- kitty/session.py | 9 ++++++++- kitty/tabs.py | 13 +++++++++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/kitty/boss.py b/kitty/boss.py index a3a768703..3ca349654 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -501,13 +501,13 @@ def list_os_windows( 'background_opacity': bo, } - def serialize_state_as_session(self, ser_opts: SaveAsSessionOptions | None = None) -> Iterator[str]: + def serialize_state_as_session(self, session_path: str = '', ser_opts: SaveAsSessionOptions | None = None) -> Iterator[str]: if ser_opts is None: ser_opts = default_save_as_session_opts() s = {current_focused_os_window_id(): 2, last_focused_os_window_id(): 1} for i, os_window_id in enumerate(sorted(self.os_window_map, key=lambda wid: s.get(wid, 0))): tm = self.os_window_map[os_window_id] - yield from tm.serialize_state_as_session(is_first=i==0, ser_opts=ser_opts) + yield from tm.serialize_state_as_session(session_path, is_first=i==0, ser_opts=ser_opts) @property def all_tab_managers(self) -> Iterator[TabManager]: diff --git a/kitty/session.py b/kitty/session.py index b062af40c..05b1bd434 100644 --- a/kitty/session.py +++ b/kitty/session.py @@ -492,6 +492,13 @@ def save_as_session_options() -> str: running some dangerous command like :file:`rm` or :file:`mv` or similar in a shell, it will be re-run when the session is executed if you use this option. Note that this option requires :ref:`shell_integration` to work. + + +--relocatable +type=bool-set +When saving the working directory for windows, do so as paths relative to the directory in which +the session file will be saved. This allows the session file to work even when its containing +directory is moved elsewhere. ''' @@ -500,7 +507,7 @@ def save_as_session_part2(boss: BossType, opts: SaveAsSessionOptions, path: str) return from .config import atomic_save path = os.path.abspath(os.path.expanduser(path)) - session = '\n'.join(boss.serialize_state_as_session(opts)) + session = '\n'.join(boss.serialize_state_as_session(path, opts)) atomic_save(session.encode(), path) if not opts.save_only: boss.edit_file(path) diff --git a/kitty/tabs.py b/kitty/tabs.py index 3dc95dc2e..fe4a00846 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -295,12 +295,17 @@ def serialize_state(self) -> dict[str, Any]: 'name': self.name, } - def serialize_state_as_session(self, ser_opts: SaveAsSessionOptions) -> list[str]: + def serialize_state_as_session(self, session_path: str, ser_opts: SaveAsSessionOptions) -> list[str]: import shlex launch_cmds = [] active_idx = self.windows.active_group_idx groups = tuple(self.windows.iter_all_layoutable_groups()) - cwds = {w.id: w.cwd_for_serialization for g in groups for w in g} + session_base_dir = os.path.dirname(session_path) if session_path else '' + def make_relative(cwd: str) -> str: + if session_base_dir and ser_opts.relocatable: + cwd = os.path.relpath(cwd, session_base_dir) + return cwd + cwds = {w.id: make_relative(w.cwd_for_serialization) for g in groups for w in g} from collections import Counter most_common_cwd, _ = Counter(cwds.values()).most_common(1)[0] for i, g in enumerate(groups): @@ -1214,7 +1219,7 @@ def serialize_state(self) -> dict[str, Any]: 'active_tab_idx': self.active_tab_idx, } - def serialize_state_as_session(self, ser_opts: SaveAsSessionOptions, is_first: bool = False) -> list[str]: + def serialize_state_as_session(self, session_path: str, ser_opts: SaveAsSessionOptions, is_first: bool = False) -> list[str]: ans = [] hmap = {tab_id: i for i, tab_id in enumerate(self.active_tab_history)} if (at := self.active_tab) is not None: @@ -1222,7 +1227,7 @@ def serialize_state_as_session(self, ser_opts: SaveAsSessionOptions, is_first: b def skey(tab: Tab) -> int: return hmap.get(tab.id, -1) for tab in sorted(self, key=skey): - ans.extend(tab.serialize_state_as_session(ser_opts)) + ans.extend(tab.serialize_state_as_session(session_path, ser_opts)) if ans: prefix = [] if is_first else ['', '', 'new_os_window'] if self.wm_class and self.wm_class != appname: