ansi_code_processor.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. """ Utilities for processing ANSI escape codes and special ASCII characters.
  2. """
  3. #-----------------------------------------------------------------------------
  4. # Imports
  5. #-----------------------------------------------------------------------------
  6. # Standard library imports
  7. from collections import namedtuple
  8. import re
  9. # System library imports
  10. from qtpy import QtGui
  11. # Local imports
  12. from ipython_genutils.py3compat import string_types
  13. from qtconsole.styles import dark_style
  14. #-----------------------------------------------------------------------------
  15. # Constants and datatypes
  16. #-----------------------------------------------------------------------------
  17. # An action for erase requests (ED and EL commands).
  18. EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])
  19. # An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP,
  20. # and HVP commands).
  21. # FIXME: Not implemented in AnsiCodeProcessor.
  22. MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])
  23. # An action for scroll requests (SU and ST) and form feeds.
  24. ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
  25. # An action for the carriage return character
  26. CarriageReturnAction = namedtuple('CarriageReturnAction', ['action'])
  27. # An action for the \n character
  28. NewLineAction = namedtuple('NewLineAction', ['action'])
  29. # An action for the beep character
  30. BeepAction = namedtuple('BeepAction', ['action'])
  31. # An action for backspace
  32. BackSpaceAction = namedtuple('BackSpaceAction', ['action'])
  33. # Regular expressions.
  34. CSI_COMMANDS = 'ABCDEFGHJKSTfmnsu'
  35. CSI_SUBPATTERN = '\[(.*?)([%s])' % CSI_COMMANDS
  36. OSC_SUBPATTERN = '\](.*?)[\x07\x1b]'
  37. ANSI_PATTERN = ('\x01?\x1b(%s|%s)\x02?' % \
  38. (CSI_SUBPATTERN, OSC_SUBPATTERN))
  39. ANSI_OR_SPECIAL_PATTERN = re.compile('(\a|\b|\r(?!\n)|\r?\n)|(?:%s)' % ANSI_PATTERN)
  40. SPECIAL_PATTERN = re.compile('([\f])')
  41. #-----------------------------------------------------------------------------
  42. # Classes
  43. #-----------------------------------------------------------------------------
  44. class AnsiCodeProcessor(object):
  45. """ Translates special ASCII characters and ANSI escape codes into readable
  46. attributes. It also supports a few non-standard, xterm-specific codes.
  47. """
  48. # Whether to increase intensity or set boldness for SGR code 1.
  49. # (Different terminals handle this in different ways.)
  50. bold_text_enabled = False
  51. # We provide an empty default color map because subclasses will likely want
  52. # to use a custom color format.
  53. default_color_map = {}
  54. #---------------------------------------------------------------------------
  55. # AnsiCodeProcessor interface
  56. #---------------------------------------------------------------------------
  57. def __init__(self):
  58. self.actions = []
  59. self.color_map = self.default_color_map.copy()
  60. self.reset_sgr()
  61. def reset_sgr(self):
  62. """ Reset graphics attributs to their default values.
  63. """
  64. self.intensity = 0
  65. self.italic = False
  66. self.bold = False
  67. self.underline = False
  68. self.foreground_color = None
  69. self.background_color = None
  70. def split_string(self, string):
  71. """ Yields substrings for which the same escape code applies.
  72. """
  73. self.actions = []
  74. start = 0
  75. # strings ending with \r are assumed to be ending in \r\n since
  76. # \n is appended to output strings automatically. Accounting
  77. # for that, here.
  78. last_char = '\n' if len(string) > 0 and string[-1] == '\n' else None
  79. string = string[:-1] if last_char is not None else string
  80. for match in ANSI_OR_SPECIAL_PATTERN.finditer(string):
  81. raw = string[start:match.start()]
  82. substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
  83. if substring or self.actions:
  84. yield substring
  85. self.actions = []
  86. start = match.end()
  87. groups = [g for g in match.groups() if (g is not None)]
  88. g0 = groups[0]
  89. if g0 == '\a':
  90. self.actions.append(BeepAction('beep'))
  91. yield None
  92. self.actions = []
  93. elif g0 == '\r':
  94. self.actions.append(CarriageReturnAction('carriage-return'))
  95. yield None
  96. self.actions = []
  97. elif g0 == '\b':
  98. self.actions.append(BackSpaceAction('backspace'))
  99. yield None
  100. self.actions = []
  101. elif g0 == '\n' or g0 == '\r\n':
  102. self.actions.append(NewLineAction('newline'))
  103. yield g0
  104. self.actions = []
  105. else:
  106. params = [ param for param in groups[1].split(';') if param ]
  107. if g0.startswith('['):
  108. # Case 1: CSI code.
  109. try:
  110. params = list(map(int, params))
  111. except ValueError:
  112. # Silently discard badly formed codes.
  113. pass
  114. else:
  115. self.set_csi_code(groups[2], params)
  116. elif g0.startswith(']'):
  117. # Case 2: OSC code.
  118. self.set_osc_code(params)
  119. raw = string[start:]
  120. substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
  121. if substring or self.actions:
  122. yield substring
  123. if last_char is not None:
  124. self.actions.append(NewLineAction('newline'))
  125. yield last_char
  126. def set_csi_code(self, command, params=[]):
  127. """ Set attributes based on CSI (Control Sequence Introducer) code.
  128. Parameters
  129. ----------
  130. command : str
  131. The code identifier, i.e. the final character in the sequence.
  132. params : sequence of integers, optional
  133. The parameter codes for the command.
  134. """
  135. if command == 'm': # SGR - Select Graphic Rendition
  136. if params:
  137. self.set_sgr_code(params)
  138. else:
  139. self.set_sgr_code([0])
  140. elif (command == 'J' or # ED - Erase Data
  141. command == 'K'): # EL - Erase in Line
  142. code = params[0] if params else 0
  143. if 0 <= code <= 2:
  144. area = 'screen' if command == 'J' else 'line'
  145. if code == 0:
  146. erase_to = 'end'
  147. elif code == 1:
  148. erase_to = 'start'
  149. elif code == 2:
  150. erase_to = 'all'
  151. self.actions.append(EraseAction('erase', area, erase_to))
  152. elif (command == 'S' or # SU - Scroll Up
  153. command == 'T'): # SD - Scroll Down
  154. dir = 'up' if command == 'S' else 'down'
  155. count = params[0] if params else 1
  156. self.actions.append(ScrollAction('scroll', dir, 'line', count))
  157. def set_osc_code(self, params):
  158. """ Set attributes based on OSC (Operating System Command) parameters.
  159. Parameters
  160. ----------
  161. params : sequence of str
  162. The parameters for the command.
  163. """
  164. try:
  165. command = int(params.pop(0))
  166. except (IndexError, ValueError):
  167. return
  168. if command == 4:
  169. # xterm-specific: set color number to color spec.
  170. try:
  171. color = int(params.pop(0))
  172. spec = params.pop(0)
  173. self.color_map[color] = self._parse_xterm_color_spec(spec)
  174. except (IndexError, ValueError):
  175. pass
  176. def set_sgr_code(self, params):
  177. """ Set attributes based on SGR (Select Graphic Rendition) codes.
  178. Parameters
  179. ----------
  180. params : sequence of ints
  181. A list of SGR codes for one or more SGR commands. Usually this
  182. sequence will have one element per command, although certain
  183. xterm-specific commands requires multiple elements.
  184. """
  185. # Always consume the first parameter.
  186. if not params:
  187. return
  188. code = params.pop(0)
  189. if code == 0:
  190. self.reset_sgr()
  191. elif code == 1:
  192. if self.bold_text_enabled:
  193. self.bold = True
  194. else:
  195. self.intensity = 1
  196. elif code == 2:
  197. self.intensity = 0
  198. elif code == 3:
  199. self.italic = True
  200. elif code == 4:
  201. self.underline = True
  202. elif code == 22:
  203. self.intensity = 0
  204. self.bold = False
  205. elif code == 23:
  206. self.italic = False
  207. elif code == 24:
  208. self.underline = False
  209. elif code >= 30 and code <= 37:
  210. self.foreground_color = code - 30
  211. elif code == 38 and params:
  212. _color_type = params.pop(0)
  213. if _color_type == 5 and params:
  214. # xterm-specific: 256 color support.
  215. self.foreground_color = params.pop(0)
  216. elif _color_type == 2:
  217. # 24bit true colour support.
  218. self.foreground_color = params[:3]
  219. params[:3] = []
  220. elif code == 39:
  221. self.foreground_color = None
  222. elif code >= 40 and code <= 47:
  223. self.background_color = code - 40
  224. elif code == 48 and params:
  225. _color_type = params.pop(0)
  226. if _color_type == 5 and params:
  227. # xterm-specific: 256 color support.
  228. self.background_color = params.pop(0)
  229. elif _color_type == 2:
  230. # 24bit true colour support.
  231. self.background_color = params[:3]
  232. params[:3] = []
  233. elif code == 49:
  234. self.background_color = None
  235. # Recurse with unconsumed parameters.
  236. self.set_sgr_code(params)
  237. #---------------------------------------------------------------------------
  238. # Protected interface
  239. #---------------------------------------------------------------------------
  240. def _parse_xterm_color_spec(self, spec):
  241. if spec.startswith('rgb:'):
  242. return tuple(map(lambda x: int(x, 16), spec[4:].split('/')))
  243. elif spec.startswith('rgbi:'):
  244. return tuple(map(lambda x: int(float(x) * 255),
  245. spec[5:].split('/')))
  246. elif spec == '?':
  247. raise ValueError('Unsupported xterm color spec')
  248. return spec
  249. def _replace_special(self, match):
  250. special = match.group(1)
  251. if special == '\f':
  252. self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
  253. return ''
  254. class QtAnsiCodeProcessor(AnsiCodeProcessor):
  255. """ Translates ANSI escape codes into QTextCharFormats.
  256. """
  257. # A map from ANSI color codes to SVG color names or RGB(A) tuples.
  258. darkbg_color_map = {
  259. 0 : 'black', # black
  260. 1 : 'darkred', # red
  261. 2 : 'darkgreen', # green
  262. 3 : 'brown', # yellow
  263. 4 : 'darkblue', # blue
  264. 5 : 'darkviolet', # magenta
  265. 6 : 'steelblue', # cyan
  266. 7 : 'grey', # white
  267. 8 : 'grey', # black (bright)
  268. 9 : 'red', # red (bright)
  269. 10 : 'lime', # green (bright)
  270. 11 : 'yellow', # yellow (bright)
  271. 12 : 'deepskyblue', # blue (bright)
  272. 13 : 'magenta', # magenta (bright)
  273. 14 : 'cyan', # cyan (bright)
  274. 15 : 'white' } # white (bright)
  275. # Set the default color map for super class.
  276. default_color_map = darkbg_color_map.copy()
  277. def get_color(self, color, intensity=0):
  278. """ Returns a QColor for a given color code or rgb list, or None if one
  279. cannot be constructed.
  280. """
  281. if isinstance(color, int):
  282. # Adjust for intensity, if possible.
  283. if color < 8 and intensity > 0:
  284. color += 8
  285. constructor = self.color_map.get(color, None)
  286. elif isinstance(color, (tuple, list)):
  287. constructor = color
  288. else:
  289. return None
  290. if isinstance(constructor, string_types):
  291. # If this is an X11 color name, we just hope there is a close SVG
  292. # color name. We could use QColor's static method
  293. # 'setAllowX11ColorNames()', but this is global and only available
  294. # on X11. It seems cleaner to aim for uniformity of behavior.
  295. return QtGui.QColor(constructor)
  296. elif isinstance(constructor, (tuple, list)):
  297. return QtGui.QColor(*constructor)
  298. return None
  299. def get_format(self):
  300. """ Returns a QTextCharFormat that encodes the current style attributes.
  301. """
  302. format = QtGui.QTextCharFormat()
  303. # Set foreground color
  304. qcolor = self.get_color(self.foreground_color, self.intensity)
  305. if qcolor is not None:
  306. format.setForeground(qcolor)
  307. # Set background color
  308. qcolor = self.get_color(self.background_color, self.intensity)
  309. if qcolor is not None:
  310. format.setBackground(qcolor)
  311. # Set font weight/style options
  312. if self.bold:
  313. format.setFontWeight(QtGui.QFont.Bold)
  314. else:
  315. format.setFontWeight(QtGui.QFont.Normal)
  316. format.setFontItalic(self.italic)
  317. format.setFontUnderline(self.underline)
  318. return format
  319. def set_background_color(self, style):
  320. """
  321. Given a syntax style, attempt to set a color map that will be
  322. aesthetically pleasing.
  323. """
  324. # Set a new default color map.
  325. self.default_color_map = self.darkbg_color_map.copy()
  326. if not dark_style(style):
  327. # Colors appropriate for a terminal with a light background. For
  328. # now, only use non-bright colors...
  329. for i in range(8):
  330. self.default_color_map[i + 8] = self.default_color_map[i]
  331. # ...and replace white with black.
  332. self.default_color_map[7] = self.default_color_map[15] = 'black'
  333. # Update the current color map with the new defaults.
  334. self.color_map.update(self.default_color_map)