1from __future__ import annotations 
    2 
    3from collections import defaultdict 
    4from typing import TYPE_CHECKING, Callable 
    5 
    6from prompt_toolkit.cache import FastDictCache 
    7from prompt_toolkit.data_structures import Point 
    8from prompt_toolkit.utils import get_cwidth 
    9 
    10if TYPE_CHECKING: 
    11    from .containers import Window 
    12 
    13 
    14__all__ = [ 
    15    "Screen", 
    16    "Char", 
    17] 
    18 
    19 
    20class Char: 
    21    """ 
    22    Represent a single character in a :class:`.Screen`. 
    23 
    24    This should be considered immutable. 
    25 
    26    :param char: A single character (can be a double-width character). 
    27    :param style: A style string. (Can contain classnames.) 
    28    """ 
    29 
    30    __slots__ = ("char", "style", "width") 
    31 
    32    # If we end up having one of these special control sequences in the input string, 
    33    # we should display them as follows: 
    34    # Usually this happens after a "quoted insert". 
    35    display_mappings: dict[str, str] = { 
    36        "\x00": "^@",  # Control space 
    37        "\x01": "^A", 
    38        "\x02": "^B", 
    39        "\x03": "^C", 
    40        "\x04": "^D", 
    41        "\x05": "^E", 
    42        "\x06": "^F", 
    43        "\x07": "^G", 
    44        "\x08": "^H", 
    45        "\x09": "^I", 
    46        "\x0a": "^J", 
    47        "\x0b": "^K", 
    48        "\x0c": "^L", 
    49        "\x0d": "^M", 
    50        "\x0e": "^N", 
    51        "\x0f": "^O", 
    52        "\x10": "^P", 
    53        "\x11": "^Q", 
    54        "\x12": "^R", 
    55        "\x13": "^S", 
    56        "\x14": "^T", 
    57        "\x15": "^U", 
    58        "\x16": "^V", 
    59        "\x17": "^W", 
    60        "\x18": "^X", 
    61        "\x19": "^Y", 
    62        "\x1a": "^Z", 
    63        "\x1b": "^[",  # Escape 
    64        "\x1c": "^\\", 
    65        "\x1d": "^]", 
    66        "\x1e": "^^", 
    67        "\x1f": "^_", 
    68        "\x7f": "^?",  # ASCII Delete (backspace). 
    69        # Special characters. All visualized like Vim does. 
    70        "\x80": "<80>", 
    71        "\x81": "<81>", 
    72        "\x82": "<82>", 
    73        "\x83": "<83>", 
    74        "\x84": "<84>", 
    75        "\x85": "<85>", 
    76        "\x86": "<86>", 
    77        "\x87": "<87>", 
    78        "\x88": "<88>", 
    79        "\x89": "<89>", 
    80        "\x8a": "<8a>", 
    81        "\x8b": "<8b>", 
    82        "\x8c": "<8c>", 
    83        "\x8d": "<8d>", 
    84        "\x8e": "<8e>", 
    85        "\x8f": "<8f>", 
    86        "\x90": "<90>", 
    87        "\x91": "<91>", 
    88        "\x92": "<92>", 
    89        "\x93": "<93>", 
    90        "\x94": "<94>", 
    91        "\x95": "<95>", 
    92        "\x96": "<96>", 
    93        "\x97": "<97>", 
    94        "\x98": "<98>", 
    95        "\x99": "<99>", 
    96        "\x9a": "<9a>", 
    97        "\x9b": "<9b>", 
    98        "\x9c": "<9c>", 
    99        "\x9d": "<9d>", 
    100        "\x9e": "<9e>", 
    101        "\x9f": "<9f>", 
    102        # For the non-breaking space: visualize like Emacs does by default. 
    103        # (Print a space, but attach the 'nbsp' class that applies the 
    104        # underline style.) 
    105        "\xa0": " ", 
    106    } 
    107 
    108    def __init__(self, char: str = " ", style: str = "") -> None: 
    109        # If this character has to be displayed otherwise, take that one. 
    110        if char in self.display_mappings: 
    111            if char == "\xa0": 
    112                style += " class:nbsp "  # Will be underlined. 
    113            else: 
    114                style += " class:control-character " 
    115 
    116            char = self.display_mappings[char] 
    117 
    118        self.char = char 
    119        self.style = style 
    120 
    121        # Calculate width. (We always need this, so better to store it directly 
    122        # as a member for performance.) 
    123        self.width = get_cwidth(char) 
    124 
    125    # In theory, `other` can be any type of object, but because of performance 
    126    # we don't want to do an `isinstance` check every time. We assume "other" 
    127    # is always a "Char". 
    128    def _equal(self, other: Char) -> bool: 
    129        return self.char == other.char and self.style == other.style 
    130 
    131    def _not_equal(self, other: Char) -> bool: 
    132        # Not equal: We don't do `not char.__eq__` here, because of the 
    133        # performance of calling yet another function. 
    134        return self.char != other.char or self.style != other.style 
    135 
    136    if not TYPE_CHECKING: 
    137        __eq__ = _equal 
    138        __ne__ = _not_equal 
    139 
    140    def __repr__(self) -> str: 
    141        return f"{self.__class__.__name__}({self.char!r}, {self.style!r})" 
    142 
    143 
    144_CHAR_CACHE: FastDictCache[tuple[str, str], Char] = FastDictCache( 
    145    Char, size=1000 * 1000 
    146) 
    147Transparent = "[transparent]" 
    148 
    149 
    150class Screen: 
    151    """ 
    152    Two dimensional buffer of :class:`.Char` instances. 
    153    """ 
    154 
    155    def __init__( 
    156        self, 
    157        default_char: Char | None = None, 
    158        initial_width: int = 0, 
    159        initial_height: int = 0, 
    160    ) -> None: 
    161        if default_char is None: 
    162            default_char2 = _CHAR_CACHE[" ", Transparent] 
    163        else: 
    164            default_char2 = default_char 
    165 
    166        self.data_buffer: defaultdict[int, defaultdict[int, Char]] = defaultdict( 
    167            lambda: defaultdict(lambda: default_char2) 
    168        ) 
    169 
    170        #: Escape sequences to be injected. 
    171        self.zero_width_escapes: defaultdict[int, defaultdict[int, str]] = defaultdict( 
    172            lambda: defaultdict(str) 
    173        ) 
    174 
    175        #: Position of the cursor. 
    176        self.cursor_positions: dict[ 
    177            Window, Point 
    178        ] = {}  # Map `Window` objects to `Point` objects. 
    179 
    180        #: Visibility of the cursor. 
    181        self.show_cursor = True 
    182 
    183        #: (Optional) Where to position the menu. E.g. at the start of a completion. 
    184        #: (We can't use the cursor position, because we don't want the 
    185        #: completion menu to change its position when we browse through all the 
    186        #: completions.) 
    187        self.menu_positions: dict[ 
    188            Window, Point 
    189        ] = {}  # Map `Window` objects to `Point` objects. 
    190 
    191        #: Currently used width/height of the screen. This will increase when 
    192        #: data is written to the screen. 
    193        self.width = initial_width or 0 
    194        self.height = initial_height or 0 
    195 
    196        # Windows that have been drawn. (Each `Window` class will add itself to 
    197        # this list.) 
    198        self.visible_windows_to_write_positions: dict[Window, WritePosition] = {} 
    199 
    200        # List of (z_index, draw_func) 
    201        self._draw_float_functions: list[tuple[int, Callable[[], None]]] = [] 
    202 
    203    @property 
    204    def visible_windows(self) -> list[Window]: 
    205        return list(self.visible_windows_to_write_positions.keys()) 
    206 
    207    def set_cursor_position(self, window: Window, position: Point) -> None: 
    208        """ 
    209        Set the cursor position for a given window. 
    210        """ 
    211        self.cursor_positions[window] = position 
    212 
    213    def set_menu_position(self, window: Window, position: Point) -> None: 
    214        """ 
    215        Set the cursor position for a given window. 
    216        """ 
    217        self.menu_positions[window] = position 
    218 
    219    def get_cursor_position(self, window: Window) -> Point: 
    220        """ 
    221        Get the cursor position for a given window. 
    222        Returns a `Point`. 
    223        """ 
    224        try: 
    225            return self.cursor_positions[window] 
    226        except KeyError: 
    227            return Point(x=0, y=0) 
    228 
    229    def get_menu_position(self, window: Window) -> Point: 
    230        """ 
    231        Get the menu position for a given window. 
    232        (This falls back to the cursor position if no menu position was set.) 
    233        """ 
    234        try: 
    235            return self.menu_positions[window] 
    236        except KeyError: 
    237            try: 
    238                return self.cursor_positions[window] 
    239            except KeyError: 
    240                return Point(x=0, y=0) 
    241 
    242    def draw_with_z_index(self, z_index: int, draw_func: Callable[[], None]) -> None: 
    243        """ 
    244        Add a draw-function for a `Window` which has a >= 0 z_index. 
    245        This will be postponed until `draw_all_floats` is called. 
    246        """ 
    247        self._draw_float_functions.append((z_index, draw_func)) 
    248 
    249    def draw_all_floats(self) -> None: 
    250        """ 
    251        Draw all float functions in order of z-index. 
    252        """ 
    253        # We keep looping because some draw functions could add new functions 
    254        # to this list. See `FloatContainer`. 
    255        while self._draw_float_functions: 
    256            # Sort the floats that we have so far by z_index. 
    257            functions = sorted(self._draw_float_functions, key=lambda item: item[0]) 
    258 
    259            # Draw only one at a time, then sort everything again. Now floats 
    260            # might have been added. 
    261            self._draw_float_functions = functions[1:] 
    262            functions[0][1]() 
    263 
    264    def append_style_to_content(self, style_str: str) -> None: 
    265        """ 
    266        For all the characters in the screen. 
    267        Set the style string to the given `style_str`. 
    268        """ 
    269        b = self.data_buffer 
    270        char_cache = _CHAR_CACHE 
    271 
    272        append_style = " " + style_str 
    273 
    274        for y, row in b.items(): 
    275            for x, char in row.items(): 
    276                row[x] = char_cache[char.char, char.style + append_style] 
    277 
    278    def fill_area( 
    279        self, write_position: WritePosition, style: str = "", after: bool = False 
    280    ) -> None: 
    281        """ 
    282        Fill the content of this area, using the given `style`. 
    283        The style is prepended before whatever was here before. 
    284        """ 
    285        if not style.strip(): 
    286            return 
    287 
    288        xmin = write_position.xpos 
    289        xmax = write_position.xpos + write_position.width 
    290        char_cache = _CHAR_CACHE 
    291        data_buffer = self.data_buffer 
    292 
    293        if after: 
    294            append_style = " " + style 
    295            prepend_style = "" 
    296        else: 
    297            append_style = "" 
    298            prepend_style = style + " " 
    299 
    300        for y in range( 
    301            write_position.ypos, write_position.ypos + write_position.height 
    302        ): 
    303            row = data_buffer[y] 
    304            for x in range(xmin, xmax): 
    305                cell = row[x] 
    306                row[x] = char_cache[ 
    307                    cell.char, prepend_style + cell.style + append_style 
    308                ] 
    309 
    310 
    311class WritePosition: 
    312    def __init__(self, xpos: int, ypos: int, width: int, height: int) -> None: 
    313        assert height >= 0 
    314        assert width >= 0 
    315        # xpos and ypos can be negative. (A float can be partially visible.) 
    316 
    317        self.xpos = xpos 
    318        self.ypos = ypos 
    319        self.width = width 
    320        self.height = height 
    321 
    322    def __repr__(self) -> str: 
    323        return f"{self.__class__.__name__}(x={self.xpos!r}, y={self.ypos!r}, width={self.width!r}, height={self.height!r})"