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