You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			324 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			324 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Python
		
	
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]]] = []
 | 
						|
 | 
						|
    @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 f"{self.__class__.__name__}(x={self.xpos!r}, y={self.ypos!r}, width={self.width!r}, height={self.height!r})"
 |