diff --git a/kitty/layout/base.py b/kitty/layout/base.py index c1b812c94..554e91aad 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -10,7 +10,7 @@ from kitty.borders import BorderColor 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, WindowType +from kitty.typing_compat import TypedDict, WindowMapper, WindowType from kitty.window_list import WindowGroup, WindowList @@ -436,3 +436,17 @@ class Layout: def layout_state(self) -> dict[str, Any]: return {} + + def set_layout_state(self, layout_state: dict[str, Any], map_window_id: WindowMapper) -> bool: + return True + + def serialize(self) -> dict[str, Any]: + ans = self.layout_state() + ans['opts'] = self.layout_opts.serialized() + ans['class'] = self.__class__.__name__ + return ans + + def unserialize(self, s: dict[str, Any], map_window_id: WindowMapper) -> bool: + if s.get('class') != self.__class__.__name__: + return False + return self.set_layout_state(s, map_window_id) diff --git a/kitty/layout/splits.py b/kitty/layout/splits.py index db20e6bb1..e8227d1cf 100644 --- a/kitty/layout/splits.py +++ b/kitty/layout/splits.py @@ -2,11 +2,11 @@ # License: GPLv3 Copyright: 2020, Kovid Goyal from collections.abc import Collection, Generator, Sequence -from typing import Any, NamedTuple, Optional, Union +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, WindowType +from kitty.typing_compat import EdgeLiteral, WindowMapper, WindowType from kitty.window_list import WindowGroup, WindowList from .base import BorderLine, Layout, LayoutOpts, NeighborsMap, blank_rects_for_window, lgd, window_geometry_from_layouts @@ -17,6 +17,13 @@ class Extent(NamedTuple): end: int = 0 +class SerializedPair(TypedDict, total=False): + horizontal: bool # default to True if absent + bias: float # default to 0.5 if absent + one: Union[int, 'SerializedPair'] # default to None if absent + two: Union[int, 'SerializedPair'] # default to None if absent + + class Pair: def __init__(self, horizontal: bool = True): @@ -28,6 +35,34 @@ class Pair: self.between_borders: list[Edges] = [] self.first_extent = self.second_extent = Extent() + def serialize(self) -> SerializedPair: + ans: SerializedPair = {} + if not self.horizontal: + ans['horizontal'] = False + if self.bias != 0.5: + ans['bias'] = self.bias + if self.one is not None: + ans['one'] = self.one.serialize() if isinstance(self.one, Pair) else self.one + if self.two is not None: + ans['two'] = self.two.serialize() if isinstance(self.two, Pair) else self.two + return ans + + def unserialize(self, s: SerializedPair, map_window_id: WindowMapper) -> None: + self.bias = s.get('bias', 0.5) + self.horizontal = s.get('horizontal', True) + + 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 + ans = Pair() + ans.unserialize(x, map_window_id) + return ans if ans.one or ans.two else None + self.one = unserialize(s.get('one')) + self.two = unserialize(s.get('two')) + def __repr__(self) -> str: return 'Pair(horizontal={}, bias={:.2f}, one={}, two={}, between_borders={})'.format( self.horizontal, self.bias, self.one, self.two, self.between_borders) @@ -673,19 +708,13 @@ class Splits(Layout): return None def layout_state(self) -> dict[str, Any]: + return {'pairs': self.pairs_root.serialize(), 'opts': self.layout_opts.serialized()} - def add_pair(p: Pair) -> dict[str, Any]: - ans: dict[str, Any] = {} - ans['horizontal'] = p.horizontal - ans['bias'] = p.bias - if isinstance(p.one, Pair): - ans['one'] = add_pair(p.one) - elif p.one is not None: - ans['one'] = p.one - if isinstance(p.two, Pair): - ans['two'] = add_pair(p.two) - elif p.two is not None: - ans['two'] = p.two - return ans - - return {'pairs': add_pair(self.pairs_root)} + def set_layout_state(self, layout_state: dict[str, Any], map_window_id: WindowMapper) -> bool: + new_root = Pair() + new_root.unserialize(layout_state['pairs'], map_window_id) + if new_root.one or new_root.two: + self.pairs_root = new_root + self.layout_opts = SplitsLayoutOpts(layout_state['opts']) + return True + return False diff --git a/kitty/tabs.py b/kitty/tabs.py index 4b40f145e..3649d805a 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -272,8 +272,7 @@ class Tab: # {{{ 'window_list': self.windows.serialize_state(), 'current_layout': self._current_layout_name, 'last_used_layout': self._last_used_layout, - 'layout_opts': self.current_layout.layout_opts, - 'layout_state': self.current_layout.layout_state, + 'layout_state': self.current_layout.serialize(), 'enabled_layouts': self.enabled_layouts, 'name': self.name, } diff --git a/kitty/typing_compat.py b/kitty/typing_compat.py index 80463480a..22e6f5c11 100644 --- a/kitty/typing_compat.py +++ b/kitty/typing_compat.py @@ -13,7 +13,7 @@ ScreenSize = KittensKeyActionType = MouseEvent = MouseButton = AbstractEventLoop TermManagerType = LoopType = Debug = GraphicsCommandType = object ReadableBuffer = WriteableBuffer = bytearray -CompletedProcess = tuple +CompletedProcess = object TypedDict = dict EdgeLiteral = str UnderlineLiteral = str @@ -21,5 +21,6 @@ PowerlineStyle = str MatchType = str Protocol = object OptionsProtocol = object -NotRequired = tuple +NotRequired = object CoreTextFont = FontConfigPattern = dict +WindowMapper = object diff --git a/kitty/typing_compat.pyi b/kitty/typing_compat.pyi index e183458d0..5285188b5 100644 --- a/kitty/typing_compat.pyi +++ b/kitty/typing_compat.pyi @@ -5,7 +5,7 @@ from socket import AddressFamily as AddressFamily from socket import socket as Socket from subprocess import CompletedProcess as CompletedProcess from subprocess import Popen as PopenType -from typing import Literal +from typing import Callable, Literal from typing import NotRequired as NotRequired from typing import Protocol as Protocol from typing import TypedDict as TypedDict @@ -54,6 +54,7 @@ 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] @@ -71,5 +72,5 @@ __all__ = ( 'KeyActionType', 'KeyMap', 'KittyCommonOpts', 'AliasMap', 'CoreTextFont', 'WindowSystemMouseEvent', 'FontConfigPattern', 'ScreenType', 'StartupCtx', 'KeyEventType', 'LayoutType', 'PowerlineStyle', 'RemoteCommandType', 'SessionType', 'SessionTab', 'SpecialWindowInstance', 'TabType', 'ScreenSize', 'WindowType', - 'ReadableBuffer', 'WriteableBuffer', + 'ReadableBuffer', 'WriteableBuffer', 'WindowMapper', )