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