From e49d9406212af2472703f3519240258bef79ffc1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 Nov 2025 21:01:55 +0530 Subject: [PATCH] kitten @ ls: Also output the neighbors for every window --- docs/changelog.rst | 2 ++ kitty/layout/base.py | 13 +++---------- kitty/layout/grid.py | 20 ++++++++++++-------- kitty/layout/splits.py | 12 ++++++------ kitty/layout/stack.py | 3 ++- kitty/layout/tall.py | 5 ++--- kitty/layout/vertical.py | 17 ++++++++++++----- kitty/tabs.py | 5 ++++- kitty/types.py | 7 +++++++ kitty/window.py | 11 +++++++++-- kitty_tests/layout.py | 8 ++++---- 11 files changed, 63 insertions(+), 40 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2669aed6b..2235f28f4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -163,6 +163,8 @@ Detailed list of changes - Have reloading config also reload the custom tab bar python modules (:disc:`9221`) +- kitten @ ls: Also output the neighbors for every window (:disc:`9225`) + 0.44.0 [2025-11-03] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/layout/base.py b/kitty/layout/base.py index 8e55e893c..273e1b9bc 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -9,8 +9,8 @@ from typing import Any, Callable, NamedTuple from kitty.borders import BorderColor from kitty.fast_data_types import Region, get_options, 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.types import Edges, NeighborsMap, WindowGeometry, WindowMapper +from kitty.typing_compat import WindowType from kitty.window_list import WindowGroup, WindowList @@ -41,13 +41,6 @@ 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 @@ -437,7 +430,7 @@ class Layout: raise NotImplementedError() def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap: - return {'left': [], 'right': [], 'top': [], 'bottom': []} + return {} def compute_needs_borders_map(self, all_windows: WindowList) -> dict[int, bool]: return all_windows.compute_needs_borders_map(lgd.draw_active_borders) diff --git a/kitty/layout/grid.py b/kitty/layout/grid.py index f62ea5206..75abd6e36 100644 --- a/kitty/layout/grid.py +++ b/kitty/layout/grid.py @@ -8,11 +8,11 @@ from math import ceil, floor from typing import Any from kitty.borders import BorderColor -from kitty.types import Edges, WindowMapper +from kitty.types import Edges, NeighborsMap, WindowMapper from kitty.typing_compat import WindowType from kitty.window_list import WindowGroup, WindowList -from .base import BorderLine, Layout, LayoutData, LayoutDimension, ListOfWindows, NeighborsMap, layout_dimension, lgd +from .base import BorderLine, Layout, LayoutData, LayoutDimension, ListOfWindows, layout_dimension, lgd from .tall import neighbors_for_tall_window @@ -302,12 +302,16 @@ class Grid(Layout): xs.extend(neighbors(neighbor_row, neighbor_col)) return xs - return { - 'top': neighbors(row-1, col) if row else [], - 'bottom': neighbors(row + 1, col), - 'left': side(row, col, -1) if col else [], - 'right': side(row, col, 1) if col < ncols - 1 else [], - } + ans: NeighborsMap = {} + if row: + ans['top'] = neighbors(row-1, col) + if bottom := neighbors(row + 1, col): + ans['bottom'] = bottom + if col and (left := side(row, col, -1)): + ans['left'] = left + if col < ncols - 1: + ans['right'] = side(row, col, 1) + return ans def layout_state(self) -> dict[str, Any]: return { diff --git a/kitty/layout/splits.py b/kitty/layout/splits.py index bb83e3ded..c13f069a7 100644 --- a/kitty/layout/splits.py +++ b/kitty/layout/splits.py @@ -5,11 +5,11 @@ from collections.abc import Collection, Generator, Sequence from typing import Any, NamedTuple, Optional, TypedDict, Union from kitty.borders import BorderColor -from kitty.types import Edges, WindowGeometry, WindowMapper +from kitty.types import Edges, NeighborsMap, 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 +from .base import BorderLine, Layout, LayoutOpts, blank_rects_for_window, lgd, window_geometry_from_layouts class Extent(NamedTuple): @@ -405,14 +405,14 @@ class Pair: geometries = {group.id: group.geometry for group in all_windows.groups if group.geometry} def extend(other: Union[int, 'Pair', None], edge: EdgeLiteral, which: EdgeLiteral) -> None: - if not ans[which] and other: + if not ans.get(which) and other: if isinstance(other, Pair): neighbors = ( w for w in other.edge_windows(edge) if is_neighbouring_geometry(geometries[w], geometries[window_id], which)) - ans[which].extend(neighbors) + ans.setdefault(which, []).extend(neighbors) else: - ans[which].append(other) + ans.setdefault(which, []).append(other) def is_neighbouring_geometry(a: WindowGeometry, b: WindowGeometry, direction: str) -> bool: def edges(g: WindowGeometry) -> tuple[int, int]: @@ -610,7 +610,7 @@ class Splits(Layout): wg = all_windows.group_for_window(window) assert wg is not None pair = self.pairs_root.pair_for_window(wg.id) - ans: NeighborsMap = {'left': [], 'right': [], 'top': [], 'bottom': []} + ans: NeighborsMap = {} if pair is not None: pair.neighbors_for_window(wg.id, ans, self, all_windows) return ans diff --git a/kitty/layout/stack.py b/kitty/layout/stack.py index 6c65a0dd1..c855b2c64 100644 --- a/kitty/layout/stack.py +++ b/kitty/layout/stack.py @@ -1,10 +1,11 @@ #!/usr/bin/env python # License: GPLv3 Copyright: 2020, Kovid Goyal +from kitty.types import NeighborsMap from kitty.typing_compat import WindowType from kitty.window_list import WindowList -from .base import Layout, NeighborsMap +from .base import Layout class Stack(Layout): diff --git a/kitty/layout/tall.py b/kitty/layout/tall.py index 962925239..0d2737971 100644 --- a/kitty/layout/tall.py +++ b/kitty/layout/tall.py @@ -7,7 +7,7 @@ from typing import Any from kitty.borders import BorderColor from kitty.conf.utils import to_bool -from kitty.types import Edges, WindowMapper +from kitty.types import Edges, NeighborsMap, WindowMapper from kitty.typing_compat import EdgeLiteral, WindowType from kitty.window_list import WindowGroup, WindowList @@ -17,7 +17,6 @@ from .base import ( LayoutData, LayoutDimension, LayoutOpts, - NeighborsMap, lgd, normalize_biases, safe_increment_bias, @@ -38,7 +37,7 @@ def neighbors_for_tall_window( idx = groups.index(wg) prev = None if idx == 0 else groups[idx-1] nxt = None if idx == len(groups) - 1 else groups[idx+1] - ans: NeighborsMap = {'left': [], 'right': [], 'top': [], 'bottom': []} + ans: NeighborsMap = {} main_before: EdgeLiteral = 'left' if main_is_horizontal else 'top' main_after: EdgeLiteral = 'right' if main_is_horizontal else 'bottom' cross_before: EdgeLiteral = 'top' if main_is_horizontal else 'left' diff --git a/kitty/layout/vertical.py b/kitty/layout/vertical.py index 9e9e4b23a..000963acc 100644 --- a/kitty/layout/vertical.py +++ b/kitty/layout/vertical.py @@ -5,11 +5,11 @@ from collections.abc import Generator, Iterable from typing import Any from kitty.borders import BorderColor -from kitty.types import Edges, WindowMapper -from kitty.typing_compat import WindowType +from kitty.types import Edges, NeighborsMap, WindowMapper +from kitty.typing_compat import EdgeLiteral, WindowType from kitty.window_list import WindowGroup, WindowList -from .base import BorderLine, Layout, LayoutData, LayoutDimension, NeighborsMap, lgd +from .base import BorderLine, Layout, LayoutData, LayoutDimension, lgd def borders( @@ -133,9 +133,16 @@ class Vertical(Layout): after = [groups[(idx + 1) % lg].id] else: before, after = [], [] + ans: NeighborsMap = {} + akey: EdgeLiteral = 'top' + bkey: EdgeLiteral = 'bottom' if self.main_is_horizontal: - return {'left': before, 'right': after, 'top': [], 'bottom': []} - return {'top': before, 'bottom': after, 'left': [], 'right': []} + akey, bkey = 'left', 'right' + if before: + ans[bkey] = before + if after: + ans[akey] = after + return ans def layout_state(self) -> dict[str, Any]: return {'biased_map': self.biased_map} diff --git a/kitty/tabs.py b/kitty/tabs.py index 70b65671e..4c22ee15a 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -943,12 +943,15 @@ class Tab: # {{{ def list_windows(self, self_window: Window | None = None, window_filter: Callable[[Window], bool] | None = None) -> Generator[WindowDict, None, None]: active_window = self.active_window + cl = self.current_layout for w in self: if window_filter is None or window_filter(w): yield w.as_dict( is_active=w is active_window, is_focused=w.os_window_id == current_focused_os_window_id() and w is active_window, - is_self=w is self_window) + is_self=w is self_window, + neighbors_map=cl.neighbors_for_window(w, self.windows) + ) def list_groups(self) -> list[dict[str, Any]]: return [g.as_simple_dict() for g in self.windows.groups] diff --git a/kitty/types.py b/kitty/types.py index a63db5e9a..02906522a 100644 --- a/kitty/types.py +++ b/kitty/types.py @@ -234,3 +234,10 @@ def ac(group: ActionGroup, doc: str) -> Callable[[_T], _T]: WindowMapper = Callable[[int], int | None] DecoratedFunc = TypeVar('DecoratedFunc', bound=Callable[..., Any]) + + +class NeighborsMap(TypedDict, total=False): + left: list[int] + top: list[int] + right: list[int] + bottom: list[int] diff --git a/kitty/window.py b/kitty/window.py index 63affb6ed..2fa9a3971 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -96,7 +96,7 @@ from .options.types import Options from .progress import Progress from .rgb import to_color from .terminfo import get_capabilities -from .types import MouseEvent, OverlayType, WindowGeometry, ac, run_once +from .types import MouseEvent, NeighborsMap, OverlayType, WindowGeometry, ac, run_once from .typing_compat import BossType, ChildType, EdgeLiteral, TabType, TypedDict from .utils import ( color_as_int, @@ -250,6 +250,7 @@ class WindowDict(TypedDict): at_prompt: bool created_at: int in_alternate_screen: bool + neighbors: NeighborsMap class PipeData(TypedDict): @@ -1938,7 +1939,12 @@ class Window: return get_mouse_data_for_window(self.os_window_id, self.tab_id, self.id) # Serialization {{{ - def as_dict(self, is_focused: bool = False, is_self: bool = False, is_active: bool = False) -> WindowDict: + def as_dict( + self, is_focused: bool = False, is_self: bool = False, is_active: bool = False, + neighbors_map: NeighborsMap | None = None, + ) -> WindowDict: + if neighbors_map is None: + neighbors_map = {} return { 'id': self.id, 'is_focused': is_focused, @@ -1958,6 +1964,7 @@ class Window: 'user_vars': self.user_vars, 'created_at': self.created_at, 'in_alternate_screen': self.screen.is_using_alternate_linebuf(), + 'neighbors': neighbors_map, } def serialize_state(self) -> dict[str, Any]: diff --git a/kitty_tests/layout.py b/kitty_tests/layout.py index 10c203af9..3f045c5c3 100644 --- a/kitty_tests/layout.py +++ b/kitty_tests/layout.py @@ -267,7 +267,7 @@ class TestLayout(BaseTest): windows[1].set_geometry(WindowGeometry(11, 0, 20, 10, 0, 0)) windows[2].set_geometry(WindowGeometry(11, 11, 15, 20, 0, 0)) windows[3].set_geometry(WindowGeometry(16, 11, 20, 20, 0, 0)) - self.ae(q.neighbors_for_window(windows[0], all_windows), {'left': [], 'right': [2, 3], 'top': [], 'bottom': []}) - self.ae(q.neighbors_for_window(windows[1], all_windows), {'left': [1], 'right': [], 'top': [], 'bottom': [3, 4]}) - self.ae(q.neighbors_for_window(windows[2], all_windows), {'left': [1], 'right': [4], 'top': [2], 'bottom': []}) - self.ae(q.neighbors_for_window(windows[3], all_windows), {'left': [3], 'right': [], 'top': [2], 'bottom': []}) + self.ae(q.neighbors_for_window(windows[0], all_windows), {'right': [2, 3]}) + self.ae(q.neighbors_for_window(windows[1], all_windows), {'left': [1], 'bottom': [3, 4]}) + 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]})