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