Start work on refactoring management of windows

This commit is contained in:
Kovid Goyal 2020-05-04 12:41:00 +05:30
parent e39da2b2bb
commit 50d9718c68
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
17 changed files with 1767 additions and 1659 deletions

View File

@ -36,7 +36,7 @@ from .fast_data_types import (
toggle_fullscreen, toggle_maximized toggle_fullscreen, toggle_maximized
) )
from .keys import get_shortcut, shortcut_matches from .keys import get_shortcut, shortcut_matches
from .layout import set_layout_options from .layout.base import set_layout_options
from .options_stub import Options from .options_stub import Options
from .rgb import Color, color_from_int from .rgb import Color, color_from_int
from .session import Session, create_sessions from .session import Session, create_sessions
@ -1318,11 +1318,8 @@ class Boss:
else: else:
return return
underlaid_window, overlaid_window = src_tab.detach_window(window) for detached_window in src_tab.detach_window(window):
if underlaid_window: target_tab.attach_window(detached_window)
target_tab.attach_window(underlaid_window)
if overlaid_window:
target_tab.attach_window(overlaid_window)
self._cleanup_tab_after_window_removal(src_tab) self._cleanup_tab_after_window_removal(src_tab)
target_tab.make_active() target_tab.make_active()

View File

@ -18,7 +18,7 @@ from .conf.utils import (
) )
from .constants import FloatEdges, config_dir, is_macos from .constants import FloatEdges, config_dir, is_macos
from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
from .layout import all_layouts from .layout.interface import all_layouts
from .rgb import Color, color_as_int, color_as_sharp, color_from_int from .rgb import Color, color_as_int, color_as_sharp, color_from_int
from .utils import log_error from .utils import log_error

View File

