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