123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- 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(lambda: "")
- )
- #: 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]]] = []
- @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.
- """
- # 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 "{}(x={!r}, y={!r}, width={!r}, height={!r})".format(
- self.__class__.__name__,
- self.xpos,
- self.ypos,
- self.width,
- self.height,
- )
|