1#!/usr/local/bin/python3.8 2# vim:fileencoding=utf-8 3# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net> 4 5from functools import partial 6from itertools import repeat 7from typing import ( 8 Any, Dict, Generator, Iterable, Iterator, List, NamedTuple, Optional, 9 Sequence, Tuple 10) 11 12from kitty.borders import BorderColor 13from kitty.fast_data_types import ( 14 Region, set_active_window, viewport_for_window 15) 16from kitty.options.types import Options 17from kitty.types import Edges, WindowGeometry 18from kitty.typing import TypedDict, WindowType 19from kitty.window_list import WindowGroup, WindowList 20 21 22class BorderLine(NamedTuple): 23 edges: Edges = Edges() 24 color: BorderColor = BorderColor.inactive 25 26 27class LayoutOpts: 28 29 def __init__(self, data: Dict[str, str]): 30 pass 31 32 33class LayoutData(NamedTuple): 34 content_pos: int = 0 35 cells_per_window: int = 0 36 space_before: int = 0 37 space_after: int = 0 38 content_size: int = 0 39 40 41DecorationPairs = Sequence[Tuple[int, int]] 42LayoutDimension = Generator[LayoutData, None, None] 43ListOfWindows = List[WindowType] 44 45 46class NeighborsMap(TypedDict): 47 left: List[int] 48 top: List[int] 49 right: List[int] 50 bottom: List[int] 51 52 53class LayoutGlobalData: 54 draw_minimal_borders: bool = True 55 draw_active_borders: bool = True 56 align_top_left: bool = False 57 58 central: Region = Region((0, 0, 199, 199, 200, 200)) 59 cell_width: int = 20 60 cell_height: int = 20 61 62 63lgd = LayoutGlobalData() 64 65 66def idx_for_id(win_id: int, windows: Iterable[WindowType]) -> Optional[int]: 67 for i, w in enumerate(windows): 68 if w.id == win_id: 69 return i 70 71 72def set_layout_options(opts: Options) -> None: 73 lgd.draw_minimal_borders = opts.draw_minimal_borders and sum(opts.window_margin_width) == 0 74 lgd.draw_active_borders = opts.active_border_color is not None 75 lgd.align_top_left = opts.placement_strategy == 'top-left' 76 77 78def calculate_cells_map(bias: Optional[Sequence[float]], number_of_windows: int, number_of_cells: int) -> List[int]: 79 cells_per_window = number_of_cells // number_of_windows 80 if bias is not None and 1 < number_of_windows == len(bias) and cells_per_window > 5: 81 cells_map = [int(b * number_of_cells) for b in bias] 82 while min(cells_map) < 5: 83 maxi, mini = map(cells_map.index, (max(cells_map), min(cells_map))) 84 if maxi == mini: 85 break 86 cells_map[mini] += 1 87 cells_map[maxi] -= 1 88 else: 89 cells_map = list(repeat(cells_per_window, number_of_windows)) 90 extra = number_of_cells - sum(cells_map) 91 if extra > 0: 92 cells_map[-1] += extra 93 return cells_map 94 95 96def layout_dimension( 97 start_at: int, length: int, cell_length: int, 98 decoration_pairs: DecorationPairs, 99 left_align: bool = False, 100 bias: Optional[Sequence[float]] = None 101) -> LayoutDimension: 102 number_of_windows = len(decoration_pairs) 103 number_of_cells = length // cell_length 104 space_needed_for_decorations: int = sum(map(sum, decoration_pairs)) 105 extra = length - number_of_cells * cell_length 106 while extra < space_needed_for_decorations: 107 number_of_cells -= 1 108 extra = length - number_of_cells * cell_length 109 cells_map = calculate_cells_map(bias, number_of_windows, number_of_cells) 110 assert sum(cells_map) == number_of_cells 111 112 extra = length - number_of_cells * cell_length - space_needed_for_decorations 113 pos = start_at 114 if not left_align: 115 pos += extra // 2 116 last_i = len(cells_map) - 1 117 118 for i, cells_per_window in enumerate(cells_map): 119 before_dec, after_dec = decoration_pairs[i] 120 pos += before_dec 121 if i == 0: 122 before_space = pos - start_at 123 else: 124 before_space = before_dec 125 content_size = cells_per_window * cell_length 126 if i == last_i: 127 after_space = (start_at + length) - (pos + content_size) 128 else: 129 after_space = after_dec 130 yield LayoutData(pos, cells_per_window, before_space, after_space, content_size) 131 pos += content_size + after_space 132 133 134class Rect(NamedTuple): 135 left: int 136 top: int 137 right: int 138 bottom: int 139 140 141def blank_rects_for_window(wg: WindowGeometry) -> Generator[Rect, None, None]: 142 left_width, right_width = wg.spaces.left, wg.spaces.right 143 top_height, bottom_height = wg.spaces.top, wg.spaces.bottom 144 if left_width > 0: 145 yield Rect(wg.left - left_width, wg.top - top_height, wg.left, wg.bottom + bottom_height) 146 if top_height > 0: 147 yield Rect(wg.left, wg.top - top_height, wg.right + right_width, wg.top) 148 if right_width > 0: 149 yield Rect(wg.right, wg.top, wg.right + right_width, wg.bottom + bottom_height) 150 if bottom_height > 0: 151 yield Rect(wg.left, wg.bottom, wg.right, wg.bottom + bottom_height) 152 153 154def window_geometry(xstart: int, xnum: int, ystart: int, ynum: int, left: int, top: int, right: int, bottom: int) -> WindowGeometry: 155 return WindowGeometry( 156 left=xstart, top=ystart, xnum=xnum, ynum=ynum, 157 right=xstart + lgd.cell_width * xnum, bottom=ystart + lgd.cell_height * ynum, 158 spaces=Edges(left, top, right, bottom) 159 ) 160 161 162def window_geometry_from_layouts(x: LayoutData, y: LayoutData) -> WindowGeometry: 163 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) 164 165 166def layout_single_window(xdecoration_pairs: DecorationPairs, ydecoration_pairs: DecorationPairs, left_align: bool = False) -> WindowGeometry: 167 x = next(layout_dimension(lgd.central.left, lgd.central.width, lgd.cell_width, xdecoration_pairs, left_align=lgd.align_top_left)) 168 y = next(layout_dimension(lgd.central.top, lgd.central.height, lgd.cell_height, ydecoration_pairs, left_align=lgd.align_top_left)) 169 return window_geometry_from_layouts(x, y) 170 171 172def safe_increment_bias(old_val: float, increment: float) -> float: 173 return max(0.1, min(old_val + increment, 0.9)) 174 175 176def normalize_biases(biases: List[float]) -> List[float]: 177 s = sum(biases) 178 if s == 1: 179 return biases 180 return [x/s for x in biases] 181 182 183def distribute_indexed_bias(base_bias: Sequence[float], index_bias_map: Dict[int, float]) -> Sequence[float]: 184 if not index_bias_map: 185 return base_bias 186 ans = list(base_bias) 187 limit = len(ans) 188 for row, increment in index_bias_map.items(): 189 if row >= limit or not increment: 190 continue 191 other_increment = -increment / (limit - 1) 192 ans = [safe_increment_bias(b, increment if i == row else other_increment) for i, b in enumerate(ans)] 193 return normalize_biases(ans) 194 195 196def variable_bias(num_windows: int, candidate: Dict[int, float]) -> Sequence[float]: 197 return distribute_indexed_bias(list(repeat(1/(num_windows), num_windows)), candidate) 198 199 200class Layout: 201 202 name: Optional[str] = None 203 needs_window_borders = True 204 must_draw_borders = False # can be overridden to customize behavior from kittens 205 layout_opts = LayoutOpts({}) 206 only_active_window_visible = False 207 208 def __init__(self, os_window_id: int, tab_id: int, layout_opts: str = '') -> None: 209 self.os_window_id = os_window_id 210 self.tab_id = tab_id 211 self.set_active_window_in_os_window = partial(set_active_window, os_window_id, tab_id) 212 # A set of rectangles corresponding to the blank spaces at the edges of 213 # this layout, i.e. spaces that are not covered by any window 214 self.blank_rects: List[Rect] = [] 215 self.layout_opts = self.parse_layout_opts(layout_opts) 216 assert self.name is not None 217 self.full_name = self.name + ((':' + layout_opts) if layout_opts else '') 218 self.remove_all_biases() 219 220 def bias_increment_for_cell(self, is_horizontal: bool) -> float: 221 self._set_dimensions() 222 if is_horizontal: 223 return (lgd.cell_width + 1) / lgd.central.width 224 return (lgd.cell_height + 1) / lgd.central.height 225 226 def apply_bias(self, window_id: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool: 227 return False 228 229 def remove_all_biases(self) -> bool: 230 return False 231 232 def modify_size_of_window(self, all_windows: WindowList, window_id: int, increment: float, is_horizontal: bool = True) -> bool: 233 idx = all_windows.group_idx_for_window(window_id) 234 if idx is None: 235 return False 236 return self.apply_bias(idx, increment, all_windows, is_horizontal) 237 238 def parse_layout_opts(self, layout_opts: Optional[str] = None) -> LayoutOpts: 239 data: Dict[str, str] = {} 240 if layout_opts: 241 for x in layout_opts.split(';'): 242 k, v = x.partition('=')[::2] 243 if k and v: 244 data[k] = v 245 return type(self.layout_opts)(data) 246 247 def nth_window(self, all_windows: WindowList, num: int) -> Optional[WindowType]: 248 return all_windows.active_window_in_nth_group(num, clamp=True) 249 250 def activate_nth_window(self, all_windows: WindowList, num: int) -> None: 251 all_windows.set_active_group_idx(num) 252 253 def next_window(self, all_windows: WindowList, delta: int = 1) -> None: 254 all_windows.activate_next_window_group(delta) 255 256 def neighbors(self, all_windows: WindowList) -> NeighborsMap: 257 w = all_windows.active_window 258 assert w is not None 259 return self.neighbors_for_window(w, all_windows) 260 261 def move_window(self, all_windows: WindowList, delta: int = 1) -> bool: 262 if all_windows.num_groups < 2 or not delta: 263 return False 264 265 return all_windows.move_window_group(by=delta) 266 267 def move_window_to_group(self, all_windows: WindowList, group: int) -> bool: 268 return all_windows.move_window_group(to_group=group) 269 270 def add_window(self, all_windows: WindowList, window: WindowType, location: Optional[str] = None, overlay_for: Optional[int] = None) -> None: 271 if overlay_for is not None and overlay_for in all_windows: 272 all_windows.add_window(window, group_of=overlay_for) 273 return 274 if location == 'neighbor': 275 location = 'after' 276 self.add_non_overlay_window(all_windows, window, location) 277 278 def add_non_overlay_window(self, all_windows: WindowList, window: WindowType, location: Optional[str]) -> None: 279 next_to: Optional[WindowType] = None 280 before = False 281 next_to = all_windows.active_window 282 if location is not None: 283 if location in ('after', 'vsplit', 'hsplit'): 284 pass 285 elif location == 'before': 286 before = True 287 elif location == 'first': 288 before = True 289 next_to = None 290 elif location == 'last': 291 next_to = None 292 all_windows.add_window(window, next_to=next_to, before=before) 293 294 def update_visibility(self, all_windows: WindowList) -> None: 295 active_window = all_windows.active_window 296 for window, is_group_leader in all_windows.iter_windows_with_visibility(): 297 is_visible = window is active_window or (is_group_leader and not self.only_active_window_visible) 298 window.set_visible_in_layout(is_visible) 299 300 def _set_dimensions(self) -> None: 301 lgd.central, tab_bar, vw, vh, lgd.cell_width, lgd.cell_height = viewport_for_window(self.os_window_id) 302 303 def __call__(self, all_windows: WindowList) -> None: 304 self._set_dimensions() 305 self.update_visibility(all_windows) 306 self.blank_rects = [] 307 self.do_layout(all_windows) 308 309 def layout_single_window_group(self, wg: WindowGroup, add_blank_rects: bool = True) -> None: 310 bw = 1 if self.must_draw_borders else 0 311 xdecoration_pairs = (( 312 wg.decoration('left', border_mult=bw, is_single_window=True), 313 wg.decoration('right', border_mult=bw, is_single_window=True), 314 ),) 315 ydecoration_pairs = (( 316 wg.decoration('top', border_mult=bw, is_single_window=True), 317 wg.decoration('bottom', border_mult=bw, is_single_window=True), 318 ),) 319 geom = layout_single_window(xdecoration_pairs, ydecoration_pairs, left_align=lgd.align_top_left) 320 wg.set_geometry(geom) 321 if add_blank_rects and wg: 322 self.blank_rects.extend(blank_rects_for_window(geom)) 323 324 def xlayout( 325 self, 326 groups: Iterator[WindowGroup], 327 bias: Optional[Sequence[float]] = None, 328 start: Optional[int] = None, 329 size: Optional[int] = None, 330 offset: int = 0, 331 border_mult: int = 1 332 ) -> LayoutDimension: 333 decoration_pairs = tuple( 334 (g.decoration('left', border_mult=border_mult), g.decoration('right', border_mult=border_mult)) for i, g in 335 enumerate(groups) if i >= offset 336 ) 337 if start is None: 338 start = lgd.central.left 339 if size is None: 340 size = lgd.central.width 341 return layout_dimension(start, size, lgd.cell_width, decoration_pairs, bias=bias, left_align=lgd.align_top_left) 342 343 def ylayout( 344 self, 345 groups: Iterator[WindowGroup], 346 bias: Optional[Sequence[float]] = None, 347 start: Optional[int] = None, 348 size: Optional[int] = None, 349 offset: int = 0, 350 border_mult: int = 1 351 ) -> LayoutDimension: 352 decoration_pairs = tuple( 353 (g.decoration('top', border_mult=border_mult), g.decoration('bottom', border_mult=border_mult)) for i, g in 354 enumerate(groups) if i >= offset 355 ) 356 if start is None: 357 start = lgd.central.top 358 if size is None: 359 size = lgd.central.height 360 return layout_dimension(start, size, lgd.cell_height, decoration_pairs, bias=bias, left_align=lgd.align_top_left) 361 362 def set_window_group_geometry(self, wg: WindowGroup, xl: LayoutData, yl: LayoutData) -> WindowGeometry: 363 geom = window_geometry_from_layouts(xl, yl) 364 wg.set_geometry(geom) 365 self.blank_rects.extend(blank_rects_for_window(geom)) 366 return geom 367 368 def do_layout(self, windows: WindowList) -> None: 369 raise NotImplementedError() 370 371 def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap: 372 return {'left': [], 'right': [], 'top': [], 'bottom': []} 373 374 def compute_needs_borders_map(self, all_windows: WindowList) -> Dict[int, bool]: 375 return all_windows.compute_needs_borders_map(lgd.draw_active_borders) 376 377 def get_minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]: 378 self._set_dimensions() 379 yield from self.minimal_borders(windows) 380 381 def minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]: 382 return 383 yield BorderLine() # type: ignore 384 385 def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList) -> Optional[bool]: 386 pass 387 388 def layout_state(self) -> Dict[str, Any]: 389 return {} 390