Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/prompt_toolkit/layout/screen.py: 33%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

108 statements  

1from __future__ import annotations 

2 

3from collections import defaultdict 

4from collections.abc import Callable 

5from typing import TYPE_CHECKING 

6 

7from prompt_toolkit.cache import FastDictCache 

8from prompt_toolkit.data_structures import Point 

9from prompt_toolkit.utils import get_cwidth 

10 

11if TYPE_CHECKING: 

12 from .containers import Window 

13 

14 

15__all__ = [ 

16 "Screen", 

17 "Char", 

18] 

19 

20 

21class Char: 

22 """ 

23 Represent a single character in a :class:`.Screen`. 

24 

25 This should be considered immutable. 

26 

27 :param char: A single character (can be a double-width character). 

28 :param style: A style string. (Can contain classnames.) 

29 """ 

30 

31 __slots__ = ("char", "style", "width") 

32 

33 # If we end up having one of these special control sequences in the input string, 

34 # we should display them as follows: 

35 # Usually this happens after a "quoted insert". 

36 display_mappings: dict[str, str] = { 

37 "\x00": "^@", # Control space 

38 "\x01": "^A", 

39 "\x02": "^B", 

40 "\x03": "^C", 

41 "\x04": "^D", 

42 "\x05": "^E", 

43 "\x06": "^F", 

44 "\x07": "^G", 

45 "\x08": "^H", 

46 "\x09": "^I", 

47 "\x0a": "^J", 

48 "\x0b": "^K", 

49 "\x0c": "^L", 

50 "\x0d": "^M", 

51 "\x0e": "^N", 

52 "\x0f": "^O", 

53 "\x10": "^P", 

54 "\x11": "^Q", 

55 "\x12": "^R", 

56 "\x13": "^S", 

57 "\x14": "^T", 

58 "\x15": "^U", 

59 "\x16": "^V", 

60 "\x17": "^W", 

61 "\x18": "^X", 

62 "\x19": "^Y", 

63 "\x1a": "^Z", 

64 "\x1b": "^[", # Escape 

65 "\x1c": "^\\", 

66 "\x1d": "^]", 

67 "\x1e": "^^", 

68 "\x1f": "^_", 

69 "\x7f": "^?", # ASCII Delete (backspace). 

70 # Special characters. All visualized like Vim does. 

71 "\x80": "<80>", 

72 "\x81": "<81>", 

73 "\x82": "<82>", 

74 "\x83": "<83>", 

75 "\x84": "<84>", 

76 "\x85": "<85>", 

77 "\x86": "<86>", 

78 "\x87": "<87>", 

79 "\x88": "<88>", 

80 "\x89": "<89>", 

81 "\x8a": "<8a>", 

82 "\x8b": "<8b>", 

83 "\x8c": "<8c>", 

84 "\x8d": "<8d>", 

85 "\x8e": "<8e>", 

86 "\x8f": "<8f>", 

87 "\x90": "<90>", 

88 "\x91": "<91>", 

89 "\x92": "<92>", 

90 "\x93": "<93>", 

91 "\x94": "<94>", 

92 "\x95": "<95>", 

93 "\x96": "<96>", 

94 "\x97": "<97>", 

95 "\x98": "<98>", 

96 "\x99": "<99>", 

97 "\x9a": "<9a>", 

98 "\x9b": "<9b>", 

99 "\x9c": "<9c>", 

100 "\x9d": "<9d>", 

101 "\x9e": "<9e>", 

102 "\x9f": "<9f>", 

103 # For the non-breaking space: visualize like Emacs does by default. 

104 # (Print a space, but attach the 'nbsp' class that applies the 

105 # underline style.) 

106 "\xa0": " ", 

107 } 

108 

109 def __init__(self, char: str = " ", style: str = "") -> None: 

110 # If this character has to be displayed otherwise, take that one. 

111 if char in self.display_mappings: 

112 if char == "\xa0": 

113 style += " class:nbsp " # Will be underlined. 

114 else: 

115 style += " class:control-character " 

116 

117 char = self.display_mappings[char] 

118 

119 self.char = char 

120 self.style = style 

121 

122 # Calculate width. (We always need this, so better to store it directly 

123 # as a member for performance.) 

124 self.width = get_cwidth(char) 

125 

126 # In theory, `other` can be any type of object, but because of performance 

127 # we don't want to do an `isinstance` check every time. We assume "other" 

128 # is always a "Char". 

129 def _equal(self, other: Char) -> bool: 

130 return self.char == other.char and self.style == other.style 

131 

132 def _not_equal(self, other: Char) -> bool: 

133 # Not equal: We don't do `not char.__eq__` here, because of the 

134 # performance of calling yet another function. 

135 return self.char != other.char or self.style != other.style 

136 

137 if not TYPE_CHECKING: 

138 __eq__ = _equal 

139 __ne__ = _not_equal 

140 

141 def __repr__(self) -> str: 

142 return f"{self.__class__.__name__}({self.char!r}, {self.style!r})" 

143 

144 

145_CHAR_CACHE: FastDictCache[tuple[str, str], Char] = FastDictCache( 

146 Char, size=1000 * 1000 

147) 

148Transparent = "[transparent]" 

149 

150 

151class Screen: 

152 """ 

153 Two dimensional buffer of :class:`.Char` instances. 

154 """ 

155 

156 def __init__( 

157 self, 

158 default_char: Char | None = None, 

159 initial_width: int = 0, 

160 initial_height: int = 0, 

161 ) -> None: 

162 if default_char is None: 

163 default_char2 = _CHAR_CACHE[" ", Transparent] 

164 else: 

