Implement full serialization/unserialization for layouts

Need to go over all the non-split layouts and see if they need
any TLC
This commit is contained in:
Kovid Goyal 2025-08-04 15:51:46 +05:30
parent 6d2b17d877
commit e6f35571a5
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
8 changed files with 95 additions and 17 deletions

View file

@ -34,6 +34,7 @@ class Version(NamedTuple):
kitty_run_data: dict[str, Any] = getattr(sys, 'kitty_run_data', {})
launched_by_launch_services = kitty_run_data.get('launched_by_launch_services', False)
is_quick_access_terminal_app = kitty_run_data.get('is_quick_access_terminal_app', False)
serialize_user_var_name = 'kitty_serialize_window_id'
if getattr(sys, 'frozen', False):
extensions_dir: str = kitty_run_data['extensions_dir']

View file

@ -7,10 +7,11 @@
from typing import Any, NamedTuple
from kitty.borders import BorderColor
from kitty.constants import serialize_user_var_name
from kitty.fast_data_types import Region, set_active_window, viewport_for_window
from kitty.options.types import Options
from kitty.types import Edges, WindowGeometry
from kitty.typing_compat import TypedDict, WindowMapper, WindowType
from kitty.types import Edges, WindowGeometry, WindowMapper
from kitty.typing_compat import TypedDict, WindowType
from kitty.window_list import WindowGroup, WindowList
@ -437,16 +438,33 @@ def layout_action(self, action_name: str, args: Sequence[str], all_windows: Wind
def layout_state(self) -> dict[str, Any]:
return {}
def set_layout_state(self, layout_state: dict[str, Any], map_window_id: WindowMapper) -> bool:
def set_layout_state(self, layout_state: dict[str, Any], map_group_id: WindowMapper) -> bool:
return True
def serialize(self) -> dict[str, Any]:
def serialize(self, all_windows: WindowList) -> dict[str, Any]:
ans = self.layout_state()
ans['opts'] = self.layout_opts.serialized()
ans['class'] = self.__class__.__name__
ans['all_windows'] = aw = all_windows.serialize_state()
for wg in aw['window_groups']:
wg['window_ids'] = tuple(w['id'] for w in aw.pop('windows'))
return ans
def unserialize(self, s: dict[str, Any], map_window_id: WindowMapper) -> bool:
def unserialize(
self, s: dict[str, Any], all_windows: WindowList,
serialize_user_var_name: str = serialize_user_var_name
) -> bool:
if s.get('class') != self.__class__.__name__:
return False
return self.set_layout_state(s, map_window_id)
window_id_map = {}
for w in all_windows:
k = w.user_vars.pop(serialize_user_var_name, None)
if k is not None:
try:
window_id_map[int(k)] = w.id
except Exception:
pass
m = all_windows.unserialize_layout_state(s['all_windows'], window_id_map)
if m is None:
return False
return self.set_layout_state(s, m.get)

View file

@ -5,8 +5,8 @@
from typing import Any, NamedTuple, Optional, TypedDict, Union
from kitty.borders import BorderColor
from kitty.types import Edges, WindowGeometry
from kitty.typing_compat import EdgeLiteral, WindowMapper, WindowType
from kitty.types import Edges, WindowGeometry, WindowMapper
from kitty.typing_compat import EdgeLiteral, WindowType
from kitty.window_list import WindowGroup, WindowList
from .base import BorderLine, Layout, LayoutOpts, NeighborsMap, blank_rects_for_window, lgd, window_geometry_from_layouts
@ -55,8 +55,7 @@ def unserialize(x: int | SerializedPair | None) -> int | Pair | None:
if x is None:
return None
if isinstance(x, int):
w = map_window_id(x)
return None if w is None else w.id
return map_window_id(x)
ans = Pair()
ans.unserialize(x, map_window_id)
return ans if ans.one or ans.two else None
@ -710,9 +709,9 @@ def layout_action(self, action_name: str, args: Sequence[str], all_windows: Wind
def layout_state(self) -> dict[str, Any]:
return {'pairs': self.pairs_root.serialize()}
def set_layout_state(self, layout_state: dict[str, Any], map_window_id: WindowMapper) -> bool:
def set_layout_state(self, layout_state: dict[str, Any], map_group_id: WindowMapper) -> bool:
new_root = Pair()
new_root.unserialize(layout_state['pairs'], map_window_id)
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()):
self.pairs_root = new_root

View file

@ -272,7 +272,7 @@ def serialize_state(self) -> dict[str, Any]:
'window_list': self.windows.serialize_state(),
'current_layout': self._current_layout_name,
'last_used_layout': self._last_used_layout,
'layout_state': self.current_layout.serialize(),
'layout_state': self.current_layout.serialize(self.windows),
'enabled_layouts': self.enabled_layouts,
'name': self.name,
}

