Spaces:
Sleeping
Sleeping
| 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") | |
| # If we end up having one of these special control sequences in the input string, | |
| # we should display them as follows: | |
| # Usually this happens after a "quoted insert". | |
| display_mappings: dict[str, str] = { | |
| "\x00": "^@", # Control space | |
| "\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": "^[", # Escape | |
| "\x1c": "^\\", | |
| "\x1d": "^]", | |
| "\x1e": "^^", | |
| "\x1f": "^_", | |
| "\x7f": "^?", # ASCII Delete (backspace). | |
| # Special characters. All visualized like Vim does. | |
| "\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>", | |
| # For the non-breaking space: visualize like Emacs does by default. | |
| # (Print a space, but attach the 'nbsp' class that applies the | |
| # underline style.) | |
| "\xa0": " ", | |
| } | |
| def __init__(self, char: str = " ", style: str = "") -> None: | |
| # If this character has to be displayed otherwise, take that one. | |
| if char in self.display_mappings: | |
| if char == "\xa0": | |
| style += " class:nbsp " # Will be underlined. | |
| else: | |
| style += " class:control-character " | |
| char = self.display_mappings[char] | |
| self.char = char | |
| self.style = style | |
| # Calculate width. (We always need this, so better to store it directly | |
| # as a member for performance.) | |
| self.width = get_cwidth(char) | |
| # In theory, `other` can be any type of object, but because of performance | |
| # we don't want to do an `isinstance` check every time. We assume "other" | |
| # is always a "Char". | |
| def _equal(self, other: Char) -> bool: | |
| return self.char == other.char and self.style == other.style | |
| def _not_equal(self, other: Char) -> bool: | |
| # Not equal: We don't do `not char.__eq__` here, because of the | |
| # performance of calling yet another function. | |
| 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) | |
| ) | |
| #: Escape sequences to be injected. | |
| self.zero_width_escapes: defaultdict[int, defaultdict[int, str]] = defaultdict( | |
| lambda: defaultdict(str) | |
| ) | |
| #: Position of the cursor. | |
| self.cursor_positions: dict[ | |
| Window, Point | |
| ] = {} # Map `Window` objects to `Point` objects. | |
| #: Visibility of the cursor. | |
| self.show_cursor = True | |
| #: (Optional) Where to position the menu. E.g. at the start of a completion. | |
| #: (We can't use the cursor position, because we don't want the | |
| #: completion menu to change its position when we browse through all the | |
| #: completions.) | |
| self.menu_positions: dict[ | |
| Window, Point | |
| ] = {} # Map `Window` objects to `Point` objects. | |
| #: Currently used width/height of the screen. This will increase when | |
| #: data is written to the screen. | |
| self.width = initial_width or 0 | |
| self.height = initial_height or 0 | |
| # Windows that have been drawn. (Each `Window` class will add itself to | |
| # this list.) | |
| self.visible_windows_to_write_positions: dict[Window, WritePosition] = {} | |
| # List of (z_index, draw_func) | |
| self._draw_float_functions: list[tuple[int, Callable[[], None]]] = [] | |
| 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. | |
| """ | |
| # We keep looping because some draw functions could add new functions | |
| # to this list. See `FloatContainer`. | |
| while self._draw_float_functions: | |
| # Sort the floats that we have so far by z_index. | |
| functions = sorted(self._draw_float_functions, key=lambda item: item[0]) | |
| # Draw only one at a time, then sort everything again. Now floats | |
| # might have been added. | |
| 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 | |
| # xpos and ypos can be negative. (A float can be partially visible.) | |
| 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})" | |