ansi.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. from __future__ import annotations
  2. from string import Formatter
  3. from typing import Generator
  4. from prompt_toolkit.output.vt100 import BG_ANSI_COLORS, FG_ANSI_COLORS
  5. from prompt_toolkit.output.vt100 import _256_colors as _256_colors_table
  6. from .base import StyleAndTextTuples
  7. __all__ = [
  8. "ANSI",
  9. "ansi_escape",
  10. ]
  11. class ANSI:
  12. """
  13. ANSI formatted text.
  14. Take something ANSI escaped text, for use as a formatted string. E.g.
  15. ::
  16. ANSI('\\x1b[31mhello \\x1b[32mworld')
  17. Characters between ``\\001`` and ``\\002`` are supposed to have a zero width
  18. when printed, but these are literally sent to the terminal output. This can
  19. be used for instance, for inserting Final Term prompt commands. They will
  20. be translated into a prompt_toolkit '[ZeroWidthEscape]' fragment.
  21. """
  22. def __init__(self, value: str) -> None:
  23. self.value = value
  24. self._formatted_text: StyleAndTextTuples = []
  25. # Default style attributes.
  26. self._color: str | None = None
  27. self._bgcolor: str | None = None
  28. self._bold = False
  29. self._underline = False
  30. self._strike = False
  31. self._italic = False
  32. self._blink = False
  33. self._reverse = False
  34. self._hidden = False
  35. # Process received text.
  36. parser = self._parse_corot()
  37. parser.send(None) # type: ignore
  38. for c in value:
  39. parser.send(c)
  40. def _parse_corot(self) -> Generator[None, str, None]:
  41. """
  42. Coroutine that parses the ANSI escape sequences.
  43. """
  44. style = ""
  45. formatted_text = self._formatted_text
  46. while True:
  47. # NOTE: CSI is a special token within a stream of characters that
  48. # introduces an ANSI control sequence used to set the
  49. # style attributes of the following characters.
  50. csi = False
  51. c = yield
  52. # Everything between \001 and \002 should become a ZeroWidthEscape.
  53. if c == "\001":
  54. escaped_text = ""
  55. while c != "\002":
  56. c = yield
  57. if c == "\002":
  58. formatted_text.append(("[ZeroWidthEscape]", escaped_text))
  59. c = yield
  60. break
  61. else:
  62. escaped_text += c
  63. # Check for CSI
  64. if c == "\x1b":
  65. # Start of color escape sequence.
  66. square_bracket = yield
  67. if square_bracket == "[":
  68. csi = True
  69. else:
  70. continue
  71. elif c == "\x9b":
  72. csi = True
  73. if csi:
  74. # Got a CSI sequence. Color codes are following.
  75. current = ""
  76. params = []
  77. while True:
  78. char = yield
  79. # Construct number
  80. if char.isdigit():
  81. current += char
  82. # Eval number
  83. else:
  84. # Limit and save number value
  85. params.append(min(int(current or 0), 9999))
  86. # Get delimiter token if present
  87. if char == ";":
  88. current = ""
  89. # Check and evaluate color codes
  90. elif char == "m":
  91. # Set attributes and token.
  92. self._select_graphic_rendition(params)
  93. style = self._create_style_string()
  94. break
  95. # Check and evaluate cursor forward
  96. elif char == "C":
  97. for i in range(params[0]):
  98. # add <SPACE> using current style
  99. formatted_text.append((style, " "))
  100. break
  101. else:
  102. # Ignore unsupported sequence.
  103. break
  104. else:
  105. # Add current character.
  106. # NOTE: At this point, we could merge the current character
  107. # into the previous tuple if the style did not change,
  108. # however, it's not worth the effort given that it will
  109. # be "Exploded" once again when it's rendered to the
  110. # output.
  111. formatted_text.append((style, c))
  112. def _select_graphic_rendition(self, attrs: list[int]) -> None:
  113. """
  114. Taken a list of graphics attributes and apply changes.
  115. """
  116. if not attrs:
  117. attrs = [0]
  118. else:
  119. attrs = list(attrs[::-1])
  120. while attrs:
  121. attr = attrs.pop()
  122. if attr in _fg_colors:
  123. self._color = _fg_colors[attr]
  124. elif attr in _bg_colors:
  125. self._bgcolor = _bg_colors[attr]
  126. elif attr == 1:
  127. self._bold = True
  128. # elif attr == 2:
  129. # self._faint = True
  130. elif attr == 3:
  131. self._italic = True
  132. elif attr == 4:
  133. self._underline = True
  134. elif attr == 5:
  135. self._blink = True # Slow blink
  136. elif attr == 6:
  137. self._blink = True # Fast blink
  138. elif attr == 7:
  139. self._reverse = True
  140. elif attr == 8:
  141. self._hidden = True
  142. elif attr == 9:
  143. self._strike = True
  144. elif attr == 22:
  145. self._bold = False # Normal intensity
  146. elif attr == 23:
  147. self._italic = False
  148. elif attr == 24:
  149. self._underline = False
  150. elif attr == 25:
  151. self._blink = False
  152. elif attr == 27:
  153. self._reverse = False
  154. elif attr == 28:
  155. self._hidden = False
  156. elif attr == 29:
  157. self._strike = False
  158. elif not attr:
  159. # Reset all style attributes
  160. self._color = None
  161. self._bgcolor = None
  162. self._bold = False
  163. self._underline = False
  164. self._strike = False
  165. self._italic = False
  166. self._blink = False
  167. self._reverse = False
  168. self._hidden = False
  169. elif attr in (38, 48) and len(attrs) > 1:
  170. n = attrs.pop()
  171. # 256 colors.
  172. if n == 5 and len(attrs) >= 1:
  173. if attr == 38:
  174. m = attrs.pop()
  175. self._color = _256_colors.get(m)
  176. elif attr == 48:
  177. m = attrs.pop()
  178. self._bgcolor = _256_colors.get(m)
  179. # True colors.
  180. if n == 2 and len(attrs) >= 3:
  181. try:
  182. color_str = (
  183. f"#{attrs.pop():02x}{attrs.pop():02x}{attrs.pop():02x}"
  184. )
  185. except IndexError:
  186. pass
  187. else:
  188. if attr == 38:
  189. self._color = color_str
  190. elif attr == 48:
  191. self._bgcolor = color_str
  192. def _create_style_string(self) -> str:
  193. """
  194. Turn current style flags into a string for usage in a formatted text.
  195. """
  196. result = []
  197. if self._color:
  198. result.append(self._color)
  199. if self._bgcolor:
  200. result.append("bg:" + self._bgcolor)
  201. if self._bold:
  202. result.append("bold")
  203. if self._underline:
  204. result.append("underline")
  205. if self._strike:
  206. result.append("strike")
  207. if self._italic:
  208. result.append("italic")
  209. if self._blink:
  210. result.append("blink")
  211. if self._reverse:
  212. result.append("reverse")
  213. if self._hidden:
  214. result.append("hidden")
  215. return " ".join(result)
  216. def __repr__(self) -> str:
  217. return f"ANSI({self.value!r})"
  218. def __pt_formatted_text__(self) -> StyleAndTextTuples:
  219. return self._formatted_text
  220. def format(self, *args: str, **kwargs: str) -> ANSI:
  221. """
  222. Like `str.format`, but make sure that the arguments are properly
  223. escaped. (No ANSI escapes can be injected.)
  224. """
  225. return ANSI(FORMATTER.vformat(self.value, args, kwargs))
  226. def __mod__(self, value: object) -> ANSI:
  227. """
  228. ANSI('<b>%s</b>') % value
  229. """
  230. if not isinstance(value, tuple):
  231. value = (value,)
  232. value = tuple(ansi_escape(i) for i in value)
  233. return ANSI(self.value % value)
  234. # Mapping of the ANSI color codes to their names.
  235. _fg_colors = {v: k for k, v in FG_ANSI_COLORS.items()}
  236. _bg_colors = {v: k for k, v in BG_ANSI_COLORS.items()}
  237. # Mapping of the escape codes for 256colors to their 'ffffff' value.
  238. _256_colors = {}
  239. for i, (r, g, b) in enumerate(_256_colors_table.colors):
  240. _256_colors[i] = f"#{r:02x}{g:02x}{b:02x}"
  241. def ansi_escape(text: object) -> str:
  242. """
  243. Replace characters with a special meaning.
  244. """
  245. return str(text).replace("\x1b", "?").replace("\b", "?")
  246. class ANSIFormatter(Formatter):
  247. def format_field(self, value: object, format_spec: str) -> str:
  248. return ansi_escape(format(value, format_spec))
  249. FORMATTER = ANSIFormatter()