@ -273,7 +273,7 @@ def launch(boss: Boss, opts: LaunchCLIOptions, args: List[str], target_tab: Opti
x = str(active.id) x = str(active.id)
final_cmd.append(x) final_cmd.append(x)
kw['cmd'] = final_cmd kw['cmd'] = final_cmd
if opts.type == 'overlay' and active and not active.overlay_window_id: if opts.type == 'overlay' and active:
kw['overlay_for'] = active.id kw['overlay_for'] = active.id
if opts.stdin_source != 'none': if opts.stdin_source != 'none':
q = str(opts.stdin_source) q = str(opts.stdin_source)

File diff suppressed because it is too large Load Diff

0
kitty/layout/__init__.py Normal file
View File

552
kitty/layout/base.py Normal file
View File

@ -0,0 +1,552 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from functools import partial
from itertools import repeat
from typing import (
Dict, FrozenSet, Generator, Iterable, List, NamedTuple, Optional, Sequence,
Tuple, Union, cast
)
from kitty.constants import Edges, WindowGeometry
from kitty.fast_data_types import (
Region, set_active_window, swap_windows, viewport_for_window
)
from kitty.options_stub import Options
from kitty.typing import TypedDict, WindowType
from kitty.window_list import WindowList
class Borders(NamedTuple):
left: bool
top: bool
right: bool
bottom: bool
class LayoutOpts:
def __init__(self, data: Dict[str, str]):
pass
class LayoutData(NamedTuple):
content_pos: int
cells_per_window: int
space_before: int
space_after: int
content_size: int
all_borders = Borders(True, True, True, True)
no_borders = Borders(False, False, False, False)
DecorationPairs = Sequence[Tuple[int, int]]
LayoutDimension = Generator[LayoutData, None, None]
ListOfWindows = List[WindowType]
class InternalNeighborsMap(TypedDict):
left: List[int]
top: List[int]
right: List[int]
bottom: List[int]
class NeighborsMap(TypedDict):
left: Tuple[int, ...]
top: Tuple[int, ...]
right: Tuple[int, ...]
bottom: Tuple[int, ...]
class LayoutGlobalData:
draw_minimal_borders: bool = True
draw_active_borders: bool = True
align_top_left: bool = False
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]) -> Optional[int]:
for i, w in enumerate(windows):
if w.id == win_id:
return i
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.align_top_left = opts.placement_strategy == 'top-left'
def calculate_cells_map(bias: Optional[Sequence[float]], number_of_windows: int, number_of_cells: int) -> List[int]:
cells_per_window = number_of_cells // number_of_windows
if bias is not None and 1 < 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,
left_align: bool = False,
bias: Optional[Sequence[float]] = None
) -> LayoutDimension:
number_of_windows = len(decoration_pairs)
number_of_cells = length // cell_length
space_needed_for_decorations: int = sum(map(sum, decoration_pairs))
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
if not left_align:
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=xnum, ynum=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, left_align: bool = False) -> WindowGeometry:
x = next(layout_dimension(lgd.central.left, lgd.central.width, lgd.cell_width, xdecoration_pairs, left_align=lgd.align_top_left))
y = next(layout_dimension(lgd.central.top, lgd.central.height, lgd.cell_height, ydecoration_pairs, left_align=lgd.align_top_left))
return window_geometry_from_layouts(x, y)
def safe_increment_bias(old_val: float, increment: float) -> 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:
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 variable_bias(num_windows: int, candidate: Dict[int, float]) -> Sequence[float]:
return distribute_indexed_bias(list(repeat(1/(num_windows), num_windows)), candidate)
class Layout:
name: Optional[str] = None
needs_window_borders = True
must_draw_borders = False # can be overridden to customize behavior from kittens
needs_all_windows = False
layout_opts = LayoutOpts({})
only_active_window_visible = False
def __init__(self, os_window_id: int, tab_id: int, layout_opts: str = '') -> None:
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)
self.swap_windows_in_os_window = partial(swap_windows, 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 = self.name + ((':' + layout_opts) if layout_opts else '')
self.remove_all_biases()
def bias_increment_for_cell(self, is_horizontal: bool) -> float:
self._set_dimensions()
if is_horizontal:
return (lgd.cell_width + 1) / lgd.central.width
return (lgd.cell_height + 1) / lgd.central.height
def apply_bias(self, idx: int, increment: float, top_level_windows: ListOfWindows, 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.idx_for_window(window_id)
if idx is None:
return False
return self.apply_bias(idx, increment, list(all_windows.iter_top_level_windows()), is_horizontal)
def parse_layout_opts(self, layout_opts: Optional[str] = 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) -> Optional[WindowType]:
return all_windows.active_window_for_idx(num, clamp=True)
def activate_nth_window(self, all_windows: WindowList, num: int) -> int:
w = self.nth_window(all_windows, num)
assert w is not None
active_window_idx = all_windows.idx_for_window(w)
assert active_window_idx is not None
return self.set_active_window(all_windows, active_window_idx)
def next_window(self, all_windows: WindowList, active_window_idx: int, delta: int = 1) -> int:
w = self.nth_window(all_windows, active_window_idx)
assert w is not None
idx = all_windows.idx_for_window(w)
assert idx is not None
num_slots = all_windows.max_active_idx + 1
aidx = (idx + num_slots + delta) % num_slots
return self.set_active_window(all_windows, aidx)
def neighbors(self, all_windows: WindowList, active_window_idx: int) -> NeighborsMap:
w = all_windows.active_window_for_idx(active_window_idx)
assert w is not None
n = self.neighbors_for_window(w, all_windows)
def as_indices(windows: Iterable[int]) -> Generator[int, None, None]:
for w in windows:
idx = all_windows.idx_for_window(w)
if idx is not None:
yield idx
ans: NeighborsMap = {
'left': tuple(as_indices(n['left'])),
'top': tuple(as_indices(n['top'])),
'right': tuple(as_indices(n['right'])),
'bottom': tuple(as_indices(n['bottom']))
}
return ans
def move_window(self, all_windows: WindowList, active_window_idx: int, delta: Union[str, int] = 1) -> int:
# delta can be either a number or a string such as 'left', 'top', etc
# for neighborhood moves
if len(windows) < 2 or not delta:
return active_window_idx
wgd = WindowGroupingData(all_windows)
w = wgd.base_window_for_idx(active_window_idx)
if w is None:
return active_window_idx
idx = idx_for_id(w.id, windows)
if idx is None and w.overlay_window_id is not None:
idx = idx_for_id(w.overlay_window_id, windows)
assert idx is not None
if isinstance(delta, int):
nidx = (idx + len(windows) + delta) % len(windows)
else:
delta = delta.lower()
delta = {'up': 'top', 'down': 'bottom'}.get(delta, delta)
neighbors = self.neighbors_for_window(w, all_windows if self.needs_all_windows else windows)
q = cast(WindowList, neighbors.get(cast(str, delta), ()))
if not q:
return active_window_idx
w = q[0]
qidx = idx_for_id(getattr(w, 'id', w), windows)
assert qidx is not None
nidx = qidx
nw = windows[nidx]
qidx = idx_for_id(nw.id, all_windows)
assert qidx is not None
nidx = qidx
idx = active_window_idx
self.swap_windows_in_layout(all_windows, nidx, idx)
self.swap_windows_in_os_window(nidx, idx)
return self.set_active_window(all_windows, nidx)
def swap_windows_in_layout(self, all_windows: WindowList, a: int, b: int) -> None:
all_windows[a], all_windows[b] = all_windows[b], all_windows[a]
def add_window(self, all_windows: WindowList, window: WindowType, current_active_window_idx: int, location: Optional[str] = None) -> int:
active_window_idx = None
if window.overlay_for is not None:
i = idx_for_id(window.overlay_for, all_windows)
if i is not None:
# put the overlay window in the position occupied by the
# overlaid window and move the overlaid window to the end
self.swap_windows_in_os_window(len(all_windows), i)
all_windows.append(all_windows[i])
all_windows[i] = window
active_window_idx = i
if active_window_idx is None:
if location == 'neighbor':
location = 'after'
active_window_idx = self.do_add_window(all_windows, window, current_active_window_idx, location)
self(all_windows, active_window_idx)
self.set_active_window_in_os_window(active_window_idx)
return active_window_idx
def do_add_window(self, all_windows: WindowList, window: WindowType, current_active_window_idx: Optional[int], location: Optional[str]) -> int:
active_window_idx = None
if location is not None:
if location in ('after', 'vsplit', 'hsplit') and current_active_window_idx is not None and len(all_windows) > 1:
active_window_idx = min(current_active_window_idx + 1, len(all_windows))
elif location == 'before' and current_active_window_idx is not None and len(all_windows) > 1:
active_window_idx = current_active_window_idx
elif location == 'first':
active_window_idx = 0
if active_window_idx is not None:
for i in range(len(all_windows), active_window_idx, -1):
self.swap_windows_in_os_window(i, i - 1)
all_windows.insert(active_window_idx, window)
if active_window_idx is None:
active_window_idx = len(all_windows)
all_windows.append(window)
return active_window_idx
def remove_window(self, all_windows: WindowList, window: WindowType, current_active_window_idx: int, swapped: bool = False) -> int:
try:
active_window = all_windows[current_active_window_idx]
except Exception:
active_window = window
if not swapped and window.overlay_for is not None:
nidx = idx_for_id(window.overlay_for, all_windows)
if nidx is not None:
idx = all_windows.index(window)
all_windows[nidx], all_windows[idx] = all_windows[idx], all_windows[nidx]
self.swap_windows_in_os_window(nidx, idx)
return self.remove_window(all_windows, window, current_active_window_idx, swapped=True)
position = all_windows.index(window)
del all_windows[position]
active_window_idx = None
if window.overlay_for is not None:
i = idx_for_id(window.overlay_for, all_windows)
if i is not None:
overlaid_window = all_windows[i]
overlaid_window.overlay_window_id = None
if active_window is window:
active_window = overlaid_window
active_window_idx = idx_for_id(active_window.id, all_windows)
if active_window_idx is None:
if active_window is window:
active_window_idx = max(0, min(current_active_window_idx, len(all_windows) - 1))
else:
active_window_idx = idx_for_id(active_window.id, all_windows)
assert active_window_idx is not None
if all_windows:
self(all_windows, active_window_idx)
return self.set_active_window(all_windows, active_window_idx)
def update_visibility(self, all_windows: WindowList, active_window: WindowType, overlaid_windows: Optional[FrozenSet[WindowType]] = None) -> None:
if overlaid_windows is None:
overlaid_windows = process_overlaid_windows(all_windows)[0]
for i, w in enumerate(all_windows):
w.set_visible_in_layout(i, w is active_window or (not self.only_active_window_visible and w not in overlaid_windows))
def set_active_window(self, all_windows: WindowList, active_window_idx: int) -> int:
if not all_windows:
self.set_active_window_in_os_window(0)
return 0
w = all_windows[active_window_idx]
if w.overlay_window_id is not None:
i = idx_for_id(w.overlay_window_id, all_windows)
if i is not None:
active_window_idx = i
self.update_visibility(all_windows, all_windows[active_window_idx])
self.set_active_window_in_os_window(active_window_idx)
return active_window_idx
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, active_window_idx: int) -> int:
self._set_dimensions()
active_window = all_windows[active_window_idx]
overlaid_windows, windows = process_overlaid_windows(all_windows)
if overlaid_windows:
windows = [w for w in all_windows if w not in overlaid_windows]
q = idx_for_id(active_window.id, windows)
if q is None:
if active_window.overlay_window_id is not None:
active_window_idx = idx_for_id(active_window.overlay_window_id, windows) or 0
else:
active_window_idx = 0
else:
active_window_idx = q
active_window = windows[active_window_idx]
else:
windows = all_windows
self.update_visibility(all_windows, active_window, overlaid_windows)
self.blank_rects = []
if self.needs_all_windows:
self.do_layout_all_windows(windows, active_window_idx, all_windows)
else:
self.do_layout(windows, active_window_idx)
return cast(int, idx_for_id(active_window.id, all_windows))
# Utils {{{
def layout_single_window(self, w: WindowType, return_geometry: bool = False, left_align: bool = False) -> Optional[WindowGeometry]:
bw = w.effective_border() if self.must_draw_borders else 0
xdecoration_pairs = ((
w.effective_padding('left') + w.effective_margin('left', is_single_window=True) + bw,
w.effective_padding('right') + w.effective_margin('right', is_single_window=True) + bw,
),)
ydecoration_pairs = ((
w.effective_padding('top') + w.effective_margin('top', is_single_window=True) + bw,
w.effective_padding('bottom') + w.effective_margin('bottom', is_single_window=True) + bw,
),)
wg = layout_single_window(xdecoration_pairs, ydecoration_pairs, left_align=left_align)
if return_geometry:
return wg
w.set_geometry(0, wg)
self.blank_rects = list(blank_rects_for_window(wg))
return None
def xlayout(
self,
windows: WindowList,
bias: Optional[Sequence[float]] = None,
start: Optional[int] = None,
size: Optional[int] = None
) -> LayoutDimension:
decoration_pairs = tuple(
(
w.effective_margin('left') + w.effective_border() + w.effective_padding('left'),
w.effective_margin('right') + w.effective_border() + w.effective_padding('right'),
) for w in windows
)
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, left_align=lgd.align_top_left)
def ylayout(
self,
windows: WindowList,
bias: Optional[Sequence[float]] = None,
start: Optional[int] = None,
size: Optional[int] = None
) -> LayoutDimension:
decoration_pairs = tuple(
(
w.effective_margin('top') + w.effective_border() + w.effective_padding('top'),
w.effective_margin('bottom') + w.effective_border() + w.effective_padding('bottom'),
) for w in windows
)
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, left_align=lgd.align_top_left)
def set_window_geometry(self, w: WindowType, idx: int, xl: LayoutData, yl: LayoutData) -> None:
wg = window_geometry_from_layouts(xl, yl)
w.set_geometry(idx, wg)
self.blank_rects.extend(blank_rects_for_window(wg))
# }}}
def do_layout(self, windows: WindowList, active_window_idx: int) -> None:
raise NotImplementedError()
def do_layout_all_windows(self, windows: WindowList, active_window_idx: int, all_windows: WindowList) -> None:
raise NotImplementedError()
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
return {'left': [], 'right': [], 'top': [], 'bottom': []}
def compute_needs_borders_map(self, windows: WindowList, active_window: Optional[WindowType]) -> Dict[int, bool]:
return {w.id: ((w is active_window and lgd.draw_active_borders) or w.needs_attention) for w in windows}
def resolve_borders(self, windows: WindowList, active_window: Optional[WindowType]) -> Generator[Borders, None, None]:
if lgd.draw_minimal_borders:
needs_borders_map = self.compute_needs_borders_map(windows, active_window)
yield from self.minimal_borders(windows, active_window, needs_borders_map)
else:
yield from Layout.minimal_borders(self, windows, active_window, {})
def window_independent_borders(self, windows: WindowList, active_window: Optional[WindowType] = None) -> Generator[Edges, None, None]:
return
yield Edges() # type: ignore
def minimal_borders(self, windows: WindowList, active_window: Optional[WindowType], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
for w in windows:
if w is not active_window or lgd.draw_active_borders or w.needs_attention:
yield all_borders
else:
yield no_borders
def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList, active_window_idx: int) -> Optional[Union[bool, int]]:
pass

253
kitty/layout/grid.py Normal file
View File

