mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-10-24 06:04:08 +00:00
Save the layout state of every layout a tab has used to sessions
save_as_session recorded only the state of the tab's active layout, so the state of any layout the tab had been toggled away from was lost. Toggling a splits tab to the stack layout and saving discarded the splits tree entirely, because Stack has no layout_state() of its own, and switching back to splits after restoring rebuilt the tree from scratch along the default axis. The session file also carried no last_used_layout, so _last_used_layout was None after a restore and toggle_layout <current layout> silently did nothing, since it delegates to last_used_layout() which returns early when unset. The key that produced the stack layout no longer undid it. Tab now keeps the layouts it has used, by name, and writes their state plus last_used_layout into the existing set_layout_state JSON rather than adding new session commands. Older versions of kitty ignore unknown keys in that dict but abort on unknown commands, so nesting keeps session files readable by them. Restoring the state of a layout that is not being made current must not reorder the window list, since only the current layout determines window order, hence apply_to_window_list. Splits.set_layout_state also has to accept state for a layout whose tree is still empty, which is the case for a layout that has not laid out any windows yet. Fixes #10320
This commit is contained in:
parent
51b0993b5c
commit
1b81045008
5 changed files with 79 additions and 6 deletions
|
|
@ -575,11 +575,17 @@ class Layout:
|
|||
s: dict[str, Any],
|
||||
all_windows: WindowList,
|
||||
window_id_mapper: Callable[[WindowList], dict[int, int]] = create_window_id_map_for_unserialize,
|
||||
apply_to_window_list: bool = True,
|
||||
) -> bool:
|
||||
"""
|
||||
apply_to_window_list=False computes the group id mapping without changing the
|
||||
window list. Needed when restoring the state of more than one layout, since
|
||||
only the layout being made current should order the windows.
|
||||
"""
|
||||
if s.get('class') != self.__class__.__name__:
|
||||
return False
|
||||
window_id_map = create_window_id_map_for_unserialize(all_windows)
|
||||
m = all_windows.unserialize_layout_state(s['all_windows'], window_id_map)
|
||||
m = all_windows.unserialize_layout_state(s['all_windows'], window_id_map, apply=apply_to_window_list)
|
||||
if m is None:
|
||||
return False
|
||||
return self.set_layout_state(s, m.get)
|
||||
|
|
|
|||
|
|
@ -964,7 +964,10 @@ class Splits(Layout):
|
|||
new_root = Pair()
|
||||
new_root.unserialize(layout_state['pairs'], map_group_id)
|
||||
before = frozenset(self.pairs_root.all_window_ids())
|
||||
if before == frozenset(new_root.all_window_ids()):
|
||||
# An empty tree means this layout has not laid out any windows yet, which is
|
||||
# the case when restoring the state of a layout that was not the active one.
|
||||
# do_layout() places any groups the restored tree is missing.
|
||||
if not before or before == frozenset(new_root.all_window_ids()):
|
||||
self.pairs_root = new_root
|
||||
self.layout_opts = SplitsLayoutOpts(layout_state['opts'])
|
||||
if 'maximized' in layout_state:
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ from .fast_data_types import (
|
|||
sync_os_window_title,
|
||||
)
|
||||
from .layout.base import DragOverlayMode, Layout
|
||||
from .layout.interface import create_layout_object_for, evict_cached_layouts
|
||||
from .layout.interface import all_layouts, create_layout_object_for, evict_cached_layouts
|
||||
from .progress import ProgressState
|
||||
from .tab_bar import TabBar, TabBarData, apply_title_template
|
||||
from .types import ac
|
||||
|
|
@ -216,6 +216,9 @@ class Tab: # {{{
|
|||
self.windows: WindowList = WindowList(self)
|
||||
self._last_used_layout: str | None = None
|
||||
self._current_layout_name: str | None = None
|
||||
# Layouts this tab has used, by name. Their state is preserved when
|
||||
# switching away, so it can be saved to a session as well.
|
||||
self._used_layouts: dict[str, Layout] = {}
|
||||
self.cwd = self.args.directory
|
||||
if no_initial_window:
|
||||
self._set_current_layout(self.enabled_layouts[0])
|
||||
|
|
@ -302,6 +305,7 @@ class Tab: # {{{
|
|||
self._last_used_layout = self._current_layout_name
|
||||
self.current_layout = self.create_layout_object(layout_name)
|
||||
self._current_layout_name = layout_name
|
||||
self._used_layouts[layout_name] = self.current_layout
|
||||
self.mark_tab_bar_dirty()
|
||||
|
||||
def startup(self, session_tab: SessionTab) -> None:
|
||||
|
|
@ -370,7 +374,22 @@ class Tab: # {{{
|
|||
if active_window_id and not did_focus_matching_spec:
|
||||
self.windows.set_active_window_group_for(active_window_id)
|
||||
if session_tab.layout_state:
|
||||
self.current_layout.unserialize(session_tab.layout_state, self.windows)
|
||||
self.unserialize_layout_state_from_session(session_tab.layout_state)
|
||||
|
||||
def unserialize_layout_state_from_session(self, layout_state: dict[str, Any]) -> None:
|
||||
self.current_layout.unserialize(layout_state, self.windows)
|
||||
# Restore the state of layouts that were not active when the session was
|
||||
# saved, so that switching to them gives back their saved arrangement
|
||||
# rather than a freshly built one.
|
||||
for name, state in (layout_state.get('other_layouts') or {}).items():
|
||||
if name.partition(':')[0] not in all_layouts:
|
||||
continue
|
||||
layout = self.create_layout_object(name)
|
||||
if layout.unserialize(state, self.windows, apply_to_window_list=False):
|
||||
self._used_layouts[name] = layout
|
||||
last_used = layout_state.get('last_used_layout')
|
||||
if last_used and last_used != self._current_layout_name:
|
||||
self._last_used_layout = last_used
|
||||
|
||||
def serialize_state(self) -> dict[str, Any]:
|
||||
return {
|
||||
|
|
@ -426,12 +445,27 @@ class Tab: # {{{
|
|||
f'new_tab {self.name}'.rstrip(),
|
||||
f'layout {layout}',
|
||||
f'enabled_layouts {",".join(enabled_layouts)}',
|
||||
f'set_layout_state {json.dumps(self.current_layout.serialize(self.windows))}',
|
||||
f'set_layout_state {json.dumps(self.serialize_layout_state_for_session())}',
|
||||
f'cd {most_common_cwd}',
|
||||
'',
|
||||
] + launch_cmds
|
||||
return []
|
||||
|
||||
def serialize_layout_state_for_session(self) -> dict[str, Any]:
|
||||
"""
|
||||
The state of the current layout, with the state of any other layouts this tab
|
||||
has used nested under other_layouts. Nesting rather than adding new session
|
||||
commands keeps the file readable by older versions of kitty, which ignore
|
||||
unknown keys in this dict but abort on unknown commands.
|
||||
"""
|
||||
ans = self.current_layout.serialize(self.windows)
|
||||
others = {name: layout.serialize(self.windows) for name, layout in self._used_layouts.items() if name != self._current_layout_name}
|
||||
if others:
|
||||
ans['other_layouts'] = others
|
||||
if self._last_used_layout:
|
||||
ans['last_used_layout'] = self._last_used_layout
|
||||
return ans
|
||||
|
||||
def data_for_tab_bar(self, is_active: bool) -> TabBarData:
|
||||
t = self
|
||||
title = t.name or t.title or appname
|
||||
|
|
|
|||
|
|
@ -203,7 +203,12 @@ class WindowList:
|
|||
'window_groups': [g.serialize_layout_state() for g in self.groups],
|
||||
}
|
||||
|
||||
def unserialize_layout_state(self, state: dict[str, Any], window_id_map: dict[int, int]) -> dict[int, int] | None:
|
||||
def unserialize_layout_state(self, state: dict[str, Any], window_id_map: dict[int, int], apply: bool = True) -> dict[int, int] | None:
|
||||
"""
|
||||
Returns a map of serialized group id to current group id. With apply=False the
|
||||
map is computed but the window list is left unchanged, so that the state of
|
||||
several layouts can be restored while only one of them orders the windows.
|
||||
"""
|
||||
if set(window_id_map.values()) != set(self.id_map):
|
||||
# some window in this collection does not correspond to a
|
||||
# serialized window
|
||||
|
|
@ -233,6 +238,8 @@ class WindowList:
|
|||
# we ignore them.
|
||||
if len(ans) != len(self.groups):
|
||||
return None
|
||||
if not apply:
|
||||
return ans
|
||||
gmap = {g.id: g for g in self.groups}
|
||||
groups = []
|
||||
for wg in state['window_groups']:
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from .base import BaseTest
|
|||
class Window:
|
||||
def __init__(self, win_id, overlay_for=None, overlay_window_id=None):
|
||||
self.id = win_id
|
||||
self.serialized_id = 0
|
||||
self.overlay_for = overlay_for
|
||||
self.overlay_window_id = overlay_window_id
|
||||
self.is_visible_in_layout = True
|
||||
|
|
@ -273,6 +274,28 @@ class TestLayout(BaseTest):
|
|||
self.ae(q.neighbors_for_window(windows[2], all_windows), {'left': [1], 'right': [4], 'top': [2]})
|
||||
self.ae(q.neighbors_for_window(windows[3], all_windows), {'left': [3], 'top': [2]})
|
||||
|
||||
def test_splits_unserialize_into_unused_layout(self):
|
||||
# Restoring the state of a layout that was not the active one when the session
|
||||
# was saved: its pairs tree is empty, and the window list must not be
|
||||
# reordered, since only the layout being made current does that.
|
||||
q = create_layout(Splits)
|
||||
all_windows = create_windows(q, num=0)
|
||||
for wid, loc in ((1, None), (2, 'hsplit'), (3, 'vsplit')):
|
||||
win = Window(wid)
|
||||
win.serialized_id = wid
|
||||
q.add_window(all_windows, win, location=loc)
|
||||
q(all_windows)
|
||||
expected = {'horizontal': False, 'one': 1, 'two': {'one': 2, 'two': 3}}
|
||||
self.ae(q.pairs_root.serialize(), expected)
|
||||
state = q.serialize(all_windows)
|
||||
groups_before = [g.id for g in all_windows.groups]
|
||||
|
||||
fresh = create_layout(Splits)
|
||||
self.ae(fresh.pairs_root.serialize(), {})
|
||||
self.assertTrue(fresh.unserialize(state, all_windows, apply_to_window_list=False))
|
||||
self.ae(fresh.pairs_root.serialize(), expected)
|
||||
self.ae([g.id for g in all_windows.groups], groups_before)
|
||||
|
||||
def test_splits_maximize(self):
|
||||
q = create_layout(Splits)
|
||||
all_windows = create_windows(q, num=0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue