summaryrefslogtreecommitdiffstats
path: root/src/prompt_toolkit/layout/screen.py
blob: 49aebbd626279c4ce684853689f043b8207f8683 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
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,
        )