mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-02-01 11:34:59 +01:00
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.
469 lines
18 KiB
Python
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)
|