vt100_output.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. """
  2. Output for vt100 terminals.
  3. A lot of thanks, regarding outputting of colors, goes to the Pygments project:
  4. (We don't rely on Pygments anymore, because many things are very custom, and
  5. everything has been highly optimized.)
  6. http://pygments.org/
  7. """
  8. from __future__ import unicode_literals
  9. from prompt_toolkit.filters import to_simple_filter, Condition
  10. from prompt_toolkit.layout.screen import Size
  11. from prompt_toolkit.renderer import Output
  12. from prompt_toolkit.styles import ANSI_COLOR_NAMES
  13. from six.moves import range
  14. import array
  15. import errno
  16. import os
  17. import six
  18. __all__ = (
  19. 'Vt100_Output',
  20. )
  21. FG_ANSI_COLORS = {
  22. 'ansidefault': 39,
  23. # Low intensity.
  24. 'ansiblack': 30,
  25. 'ansidarkred': 31,
  26. 'ansidarkgreen': 32,
  27. 'ansibrown': 33,
  28. 'ansidarkblue': 34,
  29. 'ansipurple': 35,
  30. 'ansiteal': 36,
  31. 'ansilightgray': 37,
  32. # High intensity.
  33. 'ansidarkgray': 90,
  34. 'ansired': 91,
  35. 'ansigreen': 92,
  36. 'ansiyellow': 93,
  37. 'ansiblue': 94,
  38. 'ansifuchsia': 95,
  39. 'ansiturquoise': 96,
  40. 'ansiwhite': 97,
  41. }
  42. BG_ANSI_COLORS = {
  43. 'ansidefault': 49,
  44. # Low intensity.
  45. 'ansiblack': 40,
  46. 'ansidarkred': 41,
  47. 'ansidarkgreen': 42,
  48. 'ansibrown': 43,
  49. 'ansidarkblue': 44,
  50. 'ansipurple': 45,
  51. 'ansiteal': 46,
  52. 'ansilightgray': 47,
  53. # High intensity.
  54. 'ansidarkgray': 100,
  55. 'ansired': 101,
  56. 'ansigreen': 102,
  57. 'ansiyellow': 103,
  58. 'ansiblue': 104,
  59. 'ansifuchsia': 105,
  60. 'ansiturquoise': 106,
  61. 'ansiwhite': 107,
  62. }
  63. ANSI_COLORS_TO_RGB = {
  64. 'ansidefault': (0x00, 0x00, 0x00), # Don't use, 'default' doesn't really have a value.
  65. 'ansiblack': (0x00, 0x00, 0x00),
  66. 'ansidarkgray': (0x7f, 0x7f, 0x7f),
  67. 'ansiwhite': (0xff, 0xff, 0xff),
  68. 'ansilightgray': (0xe5, 0xe5, 0xe5),
  69. # Low intensity.
  70. 'ansidarkred': (0xcd, 0x00, 0x00),
  71. 'ansidarkgreen': (0x00, 0xcd, 0x00),
  72. 'ansibrown': (0xcd, 0xcd, 0x00),
  73. 'ansidarkblue': (0x00, 0x00, 0xcd),
  74. 'ansipurple': (0xcd, 0x00, 0xcd),
  75. 'ansiteal': (0x00, 0xcd, 0xcd),
  76. # High intensity.
  77. 'ansired': (0xff, 0x00, 0x00),
  78. 'ansigreen': (0x00, 0xff, 0x00),
  79. 'ansiyellow': (0xff, 0xff, 0x00),
  80. 'ansiblue': (0x00, 0x00, 0xff),
  81. 'ansifuchsia': (0xff, 0x00, 0xff),
  82. 'ansiturquoise': (0x00, 0xff, 0xff),
  83. }
  84. assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
  85. assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
  86. assert set(ANSI_COLORS_TO_RGB) == set(ANSI_COLOR_NAMES)
  87. def _get_closest_ansi_color(r, g, b, exclude=()):
  88. """
  89. Find closest ANSI color. Return it by name.
  90. :param r: Red (Between 0 and 255.)
  91. :param g: Green (Between 0 and 255.)
  92. :param b: Blue (Between 0 and 255.)
  93. :param exclude: A tuple of color names to exclude. (E.g. ``('ansired', )``.)
  94. """
  95. assert isinstance(exclude, tuple)
  96. # When we have a bit of saturation, avoid the gray-like colors, otherwise,
  97. # too often the distance to the gray color is less.
  98. saturation = abs(r - g) + abs(g - b) + abs(b - r) # Between 0..510
  99. if saturation > 30:
  100. exclude += ('ansilightgray', 'ansidarkgray', 'ansiwhite', 'ansiblack')
  101. # Take the closest color.
  102. # (Thanks to Pygments for this part.)
  103. distance = 257*257*3 # "infinity" (>distance from #000000 to #ffffff)
  104. match = 'ansidefault'
  105. for name, (r2, g2, b2) in ANSI_COLORS_TO_RGB.items():
  106. if name != 'ansidefault' and name not in exclude:
  107. d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2
  108. if d < distance:
  109. match = name
  110. distance = d
  111. return match
  112. class _16ColorCache(dict):
  113. """
  114. Cache which maps (r, g, b) tuples to 16 ansi colors.
  115. :param bg: Cache for background colors, instead of foreground.
  116. """
  117. def __init__(self, bg=False):
  118. assert isinstance(bg, bool)
  119. self.bg = bg
  120. def get_code(self, value, exclude=()):
  121. """
  122. Return a (ansi_code, ansi_name) tuple. (E.g. ``(44, 'ansiblue')``.) for
  123. a given (r,g,b) value.
  124. """
  125. key = (value, exclude)
  126. if key not in self:
  127. self[key] = self._get(value, exclude)
  128. return self[key]
  129. def _get(self, value, exclude=()):
  130. r, g, b = value
  131. match = _get_closest_ansi_color(r, g, b, exclude=exclude)
  132. # Turn color name into code.
  133. if self.bg:
  134. code = BG_ANSI_COLORS[match]
  135. else:
  136. code = FG_ANSI_COLORS[match]
  137. self[value] = code
  138. return code, match
  139. class _256ColorCache(dict):
  140. """
  141. Cach which maps (r, g, b) tuples to 256 colors.
  142. """
  143. def __init__(self):
  144. # Build color table.
  145. colors = []
  146. # colors 0..15: 16 basic colors
  147. colors.append((0x00, 0x00, 0x00)) # 0
  148. colors.append((0xcd, 0x00, 0x00)) # 1
  149. colors.append((0x00, 0xcd, 0x00)) # 2
  150. colors.append((0xcd, 0xcd, 0x00)) # 3
  151. colors.append((0x00, 0x00, 0xee)) # 4
  152. colors.append((0xcd, 0x00, 0xcd)) # 5
  153. colors.append((0x00, 0xcd, 0xcd)) # 6
  154. colors.append((0xe5, 0xe5, 0xe5)) # 7
  155. colors.append((0x7f, 0x7f, 0x7f)) # 8
  156. colors.append((0xff, 0x00, 0x00)) # 9
  157. colors.append((0x00, 0xff, 0x00)) # 10
  158. colors.append((0xff, 0xff, 0x00)) # 11
  159. colors.append((0x5c, 0x5c, 0xff)) # 12
  160. colors.append((0xff, 0x00, 0xff)) # 13
  161. colors.append((0x00, 0xff, 0xff)) # 14
  162. colors.append((0xff, 0xff, 0xff)) # 15
  163. # colors 16..232: the 6x6x6 color cube
  164. valuerange = (0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff)
  165. for i in range(217):
  166. r = valuerange[(i // 36) % 6]
  167. g = valuerange[(i // 6) % 6]
  168. b = valuerange[i % 6]
  169. colors.append((r, g, b))
  170. # colors 233..253: grayscale
  171. for i in range(1, 22):
  172. v = 8 + i * 10
  173. colors.append((v, v, v))
  174. self.colors = colors
  175. def __missing__(self, value):
  176. r, g, b = value
  177. # Find closest color.
  178. # (Thanks to Pygments for this!)
  179. distance = 257*257*3 # "infinity" (>distance from #000000 to #ffffff)
  180. match = 0
  181. for i, (r2, g2, b2) in enumerate(self.colors):
  182. d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2
  183. if d < distance:
  184. match = i
  185. distance = d
  186. # Turn color name into code.
  187. self[value] = match
  188. return match
  189. _16_fg_colors = _16ColorCache(bg=False)
  190. _16_bg_colors = _16ColorCache(bg=True)
  191. _256_colors = _256ColorCache()
  192. class _EscapeCodeCache(dict):
  193. """
  194. Cache for VT100 escape codes. It maps
  195. (fgcolor, bgcolor, bold, underline, reverse) tuples to VT100 escape sequences.
  196. :param true_color: When True, use 24bit colors instead of 256 colors.
  197. """
  198. def __init__(self, true_color=False, ansi_colors_only=False):
  199. assert isinstance(true_color, bool)
  200. self.true_color = true_color
  201. self.ansi_colors_only = to_simple_filter(ansi_colors_only)
  202. def __missing__(self, attrs):
  203. fgcolor, bgcolor, bold, underline, italic, blink, reverse = attrs
  204. parts = []
  205. parts.extend(self._colors_to_code(fgcolor, bgcolor))
  206. if bold:
  207. parts.append('1')
  208. if italic:
  209. parts.append('3')
  210. if blink:
  211. parts.append('5')
  212. if underline:
  213. parts.append('4')
  214. if reverse:
  215. parts.append('7')
  216. if parts:
  217. result = '\x1b[0;' + ';'.join(parts) + 'm'
  218. else:
  219. result = '\x1b[0m'
  220. self[attrs] = result
  221. return result
  222. def _color_name_to_rgb(self, color):
  223. " Turn 'ffffff', into (0xff, 0xff, 0xff). "
  224. try:
  225. rgb = int(color, 16)
  226. except ValueError:
  227. raise
  228. else:
  229. r = (rgb >> 16) & 0xff
  230. g = (rgb >> 8) & 0xff
  231. b = rgb & 0xff
  232. return r, g, b
  233. def _colors_to_code(self, fg_color, bg_color):
  234. " Return a tuple with the vt100 values that represent this color. "
  235. # When requesting ANSI colors only, and both fg/bg color were converted
  236. # to ANSI, ensure that the foreground and background color are not the
  237. # same. (Unless they were explicitely defined to be the same color.)
  238. fg_ansi = [()]
  239. def get(color, bg):
  240. table = BG_ANSI_COLORS if bg else FG_ANSI_COLORS
  241. if color is None:
  242. return ()
  243. # 16 ANSI colors. (Given by name.)
  244. elif color in table:
  245. return (table[color], )
  246. # RGB colors. (Defined as 'ffffff'.)
  247. else:
  248. try:
  249. rgb = self._color_name_to_rgb(color)
  250. except ValueError:
  251. return ()
  252. # When only 16 colors are supported, use that.
  253. if self.ansi_colors_only():
  254. if bg: # Background.
  255. if fg_color != bg_color:
  256. exclude = (fg_ansi[0], )
  257. else:
  258. exclude = ()
  259. code, name = _16_bg_colors.get_code(rgb, exclude=exclude)
  260. return (code, )
  261. else: # Foreground.
  262. code, name = _16_fg_colors.get_code(rgb)
  263. fg_ansi[0] = name
  264. return (code, )
  265. # True colors. (Only when this feature is enabled.)
  266. elif self.true_color:
  267. r, g, b = rgb
  268. return (48 if bg else 38, 2, r, g, b)
  269. # 256 RGB colors.
  270. else:
  271. return (48 if bg else 38, 5, _256_colors[rgb])
  272. result = []
  273. result.extend(get(fg_color, False))
  274. result.extend(get(bg_color, True))
  275. return map(six.text_type, result)
  276. def _get_size(fileno):
  277. # Thanks to fabric (fabfile.org), and
  278. # http://sqizit.bartletts.id.au/2011/02/14/pseudo-terminals-in-python/
  279. """
  280. Get the size of this pseudo terminal.
  281. :param fileno: stdout.fileno()
  282. :returns: A (rows, cols) tuple.
  283. """
  284. # Inline imports, because these modules are not available on Windows.
  285. # (This file is used by ConEmuOutput, which is used on Windows.)
  286. import fcntl
  287. import termios
  288. # Buffer for the C call
  289. buf = array.array(b'h' if six.PY2 else u'h', [0, 0, 0, 0])
  290. # Do TIOCGWINSZ (Get)
  291. # Note: We should not pass 'True' as a fourth parameter to 'ioctl'. (True
  292. # is the default.) This causes segmentation faults on some systems.
  293. # See: https://github.com/jonathanslenders/python-prompt-toolkit/pull/364
  294. fcntl.ioctl(fileno, termios.TIOCGWINSZ, buf)
  295. # Return rows, cols
  296. return buf[0], buf[1]
  297. class Vt100_Output(Output):
  298. """
  299. :param get_size: A callable which returns the `Size` of the output terminal.
  300. :param stdout: Any object with has a `write` and `flush` method + an 'encoding' property.
  301. :param true_color: Use 24bit color instead of 256 colors. (Can be a :class:`SimpleFilter`.)
  302. When `ansi_colors_only` is set, only 16 colors are used.
  303. :param ansi_colors_only: Restrict to 16 ANSI colors only.
  304. :param term: The terminal environment variable. (xterm, xterm-256color, linux, ...)
  305. :param write_binary: Encode the output before writing it. If `True` (the
  306. default), the `stdout` object is supposed to expose an `encoding` attribute.
  307. """
  308. def __init__(self, stdout, get_size, true_color=False,
  309. ansi_colors_only=None, term=None, write_binary=True):
  310. assert callable(get_size)
  311. assert term is None or isinstance(term, six.text_type)
  312. assert all(hasattr(stdout, a) for a in ('write', 'flush'))
  313. if write_binary:
  314. assert hasattr(stdout, 'encoding')
  315. self._buffer = []
  316. self.stdout = stdout
  317. self.write_binary = write_binary
  318. self.get_size = get_size
  319. self.true_color = to_simple_filter(true_color)
  320. self.term = term or 'xterm'
  321. # ANSI colors only?
  322. if ansi_colors_only is None:
  323. # When not given, use the following default.
  324. ANSI_COLORS_ONLY = bool(os.environ.get(
  325. 'PROMPT_TOOLKIT_ANSI_COLORS_ONLY', False))
  326. @Condition
  327. def ansi_colors_only():
  328. return ANSI_COLORS_ONLY or term in ('linux', 'eterm-color')
  329. else:
  330. ansi_colors_only = to_simple_filter(ansi_colors_only)
  331. self.ansi_colors_only = ansi_colors_only
  332. # Cache for escape codes.
  333. self._escape_code_cache = _EscapeCodeCache(ansi_colors_only=ansi_colors_only)
  334. self._escape_code_cache_true_color = _EscapeCodeCache(
  335. true_color=True, ansi_colors_only=ansi_colors_only)
  336. @classmethod
  337. def from_pty(cls, stdout, true_color=False, ansi_colors_only=None, term=None):
  338. """
  339. Create an Output class from a pseudo terminal.
  340. (This will take the dimensions by reading the pseudo
  341. terminal attributes.)
  342. """
  343. assert stdout.isatty()
  344. def get_size():
  345. rows, columns = _get_size(stdout.fileno())
  346. # If terminal (incorrectly) reports its size as 0, pick a reasonable default.
  347. # See https://github.com/ipython/ipython/issues/10071
  348. return Size(rows=(rows or 24), columns=(columns or 80))
  349. return cls(stdout, get_size, true_color=true_color,
  350. ansi_colors_only=ansi_colors_only, term=term)
  351. def fileno(self):
  352. " Return file descriptor. "
  353. return self.stdout.fileno()
  354. def encoding(self):
  355. " Return encoding used for stdout. "
  356. return self.stdout.encoding
  357. def write_raw(self, data):
  358. """
  359. Write raw data to output.
  360. """
  361. self._buffer.append(data)
  362. def write(self, data):
  363. """
  364. Write text to output.
  365. (Removes vt100 escape codes. -- used for safely writing text.)
  366. """
  367. self._buffer.append(data.replace('\x1b', '?'))
  368. def set_title(self, title):
  369. """
  370. Set terminal title.
  371. """
  372. if self.term not in ('linux', 'eterm-color'): # Not supported by the Linux console.
  373. self.write_raw('\x1b]2;%s\x07' % title.replace('\x1b', '').replace('\x07', ''))
  374. def clear_title(self):
  375. self.set_title('')
  376. def erase_screen(self):
  377. """
  378. Erases the screen with the background colour and moves the cursor to
  379. home.
  380. """
  381. self.write_raw('\x1b[2J')
  382. def enter_alternate_screen(self):
  383. self.write_raw('\x1b[?1049h\x1b[H')
  384. def quit_alternate_screen(self):
  385. self.write_raw('\x1b[?1049l')
  386. def enable_mouse_support(self):
  387. self.write_raw('\x1b[?1000h')
  388. # Enable urxvt Mouse mode. (For terminals that understand this.)
  389. self.write_raw('\x1b[?1015h')
  390. # Also enable Xterm SGR mouse mode. (For terminals that understand this.)
  391. self.write_raw('\x1b[?1006h')
  392. # Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr
  393. # extensions.
  394. def disable_mouse_support(self):
  395. self.write_raw('\x1b[?1000l')
  396. self.write_raw('\x1b[?1015l')
  397. self.write_raw('\x1b[?1006l')
  398. def erase_end_of_line(self):
  399. """
  400. Erases from the current cursor position to the end of the current line.
  401. """
  402. self.write_raw('\x1b[K')
  403. def erase_down(self):
  404. """
  405. Erases the screen from the current line down to the bottom of the
  406. screen.
  407. """
  408. self.write_raw('\x1b[J')
  409. def reset_attributes(self):
  410. self.write_raw('\x1b[0m')
  411. def set_attributes(self, attrs):
  412. """
  413. Create new style and output.
  414. :param attrs: `Attrs` instance.
  415. """
  416. if self.true_color() and not self.ansi_colors_only():
  417. self.write_raw(self._escape_code_cache_true_color[attrs])
  418. else:
  419. self.write_raw(self._escape_code_cache[attrs])
  420. def disable_autowrap(self):
  421. self.write_raw('\x1b[?7l')
  422. def enable_autowrap(self):
  423. self.write_raw('\x1b[?7h')
  424. def enable_bracketed_paste(self):
  425. self.write_raw('\x1b[?2004h')
  426. def disable_bracketed_paste(self):
  427. self.write_raw('\x1b[?2004l')
  428. def cursor_goto(self, row=0, column=0):
  429. """ Move cursor position. """
  430. self.write_raw('\x1b[%i;%iH' % (row, column))
  431. def cursor_up(self, amount):
  432. if amount == 0:
  433. pass
  434. elif amount == 1:
  435. self.write_raw('\x1b[A')
  436. else:
  437. self.write_raw('\x1b[%iA' % amount)
  438. def cursor_down(self, amount):
  439. if amount == 0:
  440. pass
  441. elif amount == 1:
  442. # Note: Not the same as '\n', '\n' can cause the window content to
  443. # scroll.
  444. self.write_raw('\x1b[B')
  445. else:
  446. self.write_raw('\x1b[%iB' % amount)
  447. def cursor_forward(self, amount):
  448. if amount == 0:
  449. pass
  450. elif amount == 1:
  451. self.write_raw('\x1b[C')
  452. else:
  453. self.write_raw('\x1b[%iC' % amount)
  454. def cursor_backward(self, amount):
  455. if amount == 0:
  456. pass
  457. elif amount == 1:
  458. self.write_raw('\b') # '\x1b[D'
  459. else:
  460. self.write_raw('\x1b[%iD' % amount)
  461. def hide_cursor(self):
  462. self.write_raw('\x1b[?25l')
  463. def show_cursor(self):
  464. self.write_raw('\x1b[?12l\x1b[?25h') # Stop blinking cursor and show.
  465. def flush(self):
  466. """
  467. Write to output stream and flush.
  468. """
  469. if not self._buffer:
  470. return
  471. data = ''.join(self._buffer)
  472. try:
  473. # (We try to encode ourself, because that way we can replace
  474. # characters that don't exist in the character set, avoiding
  475. # UnicodeEncodeError crashes. E.g. u'\xb7' does not appear in 'ascii'.)
  476. # My Arch Linux installation of july 2015 reported 'ANSI_X3.4-1968'
  477. # for sys.stdout.encoding in xterm.
  478. if self.write_binary:
  479. if hasattr(self.stdout, 'buffer'):
  480. out = self.stdout.buffer # Py3.
  481. else:
  482. out = self.stdout
  483. out.write(data.encode(self.stdout.encoding or 'utf-8', 'replace'))
  484. else:
  485. self.stdout.write(data)
  486. self.stdout.flush()
  487. except IOError as e:
  488. if e.args and e.args[0] == errno.EINTR:
  489. # Interrupted system call. Can happpen in case of a window
  490. # resize signal. (Just ignore. The resize handler will render
  491. # again anyway.)
  492. pass
  493. elif e.args and e.args[0] == 0:
  494. # This can happen when there is a lot of output and the user
  495. # sends a KeyboardInterrupt by pressing Control-C. E.g. in
  496. # a Python REPL when we execute "while True: print('test')".
  497. # (The `ptpython` REPL uses this `Output` class instead of
  498. # `stdout` directly -- in order to be network transparent.)
  499. # So, just ignore.
  500. pass
  501. else:
  502. raise
  503. self._buffer = []
  504. def ask_for_cpr(self):
  505. """
  506. Asks for a cursor position report (CPR).
  507. """
  508. self.write_raw('\x1b[6n')
  509. self.flush()
  510. def bell(self):
  511. " Sound bell. "
  512. self.write_raw('\a')
  513. self.flush()