screen.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. from __future__ import annotations
  2. from collections import defaultdict
  3. from typing import TYPE_CHECKING, Callable
  4. from prompt_toolkit.cache import FastDictCache
  5. from prompt_toolkit.data_structures import Point
  6. from prompt_toolkit.utils import get_cwidth
  7. if TYPE_CHECKING:
  8. from .containers import Window
  9. __all__ = [
  10. "Screen",
  11. "Char",
  12. ]
  13. class Char:
  14. """
  15. Represent a single character in a :class:`.Screen`.
  16. This should be considered immutable.
  17. :param char: A single character (can be a double-width character).
  18. :param style: A style string. (Can contain classnames.)
  19. """
  20. __slots__ = ("char", "style", "width")
  21. # If we end up having one of these special control sequences in the input string,
  22. # we should display them as follows:
  23. # Usually this happens after a "quoted insert".
  24. display_mappings: dict[str, str] = {
  25. "\x00": "^@", # Control space
  26. "\x01": "^A",
  27. "\x02": "^B",
  28. "\x03": "^C",
  29. "\x04": "^D",
  30. "\x05": "^E",
  31. "\x06": "^F",
  32. "\x07": "^G",
  33. "\x08": "^H",
  34. "\x09": "^I",
  35. "\x0a": "^J",
  36. "\x0b": "^K",
  37. "\x0c": "^L",
  38. "\x0d": "^M",
  39. "\x0e": "^N",
  40. "\x0f": "^O",
  41. "\x10": "^P",
  42. "\x11": "^Q",
  43. "\x12": "^R",
  44. "\x13": "^S",
  45. "\x14": "^T",
  46. "\x15": "^U",
  47. "\x16": "^V",
  48. "\x17": "^W",
  49. "\x18": "^X",
  50. "\x19": "^Y",
  51. "\x1a": "^Z",
  52. "\x1b": "^[", # Escape
  53. "\x1c": "^\\",
  54. "\x1d": "^]",
  55. "\x1e": "^^",
  56. "\x1f": "^_",
  57. "\x7f": "^?", # ASCII Delete (backspace).
  58. # Special characters. All visualized like Vim does.
  59. "\x80": "<80>",
  60. "\x81": "<81>",
  61. "\x82": "<82>",
  62. "\x83": "<83>",
  63. "\x84": "<84>",
  64. "\x85": "<85>",
  65. "\x86": "<86>",
  66. "\x87": "<87>",
  67. "\x88": "<88>",
  68. "\x89": "<89>",
  69. "\x8a": "<8a>",
  70. "\x8b": "<8b>",
  71. "\x8c": "<8c>",
  72. "\x8d": "<8d>",
  73. "\x8e": "<8e>",
  74. "\x8f": "<8f>",
  75. "\x90": "<90>",
  76. "\x91": "<91>",
  77. "\x92": "<92>",
  78. "\x93": "<93>",
  79. "\x94": "<94>",
  80. "\x95": "<95>",
  81. "\x96": "<96>",
  82. "\x97": "<97>",
  83. "\x98": "<98>",
  84. "\x99": "<99>",
  85. "\x9a": "<9a>",
  86. "\x9b": "<9b>",
  87. "\x9c": "<9c>",
  88. "\x9d": "<9d>",
  89. "\x9e": "<9e>",
  90. "\x9f": "<9f>",
  91. # For the non-breaking space: visualize like Emacs does by default.
  92. # (Print a space, but attach the 'nbsp' class that applies the
  93. # underline style.)
  94. "\xa0": " ",
  95. }
  96. def __init__(self, char: str = " ", style: str = "") -> None:
  97. # If this character has to be displayed otherwise, take that one.
  98. if char in self.display_mappings:
  99. if char == "\xa0":
  100. style += " class:nbsp " # Will be underlined.
  101. else:
  102. style += " class:control-character "
  103. char = self.display_mappings[char]
  104. self.char = char
  105. self.style = style
  106. # Calculate width. (We always need this, so better to store it directly
  107. # as a member for performance.)
  108. self.width = get_cwidth(char)
  109. # In theory, `other` can be any type of object, but because of performance
  110. # we don't want to do an `isinstance` check every time. We assume "other"
  111. # is always a "Char".
  112. def _equal(self, other: Char) -> bool:
  113. return self.char == other.char and self.style == other.style
  114. def _not_equal(self, other: Char) -> bool:
  115. # Not equal: We don't do `not char.__eq__` here, because of the
  116. # performance of calling yet another function.
  117. return self.char != other.char or self.style != other.style
  118. if not TYPE_CHECKING:
  119. __eq__ = _equal
  120. __ne__ = _not_equal
  121. def __repr__(self) -> str:
  122. return f"{self.__class__.__name__}({self.char!r}, {self.style!r})"
  123. _CHAR_CACHE: FastDictCache[tuple[str, str], Char] = FastDictCache(
  124. Char, size=1000 * 1000
  125. )
  126. Transparent = "[transparent]"
  127. class Screen:
  128. """
  129. Two dimensional buffer of :class:`.Char` instances.
  130. """
  131. def __init__(
  132. self,
  133. default_char: Char | None = None,
  134. initial_width: int = 0,
  135. initial_height: int = 0,
  136. ) -> None:
  137. if default_char is None:
  138. default_char2 = _CHAR_CACHE[" ", Transparent]
  139. else:
  140. default_char2 = default_char
  141. self.data_buffer: defaultdict[int, defaultdict[int, Char]] = defaultdict(
  142. lambda: defaultdict(lambda: default_char2)
  143. )
  144. #: Escape sequences to be injected.
  145. self.zero_width_escapes: defaultdict[int, defaultdict[int, str]] = defaultdict(
  146. lambda: defaultdict(str)
  147. )
  148. #: Position of the cursor.
  149. self.cursor_positions: dict[
  150. Window, Point
  151. ] = {} # Map `Window` objects to `Point` objects.
  152. #: Visibility of the cursor.
  153. self.show_cursor = True
  154. #: (Optional) Where to position the menu. E.g. at the start of a completion.
  155. #: (We can't use the cursor position, because we don't want the
  156. #: completion menu to change its position when we browse through all the
  157. #: completions.)
  158. self.menu_positions: dict[
  159. Window, Point
  160. ] = {} # Map `Window` objects to `Point` objects.
  161. #: Currently used width/height of the screen. This will increase when
  162. #: data is written to the screen.
  163. self.width = initial_width or 0
  164. self.height = initial_height or 0
  165. # Windows that have been drawn. (Each `Window` class will add itself to
  166. # this list.)
  167. self.visible_windows_to_write_positions: dict[Window, WritePosition] = {}
  168. # List of (z_index, draw_func)
  169. self._draw_float_functions: list[tuple[int, Callable[[], None]]] = []
  170. @property
  171. def visible_windows(self) -> list[Window]:
  172. return list(self.visible_windows_to_write_positions.keys())
  173. def set_cursor_position(self, window: Window, position: Point) -> None:
  174. """
  175. Set the cursor position for a given window.
  176. """
  177. self.cursor_positions[window] = position
  178. def set_menu_position(self, window: Window, position: Point) -> None:
  179. """
  180. Set the cursor position for a given window.
  181. """
  182. self.menu_positions[window] = position
  183. def get_cursor_position(self, window: Window) -> Point:
  184. """
  185. Get the cursor position for a given window.
  186. Returns a `Point`.
  187. """
  188. try:
  189. return self.cursor_positions[window]
  190. except KeyError:
  191. return Point(x=0, y=0)
  192. def get_menu_position(self, window: Window) -> Point:
  193. """
  194. Get the menu position for a given window.
  195. (This falls back to the cursor position if no menu position was set.)
  196. """
  197. try:
  198. return self.menu_positions[window]
  199. except KeyError:
  200. try:
  201. return self.cursor_positions[window]
  202. except KeyError:
  203. return Point(x=0, y=0)
  204. def draw_with_z_index(self, z_index: int, draw_func: Callable[[], None]) -> None:
  205. """
  206. Add a draw-function for a `Window` which has a >= 0 z_index.
  207. This will be postponed until `draw_all_floats` is called.
  208. """
  209. self._draw_float_functions.append((z_index, draw_func))
  210. def draw_all_floats(self) -> None:
  211. """
  212. Draw all float functions in order of z-index.
  213. """
  214. # We keep looping because some draw functions could add new functions
  215. # to this list. See `FloatContainer`.
  216. while self._draw_float_functions:
  217. # Sort the floats that we have so far by z_index.
  218. functions = sorted(self._draw_float_functions, key=lambda item: item[0])
  219. # Draw only one at a time, then sort everything again. Now floats
  220. # might have been added.
  221. self._draw_float_functions = functions[1:]
  222. functions[0][1]()
  223. def append_style_to_content(self, style_str: str) -> None:
  224. """
  225. For all the characters in the screen.
  226. Set the style string to the given `style_str`.
  227. """
  228. b = self.data_buffer
  229. char_cache = _CHAR_CACHE
  230. append_style = " " + style_str
  231. for y, row in b.items():
  232. for x, char in row.items():
  233. row[x] = char_cache[char.char, char.style + append_style]
  234. def fill_area(
  235. self, write_position: WritePosition, style: str = "", after: bool = False
  236. ) -> None:
  237. """
  238. Fill the content of this area, using the given `style`.
  239. The style is prepended before whatever was here before.
  240. """
  241. if not style.strip():
  242. return
  243. xmin = write_position.xpos
  244. xmax = write_position.xpos + write_position.width
  245. char_cache = _CHAR_CACHE
  246. data_buffer = self.data_buffer
  247. if after:
  248. append_style = " " + style
  249. prepend_style = ""
  250. else:
  251. append_style = ""
  252. prepend_style = style + " "
  253. for y in range(
  254. write_position.ypos, write_position.ypos + write_position.height
  255. ):
  256. row = data_buffer[y]
  257. for x in range(xmin, xmax):
  258. cell = row[x]
  259. row[x] = char_cache[
  260. cell.char, prepend_style + cell.style + append_style
  261. ]
  262. class WritePosition:
  263. def __init__(self, xpos: int, ypos: int, width: int, height: int) -> None:
  264. assert height >= 0
  265. assert width >= 0
  266. # xpos and ypos can be negative. (A float can be partially visible.)
  267. self.xpos = xpos
  268. self.ypos = ypos
  269. self.width = width
  270. self.height = height
  271. def __repr__(self) -> str:
  272. return f"{self.__class__.__name__}(x={self.xpos!r}, y={self.ypos!r}, width={self.width!r}, height={self.height!r})"