Spaces:
Sleeping
Sleeping
File size: 10,113 Bytes
2d876d1 |
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 |
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})"
|