@ -0,0 +1,253 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from functools import lru_cache
from itertools import repeat
from typing import Callable, Dict, Generator, List, Optional, Sequence, Tuple
from kitty.constants import Edges
from kitty.typing import WindowType
from kitty.window_list import WindowList
from .base import (
Borders, InternalNeighborsMap, Layout, LayoutData, LayoutDimension,
ListOfWindows, all_borders, layout_dimension, lgd, no_borders,
variable_bias
)
from .tall import neighbors_for_tall_window
@lru_cache()
def calc_grid_size(n: int) -> Tuple[int, int, int, int]:
if n <= 5:
ncols = 1 if n == 1 else 2
else:
for ncols in range(3, (n // 2) + 1):
if ncols * ncols >= n:
break
nrows = n // ncols
special_rows = n - (nrows * (ncols - 1))
special_col = 0 if special_rows < nrows else ncols - 1
return ncols, nrows, special_rows, special_col
class Grid(Layout):
name = 'grid'
def remove_all_biases(self) -> bool:
self.biased_rows: Dict[int, float] = {}
self.biased_cols: Dict[int, float] = {}
return True
def column_layout(
self,
num: int,
bias: Optional[Sequence[float]] = None,
) -> LayoutDimension:
decoration_pairs = tuple(repeat((0, 0), num))
return layout_dimension(lgd.central.left, lgd.central.width, lgd.cell_width, decoration_pairs, bias=bias, left_align=lgd.align_top_left)
def row_layout(
self,
num: int,
bias: Optional[Sequence[float]] = None,
) -> LayoutDimension:
decoration_pairs = tuple(repeat((0, 0), num))
return layout_dimension(lgd.central.top, lgd.central.height, lgd.cell_height, decoration_pairs, bias=bias, left_align=lgd.align_top_left)
def variable_layout(self, layout_func: Callable[..., LayoutDimension], num_windows: int, biased_map: Dict[int, float]) -> LayoutDimension:
return layout_func(num_windows, bias=variable_bias(num_windows, biased_map) if num_windows > 1 else None)
def apply_bias(self, idx: int, increment: float, top_level_windows: ListOfWindows, is_horizontal: bool = True) -> bool:
b = self.biased_cols if is_horizontal else self.biased_rows
num_windows = len(top_level_windows)
ncols, nrows, special_rows, special_col = calc_grid_size(num_windows)
def position_for_window_idx(idx: int) -> Tuple[int, int]:
row_num = col_num = 0
def on_col_done(col_windows: List[int]) -> None:
nonlocal col_num, row_num
row_num = 0
col_num += 1
for window_idx, xl, yl in self.layout_windows(
num_windows, nrows, ncols, special_rows, special_col, on_col_done):
if idx == window_idx:
return row_num, col_num
row_num += 1
row_num, col_num = position_for_window_idx(idx)
if is_horizontal:
b = self.biased_cols
if ncols < 2:
return False
bias_idx = col_num
attr = 'biased_cols'
def layout_func(windows: ListOfWindows, bias: Optional[Sequence[float]] = None) -> LayoutDimension:
return self.column_layout(num_windows, bias=bias)
else:
b = self.biased_rows
if max(nrows, special_rows) < 2:
return False
bias_idx = row_num
attr = 'biased_rows'
def layout_func(windows: ListOfWindows, bias: Optional[Sequence[float]] = None) -> LayoutDimension:
return self.row_layout(num_windows, bias=bias)
before_layout = list(self.variable_layout(layout_func, num_windows, b))
candidate = b.copy()
before = candidate.get(bias_idx, 0)
candidate[bias_idx] = before + increment
if before_layout == list(self.variable_layout(layout_func, num_windows, candidate)):
return False
setattr(self, attr, candidate)
return True
def layout_windows(
self,
num_windows: int,
nrows: int, ncols: int,
special_rows: int, special_col: int,
on_col_done: Callable[[List[int]], None] = lambda col_windows: None
) -> Generator[Tuple[int, LayoutData, LayoutData], None, None]:
# Distribute windows top-to-bottom, left-to-right (i.e. in columns)
xlayout = self.variable_layout(self.column_layout, ncols, self.biased_cols)
yvals_normal = tuple(self.variable_layout(self.row_layout, nrows, self.biased_rows))
yvals_special = yvals_normal if special_rows == nrows else tuple(self.variable_layout(self.row_layout, special_rows, self.biased_rows))
pos = 0
for col in range(ncols):
rows = special_rows if col == special_col else nrows
yls = yvals_special if col == special_col else yvals_normal
xl = next(xlayout)
col_windows = []
for i, yl in enumerate(yls):
window_idx = pos + i
yield window_idx, xl, yl
col_windows.append(window_idx)
pos += rows
on_col_done(col_windows)
def do_layout(self, windows: WindowList, active_window_idx: int) -> None:
n = len(windows)
if n == 1:
self.layout_single_window(windows[0])
return
ncols, nrows, special_rows, special_col = calc_grid_size(n)
win_col_map = []
def on_col_done(col_windows: List[int]) -> None:
col_windows_w = [windows[i] for i in col_windows]
win_col_map.append(col_windows_w)
def extents(ld: LayoutData) -> Tuple[int, int]:
start = ld.content_pos - ld.space_before
size = ld.space_before + ld.space_after + ld.content_size
return start, size
def layout(ld: LayoutData, cell_length: int, before_dec: int, after_dec: int) -> LayoutData:
start, size = extents(ld)
space_needed_for_decorations = before_dec + after_dec
content_size = size - space_needed_for_decorations
number_of_cells = content_size // cell_length
cell_area = number_of_cells * cell_length
extra = content_size - cell_area
if extra > 0 and not lgd.align_top_left:
before_dec += extra // 2
return LayoutData(start + before_dec, number_of_cells, before_dec, size - cell_area - before_dec, cell_area)
def position_window_in_grid_cell(window_idx: int, xl: LayoutData, yl: LayoutData) -> None:
w = windows[window_idx]
bw = w.effective_border()
edges = Edges(
w.effective_margin('left') + w.effective_padding('left') + bw,
w.effective_margin('right') + w.effective_padding('right') + bw,
w.effective_margin('top') + w.effective_padding('top') + bw,
w.effective_margin('bottom') + w.effective_padding('bottom') + bw,
)
xl = layout(xl, lgd.cell_width, edges.left, edges.right)
yl = layout(yl, lgd.cell_height, edges.top, edges.bottom)
self.set_window_geometry(w, window_idx, xl, yl)
for window_idx, xl, yl in self.layout_windows(
n, nrows, ncols, special_rows, special_col, on_col_done):
position_window_in_grid_cell(window_idx, xl, yl)
def minimal_borders(self, windows: WindowList, active_window: Optional[WindowType], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
for w in windows:
if needs_borders_map[w.id]:
yield all_borders
else:
yield no_borders
def window_independent_borders(self, windows: WindowList, active_window: Optional[WindowType] = None) -> Generator[Edges, None, None]:
n = len(windows)
if not lgd.draw_minimal_borders or n < 2:
return
ncols, nrows, special_rows, special_col = calc_grid_size(n)
row_borders: List[List[Edges]] = [[]]
col_borders: List[Edges] = []
bw = windows[0].effective_border()
def on_col_done(col_windows: List[int]) -> None:
left = xl.content_pos + xl.content_size + xl.space_after - bw // 2
col_borders.append(Edges(left, lgd.central.top, left + bw, lgd.central.bottom))
row_borders.append([])
for window_idx, xl, yl in self.layout_windows(n, nrows, ncols, special_rows, special_col, on_col_done):
top = yl.content_pos + yl.content_size + yl.space_after - bw // 2
right = xl.content_pos + xl.content_size + xl.space_after
row_borders[-1].append(Edges(xl.content_pos - xl.space_before, top, right, top + bw))
for border in col_borders[:-1]:
yield border
for rows in row_borders:
for border in rows[:-1]:
yield border
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
n = len(windows)
if n < 4:
return neighbors_for_tall_window(1, window, windows)
ncols, nrows, special_rows, special_col = calc_grid_size(n)
blank_row: List[Optional[int]] = [None for i in range(ncols)]
matrix = tuple(blank_row[:] for j in range(max(nrows, special_rows)))
wi = iter(windows)
pos_map: Dict[int, Tuple[int, int]] = {}
col_counts: List[int] = []
for col in range(ncols):
rows = special_rows if col == special_col else nrows
for row in range(rows):
w = next(wi)
matrix[row][col] = wid = w.id
pos_map[wid] = row, col
col_counts.append(rows)
row, col = pos_map[window.id]
def neighbors(row: int, col: int) -> List[int]:
try:
ans = matrix[row][col]
except IndexError:
ans = None
return [] if ans is None else [ans]
def side(row: int, col: int, delta: int) -> List[int]:
neighbor_col = col + delta
if col_counts[neighbor_col] == col_counts[col]:
return neighbors(row, neighbor_col)
return neighbors(min(row, col_counts[neighbor_col] - 1), neighbor_col)
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 [],
}

50
kitty/layout/interface.py Normal file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Dict, Tuple, Type
from .base import Layout
from .grid import Grid
from .splits import Splits
from .stack import Stack
from .tall import Fat, Tall
from .vertical import Horizontal, Vertical
all_layouts: Dict[str, Type[Layout]] = {
Stack.name: Stack,
Tall.name: Tall,
Fat.name: Fat,
Vertical.name: Vertical,
Horizontal.name: Horizontal,
Grid.name: Grid,
Splits.name: Splits,
}
class CreateLayoutObjectFor:
cache: Dict[Tuple, Layout] = {}
def __call__(
self,
name: str,
os_window_id: int,
tab_id: int,
layout_opts: str = ''
) -> Layout:
key = name, os_window_id, tab_id, layout_opts
ans = create_layout_object_for.cache.get(key)
if ans is None:
name, layout_opts = name.partition(':')[::2]
ans = create_layout_object_for.cache[key] = all_layouts[name](
os_window_id, tab_id, layout_opts)
return ans
create_layout_object_for = CreateLayoutObjectFor()
def evict_cached_layouts(tab_id: int) -> None:
remove = [key for key in create_layout_object_for.cache if key[2] == tab_id]
for key in remove:
del create_layout_object_for.cache[key]

465
kitty/layout/splits.py Normal file
View File

@ -0,0 +1,465 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import (
Collection, Dict, Generator, Optional, Sequence, Tuple, Union
)
from kitty.constants import Edges, WindowGeometry
from kitty.typing import EdgeLiteral, WindowType
from kitty.window_list import WindowList
from .base import (
Borders, InternalNeighborsMap, Layout, LayoutOpts, all_borders,
blank_rects_for_window, lgd, no_borders, window_geometry_from_layouts
)
class Pair:
def __init__(self, horizontal: bool = True):
self.horizontal = horizontal
self.one: Optional[Union[Pair, int]] = None
self.two: Optional[Union[Pair, int]] = None
self.bias = 0.5
self.between_border: Optional[Edges] = None
def __repr__(self) -> str:
return 'Pair(horizontal={}, bias={:.2f}, one={}, two={}, between_border={})'.format(
self.horizontal, self.bias, self.one, self.two, self.between_border)
def all_window_ids(self) -> Generator[int, None, None]:
if self.one is not None:
if isinstance(self.one, Pair):
yield from self.one.all_window_ids()
else:
yield self.one
if self.two is not None:
if isinstance(self.two, Pair):
yield from self.two.all_window_ids()
else:
yield self.two
def self_and_descendants(self) -> Generator['Pair', None, None]:
yield self
if isinstance(self.one, Pair):
yield from self.one.self_and_descendants()
if isinstance(self.two, Pair):
yield from self.two.self_and_descendants()
def pair_for_window(self, window_id: int) -> Optional['Pair']:
if self.one == window_id or self.two == window_id:
return self
ans = None
if isinstance(self.one, Pair):
ans = self.one.pair_for_window(window_id)
if ans is None and isinstance(self.two, Pair):
ans = self.two.pair_for_window(window_id)
return ans
def parent(self, root: 'Pair') -> Optional['Pair']:
for q in root.self_and_descendants():
if q.one is self or q.two is self:
return q
def remove_windows(self, window_ids: Collection[int]) -> None:
if isinstance(self.one, int) and self.one in window_ids:
self.one = None
if isinstance(self.two, int) and self.two in window_ids:
self.two = None
if self.one is None and self.two is not None:
self.one, self.two = self.two, None
@property
def is_redundant(self) -> bool:
return self.one is None or self.two is None
def collapse_redundant_pairs(self) -> None:
while isinstance(self.one, Pair) and self.one.is_redundant:
self.one = self.one.one or self.one.two
while isinstance(self.two, Pair) and self.two.is_redundant:
self.two = self.two.one or self.two.two
if isinstance(self.one, Pair):
self.one.collapse_redundant_pairs()
if isinstance(self.two, Pair):
self.two.collapse_redundant_pairs()
def balanced_add(self, window_id: int) -> 'Pair':
if self.one is None or self.two is None:
if self.one is None:
if self.two is None:
self.one = window_id
return self
self.one, self.two = self.two, self.one
self.two = window_id
return self
if isinstance(self.one, Pair) and isinstance(self.two, Pair):
one_count = sum(1 for _ in self.one.all_window_ids())
two_count = sum(1 for _ in self.two.all_window_ids())
q = self.one if one_count < two_count else self.two
return q.balanced_add(window_id)
if not isinstance(self.one, Pair) and not isinstance(self.two, Pair):
pair = Pair(horizontal=self.horizontal)
pair.balanced_add(self.one)
pair.balanced_add(self.two)
self.one, self.two = pair, window_id
return self
if isinstance(self.one, Pair):
window_to_be_split = self.two
self.two = pair = Pair(horizontal=self.horizontal)
else:
window_to_be_split = self.one
self.one = pair = Pair(horizontal=self.horizontal)
assert isinstance(window_to_be_split, int)
pair.balanced_add(window_to_be_split)
pair.balanced_add(window_id)
return pair
def split_and_add(self, existing_window_id: int, new_window_id: int, horizontal: bool, after: bool) -> 'Pair':
q = (existing_window_id, new_window_id) if after else (new_window_id, existing_window_id)
if self.is_redundant:
pair = self
pair.horizontal = horizontal
self.one, self.two = q
else:
pair = Pair(horizontal=horizontal)
if self.one == existing_window_id:
self.one = pair
else:
self.two = pair
tuple(map(pair.balanced_add, q))
return pair
def apply_window_geometry(
self, window_id: int,
window_geometry: WindowGeometry,
id_window_map: Dict[int, WindowType],
id_idx_map: Dict[int, int],
layout_object: Layout
) -> None:
w = id_window_map[window_id]
w.set_geometry(id_idx_map[window_id], window_geometry)
if w.overlay_window_id is not None:
q = id_window_map.get(w.overlay_window_id)
if q is not None:
q.set_geometry(id_idx_map[q.id], window_geometry)
layout_object.blank_rects.extend(blank_rects_for_window(window_geometry))
def effective_border(self, id_window_map: Dict[int, WindowType]) -> int:
for wid in self.all_window_ids():
return id_window_map[wid].effective_border()
return 0
def layout_pair(
self,
left: int, top: int, width: int, height: int,
id_window_map: Dict[int, WindowType],
id_idx_map: Dict[int, int],
layout_object: Layout
) -> None:
self.between_border = None
if self.one is None or self.two is None:
q = self.one or self.two
if isinstance(q, Pair):
return q.layout_pair(left, top, width, height, id_window_map, id_idx_map, layout_object)
if q is None:
return
w = id_window_map[q]
xl = next(layout_object.xlayout([w], start=left, size=width))
yl = next(layout_object.ylayout([w], start=top, size=height))
geom = window_geometry_from_layouts(xl, yl)
self.apply_window_geometry(q, geom, id_window_map, id_idx_map, layout_object)
return
bw = self.effective_border(id_window_map) if lgd.draw_minimal_borders else 0
b1 = bw // 2
b2 = bw - b1
if self.horizontal:
w1 = max(2*lgd.cell_width + 1, int(self.bias * width) - b1)
w2 = max(2*lgd.cell_width + 1, width - w1 - b1 - b2)
if isinstance(self.one, Pair):
self.one.layout_pair(left, top, w1, height, id_window_map, id_idx_map, layout_object)
else:
w = id_window_map[self.one]
yl = next(layout_object.ylayout([w], start=top, size=height))
xl = next(layout_object.xlayout([w], start=left, size=w1))
geom = window_geometry_from_layouts(xl, yl)
self.apply_window_geometry(self.one, geom, id_window_map, id_idx_map, layout_object)
if b1 + b2:
self.between_border = Edges(left + w1, top, left + w1 + b1 + b2, top + height)
left += b1 + b2
if isinstance(self.two, Pair):
self.two.layout_pair(left + w1, top, w2, height, id_window_map, id_idx_map, layout_object)
else:
w = id_window_map[self.two]
xl = next(layout_object.xlayout([w], start=left + w1, size=w2))
yl = next(layout_object.ylayout([w], start=top, size=height))
geom = window_geometry_from_layouts(xl, yl)
self.apply_window_geometry(self.two, geom, id_window_map, id_idx_map, layout_object)
else:
h1 = max(2*lgd.cell_height + 1, int(self.bias * height) - b1)
h2 = max(2*lgd.cell_height + 1, height - h1 - b1 - b2)
if isinstance(self.one, Pair):
self.one.layout_pair(left, top, width, h1, id_window_map, id_idx_map, layout_object)
else:
w = id_window_map[self.one]
xl = next(layout_object.xlayout([w], start=left, size=width))
yl = next(layout_object.ylayout([w], start=top, size=h1))
geom = window_geometry_from_layouts(xl, yl)
self.apply_window_geometry(self.one, geom, id_window_map, id_idx_map, layout_object)
if b1 + b2:
self.between_border = Edges(left, top + h1, left + width, top + h1 + b1 + b2)
top += b1 + b2
if isinstance(self.two, Pair):
self.two.layout_pair(left, top + h1, width, h2, id_window_map, id_idx_map, layout_object)
else:
w = id_window_map[self.two]
xl = next(layout_object.xlayout([w], start=left, size=width))
yl = next(layout_object.ylayout([w], start=top + h1, size=h2))
geom = window_geometry_from_layouts(xl, yl)
self.apply_window_geometry(self.two, geom, id_window_map, id_idx_map, layout_object)
def modify_size_of_child(self, which: int, increment: float, is_horizontal: bool, layout_object: 'Splits') -> bool:
if is_horizontal == self.horizontal and not self.is_redundant:
if which == 2:
increment *= -1
new_bias = max(0.1, min(self.bias + increment, 0.9))
if new_bias != self.bias:
self.bias = new_bias
return True
return False
parent = self.parent(layout_object.pairs_root)
if parent is not None:
which = 1 if parent.one is self else 2
return parent.modify_size_of_child(which, increment, is_horizontal, layout_object)
return False
def neighbors_for_window(self, window_id: int, ans: InternalNeighborsMap, layout_object: 'Splits') -> None:
def quadrant(is_horizontal: bool, is_first: bool) -> Tuple[EdgeLiteral, EdgeLiteral]:
if is_horizontal:
if is_first:
return 'left', 'right'
return 'right', 'left'
if is_first:
return 'top', 'bottom'
return 'bottom', 'top'
def extend(other: Union[int, 'Pair', None], edge: EdgeLiteral, which: EdgeLiteral) -> None:
if not ans[which] and other:
if isinstance(other, Pair):
ans[which].extend(other.edge_windows(edge))
else:
ans[which].append(other)
other = self.two if self.one == window_id else self.one
extend(other, *quadrant(self.horizontal, self.one == window_id))
child = self
while True:
parent = child.parent(layout_object.pairs_root)
if parent is None:
break
other = parent.two if child is parent.one else parent.one
extend(other, *quadrant(parent.horizontal, child is parent.one))
child = parent
def edge_windows(self, edge: str) -> Generator[int, None, None]:
if self.is_redundant:
q = self.one or self.two
if q:
if isinstance(q, Pair):
yield from q.edge_windows(edge)
else:
yield q
edges = ('left', 'right') if self.horizontal else ('top', 'bottom')
if edge in edges:
q = self.one if edge in ('left', 'top') else self.two
if q:
if isinstance(q, Pair):
yield from q.edge_windows(edge)
else:
yield q
else:
for q in (self.one, self.two):
if q:
if isinstance(q, Pair):
yield from q.edge_windows(edge)
else:
yield q
class SplitsLayoutOpts(LayoutOpts):
default_axis_is_horizontal: bool = True
def __init__(self, data: Dict[str, str]):
self.default_axis_is_horizontal = data.get('split_axis', 'horizontal') == 'horizontal'
class Splits(Layout):
name = 'splits'
needs_all_windows = True
layout_opts = SplitsLayoutOpts({})
@property
def default_axis_is_horizontal(self) -> bool:
return self.layout_opts.default_axis_is_horizontal
@property
def pairs_root(self) -> Pair:
root: Optional[Pair] = getattr(self, '_pairs_root', None)
if root is None:
self._pairs_root = root = Pair(horizontal=self.default_axis_is_horizontal)
return root
@pairs_root.setter
def pairs_root(self, root: Pair) -> None:
self._pairs_root = root
def do_layout_all_windows(self, windows: WindowList, active_window_idx: int, all_windows: WindowList) -> None:
window_count = len(windows)
root = self.pairs_root
all_present_window_ids = frozenset(w.overlay_for or w.id for w in windows)
already_placed_window_ids = frozenset(root.all_window_ids())
windows_to_remove = already_placed_window_ids - all_present_window_ids
if windows_to_remove:
for pair in root.self_and_descendants():
pair.remove_windows(windows_to_remove)
root.collapse_redundant_pairs()
if root.one is None or root.two is None:
q = root.one or root.two
if isinstance(q, Pair):
root = self.pairs_root = q
id_window_map = {w.id: w for w in all_windows}
id_idx_map = {w.id: i for i, w in enumerate(all_windows)}
windows_to_add = all_present_window_ids - already_placed_window_ids
if windows_to_add:
for wid in sorted(windows_to_add, key=id_idx_map.__getitem__):
root.balanced_add(wid)
if window_count == 1:
self.layout_single_window(windows[0])
else:
root.layout_pair(lgd.central.left, lgd.central.top, lgd.central.width, lgd.central.height, id_window_map, id_idx_map, self)
def do_add_window(
self,
all_windows: WindowList,
window: WindowType,
current_active_window_idx: Optional[int],
location: Optional[str]
) -> int:
horizontal = self.default_axis_is_horizontal
after = True
if location is not None:
if location == 'vsplit':
horizontal = True
elif location == 'hsplit':
horizontal = False
if location in ('before', 'first'):
after = False
active_window_idx = None
if current_active_window_idx is not None and 0 <= current_active_window_idx < len(all_windows):
cw = all_windows[current_active_window_idx]
window_id = cw.overlay_for or cw.id
pair = self.pairs_root.pair_for_window(window_id)
if pair is not None:
pair.split_and_add(window_id, window.id, horizontal, after)
active_window_idx = current_active_window_idx
if after:
active_window_idx += 1
for i in range(len(all_windows), active_window_idx, -1):
self.swap_windows_in_os_window(i, i - 1)
all_windows.insert(active_window_idx, window)
if active_window_idx is None:
active_window_idx = len(all_windows)
all_windows.append(window)
return active_window_idx
def modify_size_of_window(
self,
all_windows: WindowList,
window_id: int,
increment: float,
is_horizontal: bool = True
) -> bool:
idx = idx_for_id(window_id, all_windows)
if idx is None:
return False
w = all_windows[idx]
window_id = w.overlay_for or w.id
pair = self.pairs_root.pair_for_window(window_id)
if pair is None:
return False
which = 1 if pair.one == window_id else 2
return pair.modify_size_of_child(which, increment, is_horizontal, self)
def remove_all_biases(self) -> bool:
for pair in self.pairs_root.self_and_descendants():
pair.bias = 0.5
return True
def window_independent_borders(self, windows: WindowList, active_window: Optional[WindowType] = None) -> Generator[Edges, None, None]:
if not lgd.draw_minimal_borders:
return
for pair in self.pairs_root.self_and_descendants():
if pair.between_border is not None:
yield pair.between_border
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
window_id = window.overlay_for or window.id
pair = self.pairs_root.pair_for_window(window_id)
ans: InternalNeighborsMap = {'left': [], 'right': [], 'top': [], 'bottom': []}
if pair is not None:
pair.neighbors_for_window(window_id, ans, self)
return ans
def swap_windows_in_layout(self, all_windows: WindowList, a: int, b: int) -> None:
w1_, w2_ = all_windows[a], all_windows[b]
super().swap_windows_in_layout(all_windows, a, b)
w1 = w1_.overlay_for or w1_.id
w2 = w2_.overlay_for or w2_.id
p1 = self.pairs_root.pair_for_window(w1)
p2 = self.pairs_root.pair_for_window(w2)
if p1 and p2:
if p1 is p2:
p1.one, p1.two = p1.two, p1.one
else:
if p1.one == w1:
p1.one = w2
else:
p1.two = w2
if p2.one == w2:
p2.one = w1
else:
p2.two = w1
def minimal_borders(self, windows: WindowList, active_window: Optional[WindowType], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
for w in windows:
if (w is active_window and lgd.draw_active_borders) or w.needs_attention:
yield all_borders
else:
yield no_borders
def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList, active_window_idx: int) -> Optional[Union[bool, int]]:
if action_name == 'rotate':
args = args or ('90',)
try:
amt = int(args[0])
except Exception:
amt = 90
if amt not in (90, 180, 270):
amt = 90
rotate = amt in (90, 270)
swap = amt in (180, 270)
w = all_windows[active_window_idx]
wid = w.overlay_for or w.id
pair = self.pairs_root.pair_for_window(wid)
if pair is not None and not pair.is_redundant:
if rotate:
pair.horizontal = not pair.horizontal
if swap:
pair.one, pair.two = pair.two, pair.one
return True

22
kitty/layout/stack.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from kitty.window_list import WindowList
from .base import Layout, blank_rects_for_window, lgd
class Stack(Layout):
name = 'stack'
needs_window_borders = False
only_active_window_visible = True
def do_layout(self, windows: WindowList, active_window_idx: int) -> None:
for i, w in enumerate(windows):
wg = self.layout_single_window(w, left_align=lgd.align_top_left, return_geometry=True)
if wg is not None:
w.set_geometry(i, wg)
if w.is_visible_in_layout:
self.blank_rects = list(blank_rects_for_window(wg))

191
kitty/layout/tall.py Normal file
View File

@ -0,0 +1,191 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from itertools import repeat
from typing import Dict, Generator, List, Optional, Tuple
from kitty.typing import EdgeLiteral, WindowType
from kitty.window_list import WindowList
from .base import (
Borders, InternalNeighborsMap, Layout, LayoutDimension, LayoutOpts,
ListOfWindows, all_borders, lgd, no_borders, normalize_biases,
safe_increment_bias, variable_bias
)
def neighbors_for_tall_window(num_full_size_windows: int, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
idx = windows.index(window)
prev = None if idx == 0 else windows[idx-1]
nxt = None if idx == len(windows) - 1 else windows[idx+1]
ans: InternalNeighborsMap = {'left': [prev.id] if prev is not None else [], 'right': [], 'top': [], 'bottom': []}
if idx < num_full_size_windows - 1:
if nxt is not None:
ans['right'] = [nxt.id]
elif idx == num_full_size_windows - 1:
ans['right'] = [w.id for w in windows[idx+1:]]
else:
ans['left'] = [windows[num_full_size_windows - 1].id]
if idx > num_full_size_windows and prev is not None:
ans['top'] = [prev.id]
if nxt is not None:
ans['bottom'] = [nxt.id]
return ans
class TallLayoutOpts(LayoutOpts):
bias: Tuple[float, ...] = ()
full_size: int = 1
def __init__(self, data: Dict[str, str]):
try:
self.full_size = int(data.get('full_size', 1))
except Exception:
self.full_size = 1
self.full_size = fs = max(1, min(self.full_size, 100))
try:
b = int(data.get('bias', 50)) / 100
except Exception:
b = 0.5
b = max(0.1, min(b, 0.9))
self.bias = tuple(repeat(b / fs, fs)) + (1.0 - b,)
class Tall(Layout):
name = 'tall'
main_is_horizontal = True
only_between_border = Borders(False, False, False, True)
only_main_border = Borders(False, False, True, False)
layout_opts = TallLayoutOpts({})
main_axis_layout = Layout.xlayout
perp_axis_layout = Layout.ylayout
@property
def num_full_size_windows(self) -> int:
return self.layout_opts.full_size
def remove_all_biases(self) -> bool:
self.main_bias: List[float] = list(self.layout_opts.bias)
self.biased_map: Dict[int, float] = {}
return True
def variable_layout(self, windows: ListOfWindows, biased_map: Dict[int, float]) -> LayoutDimension:
windows = windows[self.num_full_size_windows:]
bias = variable_bias(len(windows), biased_map) if len(windows) > 1 else None
return self.perp_axis_layout(windows, bias=bias)
def apply_bias(self, idx: int, increment: float, top_level_windows: ListOfWindows, is_horizontal: bool = True) -> bool:
num_windows = len(top_level_windows)
if self.main_is_horizontal == is_horizontal:
before_main_bias = self.main_bias
ncols = self.num_full_size_windows + 1
biased_col = idx if idx < self.num_full_size_windows else (ncols - 1)
self.main_bias = [
safe_increment_bias(self.main_bias[i], increment * (1 if i == biased_col else -1)) for i in range(ncols)
]
self.main_bias = normalize_biases(self.main_bias)
return self.main_bias != before_main_bias
num_of_short_windows = num_windows - self.num_full_size_windows
if idx < self.num_full_size_windows or num_of_short_windows < 2:
return False
idx -= self.num_full_size_windows
before_layout = list(self.variable_layout(top_level_windows, self.biased_map))
before = self.biased_map.get(idx, 0.)
candidate = self.biased_map.copy()
candidate[idx] = after = before + increment
if before_layout == list(self.variable_layout(top_level_windows, candidate)):
return False
self.biased_map = candidate
return before != after
def do_layout(self, windows: WindowList, active_window_idx: int) -> None:
if len(windows) == 1:
self.layout_single_window(windows[0])
return
is_fat = not self.main_is_horizontal
if len(windows) <= self.num_full_size_windows + 1:
xlayout = self.main_axis_layout(windows, bias=self.main_bias)
for i, (w, xl) in enumerate(zip(windows, xlayout)):
yl = next(self.perp_axis_layout([w]))
if is_fat:
xl, yl = yl, xl
self.set_window_geometry(w, i, xl, yl)
return
xlayout = self.main_axis_layout(windows[:self.num_full_size_windows + 1], bias=self.main_bias)
attr: EdgeLiteral = 'bottom' if is_fat else 'right'
start = lgd.central.top if is_fat else lgd.central.left
for i, w in enumerate(windows):
if i >= self.num_full_size_windows:
break
xl = next(xlayout)
yl = next(self.perp_axis_layout([w]))
if is_fat:
xl, yl = yl, xl
self.set_window_geometry(w, i, xl, yl)
start = getattr(w.geometry, attr) + w.effective_border() + w.effective_margin(attr) + w.effective_padding(attr)
ylayout = self.variable_layout(windows, self.biased_map)
size = (lgd.central.height if is_fat else lgd.central.width) - start
for i, w in enumerate(windows):
if i < self.num_full_size_windows:
continue
yl = next(ylayout)
xl = next(self.main_axis_layout([w], start=start, size=size))
if is_fat:
xl, yl = yl, xl
self.set_window_geometry(w, i, xl, yl)
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
return neighbors_for_tall_window(self.num_full_size_windows, window, windows)
def minimal_borders(self, windows: WindowList, active_window: Optional[WindowType], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
last_i = len(windows) - 1
for i, w in enumerate(windows):
if needs_borders_map[w.id]:
yield all_borders
continue
if i < self.num_full_size_windows:
if (last_i == i+1 or i+1 < self.num_full_size_windows) and needs_borders_map[windows[i+1].id]:
yield no_borders
else:
yield no_borders if i == last_i else self.only_main_border
continue
if i == last_i:
yield no_borders
break
if needs_borders_map[windows[i+1].id]:
yield no_borders
else:
yield self.only_between_border
class Fat(Tall):
name = 'fat'
main_is_horizontal = False
only_between_border = Borders(False, False, True, False)
only_main_border = Borders(False, False, False, True)
main_axis_layout = Layout.ylayout
perp_axis_layout = Layout.xlayout
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
idx = windows.index(window)
prev = None if idx == 0 else windows[idx-1]
nxt = None if idx == len(windows) - 1 else windows[idx+1]
ans: InternalNeighborsMap = {'left': [], 'right': [], 'top': [] if prev is None else [prev.id], 'bottom': []}
if idx < self.num_full_size_windows - 1:
if nxt is not None:
ans['bottom'] = [nxt.id]
elif idx == self.num_full_size_windows - 1:
ans['bottom'] = [w.id for w in windows[idx+1:]]
else:
ans['top'] = [windows[self.num_full_size_windows - 1].id]
if idx > self.num_full_size_windows and prev is not None:
ans['left'] = [prev.id]
if nxt is not None:
ans['right'] = [nxt.id]
return ans

90
kitty/layout/vertical.py Normal file
View File

@ -0,0 +1,90 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Dict, Generator, Optional
from kitty.typing import WindowType
from kitty.window_list import WindowList
from .base import (
Borders, InternalNeighborsMap, Layout, LayoutDimension, ListOfWindows,
all_borders, no_borders, variable_bias
)
class Vertical(Layout):
name = 'vertical'
main_is_horizontal = False
only_between_border = Borders(False, False, False, True)
main_axis_layout = Layout.ylayout
perp_axis_layout = Layout.xlayout
def variable_layout(self, windows: ListOfWindows, biased_map: Dict[int, float]) -> LayoutDimension:
num_windows = len(windows)
bias = variable_bias(num_windows, biased_map) if num_windows else None
return self.main_axis_layout(windows, bias=bias)
def remove_all_biases(self) -> bool:
self.biased_map: Dict[int, float] = {}
return True
def apply_bias(self, idx: int, increment: float, top_level_windows: ListOfWindows, is_horizontal: bool = True) -> bool:
if self.main_is_horizontal != is_horizontal:
return False
num_windows = len(top_level_windows)
if num_windows < 2:
return False
before_layout = list(self.variable_layout(top_level_windows, self.biased_map))
candidate = self.biased_map.copy()
before = candidate.get(idx, 0)
candidate[idx] = before + increment
if before_layout == list(self.variable_layout(top_level_windows, candidate)):
return False
self.biased_map = candidate
return True
def do_layout(self, windows: WindowList, active_window_idx: int) -> None:
window_count = len(windows)
if window_count == 1:
self.layout_single_window(windows[0])
return
ylayout = self.variable_layout(windows, self.biased_map)
for i, (w, yl) in enumerate(zip(windows, ylayout)):
xl = next(self.perp_axis_layout([w]))
if self.main_is_horizontal:
xl, yl = yl, xl
self.set_window_geometry(w, i, xl, yl)
def minimal_borders(self, windows: WindowList, active_window: Optional[WindowType], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
last_i = len(windows) - 1
for i, w in enumerate(windows):
if needs_borders_map[w.id]:
yield all_borders
continue
if i == last_i:
yield no_borders
break
if needs_borders_map[windows[i+1].id]:
yield no_borders
else:
yield self.only_between_border
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
idx = windows.index(window)
before = [] if window is windows[0] else [windows[idx-1].id]
after = [] if window is windows[-1] else [windows[idx+1].id]
if self.main_is_horizontal:
return {'left': before, 'right': after, 'top': [], 'bottom': []}
return {'top': before, 'bottom': after, 'left': [], 'right': []}
class Horizontal(Vertical):
name = 'horizontal'
main_is_horizontal = True
only_between_border = Borders(False, False, True, False)
main_axis_layout = Layout.xlayout
perp_axis_layout = Layout.ylayout

View File

@ -9,7 +9,7 @@ from typing import Generator, List, NamedTuple, Optional, Tuple, Union
from .cli_stub import CLIOptions from .cli_stub import CLIOptions
from .config_data import to_layout_names from .config_data import to_layout_names
from .constants import FloatEdges, kitty_exe from .constants import FloatEdges, kitty_exe
from .layout import all_layouts from .layout.interface import all_layouts
from .options_stub import Options from .options_stub import Options
from .typing import SpecialWindowInstance from .typing import SpecialWindowInstance
from .utils import log_error, resolved_shell from .utils import log_error, resolved_shell

View File

@ -10,7 +10,7 @@ from .fast_data_types import (
DECAWM, Screen, cell_size_for_window, pt_to_px, set_tab_bar_render_data, DECAWM, Screen, cell_size_for_window, pt_to_px, set_tab_bar_render_data,
viewport_for_window viewport_for_window
) )
from .layout import Rect from .layout.base import Rect
from .options_stub import Options from .options_stub import Options
from .rgb import Color, alpha_blend, color_from_int from .rgb import Color, alpha_blend, color_from_int
from .utils import color_as_int, log_error from .utils import color_as_int, log_error

View File

@ -6,6 +6,7 @@ import weakref
from collections import deque from collections import deque
from contextlib import suppress from contextlib import suppress
from functools import partial from functools import partial
from operator import attrgetter
from typing import ( from typing import (
Any, Deque, Dict, Generator, Iterator, List, NamedTuple, Optional, Pattern, Any, Deque, Dict, Generator, Iterator, List, NamedTuple, Optional, Pattern,
Sequence, Tuple, cast Sequence, Tuple, cast
@ -20,14 +21,16 @@ from .fast_data_types import (
next_window_id, remove_tab, remove_window, ring_bell, set_active_tab, next_window_id, remove_tab, remove_window, ring_bell, set_active_tab,
swap_tabs, sync_os_window_title, x11_window_id swap_tabs, sync_os_window_title, x11_window_id
) )
from .layout import ( from .layout.base import Layout, Rect
Layout, Rect, create_layout_object_for, evict_cached_layouts from .layout.interface import (
create_layout_object_for, evict_cached_layouts
) )
from .options_stub import Options from .options_stub import Options
from .tab_bar import TabBar, TabBarData from .tab_bar import TabBar, TabBarData
from .typing import SessionTab, SessionType, TypedDict from .typing import SessionTab, SessionType, TypedDict
from .utils import log_error, resolved_shell from .utils import log_error, resolved_shell
from .window import Watchers, Window, WindowDict from .window import Watchers, Window, WindowDict
from .window_list import WindowList
class TabDict(TypedDict): class TabDict(TypedDict):
@ -90,7 +93,7 @@ class Tab: # {{{
self.name = getattr(session_tab, 'name', '') self.name = getattr(session_tab, 'name', '')
self.enabled_layouts = [x.lower() for x in getattr(session_tab, 'enabled_layouts', None) or self.opts.enabled_layouts] self.enabled_layouts = [x.lower() for x in getattr(session_tab, 'enabled_layouts', None) or self.opts.enabled_layouts]
self.borders = Borders(self.os_window_id, self.id, self.opts) self.borders = Borders(self.os_window_id, self.id, self.opts)
self.windows: List[Window] = [] self.windows = WindowList()
for i, which in enumerate('first second third fourth fifth sixth seventh eighth ninth tenth'.split()): for i, which in enumerate('first second third fourth fifth sixth seventh eighth ninth tenth'.split()):
setattr(self, which + '_window', partial(self.nth_window, num=i)) setattr(self, which + '_window', partial(self.nth_window, num=i))
self._last_used_layout: Optional[str] = None self._last_used_layout: Optional[str] = None
@ -118,16 +121,14 @@ class Tab: # {{{
if other_tab._current_layout_name: if other_tab._current_layout_name:
self._set_current_layout(other_tab._current_layout_name) self._set_current_layout(other_tab._current_layout_name)
self._last_used_layout = other_tab._last_used_layout self._last_used_layout = other_tab._last_used_layout
orig_windows = list(other_tab.windows)
orig_history = deque(other_tab.active_window_history) orig_history = deque(other_tab.active_window_history)
orig_active = other_tab._active_window_idx orig_active = other_tab._active_window_idx
for window in other_tab.windows: for window in other_tab.windows:
detach_window(other_tab.os_window_id, other_tab.id, window.id) detach_window(other_tab.os_window_id, other_tab.id, window.id)
other_tab.windows = []
other_tab._active_window_idx = 0 other_tab._active_window_idx = 0
self.active_window_history = orig_history self.active_window_history = orig_history
self.windows = orig_windows self.windows = other_tab.windows
other_tab.windows = WindowList()
self._active_window_idx = orig_active self._active_window_idx = orig_active
for window in self.windows: for window in self.windows:
window.change_tab(self) window.change_tab(self)
@ -166,19 +167,11 @@ class Tab: # {{{
@active_window_idx.setter @active_window_idx.setter
def active_window_idx(self, val: int) -> None: def active_window_idx(self, val: int) -> None:
try: old_active_window = self.windows.active_window_for_idx(self._active_window_idx)
old_active_window: Optional[Window] = self.windows[self._active_window_idx] if old_active_window is not None:
except Exception: add_active_id_to_history(self.active_window_history, self.windows.overlaid_window_for(old_active_window))
old_active_window = None self._active_window_idx = max(0, min(val, self.windows.max_active_idx))
else: new_active_window = self.windows.active_window_for_idx(self._active_window_idx)
assert old_active_window is not None
wid = old_active_window.id if old_active_window.overlay_for is None else old_active_window.overlay_for
add_active_id_to_history(self.active_window_history, wid)
self._active_window_idx = max(0, min(val, len(self.windows) - 1))
try:
new_active_window: Optional[Window] = self.windows[self._active_window_idx]
except Exception:
new_active_window = None
if old_active_window is not new_active_window: if old_active_window is not new_active_window:
if old_active_window is not None: if old_active_window is not None:
old_active_window.focus_changed(False) old_active_window.focus_changed(False)
@ -194,7 +187,7 @@ class Tab: # {{{
@property @property
def active_window(self) -> Optional[Window]: def active_window(self) -> Optional[Window]:
return self.windows[self.active_window_idx] if self.windows else None return self.windows.active_window_for_idx(self.active_window_idx)
@property @property
def title(self) -> str: def title(self) -> str:
@ -231,7 +224,7 @@ class Tab: # {{{
def relayout_borders(self) -> None: def relayout_borders(self) -> None:
tm = self.tab_manager_ref() tm = self.tab_manager_ref()
if tm is not None: if tm is not None:
visible_windows = [w for w in self.windows if w.is_visible_in_layout] visible_windows = list(self.visible_windows())
w = self.active_window w = self.active_window
ly = self.current_layout ly = self.current_layout
self.borders( self.borders(
@ -284,9 +277,9 @@ class Tab: # {{{
raise ValueError(increment) raise ValueError(increment)
is_horizontal = quality in ('wider', 'narrower') is_horizontal = quality in ('wider', 'narrower')
increment *= 1 if quality in ('wider', 'taller') else -1 increment *= 1 if quality in ('wider', 'taller') else -1
if self.resize_window_by( w = self.active_window
self.windows[self.active_window_idx].id, if w is not None and self.resize_window_by(
increment, is_horizontal) is not None: w.id, increment, is_horizontal) is not None:
ring_bell() ring_bell()
def reset_window_sizes(self) -> None: def reset_window_sizes(self) -> None:
@ -359,9 +352,7 @@ class Tab: # {{{
copy_colors_from=copy_colors_from, watchers=watchers copy_colors_from=copy_colors_from, watchers=watchers
) )
if overlay_for is not None: if overlay_for is not None:
overlaid = next(w for w in self.windows if w.id == overlay_for)
window.overlay_for = overlay_for window.overlay_for = overlay_for
overlaid.overlay_window_id = window.id
# Must add child before laying out so that resize_pty succeeds # Must add child before laying out so that resize_pty succeeds
get_boss().add_child(window) get_boss().add_child(window)
self._add_window(window, location=location) self._add_window(window, location=location)
@ -389,12 +380,13 @@ class Tab: # {{{
) )
def close_window(self) -> None: def close_window(self) -> None:
if self.windows: w = self.active_window
self.remove_window(self.windows[self.active_window_idx]) if w is not None:
self.remove_window(w)
def close_other_windows_in_tab(self) -> None: def close_other_windows_in_tab(self) -> None:
if len(self.windows) > 1: if len(self.windows) > 1:
active_window = self.windows[self.active_window_idx] active_window = self.active_window
for window in tuple(self.windows): for window in tuple(self.windows):
if window is not active_window: if window is not active_window:
self.remove_window(window) self.remove_window(window)
@ -404,28 +396,25 @@ class Tab: # {{{
old_window_id = self.active_window_history[-num] old_window_id = self.active_window_history[-num]
except IndexError: except IndexError:
return None return None
for idx, w in enumerate(self.windows): return self.windows.idx_for_window(old_window_id)
if w.id == old_window_id:
return idx
def remove_window(self, window: Window, destroy: bool = True) -> None: def remove_window(self, window: Window, destroy: bool = True) -> None:
idx = self.previous_active_window_idx(1) next_window_id = self.windows.next_id_in_stack_on_remove(window)
next_window_id = None if next_window_id is None:
if idx is not None: try:
next_window_id = self.windows[idx].id next_window_id = self.active_window_history[-1]
except IndexError:
pass
active_window_idx = self.current_layout.remove_window(self.windows, window, self.active_window_idx) active_window_idx = self.current_layout.remove_window(self.windows, window, self.active_window_idx)
if destroy: if destroy:
remove_window(self.os_window_id, self.id, window.id) remove_window(self.os_window_id, self.id, window.id)
else: else:
detach_window(self.os_window_id, self.id, window.id) detach_window(self.os_window_id, self.id, window.id)
if window.overlay_for is not None: if next_window_id is None:
for idx, q in enumerate(self.windows): w = self.windows.active_window_for_idx(active_window_idx)
if q.id == window.overlay_for: if w is not None:
active_window_idx = idx next_window_id = w.id
next_window_id = q.id
break
if next_window_id is None and len(self.windows) > active_window_idx:
next_window_id = self.windows[active_window_idx].id
if next_window_id is not None: if next_window_id is not None:
for idx, window in enumerate(self.windows): for idx, window in enumerate(self.windows):
if window.id == next_window_id: if window.id == next_window_id:
@ -441,26 +430,12 @@ class Tab: # {{{
if active_window: if active_window:
self.title_changed(active_window) self.title_changed(active_window)
def detach_window(self, window: Window) -> Tuple[Optional[Window], Optional[Window]]: def detach_window(self, window: Window) -> Tuple[Window, ...]:
underlaid_window: Optional[Window] = None windows = list(self.windows.iter_stack_for_window(window))
overlaid_window: Optional[Window] = window windows.sort(key=attrgetter('id')) # since ids increase in order of creation
if window.overlay_for: for w in reversed(windows):
for x in self.windows: self.remove_window(w, destroy=False)
if x.id == window.overlay_for: return tuple(windows)
underlaid_window = x
break
elif window.overlay_window_id:
underlaid_window = window
overlaid_window = None
for x in self.windows:
if x.id == window.overlay_window_id:
overlaid_window = x
break
if overlaid_window is not None:
self.remove_window(overlaid_window, destroy=False)
if underlaid_window is not None:
self.remove_window(underlaid_window, destroy=False)
return underlaid_window, overlaid_window
def attach_window(self, window: Window) -> None: def attach_window(self, window: Window) -> None:
window.change_tab(self) window.change_tab(self)
@ -473,10 +448,8 @@ class Tab: # {{{
self.relayout_borders() self.relayout_borders()
def set_active_window(self, window: Window) -> None: def set_active_window(self, window: Window) -> None:
try: idx = self.windows.idx_for_window(window)
idx = self.windows.index(window) if idx is not None:
except ValueError:
return
self.set_active_window_idx(idx) self.set_active_window_idx(idx)
def get_nth_window(self, n: int) -> Optional[Window]: def get_nth_window(self, n: int) -> Optional[Window]:
@ -551,7 +524,7 @@ class Tab: # {{{
evict_cached_layouts(self.id) evict_cached_layouts(self.id)
for w in self.windows: for w in self.windows:
w.destroy() w.destroy()
self.windows = [] self.windows = WindowList()
def __repr__(self) -> str: def __repr__(self) -> str:
return 'Tab(title={}, id={})'.format(self.name or self.title, hex(id(self))) return 'Tab(title={}, id={})'.format(self.name or self.title, hex(id(self)))

View File

@ -224,13 +224,12 @@ class Window:
self.pty_resized_once = False self.pty_resized_once = False
self.needs_attention = False self.needs_attention = False
self.override_title = override_title self.override_title = override_title
self.overlay_window_id: Optional[int] = None
self.overlay_for: Optional[int] = None self.overlay_for: Optional[int] = None
self.default_title = os.path.basename(child.argv[0] or appname) self.default_title = os.path.basename(child.argv[0] or appname)
self.child_title = self.default_title self.child_title = self.default_title
self.title_stack: Deque[str] = deque(maxlen=10) self.title_stack: Deque[str] = deque(maxlen=10)
self.allow_remote_control = child.allow_remote_control self.allow_remote_control = child.allow_remote_control
self.id = add_window(tab.os_window_id, tab.id, self.title) self.id: int = add_window(tab.os_window_id, tab.id, self.title)
self.margin = EdgeWidths() self.margin = EdgeWidths()
self.padding = EdgeWidths() self.padding = EdgeWidths()
if not self.id: if not self.id:
@ -297,8 +296,8 @@ class Window:
return self.override_title or self.child_title return self.override_title or self.child_title
def __repr__(self) -> str: def __repr__(self) -> str:
return 'Window(title={}, id={}, overlay_for={}, overlay_window_id={})'.format( return 'Window(title={}, id={}, overlay_for={})'.format(
self.title, self.id, self.overlay_for, self.overlay_window_id) self.title, self.id, self.overlay_for)
def as_dict(self, is_focused: bool = False) -> WindowDict: def as_dict(self, is_focused: bool = False) -> WindowDict:
return dict( return dict(
@ -321,7 +320,6 @@ class Window:
'default_title': self.default_title, 'default_title': self.default_title,
'title_stack': list(self.title_stack), 'title_stack': list(self.title_stack),
'allow_remote_control': self.allow_remote_control, 'allow_remote_control': self.allow_remote_control,
'overlay_window_id': self.overlay_window_id,
'overlay_for': self.overlay_for, 'overlay_for': self.overlay_for,
'cwd': self.child.current_cwd or self.child.cwd, 'cwd': self.child.current_cwd or self.child.cwd,
'env': self.child.environ, 'env': self.child.environ,

90
kitty/window_list.py Normal file
View File

@ -0,0 +1,90 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Dict, Generator, Iterator, List, Optional, Union
from .typing import WindowType
class WindowList:
def __init__(self) -> None:
self.all_windows: List[WindowType] = []
self.id_map: Dict[int, WindowType] = {}
self.overlay_stacks: Dict[int, List[int]] = {}
self.id_to_idx_map: Dict[int, int] = {}
self.idx_to_base_id_map: Dict[int, int] = {}
self.max_active_idx = 0
def __len__(self) -> int:
return len(self.all_windows)
def __bool__(self) -> bool:
return bool(self.all_windows)
def __iter__(self) -> Iterator[WindowType]:
return iter(self.all_windows)
def __contains__(self, window: WindowType) -> bool:
return window.id in self.id_map
def stack_for_window_id(self, q: int) -> List[int]:
' The stack of overlaid windows this window belongs to '
w = self.id_map[q]
if w.overlay_for is not None and w.overlay_for in self.id_map:
q = self.id_map[w.overlay_for].id
return self.overlay_stacks[q]
def iter_top_level_windows(self) -> Generator[WindowType, None, None]:
' Iterator over all top level windows '
for stack in self.overlay_stacks.values():
yield self.id_map[stack[-1]]
def iter_stack_for_window(self, x: Union[WindowType, int], reverse: bool = False) -> Generator[WindowType, None, None]:
' Iterator over all windows in the stack for this window '
q = x if isinstance(x, int) else x.id
stack = self.stack_for_window_id(q)
y = reversed(stack) if reverse else iter(stack)
for wid in y:
yield self.id_map[wid]
def overlay_for(self, x: Union[WindowType, int]) -> int:
' id of the top-most window overlaying this window, same as this window id if not overlaid '
q = x if isinstance(x, int) else x.id
return self.stack_for_window_id(q)[-1]
def overlaid_window_for(self, x: Union[WindowType, int]) -> int:
' id of the bottom-most window in this windows overlay stack '
q = x if isinstance(x, int) else x.id
return self.stack_for_window_id(q)[0]
def is_overlaid(self, x: Union[WindowType, int]) -> bool:
' Return False if there is a window overlaying this one '
q = x if isinstance(x, int) else x.id
return self.overlay_for(q) != q
def idx_for_window(self, x: Union[WindowType, int]) -> Optional[int]:
' Return the index of the window in the list of top-level windows '
q = x if isinstance(x, int) else x.id
return self.id_to_idx_map[q]
def active_window_for_idx(self, idx: int, clamp: bool = False) -> Optional[WindowType]:
' Return the active window at the specified index '
if clamp:
idx = max(0, min(idx, self.max_active_idx))
q = self.idx_to_base_id_map.get(idx)
if q is not None:
return self.id_map[self.overlay_stacks[q][-1]]
return None
def next_id_in_stack_on_remove(self, x: Union[WindowType, int]) -> Optional[int]:
' The id of the window that should become active when this window is removed, or None if there is no other window in the stack '
q = x if isinstance(x, int) else x.id
stack = self.stack_for_window_id(q)
idx = stack.index(q)
if idx < len(stack) - 1:
return stack[idx + 1]
if idx > 0:
return stack[idx - 1]
return None