165 default_char2 = default_char 

166 

167 self.data_buffer: defaultdict[int, defaultdict[int, Char]] = defaultdict( 

168 lambda: defaultdict(lambda: default_char2) 

169 ) 

170 

171 #: Escape sequences to be injected. 

172 self.zero_width_escapes: defaultdict[int, defaultdict[int, str]] = defaultdict( 

173 lambda: defaultdict(str) 

174 ) 

175 

176 #: Position of the cursor. 

177 self.cursor_positions: dict[ 

178 Window, Point 

179 ] = {} # Map `Window` objects to `Point` objects. 

180 

181 #: Visibility of the cursor. 

182 self.show_cursor = True 

183 

184 #: (Optional) Where to position the menu. E.g. at the start of a completion. 

185 #: (We can't use the cursor position, because we don't want the 

186 #: completion menu to change its position when we browse through all the 

187 #: completions.) 

188 self.menu_positions: dict[ 

189 Window, Point 

190 ] = {} # Map `Window` objects to `Point` objects. 

191 

192 #: Currently used width/height of the screen. This will increase when 

193 #: data is written to the screen. 

194 self.width = initial_width or 0 

195 self.height = initial_height or 0 

196 

197 # Windows that have been drawn. (Each `Window` class will add itself to 

198 # this list.) 

199 self.visible_windows_to_write_positions: dict[Window, WritePosition] = {} 

200 

201 # List of (z_index, draw_func) 

202 self._draw_float_functions: list[tuple[int, Callable[[], None]]] = [] 

203 

204 @property 

205 def visible_windows(self) -> list[Window]: 

206 return list(self.visible_windows_to_write_positions.keys()) 

207 

208 def set_cursor_position(self, window: Window, position: Point) -> None: 

209 """ 

210 Set the cursor position for a given window. 

211 """ 

212 self.cursor_positions[window] = position 

213 

214 def set_menu_position(self, window: Window, position: Point) -> None: 

215 """ 

216 Set the cursor position for a given window. 

217 """ 

218 self.menu_positions[window] = position 

219 

220 def get_cursor_position(self, window: Window) -> Point: 

221 """ 

222 Get the cursor position for a given window. 

223 Returns a `Point`. 

224 """ 

225 try: 

226 return self.cursor_positions[window] 

227 except KeyError: 

228 return Point(x=0, y=0) 

229 

230 def get_menu_position(self, window: Window) -> Point: 

231 """ 

232 Get the menu position for a given window. 

233 (This falls back to the cursor position if no menu position was set.) 

234 """ 

235 try: 

236 return self.menu_positions[window] 

237 except KeyError: 

238 try: 

239 return self.cursor_positions[window] 

240 except KeyError: 

241 return Point(x=0, y=0) 

242 

243 def draw_with_z_index(self, z_index: int, draw_func: Callable[[], None]) -> None: 

244 """ 

245 Add a draw-function for a `Window` which has a >= 0 z_index. 

246 This will be postponed until `draw_all_floats` is called. 

247 """ 

248 self._draw_float_functions.append((z_index, draw_func)) 

249 

250 def draw_all_floats(self) -> None: 

251 """ 

252 Draw all float functions in order of z-index. 

253 """ 

254 # We keep looping because some draw functions could add new functions 

255 # to this list. See `FloatContainer`. 

256 while self._draw_float_functions: 

257 # Sort the floats that we have so far by z_index. 

258 functions = sorted(self._draw_float_functions, key=lambda item: item[0]) 

259 

260 # Draw only one at a time, then sort everything again. Now floats 

261 # might have been added. 

262 self._draw_float_functions = functions[1:] 

263 functions[0][1]() 

264 

265 def append_style_to_content(self, style_str: str) -> None: 

266 """ 

267 For all the characters in the screen. 

268 Set the style string to the given `style_str`. 

269 """ 

270 b = self.data_buffer 

271 char_cache = _CHAR_CACHE 

272 

273 append_style = " " + style_str 

274 

275 for y, row in b.items(): 

276 for x, char in row.items(): 

277 row[x] = char_cache[char.char, char.style + append_style] 

278 

279 def fill_area( 

280 self, write_position: WritePosition, style: str = "", after: bool = False 

281 ) -> None: 

282 """ 

283 Fill the content of this area, using the given `style`. 

284 The style is prepended before whatever was here before. 

285 """ 

286 if not style.strip(): 

287 return 

288 

289 xmin = write_position.xpos 

290 xmax = write_position.xpos + write_position.width 

291 char_cache = _CHAR_CACHE 

292 data_buffer = self.data_buffer 

293 

294 if after: 

295 append_style = " " + style 

296 prepend_style = "" 

297 else: 

298 append_style = "" 

299 prepend_style = style + " " 

300 

301 for y in range( 

302 write_position.ypos, write_position.ypos + write_position.height 

303 ): 

304 row = data_buffer[y] 

305 for x in range(xmin, xmax): 

306 cell = row[x] 

307 row[x] = char_cache[ 

308 cell.char, prepend_style + cell.style + append_style 

309 ] 

310 

311 

312class WritePosition: 

313 def __init__(self, xpos: int, ypos: int, width: int, height: int) -> None: 

314 assert height >= 0 

315 assert width >= 0 

316 # xpos and ypos can be negative. (A float can be partially visible.) 

317 

318 self.xpos = xpos 

319 self.ypos = ypos 

320 self.width = width 

321 self.height = height 

322 

323 def __repr__(self) -> str: 

324 return f"{self.__class__.__name__}(x={self.xpos!r}, y={self.ypos!r}, width={self.width!r}, height={self.height!r})"