| | from __future__ import annotations |
| |
|
| | from collections import defaultdict |
| | from typing import TYPE_CHECKING, Callable |
| |
|
| | from prompt_toolkit.cache import FastDictCache |
| | from prompt_toolkit.data_structures import Point |
| | from prompt_toolkit.utils import get_cwidth |
| |
|
| | if TYPE_CHECKING: |
| | from .containers import Window |
| |
|
| |
|
| | __all__ = [ |
| | "Screen", |
| | "Char", |
| | ] |
| |
|
| |
|
| | class Char: |
| | """ |
| | Represent a single character in a :class:`.Screen`. |
| | |
| | This should be considered immutable. |
| | |
| | :param char: A single character (can be a double-width character). |
| | :param style: A style string. (Can contain classnames.) |
| | """ |
| |
|
| | __slots__ = ("char", "style", "width") |
| |
|
| | |
| | |
| | |
| | display_mappings: dict[str, str] = { |
| | "\x00": "^@", |
| | "\x01": "^A", |
| | "\x02": "^B", |
| | "\x03": "^C", |
| | "\x04": "^D", |
| | "\x05": "^E", |
| | "\x06": "^F", |
| | "\x07": "^G", |
| | "\x08": "^H", |
| | "\x09": "^I", |
| | "\x0a": "^J", |
| | "\x0b": "^K", |
| | "\x0c": "^L", |
| | "\x0d": "^M", |
| | "\x0e": "^N", |
| | "\x0f": "^O", |
| | "\x10": "^P", |
| | "\x11": "^Q", |
| | "\x12": "^R", |
| | "\x13": "^S", |
| | "\x14": "^T", |
| | "\x15": "^U", |
| | "\x16": "^V", |
| | "\x17": "^W", |
| | "\x18": "^X", |
| | "\x19": "^Y", |
| | "\x1a": "^Z", |
| | "\x1b": "^[", |
| | "\x1c": "^\\", |
| | "\x1d": "^]", |
| | "\x1e": "^^", |
| | "\x1f": "^_", |
| | "\x7f": "^?", |
| | |
| | "\x80": "<80>", |
| | "\x81": "<81>", |
| | "\x82": "<82>", |
| | "\x83": "<83>", |
| | "\x84": "<84>", |
| | "\x85": "<85>", |
| | "\x86": "<86>", |
| | "\x87": "<87>", |
| | "\x88": "<88>", |
| | "\x89": "<89>", |
| | "\x8a": "<8a>", |
| | "\x8b": "<8b>", |
| | "\x8c": "<8c>", |
| | "\x8d": "<8d>", |
| | "\x8e": "<8e>", |
| | "\x8f": "<8f>", |
| | "\x90": "<90>", |
| | "\x91": "<91>", |
| | "\x92": "<92>", |
| | "\x93": "<93>", |
| | "\x94": "<94>", |
| | "\x95": "<95>", |
| | "\x96": "<96>", |
| | "\x97": "<97>", |
| | "\x98": "<98>", |
| | "\x99": "<99>", |
| | "\x9a": "<9a>", |
| | "\x9b": "<9b>", |
| | "\x9c": "<9c>", |
| | "\x9d": "<9d>", |
| | "\x9e": "<9e>", |
| | "\x9f": "<9f>", |
| | |
| | |
| | |
| | "\xa0": " ", |
| | } |
| |
|
| | def __init__(self, char: str = " ", style: str = "") -> None: |
| | |
| | if char in self.display_mappings: |
| | if char == "\xa0": |
| | style += " class:nbsp " |
| | else: |
| | style += " class:control-character " |
| |
|
| | char = self.display_mappings[char] |
| |
|
| | self.char = char |
| | self.style = style |
| |
|
| | |
| | |
| | self.width = get_cwidth(char) |
| |
|
| | |
| | |
| | |
| | def _equal(self, other: Char) -> bool: |
| | return self.char == other.char and self.style == other.style |
| |
|
| | def _not_equal(self, other: Char) -> bool: |
| | |
| | |
| | return self.char != other.char or self.style != other.style |
| |
|
| | if not TYPE_CHECKING: |
| | __eq__ = _equal |
| | __ne__ = _not_equal |
| |
|
| | def __repr__(self) -> str: |
| | return f"{self.__class__.__name__}({self.char!r}, {self.style!r})" |
| |
|
| |
|
| | _CHAR_CACHE: FastDictCache[tuple[str, str], Char] = FastDictCache( |
| | Char, size=1000 * 1000 |
| | ) |
| | Transparent = "[transparent]" |
| |
|
| |
|
| | class Screen: |
| | """ |
| | Two dimensional buffer of :class:`.Char` instances. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | default_char: Char | None = None, |
| | initial_width: int = 0, |
| | initial_height: int = 0, |
| | ) -> None: |
| | if default_char is None: |
| | default_char2 = _CHAR_CACHE[" ", Transparent] |
| | else: |
| | default_char2 = default_char |
| |
|
| | self.data_buffer: defaultdict[int, defaultdict[int, Char]] = defaultdict( |
| | lambda: defaultdict(lambda: default_char2) |
| | ) |
| |
|
| | |
| | self.zero_width_escapes: defaultdict[int, defaultdict[int, str]] = defaultdict( |
| | lambda: defaultdict(str) |
| | ) |
| |
|
| | |
| | self.cursor_positions: dict[ |
| | Window, Point |
| | ] = {} |
| |
|
| | |
| | self.show_cursor = True |
| |
|
| | |
| | |
| | |
| | |
| | self.menu_positions: dict[ |
| | Window, Point |
| | ] = {} |
| |
|
| | |
| | |
| | self.width = initial_width or 0 |
| | self.height = initial_height or 0 |
| |
|
| | |
| | |
| | self.visible_windows_to_write_positions: dict[Window, WritePosition] = {} |
| |
|
| | |
| | self._draw_float_functions: list[tuple[int, Callable[[], None]]] = [] |
| |
|
| | @property |
| | def visible_windows(self) -> list[Window]: |
| | return list(self.visible_windows_to_write_positions.keys()) |
| |
|
| | def set_cursor_position(self, window: Window, position: Point) -> None: |
| | """ |
| | Set the cursor position for a given window. |
| | """ |
| | self.cursor_positions[window] = position |
| |
|
| | def set_menu_position(self, window: Window, position: Point) -> None: |
| | """ |
| | Set the cursor position for a given window. |
| | """ |
| | self.menu_positions[window] = position |
| |
|
| | def get_cursor_position(self, window: Window) -> Point: |
| | """ |
| | Get the cursor position for a given window. |
| | Returns a `Point`. |
| | """ |
| | try: |
| | return self.cursor_positions[window] |
| | except KeyError: |
| | return Point(x=0, y=0) |
| |
|
| | def get_menu_position(self, window: Window) -> Point: |
| | """ |
| | Get the menu position for a given window. |
| | (This falls back to the cursor position if no menu position was set.) |
| | """ |
| | try: |
| | return self.menu_positions[window] |
| | except KeyError: |
| | try: |
| | return self.cursor_positions[window] |
| | except KeyError: |
| | return Point(x=0, y=0) |
| |
|
| | def draw_with_z_index(self, z_index: int, draw_func: Callable[[], None]) -> None: |
| | """ |
| | Add a draw-function for a `Window` which has a >= 0 z_index. |
| | This will be postponed until `draw_all_floats` is called. |
| | """ |
| | self._draw_float_functions.append((z_index, draw_func)) |
| |
|
| | def draw_all_floats(self) -> None: |
| | """ |
| | Draw all float functions in order of z-index. |
| | """ |
| | |
| | |
| | while self._draw_float_functions: |
| | |
| | functions = sorted(self._draw_float_functions, key=lambda item: item[0]) |
| |
|
| | |
| | |
| | self._draw_float_functions = functions[1:] |
| | functions[0][1]() |
| |
|
| | def append_style_to_content(self, style_str: str) -> None: |
| | """ |
| | For all the characters in the screen. |
| | Set the style string to the given `style_str`. |
| | """ |
| | b = self.data_buffer |
| | char_cache = _CHAR_CACHE |
| |
|
| | append_style = " " + style_str |
| |
|
| | for y, row in b.items(): |
| | for x, char in row.items(): |
| | row[x] = char_cache[char.char, char.style + append_style] |
| |
|
| | def fill_area( |
| | self, write_position: WritePosition, style: str = "", after: bool = False |
| | ) -> None: |
| | """ |
| | Fill the content of this area, using the given `style`. |
| | The style is prepended before whatever was here before. |
| | """ |
| | if not style.strip(): |
| | return |
| |
|
| | xmin = write_position.xpos |
| | xmax = write_position.xpos + write_position.width |
| | char_cache = _CHAR_CACHE |
| | data_buffer = self.data_buffer |
| |
|
| | if after: |
| | append_style = " " + style |
| | prepend_style = "" |
| | else: |
| | append_style = "" |
| | prepend_style = style + " " |
| |
|
| | for y in range( |
| | write_position.ypos, write_position.ypos + write_position.height |
| | ): |
| | row = data_buffer[y] |
| | for x in range(xmin, xmax): |
| | cell = row[x] |
| | row[x] = char_cache[ |
| | cell.char, prepend_style + cell.style + append_style |
| | ] |
| |
|
| |
|
| | class WritePosition: |
| | def __init__(self, xpos: int, ypos: int, width: int, height: int) -> None: |
| | assert height >= 0 |
| | assert width >= 0 |
| | |
| |
|
| | self.xpos = xpos |
| | self.ypos = ypos |
| | self.width = width |
| | self.height = height |
| |
|
| | def __repr__(self) -> str: |
| | return f"{self.__class__.__name__}(x={self.xpos!r}, y={self.ypos!r}, width={self.width!r}, height={self.height!r})" |
| |
|