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

107 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 06:09 +0000

1from __future__ import annotations 

2 

3from collections import defaultdict 

4from typing import TYPE_CHECKING, Callable 

5 

6from prompt_toolkit.cache import FastDictCache 

7from prompt_toolkit.data_structures import Point 

8from prompt_toolkit.utils import get_cwidth 

9 

10if TYPE_CHECKING: 

11 from .containers import Window 

12 

13 

14__all__ = [ 

15 "Screen", 

16 "Char", 

17] 

18 

19 

20class Char: 

21 """ 

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

23 

24 This should be considered immutable. 

25 

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

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

28 """ 

29 

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

31 

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

33 # we should display them as follows: 

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

35 display_mappings: dict[str, str] = { 

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

37 "\x01": "^A", 

38 "\x02": "^B", 

39 "\x03": "^C", 

40 "\x04": "^D", 

41 "\x05": "^E", 

42 "\x06": "^F", 

43 "\x07": "^G", 

44 "\x08": "^H", 

45 "\x09": "^I", 

46 "\x0a": "^J", 

47 "\x0b": "^K", 

48 "\x0c": "^L", 

49 "\x0d": "^M", 

50 "\x0e": "^N", 

51 "\x0f": "^O", 

52 "\x10": "^P", 

53 "\x11": "^Q", 

54 "\x12": "^R", 

55 "\x13": "^S", 

56 "\x14": "^T", 

57 "\x15": "^U", 

58 "\x16": "^V", 

59 "\x17": "^W", 

60 "\x18": "^X", 

61 "\x19": "^Y", 

62 "\x1a": "^Z", 

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

64 "\x1c": "^\\", 

65 "\x1d": "^]", 

66 "\x1e": "^^", 

67 "\x1f": "^_", 

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

69 # Special characters. All visualized like Vim does. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

104 # underline style.) 

105 "\xa0": " ", 

106 } 

107 

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

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

110 if char in self.display_mappings: 

111 if char == "\xa0": 

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

113 else: 

114 style += " class:control-character " 

115 

116 char = self.display_mappings[char] 

117 

118 self.char = char 

119 self.style = style 

120 

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

122 # as a member for performance.) 

123 self.width = get_cwidth(char) 

124 

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

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

127 # is always a "Char". 

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

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

130 

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

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

133 # performance of calling yet another function. 

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

135 

136 if not TYPE_CHECKING: 

137 __eq__ = _equal 

138 __ne__ = _not_equal 

139 

140 def __repr__(self) -> str: 

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

142 

143 

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

145 Char, size=1000 * 1000 

146) 

147Transparent = "[transparent]" 

148 

149 

150class Screen: 

151 """ 

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

153 """ 

154 

155 def __init__( 

156 self, 

157 default_char: Char | None = None, 

158 initial_width: int = 0, 

159 initial_height: int = 0, 

160 ) -> None: 

161 if default_char is None: 

162 default_char2 = _CHAR_CACHE[" ", Transparent] 

163 else: 

164 default_char2 = default_char 

165 

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

167 lambda: defaultdict(lambda: default_char2) 

168 ) 

169 

170 #: Escape sequences to be injected. 

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

172 lambda: defaultdict(lambda: "") 

173 ) 

174 

175 #: Position of the cursor. 

176 self.cursor_positions: dict[ 

177 Window, Point 

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

179 

180 #: Visibility of the cursor. 

181 self.show_cursor = True 

182 

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

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

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

186 #: completions.) 

187 self.menu_positions: dict[ 

188 Window, Point 

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

190 

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

192 #: data is written to the screen. 

193 self.width = initial_width or 0 

194 self.height = initial_height or 0 

195 

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

197 # this list.) 

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

199 

200 # List of (z_index, draw_func) 

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

202 

203 @property 

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

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

206 

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

208 """ 

209 Set the cursor position for a given window. 

210 """ 

211 self.cursor_positions[window] = position 

212 

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

214 """ 

215 Set the cursor position for a given window. 

216 """ 

217 self.menu_positions[window] = position 

218 

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

220 """ 

221 Get the cursor position for a given window. 

222 Returns a `Point`. 

223 """ 

224 try: 

225 return self.cursor_positions[window] 

226 except KeyError: 

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

228 

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

230 """ 

231 Get the menu position for a given window. 

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

233 """ 

234 try: 

235 return self.menu_positions[window] 

236 except KeyError: 

237 try: 

238 return self.cursor_positions[window] 

239 except KeyError: 

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

241 

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

243 """ 

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

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

246 """ 

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

248 

249 def draw_all_floats(self) -> None: 

250 """ 

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

252 """ 

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

254 # to this list. See `FloatContainer`. 

255 while self._draw_float_functions: 

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

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

258 

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

260 # might have been added. 

261 self._draw_float_functions = functions[1:] 

262 functions[0][1]() 

263 

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

265 """ 

266 For all the characters in the screen. 

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

268 """ 

269 b = self.data_buffer 

270 char_cache = _CHAR_CACHE 

271 

272 append_style = " " + style_str 

273 

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

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

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

277 

278 def fill_area( 

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

280 ) -> None: 

281 """ 

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

283 The style is prepended before whatever was here before. 

284 """ 

285 if not style.strip(): 

286 return 

287 

288 xmin = write_position.xpos 

289 xmax = write_position.xpos + write_position.width 

290 char_cache = _CHAR_CACHE 

291 data_buffer = self.data_buffer 

292 

293 if after: 

294 append_style = " " + style 

295 prepend_style = "" 

296 else: 

297 append_style = "" 

298 prepend_style = style + " " 

299 

300 for y in range( 

301 write_position.ypos, write_position.ypos + write_position.height 

302 ): 

303 row = data_buffer[y] 

304 for x in range(xmin, xmax): 

305 cell = row[x] 

306 row[x] = char_cache[ 

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

308 ] 

309 

310 

311class WritePosition: 

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

313 assert height >= 0 

314 assert width >= 0 

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

316 

317 self.xpos = xpos 

318 self.ypos = ypos 

319 self.width = width 

320 self.height = height 

321 

322 def __repr__(self) -> str: 

323 return "{}(x={!r}, y={!r}, width={!r}, height={!r})".format( 

324 self.__class__.__name__, 

325 self.xpos, 

326 self.ypos, 

327 self.width, 

328 self.height, 

329 )