summaryrefslogtreecommitdiffstats
path: root/src/prompt_toolkit/layout/screen.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/prompt_toolkit/layout/screen.py')
-rw-r--r--src/prompt_toolkit/layout/screen.py329
1 files changed, 329 insertions, 0 deletions
diff --git a/src/prompt_toolkit/layout/screen.py b/src/prompt_toolkit/layout/screen.py
new file mode 100644
index 0000000..49aebbd
--- /dev/null
+++ b/src/prompt_toolkit/layout/screen.py
@@ -0,0 +1,329 @@
+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,
+ )