From 917559f8834a7caabe9685466a277a619ab2bda2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 13 Mar 2020 16:13:26 +0530 Subject: [PATCH] more typing work --- kitty/borders.py | 35 ++- kitty/child.py | 4 +- kitty/cli_stub.py | 2 +- kitty/conf/definition.py | 6 +- kitty/fast_data_types.pyi | 16 +- kitty/layout.py | 528 ++++++++++++++++++++++---------------- kitty/main.py | 2 +- kitty/notify.py | 43 ++-- kitty/rc/new_window.py | 4 +- kitty/tab_bar.py | 58 +++-- kitty/tabs.py | 246 +++++++++++------- kitty/window.py | 231 ++++++++++------- kitty_tests/layout.py | 4 +- setup.cfg | 2 +- 14 files changed, 693 insertions(+), 488 deletions(-) diff --git a/kitty/borders.py b/kitty/borders.py index d52a9426c..2d48463dc 100644 --- a/kitty/borders.py +++ b/kitty/borders.py @@ -3,11 +3,14 @@ # License: GPL v3 Copyright: 2016, Kovid Goyal from itertools import chain +from typing import TYPE_CHECKING, Optional, Sequence, Tuple +from .constants import WindowGeometry from .fast_data_types import ( BORDERS_PROGRAM, add_borders_rect, compile_program, init_borders_program, os_window_has_background_image ) +from .options_stub import Options from .utils import load_shaders try: @@ -16,20 +19,26 @@ except ImportError: from enum import IntEnum as IntFlag # type: ignore +if TYPE_CHECKING: + from .window import Window + from .layout import Layout + Window, Layout + + class BorderColor(IntFlag): # See the border vertex shader for how these flags become actual colors default_bg, active, inactive, window_bg, bell = ((1 << i) for i in range(5)) -def vertical_edge(os_window_id, tab_id, color, width, top, bottom, left): +def vertical_edge(os_window_id: int, tab_id: int, color: int, width: int, top: int, bottom: int, left: int) -> None: add_borders_rect(os_window_id, tab_id, left, top, left + width, bottom, color) -def horizontal_edge(os_window_id, tab_id, color, height, left, right, top): +def horizontal_edge(os_window_id: int, tab_id: int, color: int, height: int, left: int, right: int, top: int) -> None: add_borders_rect(os_window_id, tab_id, left, top, right, top + height, color) -def draw_edges(os_window_id, tab_id, colors, width, geometry, base_width=0): +def draw_edges(os_window_id: int, tab_id: int, colors: Sequence[int], width: int, geometry: 'WindowGeometry', base_width: int = 0) -> None: left = geometry.left - (width + base_width) top = geometry.top - (width + base_width) right = geometry.right + (width + base_width) @@ -40,28 +49,28 @@ def draw_edges(os_window_id, tab_id, colors, width, geometry, base_width=0): vertical_edge(os_window_id, tab_id, colors[2], width, top, bottom, geometry.right + base_width) -def load_borders_program(): +def load_borders_program() -> None: compile_program(BORDERS_PROGRAM, *load_shaders('border')) init_borders_program() class Borders: - def __init__(self, os_window_id, tab_id, opts): + def __init__(self, os_window_id: int, tab_id: int, opts: Options): self.os_window_id = os_window_id self.tab_id = tab_id self.draw_active_borders = opts.active_border_color is not None def __call__( self, - windows, - active_window, - current_layout, - extra_blank_rects, - padding_width, - border_width, - draw_window_borders=True, - ): + windows: Sequence['Window'], + active_window: Optional['Window'], + current_layout: 'Layout', + extra_blank_rects: Sequence[Tuple[int, int, int, int]], + padding_width: int, + border_width: int, + draw_window_borders: bool = True, + ) -> None: add_borders_rect(self.os_window_id, self.tab_id, 0, 0, 0, 0, BorderColor.default_bg) has_background_image = os_window_has_background_image(self.os_window_id) if not has_background_image: diff --git a/kitty/child.py b/kitty/child.py index a0188590b..dfdeab985 100644 --- a/kitty/child.py +++ b/kitty/child.py @@ -8,7 +8,7 @@ import sys from collections import defaultdict from contextlib import contextmanager, suppress from typing import ( - DefaultDict, Dict, Generator, Iterable, List, Optional, Sequence, Tuple + DefaultDict, Dict, Generator, List, Optional, Sequence, Tuple ) import kitty.fast_data_types as fast_data_types @@ -182,7 +182,7 @@ class Child: def __init__( self, - argv: Iterable[str], + argv: Sequence[str], cwd: str, opts: Options, stdin: Optional[bytes] = None, diff --git a/kitty/cli_stub.py b/kitty/cli_stub.py index 1dd128404..c42c74ee5 100644 --- a/kitty/cli_stub.py +++ b/kitty/cli_stub.py @@ -24,7 +24,7 @@ def generate_stub() -> None: nonlocal text text += as_type_stub(*parse_option_spec(otext), class_name=cls, extra_fields=extra_fields) - do(extra_fields=('args: typing.Sequence[str]',)) + do(extra_fields=('args: typing.List[str]',)) from .launch import options_spec do(options_spec(), 'LaunchCLIOptions') diff --git a/kitty/conf/definition.py b/kitty/conf/definition.py index d047e4d06..9d166c151 100644 --- a/kitty/conf/definition.py +++ b/kitty/conf/definition.py @@ -332,9 +332,9 @@ def as_type_stub( ans.insert(0, 'import {}'.format(mod)) for field_name, type_def in extra_fields: ans.append(' {}: {}'.format(field_name, type_def)) - ans.append(' def __iter__(self): pass') - ans.append(' def __len__(self): pass') - ans.append(' def _replace(self, **kw) -> {}: pass'.format(class_name)) + ans.append(' def __iter__(self) -> None: pass') + ans.append(' def __len__(self) -> int: pass') + ans.append(' def _replace(self, **kw: typing.Any) -> {}: pass'.format(class_name)) return '\n'.join(ans) + '\n\n\n' diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index c65e59cb9..c8d6b8650 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -554,7 +554,7 @@ def resolve_key_mods(kitty_mod: int, mods: int) -> int: pass -def parse_font_feature(str) -> bytes: +def parse_font_feature(ff: str) -> bytes: pass @@ -863,7 +863,7 @@ def parse_input_from_terminal( csi_callback: Callable[[str], None], osc_callback: Callable[[str], None], pm_callback: Callable[[str], None], apc_callback: Callable[[str], None], data: str, in_bracketed_paste: bool -): +) -> str: pass @@ -903,11 +903,11 @@ def set_font_data( bold: int, italic: int, bold_italic: int, num_symbol_fonts: int, symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float, font_feature_settings: Dict[str, Tuple[str, ...]] -): +) -> None: pass -def get_fallback_font(text: str, bold: bool, italic: bool): +def get_fallback_font(text: str, bold: bool, italic: bool) -> Any: pass @@ -963,10 +963,10 @@ class Screen: ): pass - def line(self, int) -> Line: + def line(self, num: int) -> Line: pass - def draw(self, text) -> None: + def draw(self, text: str) -> None: pass def copy_colors_from(self, other: 'Screen') -> None: @@ -1038,7 +1038,7 @@ def set_window_render_data( os_window_id: int, tab_id: int, window_id: int, window_idx: int, xstart: float, ystart: float, dx: float, dy: float, screen: Screen, left: int, top: int, right: int, bottom: int -): +) -> None: pass @@ -1068,7 +1068,7 @@ class ChildMonitor: def resize_pty(self, window_id: int, rows: int, cols: int, x_pixels: int, y_pixels: int) -> None: pass - def needs_write(self, child_id: int, data: bytes) -> bool: + def needs_write(self, child_id: int, data: Union[bytes, str]) -> bool: pass def set_iutf8_winid(self, win_id: int, on: bool) -> bool: diff --git a/kitty/layout.py b/kitty/layout.py index d134d83af..f6b5927a8 100644 --- a/kitty/layout.py +++ b/kitty/layout.py @@ -5,8 +5,8 @@ from functools import lru_cache, partial from itertools import islice, repeat from typing import ( - TYPE_CHECKING, Dict, FrozenSet, Generator, Iterable, List, NamedTuple, - Optional, Sequence, Tuple, Union, cast + TYPE_CHECKING, Callable, Collection, Dict, FrozenSet, Generator, Iterable, + List, NamedTuple, Optional, Sequence, Tuple, Union, cast ) from .constants import WindowGeometry @@ -16,32 +16,51 @@ from .fast_data_types import ( from .options_stub import Options try: - from typing import TypedDict + from typing import TypedDict, Literal + EdgeLiteral = Literal['left', 'top', 'right', 'bottom'] except ImportError: TypedDict = dict + EdgeLiteral = str # type: ignore if TYPE_CHECKING: from .window import Window Window +else: + Window = object + + +class Borders(NamedTuple): + left: bool + top: bool + right: bool + bottom: bool + + +class LayoutOpts: + + def __init__(self, data: Dict[str, str]): + pass + # Utils {{{ central = Region((0, 0, 199, 199, 200, 200)) cell_width = cell_height = 20 -all_borders = True, True, True, True -no_borders = False, False, False, False +all_borders = Borders(True, True, True, True) +no_borders = Borders(False, False, False, False) draw_minimal_borders = False draw_active_borders = True align_top_left = False LayoutDimension = Generator[Tuple[int, int], None, None] DecorationPairs = Sequence[Tuple[int, int]] +WindowList = List[Window] class InternalNeighborsMap(TypedDict): - left: List[Union[int, 'Window']] - top: List[Union[int, 'Window']] - right: List[Union[int, 'Window']] - bottom: List[Union[int, 'Window']] + left: List[int] + top: List[int] + right: List[int] + bottom: List[int] class NeighborsMap(TypedDict): @@ -51,7 +70,7 @@ class NeighborsMap(TypedDict): bottom: Tuple[int, ...] -def idx_for_id(win_id: int, windows: Iterable['Window']) -> Optional[int]: +def idx_for_id(win_id: int, windows: Iterable[Window]) -> Optional[int]: for i, w in enumerate(windows): if w.id == win_id: return i @@ -115,7 +134,7 @@ class Rect(NamedTuple): bottom: int -def process_overlaid_windows(all_windows: Sequence['Window']) -> Tuple[FrozenSet['Window'], List['Window']]: +def process_overlaid_windows(all_windows: WindowList) -> Tuple[FrozenSet[Window], List[Window]]: id_map = {w.id: w for w in all_windows} overlaid_windows = frozenset(w for w in all_windows if w.overlay_window_id is not None and w.overlay_window_id in id_map) windows = [w for w in all_windows if w not in overlaid_windows] @@ -132,25 +151,25 @@ def layout_single_window(xdecoration_pairs: DecorationPairs, ydecoration_pairs: return window_geometry(xstart, xnum, ystart, ynum) -def left_blank_rect(w: 'Window', rects: List[Rect]) -> None: +def left_blank_rect(w: Window, rects: List[Rect]) -> None: lt = w.geometry.left if lt > central.left: rects.append(Rect(central.left, central.top, lt, central.bottom + 1)) -def right_blank_rect(w: 'Window', rects: List[Rect]) -> None: +def right_blank_rect(w: Window, rects: List[Rect]) -> None: r = w.geometry.right if r < central.right: rects.append(Rect(r, central.top, central.right + 1, central.bottom + 1)) -def top_blank_rect(w: 'Window', rects: List[Rect]) -> None: +def top_blank_rect(w: Window, rects: List[Rect]) -> None: t = w.geometry.top if t > central.top: rects.append(Rect(central.left, central.top, central.right + 1, t)) -def bottom_blank_rect(w: 'Window', rects: List[Rect]) -> None: +def bottom_blank_rect(w: Window, rects: List[Rect]) -> None: b = w.geometry.bottom # Need to use <= here as otherwise a single pixel row at the bottom of the # window is sometimes not covered. See https://github.com/kovidgoyal/kitty/issues/506 @@ -158,7 +177,7 @@ def bottom_blank_rect(w: 'Window', rects: List[Rect]) -> None: rects.append(Rect(central.left, b, central.right + 1, central.bottom + 1)) -def blank_rects_for_window(w: 'Window') -> List[Rect]: +def blank_rects_for_window(w: Window) -> List[Rect]: ans: List[Rect] = [] left_blank_rect(w, ans) top_blank_rect(w, ans) @@ -171,7 +190,7 @@ def safe_increment_bias(old_val: float, increment: float) -> float: return max(0.1, min(old_val + increment, 0.9)) -def normalize_biases(biases: Sequence[float]) -> Sequence[float]: +def normalize_biases(biases: List[float]) -> List[float]: s = sum(biases) if s == 1: return biases @@ -203,6 +222,7 @@ class Layout: # {{{ name: Optional[str] = None needs_window_borders = True needs_all_windows = False + layout_opts = LayoutOpts({}) only_active_window_visible = False def __init__( @@ -228,7 +248,7 @@ class Layout: # {{{ self.full_name = self.name + ((':' + layout_opts) if layout_opts else '') self.remove_all_biases() - def update_sizes(self, margin_width: int, single_window_margin_width: int, padding_width, border_width: int) -> None: + def update_sizes(self, margin_width: int, single_window_margin_width: int, padding_width: int, border_width: int) -> None: self.border_width = border_width self.margin_width = margin_width self.single_window_margin_width = single_window_margin_width @@ -240,55 +260,58 @@ class Layout: # {{{ return (cell_width + 1) / central.width return (cell_height + 1) / central.height - def apply_bias(self, idx: int, increment_as_percent: int, num_windows: int, is_horizontal: bool) -> bool: + def apply_bias(self, idx: int, increment: float, num_windows: int, is_horizontal: bool = True) -> bool: return False def remove_all_biases(self) -> bool: return False - def modify_size_of_window(self, all_windows, window_id, increment, is_horizontal=True): + 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] windows = process_overlaid_windows(all_windows)[1] idx = idx_for_id(w.id, windows) - if idx is None: + if idx is None and w.overlay_window_id is not None: idx = idx_for_id(w.overlay_window_id, windows) if idx is not None: return self.apply_bias(idx, increment, len(windows), is_horizontal) return False - def parse_layout_opts(self, layout_opts): - if not layout_opts: - return {} - ans = {} - for x in layout_opts.split(';'): - k, v = x.partition('=')[::2] - if k and v: - ans[k] = v - return ans + 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, num, make_active=True): + def nth_window(self, all_windows: WindowList, num: int) -> Window: windows = process_overlaid_windows(all_windows)[1] w = windows[min(num, len(windows) - 1)] - if not make_active: - return w + return w + + def activate_nth_window(self, all_windows: WindowList, num: int) -> int: + w = self.nth_window(all_windows, num) active_window_idx = idx_for_id(w.id, all_windows) + assert active_window_idx is not None return self.set_active_window(all_windows, active_window_idx) - def next_window(self, all_windows, active_window_idx, delta=1): + def next_window(self, all_windows: WindowList, active_window_idx: int, delta: int = 1) -> int: w = all_windows[active_window_idx] windows = process_overlaid_windows(all_windows)[1] idx = idx_for_id(w.id, windows) - if idx is None: + 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 - active_window_idx = (idx + len(windows) + delta) % len(windows) - active_window_idx = idx_for_id(windows[active_window_idx].id, all_windows) - return self.set_active_window(all_windows, active_window_idx) + aidx = (idx + len(windows) + delta) % len(windows) + ans = idx_for_id(windows[aidx].id, all_windows) + assert ans is not None + return self.set_active_window(all_windows, ans) - def neighbors(self, all_windows: Sequence['Window'], active_window_idx: int) -> NeighborsMap: + def neighbors(self, all_windows: WindowList, active_window_idx: int) -> NeighborsMap: w = all_windows[active_window_idx] if self.needs_all_windows: windows = all_windows @@ -296,10 +319,9 @@ class Layout: # {{{ windows = process_overlaid_windows(all_windows)[1] n = self.neighbors_for_window(w, windows) - def as_indices(windows: Iterable[Union['Window', int]]) -> Generator[int, None, None]: + def as_indices(windows: Iterable[int]) -> Generator[int, None, None]: for w in windows: - q = w if isinstance(w, int) else w.id - idx = idx_for_id(q, all_windows) + idx = idx_for_id(w, all_windows) if idx is not None: yield idx @@ -311,7 +333,7 @@ class Layout: # {{{ } return ans - def move_window(self, all_windows, active_window_idx, delta=1): + 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 w = all_windows[active_window_idx] @@ -319,7 +341,7 @@ class Layout: # {{{ if len(windows) < 2 or not delta: return active_window_idx idx = idx_for_id(w.id, windows) - if idx is None: + 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): @@ -328,7 +350,7 @@ class Layout: # {{{ 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(Sequence['Window'], neighbors.get(delta, ())) + q = cast(WindowList, neighbors.get(cast(str, delta), ())) if not q: return active_window_idx w = q[0] @@ -345,10 +367,10 @@ class Layout: # {{{ self.swap_windows_in_os_window(nidx, idx) return self.set_active_window(all_windows, nidx) - def swap_windows_in_layout(self, all_windows, a, b): + 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, window, current_active_window_idx, location=None): + def add_window(self, all_windows: WindowList, window: Window, 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) @@ -368,7 +390,7 @@ class Layout: # {{{ self.set_active_window_in_os_window(active_window_idx) return active_window_idx - def do_add_window(self, all_windows, window, current_active_window_idx, location): + def do_add_window(self, all_windows: WindowList, window: Window, 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: @@ -387,7 +409,7 @@ class Layout: # {{{ all_windows.append(window) return active_window_idx - def remove_window(self, all_windows, window, current_active_window_idx, swapped=False): + def remove_window(self, all_windows: WindowList, window: Window, current_active_window_idx: int, swapped: bool = False) -> int: try: active_window = all_windows[current_active_window_idx] except Exception: @@ -416,18 +438,18 @@ class Layout: # {{{ 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) - self.set_active_window(all_windows, active_window_idx) - return active_window_idx + return self.set_active_window(all_windows, active_window_idx) - def update_visibility(self, all_windows, active_window, overlaid_windows=None): + def update_visibility(self, all_windows: WindowList, active_window: Window, overlaid_windows: Optional[FrozenSet[Window]] = 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, active_window_idx): + 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 @@ -440,19 +462,24 @@ class Layout: # {{{ self.set_active_window_in_os_window(active_window_idx) return active_window_idx - def _set_dimensions(self): + def _set_dimensions(self) -> None: global central, cell_width, cell_height central, tab_bar, vw, vh, cell_width, cell_height = viewport_for_window(self.os_window_id) - def __call__(self, all_windows, active_window_idx): + 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] - active_window_idx = idx_for_id(active_window.id, windows) - if active_window_idx is None: - active_window_idx = idx_for_id(active_window.overlay_window_id, windows) or 0 + 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 @@ -462,10 +489,10 @@ class Layout: # {{{ self.do_layout_all_windows(windows, active_window_idx, all_windows) else: self.do_layout(windows, active_window_idx) - return idx_for_id(active_window.id, all_windows) + return cast(int, idx_for_id(active_window.id, all_windows)) # Utils {{{ - def layout_single_window(self, w): + def layout_single_window(self, w: Window) -> None: mw = self.margin_width if self.single_window_margin_width < 0 else self.single_window_margin_width decoration_pairs = ((self.padding_width + mw, self.padding_width + mw),) wg = layout_single_window(decoration_pairs, decoration_pairs) @@ -494,54 +521,54 @@ class Layout: # {{{ height = central.height return layout_dimension(top, height, cell_height, decoration_pairs, bias=bias, left_align=align_top_left) - def simple_blank_rects(self, first_window: 'Window', last_window: 'Window') -> None: + def simple_blank_rects(self, first_window: Window, last_window: Window) -> None: br = self.blank_rects left_blank_rect(first_window, br) top_blank_rect(first_window, br) right_blank_rect(last_window, br) - def between_blank_rect(self, left_window, right_window, vertical=True): + def between_blank_rect(self, left_window: Window, right_window: Window, vertical: bool = True) -> None: if vertical: self.blank_rects.append(Rect(left_window.geometry.right, central.top, right_window.geometry.left, central.bottom + 1)) else: self.blank_rects.append(Rect(central.left, left_window.geometry.top, central.right + 1, right_window.geometry.bottom)) - def bottom_blank_rect(self, window): + def bottom_blank_rect(self, window: Window) -> None: self.blank_rects.append(Rect(window.geometry.left, window.geometry.bottom, window.geometry.right, central.bottom + 1)) # }}} - def do_layout(self, windows, active_window_idx): + def do_layout(self, windows: WindowList, active_window_idx: int) -> None: raise NotImplementedError() - def do_layout_all_windows(self, windows, active_window_idx, all_windows): + def do_layout_all_windows(self, windows: WindowList, active_window_idx: int, all_windows: WindowList) -> None: raise NotImplementedError() - def neighbors_for_window(self, window, windows) -> InternalNeighborsMap: + def neighbors_for_window(self, window: Window, windows: WindowList) -> InternalNeighborsMap: return {'left': [], 'right': [], 'top': [], 'bottom': []} - def compute_needs_borders_map(self, windows, active_window): + def compute_needs_borders_map(self, windows: WindowList, active_window: Window) -> Dict[int, bool]: return {w.id: ((w is active_window and draw_active_borders) or w.needs_attention) for w in windows} - def resolve_borders(self, windows, active_window): + def resolve_borders(self, windows: WindowList, active_window: Window) -> Generator[Borders, None, None]: if 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, None) + yield from Layout.minimal_borders(self, windows, active_window, {}) - def window_independent_borders(self, windows, active_windows): + def window_independent_borders(self, windows: WindowList, active_windows: WindowList) -> Generator[Tuple[int, int, int, int], None, None]: return - yield # type:ignore + yield (0, 0, 0, 0) # type: ignore - def minimal_borders(self, windows, active_window, needs_borders_map): + def minimal_borders(self, windows: WindowList, active_window: Window, needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]: for w in windows: if (w is active_window and draw_active_borders) or w.needs_attention: yield all_borders else: yield no_borders - def layout_action(self, action_name, args, all_windows, active_window_idx): - pass + def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList, active_window_idx: int) -> bool: + return False # }}} @@ -551,7 +578,7 @@ class Stack(Layout): # {{{ needs_window_borders = False only_active_window_visible = True - def do_layout(self, windows, active_window_idx): + def do_layout(self, windows: WindowList, active_window_idx: int) -> None: mw = self.margin_width if self.single_window_margin_width < 0 else self.single_window_margin_width decoration_pairs = ((mw + self.padding_width, mw + self.padding_width),) wg = layout_single_window(decoration_pairs, decoration_pairs, left_align=align_top_left) @@ -565,89 +592,93 @@ class Stack(Layout): # {{{ # Tall {{{ -def neighbors_for_tall_window(num_full_size_windows, window, windows): +def neighbors_for_tall_window(num_full_size_windows: int, window: Window, 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 = {'left': [prev] if prev is not None else [], 'right': [], 'top': [], 'bottom': []} + 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] + ans['right'] = [nxt.id] elif idx == num_full_size_windows - 1: - ans['right'] = windows[idx+1:] + ans['right'] = [w.id for w in windows[idx+1:]] else: - ans['left'] = [windows[num_full_size_windows - 1]] - if idx > num_full_size_windows: - ans['top'] = [prev] + 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] + 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 = False, False, False, True - only_main_border = False, False, True, False + only_between_border = Borders(False, False, False, True) + only_main_border = Borders(False, False, True, False) + layout_opts = TallLayoutOpts({}) @property - def num_full_size_windows(self): - return self.layout_opts['full_size'] + def num_full_size_windows(self) -> int: + return self.layout_opts.full_size - def remove_all_biases(self): - self.main_bias: Sequence[float] = list(self.layout_opts['bias']) - self.biased_map = {} + 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, num_windows, biased_map): + def variable_layout(self, num_windows: int, biased_map: Dict[int, float]) -> LayoutDimension: num_windows -= self.num_full_size_windows bias = variable_bias(num_windows, biased_map) if num_windows > 1 else None if self.main_is_horizontal: return self.ylayout(num_windows, bias=bias) return self.xlayout(num_windows, bias=bias) - def apply_bias(self, idx, increment, num_windows, is_horizontal): + def apply_bias(self, idx: int, increment: float, num_windows: int, is_horizontal: bool = True) -> bool: if self.main_is_horizontal == is_horizontal: - before = self.main_bias + 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) - after = self.main_bias - else: - 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(num_windows, self.biased_map)) - candidate = self.biased_map.copy() - before = candidate.get(idx, 0) - candidate[idx] = after = before + increment - if before_layout == list(self.variable_layout(num_windows, candidate)): - return False - self.biased_map = candidate + 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(num_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(num_windows, candidate)): + return False + self.biased_map = candidate return before != after - def parse_layout_opts(self, layout_opts): - ans = Layout.parse_layout_opts(self, layout_opts) - try: - ans['full_size'] = int(ans.get('full_size', 1)) - except Exception: - ans['full_size'] = 1 - ans['full_size'] = fs = max(1, min(ans['full_size'], 100)) - try: - b = int(ans.get('bias', 50)) / 100 - except Exception: - b = 0.5 - b = max(0.1, min(b, 0.9)) - ans['bias'] = tuple(repeat(b / fs, fs)) + (1.0 - b,) - return ans - - def do_layout(self, windows, active_window_idx): + def do_layout(self, windows: WindowList, active_window_idx: int) -> None: if len(windows) == 1: return self.layout_single_window(windows[0]) y, ynum = next(self.ylayout(1)) @@ -682,10 +713,10 @@ class Tall(Layout): # left, top and right blank rects self.simple_blank_rects(windows[0], windows[-1]) - def neighbors_for_window(self, window, windows): + def neighbors_for_window(self, window: Window, windows: WindowList) -> InternalNeighborsMap: return neighbors_for_tall_window(self.num_full_size_windows, window, windows) - def minimal_borders(self, windows, active_window, needs_borders_map): + def minimal_borders(self, windows: WindowList, active_window: Window, 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]: @@ -711,10 +742,10 @@ class Fat(Tall): # {{{ name = 'fat' main_is_horizontal = False - only_between_border = False, False, True, False - only_main_border = False, False, False, True + only_between_border = Borders(False, False, True, False) + only_main_border = Borders(False, False, False, True) - def do_layout(self, windows, active_window_idx): + def do_layout(self, windows: WindowList, active_window_idx: int) -> None: if len(windows) == 1: return self.layout_single_window(windows[0]) x, xnum = next(self.xlayout(1)) @@ -747,22 +778,22 @@ class Fat(Tall): # {{{ # left, top and right blank rects self.simple_blank_rects(windows[0], windows[-1]) - def neighbors_for_window(self, window, windows): + def neighbors_for_window(self, window: Window, 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 = {'left': [], 'right': [], 'top': [] if prev is None else [prev], 'bottom': []} + 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] + ans['bottom'] = [nxt.id] elif idx == self.num_full_size_windows - 1: - ans['bottom'] = windows[idx+1:] + ans['bottom'] = [w.id for w in windows[idx+1:]] else: - ans['top'] = [windows[self.num_full_size_windows - 1]] - if idx > self.num_full_size_windows: - ans['left'] = [prev] + 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] + ans['right'] = [nxt.id] return ans # }}} @@ -770,7 +801,7 @@ class Fat(Tall): # {{{ # Grid {{{ @lru_cache() -def calc_grid_size(n): +def calc_grid_size(n: int) -> Tuple[int, int, int, int]: if n <= 5: ncols = 1 if n == 1 else 2 else: @@ -787,22 +818,22 @@ class Grid(Layout): name = 'grid' - def remove_all_biases(self): - self.biased_rows = {} - self.biased_cols = {} + def remove_all_biases(self) -> bool: + self.biased_rows: Dict[int, float] = {} + self.biased_cols: Dict[int, float] = {} return True - def variable_layout(self, layout_func, num_windows, biased_map): + 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, increment, num_windows, is_horizontal): + def apply_bias(self, idx: int, increment: float, num_windows: int, is_horizontal: bool = True) -> bool: b = self.biased_cols if is_horizontal else self.biased_rows ncols, nrows, special_rows, special_col = calc_grid_size(num_windows) - def position_for_window_idx(idx): + def position_for_window_idx(idx: int) -> Tuple[int, int]: row_num = col_num = 0 - def on_col_done(col_windows): + def on_col_done(col_windows: List[int]) -> None: nonlocal col_num, row_num row_num = 0 col_num += 1 @@ -822,7 +853,7 @@ class Grid(Layout): bias_idx = col_num attr = 'biased_cols' - def layout_func(num_windows: int, bias=None) -> LayoutDimension: + def layout_func(num_windows: int, bias: Optional[Sequence[float]] = None) -> LayoutDimension: return self.xlayout(num_windows, bias=bias) else: @@ -832,7 +863,7 @@ class Grid(Layout): bias_idx = row_num attr = 'biased_rows' - def layout_func(num_windows: int, bias=None) -> LayoutDimension: + def layout_func(num_windows: int, bias: Optional[Sequence[float]] = None) -> LayoutDimension: return self.xlayout(num_windows, bias=bias) before_layout = list(self.variable_layout(layout_func, num_windows, b)) @@ -844,7 +875,13 @@ class Grid(Layout): setattr(self, attr, candidate) return True - def layout_windows(self, num_windows, nrows, ncols, special_rows, special_col, on_col_done=lambda col_windows: None): + 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, int, int, int, int], None, None]: # Distribute windows top-to-bottom, left-to-right (i.e. in columns) xlayout = self.variable_layout(self.xlayout, ncols, self.biased_cols) yvals_normal = tuple(self.variable_layout(self.ylayout, nrows, self.biased_rows)) @@ -862,7 +899,7 @@ class Grid(Layout): pos += rows on_col_done(col_windows) - def do_layout(self, windows, active_window_idx): + def do_layout(self, windows: WindowList, active_window_idx: int) -> None: n = len(windows) if n == 1: return self.layout_single_window(windows[0]) @@ -870,11 +907,11 @@ class Grid(Layout): win_col_map = [] - def on_col_done(col_windows): - col_windows = [windows[i] for i in col_windows] - win_col_map.append(col_windows) + 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) # bottom blank rect - self.bottom_blank_rect(col_windows[-1]) + self.bottom_blank_rect(col_windows_w[-1]) for window_idx, xstart, xnum, ystart, ynum in self.layout_windows( len(windows), nrows, ncols, special_rows, special_col, on_col_done): @@ -888,10 +925,10 @@ class Grid(Layout): for i in range(ncols - 1): self.between_blank_rect(win_col_map[i][0], win_col_map[i + 1][0]) - def minimal_borders(self, windows, active_window, needs_borders_map): + def minimal_borders(self, windows: WindowList, active_window: Window, needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]: n = len(windows) ncols, nrows, special_rows, special_col = calc_grid_size(n) - blank_row = [None for i in range(ncols)] + 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 = {} @@ -919,41 +956,39 @@ class Grid(Layout): bottom_neighbor_id: Optional[int] = matrix[row+1][col] except IndexError: bottom_neighbor_id = None - yield ( + yield Borders( False, False, (right_neighbor_id is not None and not needs_borders_map[right_neighbor_id]) or next_col_has_different_count, bottom_neighbor_id is not None and not needs_borders_map[bottom_neighbor_id] ) - def neighbors_for_window(self, window, windows): + def neighbors_for_window(self, window: Window, 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 = [None for i in range(ncols)] + 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 = {} col_counts = [] - id_map = {} 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 - id_map[wid] = w col_counts.append(rows) row, col = pos_map[window.id] - def neighbors(row, col): + def neighbors(row: int, col: int) -> List[int]: try: ans = matrix[row][col] except IndexError: ans = None - return [] if ans is None else [id_map[ans]] + return [] if ans is None else [ans] - def side(row, col, delta): + 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) @@ -974,19 +1009,19 @@ class Vertical(Layout): # {{{ name = 'vertical' main_is_horizontal = False - only_between_border = False, False, False, True + only_between_border = Borders(False, False, False, True) - def variable_layout(self, num_windows, biased_map): + def variable_layout(self, num_windows: int, biased_map: Dict[int, float]) -> LayoutDimension: bias = variable_bias(num_windows, biased_map) if num_windows else None if self.main_is_horizontal: return self.xlayout(num_windows, bias=bias) return self.ylayout(num_windows, bias=bias) - def remove_all_biases(self): - self.biased_map = {} + def remove_all_biases(self) -> bool: + self.biased_map: Dict[int, float] = {} return True - def apply_bias(self, idx, increment, num_windows, is_horizontal): + def apply_bias(self, idx: int, increment: float, num_windows: int, is_horizontal: bool = True) -> bool: if self.main_is_horizontal != is_horizontal: return False if num_windows < 2: @@ -1000,7 +1035,7 @@ class Vertical(Layout): # {{{ self.biased_map = candidate return True - def do_layout(self, windows, active_window_idx): + def do_layout(self, windows: WindowList, active_window_idx: int) -> None: window_count = len(windows) if window_count == 1: return self.layout_single_window(windows[0]) @@ -1016,7 +1051,7 @@ class Vertical(Layout): # {{{ # left, top and right blank rects self.simple_blank_rects(windows[0], windows[-1]) - def minimal_borders(self, windows, active_window, needs_borders_map): + def minimal_borders(self, windows: WindowList, active_window: Window, 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]: @@ -1030,10 +1065,10 @@ class Vertical(Layout): # {{{ else: yield self.only_between_border - def neighbors_for_window(self, window, windows): + def neighbors_for_window(self, window: Window, windows: WindowList) -> InternalNeighborsMap: idx = windows.index(window) - before = [] if window is windows[0] else [windows[idx-1]] - after = [] if window is windows[-1] else [windows[idx+1]] + 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': []} @@ -1045,9 +1080,9 @@ class Horizontal(Vertical): # {{{ name = 'horizontal' main_is_horizontal = True - only_between_border = False, False, True, False + only_between_border = Borders(False, False, True, False) - def do_layout(self, windows, active_window_idx): + def do_layout(self, windows: WindowList, active_window_idx: int) -> None: window_count = len(windows) if window_count == 1: return self.layout_single_window(windows[0]) @@ -1072,34 +1107,37 @@ class Horizontal(Vertical): # {{{ # Splits {{{ class Pair: - def __init__(self, horizontal=True): + def __init__(self, horizontal: bool = True): self.horizontal = horizontal - self.one = self.two = None + self.one: Optional[Union[Pair, int]] = None + self.two: Optional[Union[Pair, int]] = None self.bias = 0.5 - self.between_border = None + self.between_border: Optional[Tuple[int, int, int, int]] = None - def __repr__(self): + 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): + 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() - yield self.one + else: + yield self.one if self.two is not None: if isinstance(self.two, Pair): yield from self.two.all_window_ids() - yield self.two + else: + yield self.two - def self_and_descendants(self): + 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): + def pair_for_window(self, window_id: int) -> Optional['Pair']: if self.one == window_id or self.two == window_id: return self ans = None @@ -1109,12 +1147,12 @@ class Pair: ans = self.two.pair_for_window(window_id) return ans - def parent(self, root): + 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): + 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: @@ -1123,10 +1161,10 @@ class Pair: self.one, self.two = self.two, None @property - def is_redundant(self): + def is_redundant(self) -> bool: return self.one is None or self.two is None - def collapse_redundant_pairs(self): + 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: @@ -1136,7 +1174,7 @@ class Pair: if isinstance(self.two, Pair): self.two.collapse_redundant_pairs() - def balanced_add(self, window_id): + 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: @@ -1162,11 +1200,12 @@ class Pair: 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, new_window_id, horizontal, after): + 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 @@ -1181,15 +1220,15 @@ class Pair: tuple(map(pair.balanced_add, q)) return pair - def apply_window_geometry(self, window_id, window_geometry, id_window_map, id_idx_map): + def apply_window_geometry(self, window_id: int, window_geometry: WindowGeometry, id_window_map: Dict[int, Window], id_idx_map: Dict[int, int]) -> 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: - w = id_window_map.get(w.overlay_window_id) - if w is not None: - w.set_geometry(id_idx_map[w.id], window_geometry) + q = id_window_map.get(w.overlay_window_id) + if q is not None: + q.set_geometry(id_idx_map[q.id], window_geometry) - def blank_rects_for_window(self, layout_object, window, left, top, width, height): + def blank_rects_for_window(self, layout_object: Layout, window: Window, left: int, top: int, width: int, height: int) -> None: right = left + width - 1 bottom = top + height - 1 g = window.geometry @@ -1207,7 +1246,13 @@ class Pair: if b <= bottom: rects.append(Rect(left, b, right + 1, bottom + 1)) - def layout_pair(self, left, top, width, height, id_window_map, id_idx_map, layout_object): + def layout_pair( + self, + left: int, top: int, width: int, height: int, + id_window_map: Dict[int, Window], + 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 @@ -1263,7 +1308,7 @@ class Pair: self.apply_window_geometry(self.two, window_geometry(xstart, xnum, ystart, ynum), id_window_map, id_idx_map) self.blank_rects_for_window(layout_object, id_window_map[self.two], left, top + h1, width, h2) - def modify_size_of_child(self, which, increment, is_horizontal, 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 @@ -1278,16 +1323,18 @@ class Pair: return parent.modify_size_of_child(which, increment, is_horizontal, layout_object) return False - def neighbors_for_window(self, window_id, ans, layout_object): + def neighbors_for_window(self, window_id: int, ans: InternalNeighborsMap, layout_object: 'Splits') -> None: - def quadrant(is_horizontal, is_first): + def quadrant(is_horizontal: bool, is_first: bool) -> Tuple[EdgeLiteral, EdgeLiteral]: if is_horizontal: - edge, which = ('left', 'right') if is_first else ('right', 'left') - else: - edge, which = ('top', 'bottom') if is_first else ('bottom', 'top') - return edge, which + if is_first: + return 'left', 'right' + return 'right', 'left' + if is_first: + return 'top', 'bottom' + return 'bottom', 'top' - def extend(other, edge, which): + 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)) @@ -1306,7 +1353,7 @@ class Pair: extend(other, *quadrant(parent.horizontal, child is parent.one)) child = parent - def edge_windows(self, edge): + def edge_windows(self, edge: str) -> Generator[int, None, None]: if self.is_redundant: q = self.one or self.two if q: @@ -1331,31 +1378,35 @@ class Pair: 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): - return self.layout_opts['default_axis_is_horizontal'] + def default_axis_is_horizontal(self) -> bool: + return self.layout_opts.default_axis_is_horizontal @property - def pairs_root(self): - root = getattr(self, '_pairs_root', None) + 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): + def pairs_root(self, root: Pair) -> None: self._pairs_root = root - def parse_layout_opts(self, layout_opts): - ans = Layout.parse_layout_opts(self, layout_opts) - ans['default_axis_is_horizontal'] = ans.get('split_axis', 'horizontal') == 'horizontal' - return ans - - def do_layout_all_windows(self, windows, active_window_idx, all_windows): + 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) @@ -1382,7 +1433,13 @@ class Splits(Layout): else: root.layout_pair(central.left, central.top, central.width, central.height, id_window_map, id_idx_map, self) - def do_add_window(self, all_windows, window, current_active_window_idx, location): + def do_add_window( + self, + all_windows: WindowList, + window: Window, + current_active_window_idx: Optional[int], + location: Optional[str] + ) -> int: horizontal = self.default_axis_is_horizontal after = True if location is not None: @@ -1393,7 +1450,7 @@ class Splits(Layout): if location in ('before', 'first'): after = False active_window_idx = None - if 0 <= current_active_window_idx < len(all_windows): + 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) @@ -1410,7 +1467,13 @@ class Splits(Layout): all_windows.append(window) return active_window_idx - def modify_size_of_window(self, all_windows, window_id, increment, is_horizontal=True): + 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 @@ -1422,19 +1485,19 @@ class Splits(Layout): 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): + 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, active_windows): + def window_independent_borders(self, windows: WindowList, active_windows: WindowList) -> Generator[Tuple[int, int, int, int], None, None]: if not 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, windows) -> InternalNeighborsMap: + def neighbors_for_window(self, window: Window, 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': []} @@ -1442,11 +1505,11 @@ class Splits(Layout): pair.neighbors_for_window(window_id, ans, self) return ans - def swap_windows_in_layout(self, all_windows, a, b): - w1, w2 = all_windows[a], all_windows[b] + 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 + 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: @@ -1462,7 +1525,7 @@ class Splits(Layout): else: p2.two = w1 - def layout_action(self, action_name, args, all_windows, active_window_idx): + def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList, active_window_idx: int) -> bool: if action_name == 'rotate': args = args or ('90',) try: @@ -1482,6 +1545,7 @@ class Splits(Layout): if swap: pair.one, pair.two = pair.two, pair.one return True + return False # }}} @@ -1494,7 +1558,17 @@ all_layouts = {cast(str, o.name): o for o in globals().values() if isinstance(o, class CreateLayoutObjectFor: cache: Dict[Tuple, Layout] = {} - def __call__(self, name, os_window_id, tab_id, margin_width, single_window_margin_width, padding_width, border_width, layout_opts=''): + def __call__( + self, + name: str, + os_window_id: int, + tab_id: int, + margin_width: int, + single_window_margin_width: int, + padding_width: int, + border_width: int, + layout_opts: str = '' + ) -> Layout: key = name, os_window_id, tab_id, margin_width, single_window_margin_width, padding_width, border_width, layout_opts ans = create_layout_object_for.cache.get(key) if ans is None: @@ -1507,7 +1581,7 @@ class CreateLayoutObjectFor: create_layout_object_for = CreateLayoutObjectFor() -def evict_cached_layouts(tab_id): +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] diff --git a/kitty/main.py b/kitty/main.py index 467d1587f..4e979dd14 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -85,7 +85,7 @@ def talk_to_instance(args: CLIOptions) -> None: conn.close() -def load_all_shaders(semi_transparent: int = 0) -> None: +def load_all_shaders(semi_transparent: bool = False) -> None: load_shader_programs(semi_transparent) load_borders_program() diff --git a/kitty/notify.py b/kitty/notify.py index 931bef51c..ea4e3bcdd 100644 --- a/kitty/notify.py +++ b/kitty/notify.py @@ -2,7 +2,7 @@ # vim:fileencoding=utf-8 # License: GPLv3 Copyright: 2019, Kovid Goyal -from typing import Dict +from typing import Dict, Optional from .constants import is_macos, logo_png_file @@ -10,16 +10,14 @@ if is_macos: from .fast_data_types import cocoa_send_notification def notify( - title, - body, - timeout=5000, - application='kitty', - icon=True, - identifier=None - ): - if icon is True: - icon = None - cocoa_send_notification(identifier, title, body, icon) + title: str, + body: str, + timeout: int = 5000, + application: str = 'kitty', + icon: bool = True, + identifier: Optional[str] = None + ) -> None: + cocoa_send_notification(identifier, title, body, None) else: @@ -28,12 +26,12 @@ else: alloc_map: Dict[int, str] = {} identifier_map: Dict[str, int] = {} - def dbus_notification_created(alloc_id, notification_id): + def dbus_notification_created(alloc_id: int, notification_id: int) -> None: identifier = alloc_map.pop(alloc_id, None) if identifier is not None: identifier_map[identifier] = notification_id - def dbus_notification_activated(notification_id, action): + def dbus_notification_activated(notification_id: int, action: str) -> None: rmap = {v: k for k, v in identifier_map.items()} identifier = rmap.get(notification_id) if identifier is not None: @@ -41,15 +39,16 @@ else: notification_activated(identifier) def notify( - title, - body, - timeout=-1, - application='kitty', - icon=True, - identifier=None - ): + title: str, + body: str, + timeout: int = -1, + application: str = 'kitty', + icon: bool = True, + identifier: Optional[str] = None + ) -> None: + icf = '' if icon is True: - icon = logo_png_file - alloc_id = dbus_send_notification(application, icon, title, body, 'Click to see changes', timeout) + icf = logo_png_file + alloc_id = dbus_send_notification(application, icf, title, body, 'Click to see changes', timeout) if alloc_id and identifier is not None: alloc_map[alloc_id] = identifier diff --git a/kitty/rc/new_window.py b/kitty/rc/new_window.py index 4c823d7ff..7ccc744bf 100644 --- a/kitty/rc/new_window.py +++ b/kitty/rc/new_window.py @@ -116,10 +116,10 @@ the id of the new window will not be printed out. else: tabs = [boss.active_tab] tab = tabs[0] - w = tab.new_special_window(w) + ans = tab.new_special_window(w) if payload_get('keep_focus') and old_window: boss.set_active_window(old_window) - return None if payload_get('no_response') else str(w.id) + return None if payload_get('no_response') else str(ans.id) new_window = NewWindow() diff --git a/kitty/tab_bar.py b/kitty/tab_bar.py index 9dfbd630d..c98c049c7 100644 --- a/kitty/tab_bar.py +++ b/kitty/tab_bar.py @@ -2,8 +2,7 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2018, Kovid Goyal -from collections import namedtuple -from typing import Set, Tuple, Union +from typing import Any, Dict, NamedTuple, Optional, Sequence, Set, Tuple from .config import build_ansi_color_table from .constants import WindowGeometry @@ -12,25 +11,42 @@ from .fast_data_types import ( viewport_for_window ) from .layout import Rect +from .options_stub import Options +from .rgb import Color, alpha_blend, color_from_int from .utils import color_as_int, log_error from .window import calculate_gl_geometry -from .rgb import alpha_blend, color_from_int - -TabBarData = namedtuple('TabBarData', 'title is_active needs_attention') -DrawData = namedtuple( - 'DrawData', 'leading_spaces sep trailing_spaces bell_on_tab' - ' bell_fg alpha active_fg active_bg inactive_fg inactive_bg' - ' default_bg title_template active_title_template') -def as_rgb(x): +class TabBarData(NamedTuple): + title: str + is_active: bool + needs_attention: bool + + +class DrawData(NamedTuple): + leading_spaces: int + sep: str + trailing_spaces: int + bell_on_tab: bool + bell_fg: int + alpha: Sequence[float] + active_fg: Color + inactive_fg: Color + active_bg: Color + inactive_bg: Color + default_bg: Color + title_template: str + active_title_template: Optional[str] + + +def as_rgb(x: int) -> int: return (x << 8) | 2 template_failures: Set[str] = set() -def draw_title(draw_data, screen, tab, index): +def draw_title(draw_data: DrawData, screen: Screen, tab: TabBarData, index: int) -> None: if tab.needs_attention and draw_data.bell_on_tab: fg = screen.cursor.fg screen.cursor.fg = draw_data.bell_fg @@ -49,7 +65,7 @@ def draw_title(draw_data, screen, tab, index): screen.draw(title) -def draw_tab_with_separator(draw_data, screen, tab, before, max_title_length, index, is_last): +def draw_tab_with_separator(draw_data: DrawData, screen: Screen, tab: TabBarData, before: int, max_title_length: int, index: int, is_last: bool) -> int: if draw_data.leading_spaces: screen.draw(' ' * draw_data.leading_spaces) draw_title(draw_data, screen, tab, index) @@ -68,7 +84,7 @@ def draw_tab_with_separator(draw_data, screen, tab, before, max_title_length, in return end -def draw_tab_with_fade(draw_data, screen, tab, before, max_title_length, index, is_last): +def draw_tab_with_fade(draw_data: DrawData, screen: Screen, tab: TabBarData, before: int, max_title_length: int, index: int, is_last: bool) -> int: tab_bg = draw_data.active_bg if tab.is_active else draw_data.inactive_bg fade_colors = [as_rgb(color_as_int(alpha_blend(tab_bg, draw_data.default_bg, alpha))) for alpha in draw_data.alpha] for bg in fade_colors: @@ -96,7 +112,7 @@ def draw_tab_with_fade(draw_data, screen, tab, before, max_title_length, index, return end -def draw_tab_with_powerline(draw_data, screen, tab, before, max_title_length, index, is_last): +def draw_tab_with_powerline(draw_data: DrawData, screen: Screen, tab: TabBarData, before: int, max_title_length: int, index: int, is_last: bool) -> int: tab_bg = as_rgb(color_as_int(draw_data.active_bg if tab.is_active else draw_data.inactive_bg)) tab_fg = as_rgb(color_as_int(draw_data.active_fg if tab.is_active else draw_data.inactive_fg)) inactive_bg = as_rgb(color_as_int(draw_data.inactive_bg)) @@ -150,7 +166,7 @@ def draw_tab_with_powerline(draw_data, screen, tab, before, max_title_length, in class TabBar: - def __init__(self, os_window_id, opts): + def __init__(self, os_window_id: int, opts: Options): self.os_window_id = os_window_id self.opts = opts self.num_tabs = 1 @@ -165,7 +181,7 @@ class TabBar: color_as_int(opts.inactive_tab_foreground), color_as_int(opts.inactive_tab_background) ) - self.blank_rects: Union[Tuple, Tuple[Rect, Rect]] = () + self.blank_rects: Tuple[Rect, ...] = () sep = opts.tab_separator self.trailing_spaces = self.leading_spaces = 0 while sep and sep[0] == ' ': @@ -195,7 +211,7 @@ class TabBar: else: self.draw_func = draw_tab_with_fade - def patch_colors(self, spec): + def patch_colors(self, spec: Dict[str, Any]) -> None: if 'active_tab_foreground' in spec: self.active_fg = (spec['active_tab_foreground'] << 8) | 2 if 'active_tab_background' in spec: @@ -212,7 +228,7 @@ class TabBar: spec.get('inactive_tab_background', color_as_int(self.opts.inactive_tab_background)) ) - def layout(self): + def layout(self) -> None: central, tab_bar, vw, vh, cell_width, cell_height = viewport_for_window(self.os_window_id) if tab_bar.width < 2: return @@ -233,7 +249,7 @@ class TabBar: self.screen_geometry = sg = calculate_gl_geometry(g, vw, vh, cell_width, cell_height) set_tab_bar_render_data(self.os_window_id, sg.xstart, sg.ystart, sg.dx, sg.dy, self.screen) - def update(self, data): + def update(self, data: Sequence[TabBarData]) -> None: if not self.laid_out_once: return s = self.screen @@ -260,11 +276,11 @@ class TabBar: s.erase_in_line(0, False) # Ensure no long titles bleed after the last tab self.cell_ranges = cr - def destroy(self): + def destroy(self) -> None: self.screen.reset_callbacks() del self.screen - def tab_at(self, x): + def tab_at(self, x: int) -> Optional[int]: x = (x - self.window_geometry.left) // self.cell_width for i, (a, b) in enumerate(self.cell_ranges): if a <= x <= b: diff --git a/kitty/tabs.py b/kitty/tabs.py index be47b9d59..4bc7a9c7d 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -6,20 +6,43 @@ import weakref from collections import deque from contextlib import suppress from functools import partial -from typing import Deque, NamedTuple, Optional, List, Dict, cast +from typing import ( + TYPE_CHECKING, Deque, Dict, Generator, Iterator, List, NamedTuple, + Optional, Pattern, Sequence, Tuple, cast +) from .borders import Borders from .child import Child +from .cli_stub import CLIOptions from .constants import appname, is_macos, is_wayland from .fast_data_types import ( add_tab, attach_window, detach_window, get_boss, mark_tab_bar_dirty, next_window_id, pt_to_px, remove_tab, remove_window, ring_bell, set_active_tab, swap_tabs, x11_window_id ) -from .layout import create_layout_object_for, evict_cached_layouts +from .layout import ( + Layout, Rect, create_layout_object_for, evict_cached_layouts +) +from .options_stub import Options from .tab_bar import TabBar, TabBarData from .utils import log_error, resolved_shell -from .window import Window +from .window import Window, WindowDict + +if TYPE_CHECKING: + from .session import Session, Tab as SessionTab + SessionTab, Session + from typing import TypedDict +else: + TypedDict = dict + + +class TabDict(TypedDict): + id: int + is_focused: bool + title: str + layout: str + windows: List[WindowDict] + active_window_history: List[int] class SpecialWindowInstance(NamedTuple): @@ -32,7 +55,15 @@ class SpecialWindowInstance(NamedTuple): env: Optional[Dict[str, str]] -def SpecialWindow(cmd, stdin=None, override_title=None, cwd_from=None, cwd=None, overlay_for=None, env=None): +def SpecialWindow( + cmd: Optional[List[str]], + stdin: Optional[bytes] = None, + override_title: Optional[str] = None, + cwd_from: Optional[int] = None, + cwd: Optional[str] = None, + overlay_for: Optional[int] = None, + env: Optional[Dict[str, str]] = None +) -> SpecialWindowInstance: return SpecialWindowInstance(cmd, stdin, override_title, cwd_from, cwd, overlay_for, env) @@ -46,7 +77,14 @@ def add_active_id_to_history(items: Deque[int], item_id: int, maxlen: int = 64) class Tab: # {{{ - def __init__(self, tab_manager, session_tab=None, special_window=None, cwd_from=None, no_initial_window=False): + def __init__( + self, + tab_manager: 'TabManager', + session_tab: Optional['SessionTab'] = None, + special_window: Optional[SpecialWindowInstance] = None, + cwd_from: Optional[int] = None, + no_initial_window: bool = False + ): self._active_window_idx = 0 self.tab_manager_ref = weakref.ref(tab_manager) self.os_window_id = tab_manager.os_window_id @@ -62,7 +100,8 @@ class Tab: # {{{ self.windows: Deque[Window] = deque() 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)) - self._last_used_layout = self._current_layout_name = None + self._last_used_layout: Optional[str] = None + self._current_layout_name: Optional[str] = None self.cwd = self.args.directory if no_initial_window: self._set_current_layout(self.enabled_layouts[0]) @@ -80,7 +119,7 @@ class Tab: # {{{ self._set_current_layout(l0) self.startup(session_tab) - def recalculate_sizes(self, update_layout=True): + def recalculate_sizes(self, update_layout: bool = True) -> None: self.margin_width, self.padding_width, self.single_window_margin_width = map( lambda x: pt_to_px(getattr(self.opts, x), self.os_window_id), ( 'window_margin_width', 'window_padding_width', 'single_window_margin_width')) @@ -89,10 +128,11 @@ class Tab: # {{{ self.current_layout.update_sizes( self.margin_width, self.single_window_margin_width, self.padding_width, self.border_width) - def take_over_from(self, other_tab): + def take_over_from(self, other_tab: 'Tab') -> None: self.name, self.cwd = other_tab.name, other_tab.cwd self.enabled_layouts = list(other_tab.enabled_layouts) - self._set_current_layout(other_tab._current_layout_name) + if other_tab._current_layout_name: + self._set_current_layout(other_tab._current_layout_name) self._last_used_layout = other_tab._last_used_layout orig_windows = deque(other_tab.windows) @@ -110,12 +150,12 @@ class Tab: # {{{ attach_window(self.os_window_id, self.id, window.id) self.relayout() - def _set_current_layout(self, layout_name): + def _set_current_layout(self, layout_name: str) -> None: self._last_used_layout = self._current_layout_name self.current_layout = self.create_layout_object(layout_name) self._current_layout_name = layout_name - def startup(self, session_tab): + def startup(self, session_tab: 'SessionTab') -> None: for cmd in session_tab.windows: if isinstance(cmd, (SpecialWindowInstance,)): self.new_special_window(cmd) @@ -124,11 +164,11 @@ class Tab: # {{{ self.set_active_window_idx(session_tab.active_window_idx) @property - def active_window_idx(self): + def active_window_idx(self) -> int: return self._active_window_idx @active_window_idx.setter - def active_window_idx(self, val): + def active_window_idx(self, val: int) -> None: try: old_active_window: Optional[Window] = self.windows[self._active_window_idx] except Exception: @@ -160,35 +200,35 @@ class Tab: # {{{ def title(self) -> str: return cast(str, getattr(self.active_window, 'title', appname)) - def set_title(self, title): + def set_title(self, title: str) -> None: self.name = title or '' tm = self.tab_manager_ref() if tm is not None: tm.mark_tab_bar_dirty() - def title_changed(self, window): + def title_changed(self, window: Window) -> None: if window is self.active_window: tm = self.tab_manager_ref() if tm is not None: tm.mark_tab_bar_dirty() - def on_bell(self, window): + def on_bell(self, window: Window) -> None: tm = self.tab_manager_ref() if tm is not None: self.relayout_borders() tm.mark_tab_bar_dirty() - def visible_windows(self): + def visible_windows(self) -> Generator[Window, None, None]: for w in self.windows: if w.is_visible_in_layout: yield w - def relayout(self): + def relayout(self) -> None: if self.windows: self.active_window_idx = self.current_layout(self.windows, self.active_window_idx) self.relayout_borders() - def relayout_borders(self): + def relayout_borders(self) -> None: tm = self.tab_manager_ref() if tm is not None: visible_windows = [w for w in self.windows if w.is_visible_in_layout] @@ -202,13 +242,13 @@ class Tab: # {{{ if w is not None: w.change_titlebar_color() - def create_layout_object(self, name): + def create_layout_object(self, name: str) -> Layout: return create_layout_object_for( name, self.os_window_id, self.id, self.margin_width, self.single_window_margin_width, self.padding_width, self.border_width) - def next_layout(self): + def next_layout(self) -> None: if len(self.enabled_layouts) > 1: for i, layout_name in enumerate(self.enabled_layouts): if layout_name == self.current_layout.full_name: @@ -220,12 +260,12 @@ class Tab: # {{{ self._set_current_layout(nl) self.relayout() - def last_used_layout(self): + def last_used_layout(self) -> None: if len(self.enabled_layouts) > 1 and self._last_used_layout and self._last_used_layout != self._current_layout_name: self._set_current_layout(self._last_used_layout) self.relayout() - def goto_layout(self, layout_name, raise_exception=False): + def goto_layout(self, layout_name: str, raise_exception: bool = False) -> None: layout_name = layout_name.lower() if layout_name not in self.enabled_layouts: if raise_exception: @@ -235,14 +275,14 @@ class Tab: # {{{ self._set_current_layout(layout_name) self.relayout() - def resize_window_by(self, window_id, increment, is_horizontal): + def resize_window_by(self, window_id: int, increment: int, is_horizontal: bool) -> Optional[str]: increment_as_percent = self.current_layout.bias_increment_for_cell(is_horizontal) * increment if self.current_layout.modify_size_of_window(self.windows, window_id, increment_as_percent, is_horizontal): self.relayout() - return + return None return 'Could not resize' - def resize_window(self, quality, increment): + def resize_window(self, quality: str, increment: int) -> None: if increment < 1: raise ValueError(increment) is_horizontal = quality in ('wider', 'narrower') @@ -252,11 +292,11 @@ class Tab: # {{{ increment, is_horizontal) is not None: ring_bell() - def reset_window_sizes(self): + def reset_window_sizes(self) -> None: if self.current_layout.remove_all_biases(): self.relayout() - def layout_action(self, action_name, args): + def layout_action(self, action_name: str, args: Sequence[str]) -> None: ret = self.current_layout.layout_action(action_name, args, self.windows, self.active_window_idx) if ret is None: ring_bell() @@ -265,7 +305,16 @@ class Tab: # {{{ self.active_window_idx = ret self.relayout() - def launch_child(self, use_shell=False, cmd=None, stdin=None, cwd_from=None, cwd=None, env=None, allow_remote_control=False): + def launch_child( + self, + use_shell: bool = False, + cmd: Optional[List[str]] = None, + stdin: Optional[bytes] = None, + cwd_from: Optional[int] = None, + cwd: Optional[str] = None, + env: Optional[Dict[str, str]] = None, + allow_remote_control: bool = False + ) -> Child: if cmd is None: if use_shell: cmd = resolved_shell(self.opts) @@ -285,7 +334,7 @@ class Tab: # {{{ ans.fork() return ans - def _add_window(self, window, location=None): + def _add_window(self, window: Window, location: Optional[str] = None) -> None: self.active_window_idx = self.current_layout.add_window(self.windows, window, self.active_window_idx, location) self.relayout_borders() @@ -337,20 +386,20 @@ class Tab: # {{{ allow_remote_control=allow_remote_control ) - def close_window(self): + def close_window(self) -> None: if self.windows: self.remove_window(self.windows[self.active_window_idx]) - def previous_active_window_idx(self, num): + def previous_active_window_idx(self, num: int) -> Optional[int]: try: old_window_id = self.active_window_history[-num] except IndexError: - return + return None for idx, w in enumerate(self.windows): if w.id == old_window_id: return idx - def remove_window(self, window, destroy=True): + def remove_window(self, window: Window, destroy: bool = True) -> None: idx = self.previous_active_window_idx(1) next_window_id = None if idx is not None: @@ -382,9 +431,9 @@ class Tab: # {{{ if active_window: self.title_changed(active_window) - def detach_window(self, window): - underlaid_window = None - overlaid_window = window + def detach_window(self, window: Window) -> Tuple[Optional[Window], Optional[Window]]: + underlaid_window: Optional[Window] = None + overlaid_window: Optional[Window] = window if window.overlay_for: for x in self.windows: if x.id == window.overlay_for: @@ -403,28 +452,28 @@ class Tab: # {{{ self.remove_window(underlaid_window, destroy=False) return underlaid_window, overlaid_window - def attach_window(self, window): + def attach_window(self, window: Window) -> None: window.change_tab(self) attach_window(self.os_window_id, self.id, window.id) self._add_window(window) - def set_active_window_idx(self, idx): + def set_active_window_idx(self, idx: int) -> None: if idx != self.active_window_idx: self.active_window_idx = self.current_layout.set_active_window(self.windows, idx) self.relayout_borders() - def set_active_window(self, window): + def set_active_window(self, window: Window) -> None: try: idx = self.windows.index(window) except ValueError: return self.set_active_window_idx(idx) - def get_nth_window(self, n): + def get_nth_window(self, n: int) -> Optional[Window]: if self.windows: - return self.current_layout.nth_window(self.windows, n, make_active=False) + return self.current_layout.nth_window(self.windows, n) - def nth_window(self, num=0): + def nth_window(self, num: int = 0) -> None: if self.windows: if num < 0: idx = self.previous_active_window_idx(-num) @@ -432,72 +481,72 @@ class Tab: # {{{ return self.active_window_idx = self.current_layout.set_active_window(self.windows, idx) else: - self.active_window_idx = self.current_layout.nth_window(self.windows, num) + self.active_window_idx = self.current_layout.activate_nth_window(self.windows, num) self.relayout_borders() - def _next_window(self, delta=1): + def _next_window(self, delta: int = 1) -> None: if len(self.windows) > 1: self.active_window_idx = self.current_layout.next_window(self.windows, self.active_window_idx, delta) self.relayout_borders() - def next_window(self): + def next_window(self) -> None: self._next_window() - def previous_window(self): + def previous_window(self) -> None: self._next_window(-1) prev_window = previous_window - def neighboring_window(self, which): + def neighboring_window(self, which: str) -> None: neighbors = self.current_layout.neighbors(self.windows, self.active_window_idx) - candidates = neighbors.get(which) + candidates = cast(Optional[Tuple[int, ...]], neighbors.get(which)) if candidates: self.active_window_idx = self.current_layout.set_active_window(self.windows, candidates[0]) self.relayout_borders() - def move_window(self, delta=1): + def move_window(self, delta: int = 1) -> None: self.active_window_idx = self.current_layout.move_window(self.windows, self.active_window_idx, delta) self.relayout() - def move_window_to_top(self): + def move_window_to_top(self) -> None: self.move_window(-self.active_window_idx) - def move_window_forward(self): + def move_window_forward(self) -> None: self.move_window() - def move_window_backward(self): + def move_window_backward(self) -> None: self.move_window(-1) - def list_windows(self, active_window): + def list_windows(self, active_window: Optional[Window]) -> Generator[WindowDict, None, None]: for w in self: yield w.as_dict(is_focused=w is active_window) - def matches(self, field, pat): + def matches(self, field: str, pat: Pattern) -> bool: if field == 'id': - return pat.pattern == str(self.id) + return bool(pat.pattern == str(self.id)) if field == 'title': return pat.search(self.name or self.title) is not None return False - def __iter__(self): + def __iter__(self) -> Iterator[Window]: return iter(self.windows) - def __len__(self): + def __len__(self) -> int: return len(self.windows) - def __contains__(self, window): + def __contains__(self, window: Window) -> bool: return window in self.windows - def destroy(self): + def destroy(self) -> None: evict_cached_layouts(self.id) for w in self.windows: w.destroy() self.windows = deque() - def __repr__(self): + def __repr__(self) -> str: return 'Tab(title={}, id={})'.format(self.name or self.title, hex(id(self))) - def make_active(self): + def make_active(self) -> None: tm = self.tab_manager_ref() if tm is not None: tm.set_active_tab(self) @@ -506,7 +555,7 @@ class Tab: # {{{ class TabManager: # {{{ - def __init__(self, os_window_id, opts, args, startup_session=None): + def __init__(self, os_window_id: int, opts: Options, args: CLIOptions, startup_session: Optional['Session'] = None): self.os_window_id = os_window_id self.last_active_tab_id = None self.opts, self.args = opts, args @@ -522,11 +571,11 @@ class TabManager: # {{{ self._set_active_tab(max(0, min(startup_session.active_tab_idx, len(self.tabs) - 1))) @property - def active_tab_idx(self): + def active_tab_idx(self) -> int: return self._active_tab_idx @active_tab_idx.setter - def active_tab_idx(self, val): + def active_tab_idx(self, val: int) -> None: try: old_active_tab: Optional[Tab] = self.tabs[self._active_tab_idx] except Exception: @@ -549,48 +598,48 @@ class TabManager: # {{{ if w is not None: w.focus_changed(True) - def refresh_sprite_positions(self): + def refresh_sprite_positions(self) -> None: if not self.tab_bar_hidden: self.tab_bar.screen.refresh_sprite_positions() @property - def tab_bar_should_be_visible(self): + def tab_bar_should_be_visible(self) -> bool: return len(self.tabs) >= self.opts.tab_bar_min_tabs - def _add_tab(self, tab): + def _add_tab(self, tab: Tab) -> None: visible_before = self.tab_bar_should_be_visible self.tabs.append(tab) if not visible_before and self.tab_bar_should_be_visible: self.tabbar_visibility_changed() - def _remove_tab(self, tab): + def _remove_tab(self, tab: Tab) -> None: visible_before = self.tab_bar_should_be_visible remove_tab(self.os_window_id, tab.id) self.tabs.remove(tab) if visible_before and not self.tab_bar_should_be_visible: self.tabbar_visibility_changed() - def _set_active_tab(self, idx): + def _set_active_tab(self, idx: int) -> None: self.active_tab_idx = idx set_active_tab(self.os_window_id, idx) - def tabbar_visibility_changed(self): + def tabbar_visibility_changed(self) -> None: if not self.tab_bar_hidden: self.tab_bar.layout() self.resize(only_tabs=True) - def mark_tab_bar_dirty(self): + def mark_tab_bar_dirty(self) -> None: if self.tab_bar_should_be_visible and not self.tab_bar_hidden: mark_tab_bar_dirty(self.os_window_id) - def update_tab_bar_data(self): + def update_tab_bar_data(self) -> None: self.tab_bar.update(self.tab_bar_data) - def update_dpi_based_sizes(self): + def update_dpi_based_sizes(self) -> None: for tab in self.tabs: tab.recalculate_sizes() - def resize(self, only_tabs=False): + def resize(self, only_tabs: bool = False) -> None: if not only_tabs: if not self.tab_bar_hidden: self.tab_bar.layout() @@ -598,12 +647,14 @@ class TabManager: # {{{ for tab in self.tabs: tab.relayout() - def set_active_tab_idx(self, idx): + def set_active_tab_idx(self, idx: int) -> None: self._set_active_tab(idx) - self.active_tab.relayout_borders() + tab = self.active_tab + if tab is not None: + tab.relayout_borders() self.mark_tab_bar_dirty() - def set_active_tab(self, tab): + def set_active_tab(self, tab: Tab) -> bool: try: idx = self.tabs.index(tab) except Exception: @@ -611,11 +662,11 @@ class TabManager: # {{{ self.set_active_tab_idx(idx) return True - def next_tab(self, delta=1): + def next_tab(self, delta: int = 1) -> None: if len(self.tabs) > 1: self.set_active_tab_idx((self.active_tab_idx + len(self.tabs) + delta) % len(self.tabs)) - def goto_tab(self, tab_num): + def goto_tab(self, tab_num: int) -> None: if tab_num >= len(self.tabs): tab_num = max(0, len(self.tabs) - 1) if tab_num >= 0: @@ -630,39 +681,39 @@ class TabManager: # {{{ self.set_active_tab_idx(idx) break - def __iter__(self): + def __iter__(self) -> Iterator[Tab]: return iter(self.tabs) - def __len__(self): + def __len__(self) -> int: return len(self.tabs) - def list_tabs(self, active_tab, active_window): + def list_tabs(self, active_tab: Optional[Tab], active_window: Optional[Window]) -> Generator[TabDict, None, None]: for tab in self: yield { 'id': tab.id, 'is_focused': tab is active_tab, 'title': tab.name or tab.title, - 'layout': tab.current_layout.name, + 'layout': str(tab.current_layout.name), 'windows': list(tab.list_windows(active_window)), 'active_window_history': list(tab.active_window_history), } @property - def active_tab(self): + def active_tab(self) -> Optional[Tab]: return self.tabs[self.active_tab_idx] if self.tabs else None @property - def active_window(self): + def active_window(self) -> Optional[Window]: t = self.active_tab if t is not None: return t.active_window - def tab_for_id(self, tab_id): + def tab_for_id(self, tab_id: int) -> Optional[Tab]: for t in self.tabs: if t.id == tab_id: return t - def move_tab(self, delta=1): + def move_tab(self, delta: int = 1) -> None: if len(self.tabs) > 1: idx = self.active_tab_idx nidx = (idx + len(self.tabs) + delta) % len(self.tabs) @@ -673,7 +724,14 @@ class TabManager: # {{{ self._set_active_tab(nidx) self.mark_tab_bar_dirty() - def new_tab(self, special_window=None, cwd_from=None, as_neighbor=False, empty_tab=False, location='last'): + def new_tab( + self, + special_window: Optional[SpecialWindowInstance] = None, + cwd_from: Optional[int] = None, + as_neighbor: bool = False, + empty_tab: bool = False, + location: str = 'last' + ) -> Tab: idx = len(self.tabs) orig_active_tab_idx = self.active_tab_idx self._add_tab(Tab(self, no_initial_window=True) if empty_tab else Tab(self, special_window=special_window, cwd_from=cwd_from)) @@ -696,7 +754,7 @@ class TabManager: # {{{ self.mark_tab_bar_dirty() return self.tabs[idx] - def remove(self, tab): + def remove(self, tab: Tab) -> None: self._remove_tab(tab) next_active_tab = -1 while True: @@ -723,7 +781,7 @@ class TabManager: # {{{ tab.destroy() @property - def tab_bar_data(self): + def tab_bar_data(self) -> List[TabBarData]: at = self.active_tab ans = [] for t in self.tabs: @@ -736,16 +794,16 @@ class TabManager: # {{{ ans.append(TabBarData(title, t is at, needs_attention)) return ans - def activate_tab_at(self, x): + def activate_tab_at(self, x: int) -> None: i = self.tab_bar.tab_at(x) if i is not None: self.set_active_tab_idx(i) @property - def blank_rects(self): + def blank_rects(self) -> Tuple[Rect, ...]: return self.tab_bar.blank_rects if self.tab_bar_should_be_visible else () - def destroy(self): + def destroy(self) -> None: for t in self: t.destroy() self.tab_bar.destroy() diff --git a/kitty/window.py b/kitty/window.py index 67cadafa8..f50218f2d 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -10,8 +10,13 @@ from collections import deque from enum import IntEnum from itertools import chain from re import Pattern -from typing import TYPE_CHECKING, Callable, Deque, Dict, List, Optional, Tuple, Union +from typing import ( + TYPE_CHECKING, Any, Callable, Deque, Dict, List, Optional, Sequence, Tuple, + Union +) +from .child import ProcessDesc +from .cli_stub import CLIOptions from .config import build_ansi_color_table from .constants import ScreenGeometry, WindowGeometry, appname, wakeup from .fast_data_types import ( @@ -25,6 +30,7 @@ from .fast_data_types import ( update_window_title, update_window_visibility, viewport_for_window ) from .keys import defines, extended_key_event, keyboard_mode_name +from .options_stub import Options from .rgb import to_color from .terminfo import get_capabilities from .utils import ( @@ -35,11 +41,37 @@ from .utils import ( if TYPE_CHECKING: from .tabs import Tab - Tab + from .child import Child + from typing import TypedDict + from .rc.base import RemoteCommand + Tab, Child, RemoteCommand +else: + TypedDict = dict MatchPatternType = Union[Pattern, Tuple[Pattern, Optional[Pattern]]] +class WindowDict(TypedDict): + id: int + is_focused: bool + title: str + pid: Optional[int] + cwd: str + cmdline: List[str] + env: Dict[str, str] + foreground_processes: List[ProcessDesc] + + +class PipeData(TypedDict): + input_line_number: int + scrolled_by: int + cursor_x: int + cursor_y: int + lines: int + columns: int + text: str + + class DynamicColor(IntEnum): default_fg, default_bg, cursor_color, highlight_fg, highlight_bg = range(1, 6) @@ -54,7 +86,7 @@ DYNAMIC_COLOR_CODES = { DYNAMIC_COLOR_CODES.update({k+100: v for k, v in DYNAMIC_COLOR_CODES.items()}) -def calculate_gl_geometry(window_geometry, viewport_width, viewport_height, cell_width, cell_height): +def calculate_gl_geometry(window_geometry: WindowGeometry, viewport_width: int, viewport_height: int, cell_width: int, cell_height: int) -> ScreenGeometry: dx, dy = 2 * cell_width / viewport_width, 2 * cell_height / viewport_height xmargin = window_geometry.left / viewport_width ymargin = window_geometry.top / viewport_height @@ -67,7 +99,7 @@ class LoadShaderPrograms: use_selection_fg = True - def __call__(self, semi_transparent=False): + def __call__(self, semi_transparent: bool = False) -> None: compile_program(BLIT_PROGRAM, *load_shaders('blit')) v, f = load_shaders('cell') @@ -114,7 +146,7 @@ class LoadShaderPrograms: load_shader_programs = LoadShaderPrograms() -def setup_colors(screen, opts): +def setup_colors(screen: Screen, opts: Options) -> None: screen.color_profile.update_ansi_color_table(build_ansi_color_table(opts)) cursor_text_color = opts.cursor_text_color or (12, 12, 12) cursor_text_color_as_bg = 3 if opts.cursor_text_color is None else 1 @@ -126,7 +158,7 @@ def setup_colors(screen, opts): )) -def text_sanitizer(as_ansi, add_wrap_markers): +def text_sanitizer(as_ansi: bool, add_wrap_markers: bool) -> Callable[[str], str]: pat = getattr(text_sanitizer, 'pat', None) if pat is None: import re @@ -135,14 +167,14 @@ def text_sanitizer(as_ansi, add_wrap_markers): ansi, wrap_markers = not as_ansi, not add_wrap_markers - def remove_wrap_markers(line): + def remove_wrap_markers(line: str) -> str: return line.replace('\r', '') - def remove_sgr(line): - return pat.sub('', line) + def remove_sgr(line: str) -> str: + return str(pat.sub('', line)) - def remove_both(line): - return pat.sub('', line.replace('\r', '')) + def remove_both(line: str) -> str: + return str(pat.sub('', line.replace('\r', ''))) if ansi: return remove_both if wrap_markers else remove_sgr @@ -151,14 +183,23 @@ def text_sanitizer(as_ansi, add_wrap_markers): class Window: - def __init__(self, tab, child, opts, args, override_title=None, copy_colors_from=None): - self.action_on_close = self.action_on_removal = None - self.current_marker_spec = None + def __init__( + self, + tab: 'Tab', + child: 'Child', + opts: Options, + args: CLIOptions, + override_title: Optional[str] = None, + copy_colors_from: Optional['Window'] = None + ): + self.action_on_close: Optional[Callable] = None + self.action_on_removal: Optional[Callable] = None + self.current_marker_spec: Optional[Tuple[str, Union[str, Tuple[Tuple[int, str], ...]]]] = None self.pty_resized_once = False self.needs_attention = False self.override_title = override_title - self.overlay_window_id = None - self.overlay_for = None + self.overlay_window_id: Optional[int] = None + self.overlay_for: Optional[int] = None self.default_title = os.path.basename(child.argv[0] or appname) self.child_title = self.default_title self.title_stack: Deque[str] = deque(maxlen=10) @@ -182,20 +223,20 @@ class Window: else: setup_colors(self.screen, opts) - def change_tab(self, tab): + def change_tab(self, tab: 'Tab') -> None: self.tab_id = tab.id self.os_window_id = tab.os_window_id self.tabref = weakref.ref(tab) @property - def title(self): + def title(self) -> str: return self.override_title or self.child_title - def __repr__(self): + def __repr__(self) -> str: return 'Window(title={}, id={}, overlay_for={}, overlay_window_id={})'.format( self.title, self.id, self.overlay_for, self.overlay_window_id) - def as_dict(self, is_focused=False): + def as_dict(self, is_focused: bool = False) -> WindowDict: return dict( id=self.id, is_focused=is_focused, @@ -236,7 +277,7 @@ class Window: return False return False - def set_visible_in_layout(self, window_idx, val): + def set_visible_in_layout(self, window_idx: int, val: bool) -> None: val = bool(val) if val is not self.is_visible_in_layout: self.is_visible_in_layout = val @@ -244,16 +285,16 @@ class Window: if val: self.refresh() - def refresh(self): + def refresh(self) -> None: self.screen.mark_as_dirty() wakeup() - def update_position(self, window_geometry): + def update_position(self, window_geometry: WindowGeometry) -> ScreenGeometry: central, tab_bar, vw, vh, cw, ch = viewport_for_window(self.os_window_id) self.screen_geometry = sg = calculate_gl_geometry(window_geometry, vw, vh, cw, ch) return sg - def set_geometry(self, window_idx, new_geometry): + def set_geometry(self, window_idx: int, new_geometry: WindowGeometry) -> None: if self.destroyed: return if self.needs_layout or new_geometry.xnum != self.screen.columns or new_geometry.ynum != self.screen.lines: @@ -273,45 +314,45 @@ class Window: self.geometry = g = new_geometry set_window_render_data(self.os_window_id, self.tab_id, self.id, window_idx, sg.xstart, sg.ystart, sg.dx, sg.dy, self.screen, *g[:4]) - def contains(self, x, y): + def contains(self, x: int, y: int) -> bool: g = self.geometry return g.left <= x <= g.right and g.top <= y <= g.bottom - def close(self): + def close(self) -> None: get_boss().close_window(self) - def send_text(self, *args): + def send_text(self, *args: str) -> bool: mode = keyboard_mode_name(self.screen) - required_mode, text = args[-2:] - required_mode = frozenset(required_mode.split(',')) + required_mode_, text = args[-2:] + required_mode = frozenset(required_mode_.split(',')) if not required_mode & {mode, 'all'}: return True if not text: return True self.write_to_child(text) - def write_to_child(self, data): + def write_to_child(self, data: Union[str, bytes]) -> None: if data: if get_boss().child_monitor.needs_write(self.id, data) is not True: print('Failed to write to child %d as it does not exist' % self.id, file=sys.stderr) - def title_updated(self): + def title_updated(self) -> None: update_window_title(self.os_window_id, self.tab_id, self.id, self.title) t = self.tabref() if t is not None: t.title_changed(self) - def set_title(self, title): + def set_title(self, title: Optional[str]) -> None: if title: title = sanitize_title(title) self.override_title = title or None self.title_updated() # screen callbacks {{{ - def use_utf8(self, on): + def use_utf8(self, on: bool) -> None: get_boss().child_monitor.set_iutf8_winid(self.id, on) - def focus_changed(self, focused): + def focus_changed(self, focused: bool) -> None: if focused: self.needs_attention = False if self.screen.focus_tracking_enabled: @@ -320,19 +361,19 @@ class Window: if self.screen.focus_tracking_enabled: self.screen.send_escape_code_to_child(CSI, 'O') - def title_changed(self, new_title): + def title_changed(self, new_title: Optional[str]) -> None: self.child_title = sanitize_title(new_title or self.default_title) if self.override_title is None: self.title_updated() - def icon_changed(self, new_icon): + def icon_changed(self, new_icon: object) -> None: pass # TODO: Implement this @property - def is_active(self): + def is_active(self) -> bool: return get_boss().active_window is self - def on_bell(self): + def on_bell(self) -> None: if self.opts.command_on_bell and self.opts.command_on_bell != ['none']: import subprocess import shlex @@ -345,7 +386,7 @@ class Window: if tab is not None: tab.on_bell(self) - def change_titlebar_color(self): + def change_titlebar_color(self) -> None: val = self.opts.macos_titlebar_color if val: if (val & 0xff) == 1: @@ -354,17 +395,17 @@ class Window: val = val >> 8 set_titlebar_color(self.os_window_id, val) - def change_colors(self, changes): + def change_colors(self, changes: Dict[DynamicColor, Optional[str]]) -> None: dirtied = default_bg_changed = False - def item(raw): + def item(raw: Optional[str]) -> Optional[int]: if raw is None: return 0 val = to_color(raw) return None if val is None else (color_as_int(val) << 8) | 2 - for which, val in changes.items(): - val = item(val) + for which, val_ in changes.items(): + val = item(val_) if val is None: continue dirtied = True @@ -376,16 +417,16 @@ class Window: if default_bg_changed: get_boss().default_bg_changed_for(self.id) - def report_color(self, code, r, g, b): + def report_color(self, code: str, r: int, g: int, b: int) -> None: r |= r << 8 g |= g << 8 b |= b << 8 self.screen.send_escape_code_to_child(OSC, '{};rgb:{:04x}/{:04x}/{:04x}'.format(code, r, g, b)) - def set_dynamic_color(self, code, value): + def set_dynamic_color(self, code: int, value: Union[str, bytes]) -> None: if isinstance(value, bytes): value = value.decode('utf-8') - color_changes = {} + color_changes: Dict[DynamicColor, Optional[str]] = {} for val in value.split(';'): w = DYNAMIC_COLOR_CODES.get(code) if w is not None: @@ -393,14 +434,13 @@ class Window: col = getattr(self.screen.color_profile, w.name) self.report_color(str(code), col >> 16, (col >> 8) & 0xff, col & 0xff) else: - if code >= 110: - val = None - color_changes[w] = val + q = None if code >= 100 else val + color_changes[w] = q code += 1 if color_changes: self.change_colors(color_changes) - def set_color_table_color(self, code, value): + def set_color_table_color(self, code: int, value: str) -> None: cp = self.screen.color_profile if code == 4: changed = False @@ -416,31 +456,31 @@ class Window: if not value.strip(): cp.reset_color_table() else: - for c in value.split(';'): + for x in value.split(';'): try: - c = int(c) + y = int(x) except Exception: continue - if 0 <= c <= 255: - cp.reset_color(c) + if 0 <= y <= 255: + cp.reset_color(y) self.refresh() - def request_capabilities(self, q): + def request_capabilities(self, q: str) -> None: self.screen.send_escape_code_to_child(DCS, get_capabilities(q)) - def handle_remote_cmd(self, cmd): + def handle_remote_cmd(self, cmd: 'RemoteCommand') -> None: get_boss().handle_remote_cmd(cmd, self) - def handle_remote_print(self, msg): + def handle_remote_print(self, msg: bytes) -> None: from base64 import standard_b64decode - msg = standard_b64decode(msg).decode('utf-8') - print(msg, end='', file=sys.stderr) + text = standard_b64decode(msg).decode('utf-8') + print(text, end='', file=sys.stderr) sys.stderr.flush() - def send_cmd_response(self, response): + def send_cmd_response(self, response: Any) -> None: self.screen.send_escape_code_to_child(DCS, '@kitty-cmd' + json.dumps(response)) - def clipboard_control(self, data): + def clipboard_control(self, data: str) -> None: where, text = data.partition(';')[::2] if not where: where = 's0' @@ -464,7 +504,7 @@ class Window: except Exception: text = '' - def write(key, func): + def write(key: str, func: Callable[[str], None]) -> None: if text: if ('no-append' in self.opts.clipboard_control or len(self.clipboard_control_buffers[key]) > 1024*1024): @@ -484,7 +524,7 @@ class Window: if 'write-primary' in self.opts.clipboard_control: write('p', set_primary_selection) - def manipulate_title_stack(self, pop, title, icon): + def manipulate_title_stack(self, pop: bool, title: str, icon: Any) -> None: if title: if pop: if self.title_stack: @@ -502,14 +542,20 @@ class Window: return ''.join((l.rstrip() or '\n') for l in lines) return ''.join(lines) - def destroy(self): + def destroy(self) -> None: self.destroyed = True if hasattr(self, 'screen'): # Remove cycles so that screen is de-allocated immediately self.screen.reset_callbacks() del self.screen - def as_text(self, as_ansi=False, add_history=False, add_wrap_markers=False, alternate_screen=False) -> str: + def as_text( + self, + as_ansi: bool = False, + add_history: bool = False, + add_wrap_markers: bool = False, + alternate_screen: bool = False + ) -> str: lines: List[str] = [] add_history = add_history and not (self.screen.is_using_alternate_linebuf() ^ alternate_screen) if alternate_screen: @@ -533,25 +579,28 @@ class Window: return ''.join(lines) @property - def cwd_of_child(self): + def cwd_of_child(self) -> Optional[str]: return self.child.foreground_cwd or self.child.current_cwd - def pipe_data(self, text, has_wrap_markers=False): + def pipe_data(self, text: str, has_wrap_markers: bool = False) -> PipeData: text = text or '' if has_wrap_markers: text = text.replace('\r\n', '\n').replace('\r', '\n') lines = text.count('\n') input_line_number = (lines - (self.screen.lines - 1) - self.screen.scrolled_by) return { - 'input_line_number': input_line_number, 'scrolled_by': self.screen.scrolled_by, - 'cursor_x': self.screen.cursor.x + 1, 'cursor_y': self.screen.cursor.y + 1, - 'lines': self.screen.lines, 'columns': self.screen.columns, + 'input_line_number': input_line_number, + 'scrolled_by': self.screen.scrolled_by, + 'cursor_x': self.screen.cursor.x + 1, + 'cursor_y': self.screen.cursor.y + 1, + 'lines': self.screen.lines, + 'columns': self.screen.columns, 'text': text } # actions {{{ - def show_scrollback(self): + def show_scrollback(self) -> None: text = self.as_text(as_ansi=True, add_history=True, add_wrap_markers=True) data = self.pipe_data(text, has_wrap_markers=True) cmd = [x.replace('INPUT_LINE_NUMBER', str(data['input_line_number'])) for x in self.opts.scrollback_pager] @@ -566,13 +615,13 @@ class Window: cmd[0] = exe get_boss().display_scrollback(self, data['text'], cmd) - def paste_bytes(self, text): + def paste_bytes(self, text: Union[str, bytes]) -> None: # paste raw bytes without any processing if isinstance(text, str): text = text.encode('utf-8') self.screen.paste_bytes(text) - def paste(self, text): + def paste(self, text: Union[str, bytes]) -> None: if text and not self.destroyed: if isinstance(text, str): text = text.encode('utf-8') @@ -588,12 +637,12 @@ class Window: text = text.replace(b'\r\n', b'\n').replace(b'\n', b'\r') self.screen.paste(text) - def copy_to_clipboard(self): + def copy_to_clipboard(self) -> None: text = self.text_for_selection() if text: set_clipboard_string(text) - def copy_or_interrupt(self): + def copy_or_interrupt(self) -> None: text = self.text_for_selection() if text: set_clipboard_string(text) @@ -602,11 +651,11 @@ class Window: text = extended_key_event(defines.GLFW_KEY_C, defines.GLFW_MOD_CONTROL, defines.GLFW_PRESS) if mode == 'kitty' else b'\x03' self.write_to_child(text) - def copy_and_clear_or_interrupt(self): + def copy_and_clear_or_interrupt(self) -> None: self.copy_or_interrupt() self.screen.clear_selection() - def pass_selection_to_program(self, *args): + def pass_selection_to_program(self, *args: str) -> None: cwd = self.cwd_of_child text = self.text_for_selection() if text: @@ -615,31 +664,31 @@ class Window: else: open_url(text, cwd=cwd) - def scroll_line_up(self): + def scroll_line_up(self) -> None: if self.screen.is_main_linebuf(): self.screen.scroll(SCROLL_LINE, True) - def scroll_line_down(self): + def scroll_line_down(self) -> None: if self.screen.is_main_linebuf(): self.screen.scroll(SCROLL_LINE, False) - def scroll_page_up(self): + def scroll_page_up(self) -> None: if self.screen.is_main_linebuf(): self.screen.scroll(SCROLL_PAGE, True) - def scroll_page_down(self): + def scroll_page_down(self) -> None: if self.screen.is_main_linebuf(): self.screen.scroll(SCROLL_PAGE, False) - def scroll_home(self): + def scroll_home(self) -> None: if self.screen.is_main_linebuf(): self.screen.scroll(SCROLL_FULL, True) - def scroll_end(self): + def scroll_end(self) -> None: if self.screen.is_main_linebuf(): self.screen.scroll(SCROLL_FULL, False) - def toggle_marker(self, ftype, spec, flags): + def toggle_marker(self, ftype: str, spec: Union[str, Tuple[Tuple[int, str], ...]], flags: int) -> None: from .marks import marker_from_spec key = ftype, spec if key == self.current_marker_spec: @@ -648,22 +697,22 @@ class Window: self.screen.set_marker(marker_from_spec(ftype, spec, flags)) self.current_marker_spec = key - def set_marker(self, spec): + def set_marker(self, spec: Union[str, Sequence[str]]) -> None: from .config import toggle_marker, parse_marker_spec from .marks import marker_from_spec if isinstance(spec, str): - func, (ftype, spec, flags) = toggle_marker('toggle_marker', spec) + func, (ftype, spec_, flags) = toggle_marker('toggle_marker', spec) else: - ftype, spec, flags = parse_marker_spec(spec[0], spec[1:]) - key = ftype, spec - self.screen.set_marker(marker_from_spec(ftype, spec, flags)) + ftype, spec_, flags = parse_marker_spec(spec[0], spec[1:]) + key = ftype, spec_ + self.screen.set_marker(marker_from_spec(ftype, spec_, flags)) self.current_marker_spec = key - def remove_marker(self): + def remove_marker(self) -> None: if self.current_marker_spec is not None: self.screen.set_marker() self.current_marker_spec = None - def scroll_to_mark(self, prev=True, mark=0): + def scroll_to_mark(self, prev: bool = True, mark: int = 0) -> None: self.screen.scroll_to_next_mark(mark, prev) # }}} diff --git a/kitty_tests/layout.py b/kitty_tests/layout.py index ffc0aa226..f9b176c64 100644 --- a/kitty_tests/layout.py +++ b/kitty_tests/layout.py @@ -73,7 +73,7 @@ class TestLayout(BaseTest): check_visible() # Test nth_window for i in range(len(windows)): - active_window_idx = q.nth_window(windows, i) + active_window_idx = q.activate_nth_window(windows, i) self.ae(active_window_idx, i) expect_ids(*range(1, len(windows)+1)) check_visible() @@ -145,7 +145,7 @@ class TestLayout(BaseTest): visible_windows = [w for w in windows if w.overlay_window_id is None] # Test nth_window for i in range(len(visible_windows)): - active_window_idx = q.nth_window(windows, i) + active_window_idx = q.activate_nth_window(windows, i) self.ae(active_window_idx, aidx(i)) expect_ids(1, 6, 3, 4, 5, 2, 7) check_visible() diff --git a/setup.cfg b/setup.cfg index 2c4646cee..8baa281e8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ warn_unreachable = True warn_no_return = False warn_unused_configs = True check_untyped_defs = True -disallow_untyped_defs = True +# disallow_untyped_defs = True [mypy-conf] # ignored because on the CI server sphinx type stubs are available somehow, but