Files
Kovid Goyal f91a0f6986 When saving session add option to save the foreground process running in the shell so that it is also restarted
Useful if user builds up session to save by running programs via
the shell.

Note that the serialization format for session files has changed
slightly, becoming more robust and allowing us to add more types
of saved data in the future, without overloading user_vars and thus
risking name conflicts.
2025-08-16 16:50:45 +05:30

469 lines
18 KiB
Python

#!/usr/bin/env python
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from collections.abc import Generator, Iterable, Iterator, Sequence
from functools import partial
from itertools import repeat
from typing import Any, Callable, NamedTuple
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, WindowMapper
from kitty.typing_compat import TypedDict, WindowType
from kitty.window_list import WindowGroup, WindowList
class BorderLine(NamedTuple):
edges: Edges = Edges()
color: BorderColor = BorderColor.inactive
class LayoutOpts:
def __init__(self, data: dict[str, str]):
pass
def serialized(self) -> dict[str, Any]:
return {}
class LayoutData(NamedTuple):
content_pos: int = 0
cells_per_window: int = 0
space_before: int = 0
space_after: int = 0
content_size: int = 0
DecorationPairs = Sequence[tuple[int, int]]
LayoutDimension = Generator[LayoutData, None, None]
ListOfWindows = list[WindowType]
class NeighborsMap(TypedDict):
left: list[int]
top: list[int]
right: list[int]
bottom: list[int]
class LayoutGlobalData:
draw_minimal_borders: bool = True
draw_active_borders: bool = True
alignment_x: int = 0
alignment_y: int = 0
central: Region = Region((0, 0, 199, 199, 200, 200))
cell_width: int = 20
cell_height: int = 20
lgd = LayoutGlobalData()
def idx_for_id(win_id: int, windows: Iterable[WindowType]) -> int | None:
for i, w in enumerate(windows):
if w.id == win_id:
return i
return None
def set_layout_options(opts: Options) -> None:
lgd.draw_minimal_borders = opts.draw_minimal_borders and sum(opts.window_margin_width) == 0
lgd.draw_active_borders = opts.active_border_color is not None
lgd.alignment_x = -1 if opts.placement_strategy.endswith('left') else 1 if opts.placement_strategy.endswith('right') else 0
lgd.alignment_y = -1 if opts.placement_strategy.startswith('top') else 1 if opts.placement_strategy.startswith('bottom') else 0
def convert_bias_map(bias: dict[int, float], number_of_windows: int, number_of_cells: int) -> Sequence[float]:
cells_per_window, extra = divmod(number_of_cells, number_of_windows)
cell_map = list(repeat(cells_per_window, number_of_windows))
cell_map[-1] += extra
base_bias = [x / number_of_cells for x in cell_map]
return distribute_indexed_bias(base_bias, bias)
def calculate_cells_map(
bias: None | Sequence[float] | dict[int, float],
number_of_windows: int, number_of_cells: int
) -> list[int]:
if isinstance(bias, dict):
bias = convert_bias_map(bias, number_of_windows, number_of_cells)
cells_per_window = number_of_cells // number_of_windows
if bias is not None and number_of_windows > 1 and number_of_windows == len(bias) and cells_per_window > 5:
cells_map = [int(b * number_of_cells) for b in bias]
while min(cells_map) < 5:
maxi, mini = map(cells_map.index, (max(cells_map), min(cells_map)))
if maxi == mini:
break
cells_map[mini] += 1
cells_map[maxi] -= 1
else:
cells_map = list(repeat(cells_per_window, number_of_windows))
extra = number_of_cells - sum(cells_map)
if extra > 0:
cells_map[-1] += extra
return cells_map
def layout_dimension(
start_at: int, length: int, cell_length: int,
decoration_pairs: DecorationPairs,
alignment: int = 0,
bias: None | Sequence[float] | dict[int, float] = None
) -> LayoutDimension:
number_of_windows = len(decoration_pairs)
number_of_cells = length // cell_length
dec_vals: Iterable[int] = map(sum, decoration_pairs)
space_needed_for_decorations = sum(dec_vals)
extra = length - number_of_cells * cell_length
while extra < space_needed_for_decorations:
number_of_cells -= 1
extra = length - number_of_cells * cell_length
cells_map = calculate_cells_map(bias, number_of_windows, number_of_cells)
assert sum(cells_map) == number_of_cells
extra = length - number_of_cells * cell_length - space_needed_for_decorations
pos = start_at # start
if alignment > 0: # end
pos += extra
elif alignment == 0: # center
pos += extra // 2
last_i = len(cells_map) - 1
for i, cells_per_window in enumerate(cells_map):
before_dec, after_dec = decoration_pairs[i]
pos += before_dec
if i == 0:
before_space = pos - start_at
else:
before_space = before_dec
content_size = cells_per_window * cell_length
if i == last_i:
after_space = (start_at + length) - (pos + content_size)
else:
after_space = after_dec
yield LayoutData(pos, cells_per_window, before_space, after_space, content_size)
pos += content_size + after_space
class Rect(NamedTuple):
left: int
top: int
right: int
bottom: int
def blank_rects_for_window(wg: WindowGeometry) -> Generator[Rect, None, None]:
left_width, right_width = wg.spaces.left, wg.spaces.right
top_height, bottom_height = wg.spaces.top, wg.spaces.bottom
if left_width > 0:
yield Rect(wg.left - left_width, wg.top - top_height, wg.left, wg.bottom + bottom_height)
if top_height > 0:
yield Rect(wg.left, wg.top - top_height, wg.right + right_width, wg.top)
if right_width > 0:
yield Rect(wg.right, wg.top, wg.right + right_width, wg.bottom + bottom_height)
if bottom_height > 0:
yield Rect(wg.left, wg.bottom, wg.right, wg.bottom + bottom_height)
def window_geometry(xstart: int, xnum: int, ystart: int, ynum: int, left: int, top: int, right: int, bottom: int) -> WindowGeometry:
return WindowGeometry(
left=xstart, top=ystart, xnum=max(0, xnum), ynum=max(0, ynum),
right=xstart + lgd.cell_width * xnum, bottom=ystart + lgd.cell_height * ynum,
spaces=Edges(left, top, right, bottom)
)
def window_geometry_from_layouts(x: LayoutData, y: LayoutData) -> WindowGeometry:
return window_geometry(x.content_pos, x.cells_per_window, y.content_pos, y.cells_per_window, x.space_before, y.space_before, x.space_after, y.space_after)
def layout_single_window(
xdecoration_pairs: DecorationPairs,
ydecoration_pairs: DecorationPairs,
xalignment: int = 0,
yalignment: int = 0,
) -> WindowGeometry:
x = next(layout_dimension(lgd.central.left, lgd.central.width, lgd.cell_width, xdecoration_pairs, alignment=xalignment))
y = next(layout_dimension(lgd.central.top, lgd.central.height, lgd.cell_height, ydecoration_pairs, alignment=yalignment))
return window_geometry_from_layouts(x, y)
def safe_increment_bias(old_val: float, increment: float = 0) -> float:
return max(0.1, min(old_val + increment, 0.9))
def normalize_biases(biases: list[float]) -> list[float]:
s = sum(biases)
if s == 1.0:
return biases
return [x/s for x in biases]
def distribute_indexed_bias(base_bias: Sequence[float], index_bias_map: dict[int, float]) -> Sequence[float]:
if not index_bias_map:
return base_bias
ans = list(base_bias)
limit = len(ans)
for row, increment in index_bias_map.items():
if row >= limit or not increment:
continue
other_increment = -increment / (limit - 1)
ans = [safe_increment_bias(b, increment if i == row else other_increment) for i, b in enumerate(ans)]
return normalize_biases(ans)
def create_window_id_map_for_unserialize(all_windows: WindowList) -> dict[int, int]:
window_id_map = {}
for w in all_windows:
if w.serialized_id:
window_id_map[w.serialized_id] = w.id
return window_id_map
class Layout:
name: str = ''
needs_window_borders = True
must_draw_borders = False # can be overridden to customize behavior from kittens
layout_opts = LayoutOpts({})
only_active_window_visible = False
def __init__(self, os_window_id: int, tab_id: int, layout_opts: str = '') -> None:
self.set_owner(os_window_id, tab_id)
# A set of rectangles corresponding to the blank spaces at the edges of
# this layout, i.e. spaces that are not covered by any window
self.blank_rects: list[Rect] = []
self.layout_opts = self.parse_layout_opts(layout_opts)
assert self.name is not None
self.full_name = f'{self.name}:{layout_opts}' if layout_opts else self.name
self.remove_all_biases()
def set_owner(self, os_window_id: int, tab_id: int) -> None:
# Useful when moving a layout from one tab to another typically a detached tab being re-attached
self.os_window_id = os_window_id
self.tab_id = tab_id
self.set_active_window_in_os_window = partial(set_active_window, os_window_id, tab_id)
def bias_increment_for_cell(self, all_windows: WindowList, is_horizontal: bool) -> float:
self._set_dimensions()
return self.calculate_bias_increment_for_a_single_cell(all_windows, is_horizontal)
def calculate_bias_increment_for_a_single_cell(self, all_windows: WindowList, is_horizontal: bool) -> float:
if is_horizontal:
return (lgd.cell_width + 1) / lgd.central.width
return (lgd.cell_height + 1) / lgd.central.height
def apply_bias(self, window_id: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
return False
def remove_all_biases(self) -> bool:
return False
def modify_size_of_window(self, all_windows: WindowList, window_id: int, increment: float, is_horizontal: bool = True) -> bool:
idx = all_windows.group_idx_for_window(window_id)
if idx is None or not increment:
return False
return self.apply_bias(idx, increment, all_windows, is_horizontal)
def parse_layout_opts(self, layout_opts: str | None = None) -> LayoutOpts:
data: dict[str, str] = {}
if layout_opts:
for x in layout_opts.split(';'):
k, v = x.partition('=')[::2]
if k and v:
data[k] = v
return type(self.layout_opts)(data)
def nth_window(self, all_windows: WindowList, num: int) -> WindowType | None:
return all_windows.active_window_in_nth_group(num, clamp=True)
def activate_nth_window(self, all_windows: WindowList, num: int) -> None:
all_windows.set_active_group_idx(num)
def next_window(self, all_windows: WindowList, delta: int = 1) -> None:
all_windows.activate_next_window_group(delta)
def neighbors(self, all_windows: WindowList) -> NeighborsMap:
w = all_windows.active_window
assert w is not None
return self.neighbors_for_window(w, all_windows)
def move_window(self, all_windows: WindowList, delta: int = 1) -> bool:
if all_windows.num_groups < 2 or not delta:
return False
return all_windows.move_window_group(by=delta)
def move_window_to_group(self, all_windows: WindowList, group: int) -> bool:
return all_windows.move_window_group(to_group=group)
def add_window(
self, all_windows: WindowList, window: WindowType, location: str | None = None,
overlay_for: int | None = None, put_overlay_behind: bool = False, bias: float | None = None,
next_to: WindowType | None = None,
) -> WindowType | None:
if overlay_for is not None:
underlay = all_windows.id_map.get(overlay_for)
if underlay is not None:
window.margin, window.padding = underlay.margin.copy(), underlay.padding.copy()
all_windows.add_window(window, group_of=overlay_for, head_of_group=put_overlay_behind)
return underlay
if location == 'neighbor':
location = 'after'
self.add_non_overlay_window(all_windows, window, location, bias, next_to)
return None
def add_non_overlay_window(
self, all_windows: WindowList, window: WindowType, location: str | None, bias: float | None = None, next_to: WindowType | None = None
) -> None:
before = False
next_to = next_to or all_windows.active_window
if location is not None:
if location in ('after', 'vsplit', 'hsplit'):
pass
elif location == 'before':
before = True
elif location == 'first':
before = True
next_to = None
elif location == 'last':
next_to = None
all_windows.add_window(window, next_to=next_to, before=before)
if bias is not None:
idx = all_windows.group_idx_for_window(window)
if idx is not None:
self._set_dimensions()
self._bias_slot(all_windows, idx, bias)
def _bias_slot(self, all_windows: WindowList, idx: int, bias: float) -> bool:
fractional_bias = max(10, min(abs(bias), 90)) / 100
h, v = self.calculate_bias_increment_for_a_single_cell(all_windows, True), self.calculate_bias_increment_for_a_single_cell(all_windows, False)
nh, nv = lgd.central.width / lgd.cell_width, lgd.central.height / lgd.cell_height
f = max(-90, min(bias, 90)) / 100.
return self.bias_slot(all_windows, idx, fractional_bias, h * nh *f, v * nv * f)
def bias_slot(self, all_windows: WindowList, idx: int, fractional_bias: float, cell_increment_bias_h: float, cell_increment_bias_v: float) -> bool:
return False
def update_visibility(self, all_windows: WindowList) -> None:
active_window = all_windows.active_window
for window, is_group_leader in all_windows.iter_windows_with_visibility():
is_visible = window is active_window or (is_group_leader and not self.only_active_window_visible)
window.set_visible_in_layout(is_visible)
def _set_dimensions(self) -> None:
lgd.central, tab_bar, vw, vh, lgd.cell_width, lgd.cell_height = viewport_for_window(self.os_window_id)
def __call__(self, all_windows: WindowList) -> None:
self._set_dimensions()
self.update_visibility(all_windows)
self.blank_rects = []
self.do_layout(all_windows)
def layout_single_window_group(self, wg: WindowGroup, add_blank_rects: bool = True) -> None:
bw = 1 if self.must_draw_borders else 0
xdecoration_pairs = ((
wg.decoration('left', border_mult=bw, is_single_window=True),
wg.decoration('right', border_mult=bw, is_single_window=True),
),)
ydecoration_pairs = ((
wg.decoration('top', border_mult=bw, is_single_window=True),
wg.decoration('bottom', border_mult=bw, is_single_window=True),
),)
geom = layout_single_window(xdecoration_pairs, ydecoration_pairs, xalignment=lgd.alignment_x, yalignment=lgd.alignment_y)
wg.set_geometry(geom)
if add_blank_rects:
self.blank_rects.extend(blank_rects_for_window(geom))
def xlayout(
self,
groups: Iterator[WindowGroup],
bias: None | Sequence[float] | dict[int, float] = None,
start: int | None = None,
size: int | None = None,
offset: int = 0,
border_mult: int = 1
) -> LayoutDimension:
decoration_pairs = tuple(
(g.decoration('left', border_mult=border_mult), g.decoration('right', border_mult=border_mult)) for i, g in
enumerate(groups) if i >= offset
)
if start is None:
start = lgd.central.left
if size is None:
size = lgd.central.width
return layout_dimension(start, size, lgd.cell_width, decoration_pairs, bias=bias, alignment=lgd.alignment_x)
def ylayout(
self,
groups: Iterator[WindowGroup],
bias: None | Sequence[float] | dict[int, float] = None,
start: int | None = None,
size: int | None = None,
offset: int = 0,
border_mult: int = 1
) -> LayoutDimension:
decoration_pairs = tuple(
(g.decoration('top', border_mult=border_mult), g.decoration('bottom', border_mult=border_mult)) for i, g in
enumerate(groups) if i >= offset
)
if start is None:
start = lgd.central.top
if size is None:
size = lgd.central.height
return layout_dimension(start, size, lgd.cell_height, decoration_pairs, bias=bias, alignment=lgd.alignment_y)
def set_window_group_geometry(self, wg: WindowGroup, xl: LayoutData, yl: LayoutData) -> WindowGeometry:
geom = window_geometry_from_layouts(xl, yl)
wg.set_geometry(geom)
self.blank_rects.extend(blank_rects_for_window(geom))
return geom
def do_layout(self, windows: WindowList) -> None:
raise NotImplementedError()
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap:
return {'left': [], 'right': [], 'top': [], 'bottom': []}
def compute_needs_borders_map(self, all_windows: WindowList) -> dict[int, bool]:
return all_windows.compute_needs_borders_map(lgd.draw_active_borders)
def get_minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:
self._set_dimensions()
yield from self.minimal_borders(windows)
def minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:
return
yield BorderLine() # type: ignore
def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList) -> bool | None:
pass
def layout_state(self) -> dict[str, Any]:
return {}
def set_layout_state(self, layout_state: dict[str, Any], map_group_id: WindowMapper) -> bool:
return True
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'] = all_windows.serialize_layout_state()
return ans
def unserialize(
self, s: dict[str, Any], all_windows: WindowList,
window_id_mapper: Callable[[WindowList], dict[int, int]] = create_window_id_map_for_unserialize,
) -> bool:
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)
if m is None:
return False
return self.set_layout_state(s, m.get)