View file

@ -230,4 +230,5 @@ def w(f: _T) -> _T:
return w
WindowMapper = Callable[[int], int | None]
DecoratedFunc = TypeVar('DecoratedFunc', bound=Callable[..., Any])

View file

@ -23,4 +23,3 @@
OptionsProtocol = object
NotRequired = object
CoreTextFont = FontConfigPattern = dict
WindowMapper = object

View file

@ -54,7 +54,6 @@ GRT_C = Literal[0, 1]
GRT_d = Literal['a', 'A', 'c', 'C', 'i', 'I', 'p', 'P', 'q', 'Q', 'x', 'X', 'y', 'Y', 'z', 'Z', 'f', 'F']
ReadableBuffer = bytes | bytearray | memoryview | array.array[int] | mmap.mmap
WriteableBuffer = bytearray | memoryview | array.array[int] | mmap.mmap
WindowMapper = Callable[[int], WindowType | None]
@ -72,5 +71,5 @@ __all__ = (
'KeyActionType', 'KeyMap', 'KittyCommonOpts', 'AliasMap', 'CoreTextFont', 'WindowSystemMouseEvent',
'FontConfigPattern', 'ScreenType', 'StartupCtx', 'KeyEventType', 'LayoutType', 'PowerlineStyle',
'RemoteCommandType', 'SessionType', 'SessionTab', 'SpecialWindowInstance', 'TabType', 'ScreenSize', 'WindowType',
'ReadableBuffer', 'WriteableBuffer', 'WindowMapper',
'ReadableBuffer', 'WriteableBuffer',
)

View file

@ -6,7 +6,7 @@
from collections.abc import Iterator
from contextlib import suppress
from itertools import count
from typing import Any, Deque, Union
from typing import Any, Deque, Sequence, Union
from .fast_data_types import Color, get_options
from .types import OverlayType, WindowGeometry
@ -48,6 +48,12 @@ def __contains__(self, window: WindowType) -> bool:
return True
return False
def has_window_id(self, wid: int) -> bool:
for w in self.windows:
if w.id == wid:
return True
return False
@property
def needs_attention(self) -> bool:
for w in self.windows:
@ -93,6 +99,12 @@ def serialize_state(self) -> dict[str, Any]:
'windows': [w.serialize_state() for w in self.windows]
}
def unserialize_layout_state(self, window_ids: Sequence[int]) -> None:
order_map = {wid: i for i, wid in enumerate(window_ids)}
def sort_key(w: WindowType) -> int:
return order_map.get(w.id, -1)
self.windows.sort(key=sort_key)
def as_simple_dict(self) -> dict[str, Any]:
return {
'id': self.id,
@ -173,6 +185,55 @@ def serialize_state(self) -> dict[str, Any]:
'window_groups': [g.serialize_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:
if set(window_id_map.values()) != set(self.id_map):
# some window in this collection does not correspond to a
# serialized window
return None
ans = {}
gmap = {g.id: g for g in self.groups}
present_wids_map = {g.id: {w.id for w in g} for g in self.groups}
def unmapped_group_having_subset_of_windows(wids: Sequence[int]) -> Iterator[WindowGroup]:
mapped_wids = set()
for wid in wids:
new_wid = window_id_map.get(wid)
if new_wid is not None:
mapped_wids.add(new_wid)
for gid in tuple(gmap):
present_wids = present_wids_map[gid]
if present_wids.issubset(mapped_wids):
yield gmap.pop(gid)
break
for wg in state['window_groups']:
old_group_id = wg['id']
for group in unmapped_group_having_subset_of_windows(wg['window_ids']):
ans[old_group_id] = group.id
# check that all the groups present were also in the serialized state.
# there could have been extra windows/groups in the serialized state,
# we ignore them.
if len(ans) != len(self.groups):
return None
gmap = {g.id: g for g in self.groups}
groups = []
for wg in state['window_groups']:
old_group_id = wg['id']
if new_group_id := ans.get(old_group_id):
groups.append((g := gmap[new_group_id]))
new_window_ids = []
for old_window_id in wg['window_ids']:
if new_window_id := window_id_map.get(old_window_id):
new_window_ids.append(new_window_id)
g.unserialize_layout_state(new_window_ids)
self.groups = groups
history = []
for old_wid in state['active_group_history']:
if new_wid := window_id_map.get(old_wid):
history.append(new_wid)
self.active_group_history = deque(history, 64)
return ans
@property
def active_group_idx(self) -> int:
return self._active_group_idx