Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/prompt_toolkit/output/vt100.py: 33%
321 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:07 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:07 +0000
1"""
2Output for vt100 terminals.
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 annotations
11import io
12import os
13import sys
14from typing import (
15 Callable,
16 Dict,
17 Hashable,
18 Iterable,
19 List,
20 Optional,
21 Sequence,
22 Set,
23 TextIO,
24 Tuple,
25)
27from prompt_toolkit.cursor_shapes import CursorShape
28from prompt_toolkit.data_structures import Size
29from prompt_toolkit.output import Output
30from prompt_toolkit.styles import ANSI_COLOR_NAMES, Attrs
31from prompt_toolkit.utils import is_dumb_terminal
33from .color_depth import ColorDepth
34from .flush_stdout import flush_stdout
36__all__ = [
37 "Vt100_Output",
38]
41FG_ANSI_COLORS = {
42 "ansidefault": 39,
43 # Low intensity.
44 "ansiblack": 30,
45 "ansired": 31,
46 "ansigreen": 32,
47 "ansiyellow": 33,
48 "ansiblue": 34,
49 "ansimagenta": 35,
50 "ansicyan": 36,
51 "ansigray": 37,
52 # High intensity.
53 "ansibrightblack": 90,
54 "ansibrightred": 91,
55 "ansibrightgreen": 92,
56 "ansibrightyellow": 93,
57 "ansibrightblue": 94,
58 "ansibrightmagenta": 95,
59 "ansibrightcyan": 96,
60 "ansiwhite": 97,
61}
63BG_ANSI_COLORS = {
64 "ansidefault": 49,
65 # Low intensity.
66 "ansiblack": 40,
67 "ansired": 41,
68 "ansigreen": 42,
69 "ansiyellow": 43,
70 "ansiblue": 44,
71 "ansimagenta": 45,
72 "ansicyan": 46,
73 "ansigray": 47,
74 # High intensity.
75 "ansibrightblack": 100,
76 "ansibrightred": 101,
77 "ansibrightgreen": 102,
78 "ansibrightyellow": 103,
79 "ansibrightblue": 104,
80 "ansibrightmagenta": 105,
81 "ansibrightcyan": 106,
82 "ansiwhite": 107,
83}
86ANSI_COLORS_TO_RGB = {
87 "ansidefault": (
88 0x00,
89 0x00,
90 0x00,
91 ), # Don't use, 'default' doesn't really have a value.
92 "ansiblack": (0x00, 0x00, 0x00),
93 "ansigray": (0xE5, 0xE5, 0xE5),
94 "ansibrightblack": (0x7F, 0x7F, 0x7F),
95 "ansiwhite": (0xFF, 0xFF, 0xFF),
96 # Low intensity.
97 "ansired": (0xCD, 0x00, 0x00),
98 "ansigreen": (0x00, 0xCD, 0x00),
99 "ansiyellow": (0xCD, 0xCD, 0x00),
100 "ansiblue": (0x00, 0x00, 0xCD),
101 "ansimagenta": (0xCD, 0x00, 0xCD),
102 "ansicyan": (0x00, 0xCD, 0xCD),
103 # High intensity.
104 "ansibrightred": (0xFF, 0x00, 0x00),
105 "ansibrightgreen": (0x00, 0xFF, 0x00),
106 "ansibrightyellow": (0xFF, 0xFF, 0x00),
107 "ansibrightblue": (0x00, 0x00, 0xFF),
108 "ansibrightmagenta": (0xFF, 0x00, 0xFF),
109 "ansibrightcyan": (0x00, 0xFF, 0xFF),
110}
113assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
114assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
115assert set(ANSI_COLORS_TO_RGB) == set(ANSI_COLOR_NAMES)
118def _get_closest_ansi_color(r: int, g: int, b: int, exclude: Sequence[str] = ()) -> str:
119 """
120 Find closest ANSI color. Return it by name.
122 :param r: Red (Between 0 and 255.)
123 :param g: Green (Between 0 and 255.)
124 :param b: Blue (Between 0 and 255.)
125 :param exclude: A tuple of color names to exclude. (E.g. ``('ansired', )``.)
126 """
127 exclude = list(exclude)
129 # When we have a bit of saturation, avoid the gray-like colors, otherwise,
130 # too often the distance to the gray color is less.
131 saturation = abs(r - g) + abs(g - b) + abs(b - r) # Between 0..510
133 if saturation > 30:
134 exclude.extend(["ansilightgray", "ansidarkgray", "ansiwhite", "ansiblack"])
136 # Take the closest color.
137 # (Thanks to Pygments for this part.)
138 distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff)
139 match = "ansidefault"
141 for name, (r2, g2, b2) in ANSI_COLORS_TO_RGB.items():
142 if name != "ansidefault" and name not in exclude:
143 d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2
145 if d < distance:
146 match = name
147 distance = d
149 return match
152_ColorCodeAndName = Tuple[int, str]
155class _16ColorCache:
156 """
157 Cache which maps (r, g, b) tuples to 16 ansi colors.
159 :param bg: Cache for background colors, instead of foreground.
160 """
162 def __init__(self, bg: bool = False) -> None:
163 self.bg = bg
164 self._cache: dict[Hashable, _ColorCodeAndName] = {}
166 def get_code(
167 self, value: tuple[int, int, int], exclude: Sequence[str] = ()
168 ) -> _ColorCodeAndName:
169 """
170 Return a (ansi_code, ansi_name) tuple. (E.g. ``(44, 'ansiblue')``.) for
171 a given (r,g,b) value.
172 """
173 key: Hashable = (value, tuple(exclude))
174 cache = self._cache
176 if key not in cache:
177 cache[key] = self._get(value, exclude)
179 return cache[key]
181 def _get(
182 self, value: tuple[int, int, int], exclude: Sequence[str] = ()
183 ) -> _ColorCodeAndName:
184 r, g, b = value
185 match = _get_closest_ansi_color(r, g, b, exclude=exclude)
187 # Turn color name into code.
188 if self.bg:
189 code = BG_ANSI_COLORS[match]
190 else:
191 code = FG_ANSI_COLORS[match]
193 return code, match
196class _256ColorCache(Dict[Tuple[int, int, int], int]):
197 """
198 Cache which maps (r, g, b) tuples to 256 colors.
199 """
201 def __init__(self) -> None:
202 # Build color table.
203 colors: list[tuple[int, int, int]] = []
205 # colors 0..15: 16 basic colors
206 colors.append((0x00, 0x00, 0x00)) # 0
207 colors.append((0xCD, 0x00, 0x00)) # 1
208 colors.append((0x00, 0xCD, 0x00)) # 2
209 colors.append((0xCD, 0xCD, 0x00)) # 3
210 colors.append((0x00, 0x00, 0xEE)) # 4
211 colors.append((0xCD, 0x00, 0xCD)) # 5
212 colors.append((0x00, 0xCD, 0xCD)) # 6
213 colors.append((0xE5, 0xE5, 0xE5)) # 7
214 colors.append((0x7F, 0x7F, 0x7F)) # 8
215 colors.append((0xFF, 0x00, 0x00)) # 9
216 colors.append((0x00, 0xFF, 0x00)) # 10
217 colors.append((0xFF, 0xFF, 0x00)) # 11
218 colors.append((0x5C, 0x5C, 0xFF)) # 12
219 colors.append((0xFF, 0x00, 0xFF)) # 13
220 colors.append((0x00, 0xFF, 0xFF)) # 14
221 colors.append((0xFF, 0xFF, 0xFF)) # 15
223 # colors 16..232: the 6x6x6 color cube
224 valuerange = (0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF)
226 for i in range(217):
227 r = valuerange[(i // 36) % 6]
228 g = valuerange[(i // 6) % 6]
229 b = valuerange[i % 6]
230 colors.append((r, g, b))
232 # colors 233..253: grayscale
233 for i in range(1, 22):
234 v = 8 + i * 10
235 colors.append((v, v, v))
237 self.colors = colors
239 def __missing__(self, value: tuple[int, int, int]) -> int:
240 r, g, b = value
242 # Find closest color.
243 # (Thanks to Pygments for this!)
244 distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff)
245 match = 0
247 for i, (r2, g2, b2) in enumerate(self.colors):
248 if i >= 16: # XXX: We ignore the 16 ANSI colors when mapping RGB
249 # to the 256 colors, because these highly depend on
250 # the color scheme of the terminal.
251 d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2
253 if d < distance:
254 match = i
255 distance = d
257 # Turn color name into code.
258 self[value] = match
259 return match
262_16_fg_colors = _16ColorCache(bg=False)
263_16_bg_colors = _16ColorCache(bg=True)
264_256_colors = _256ColorCache()
267class _EscapeCodeCache(Dict[Attrs, str]):
268 """
269 Cache for VT100 escape codes. It maps
270 (fgcolor, bgcolor, bold, underline, strike, reverse) tuples to VT100
271 escape sequences.
273 :param true_color: When True, use 24bit colors instead of 256 colors.
274 """
276 def __init__(self, color_depth: ColorDepth) -> None:
277 self.color_depth = color_depth
279 def __missing__(self, attrs: Attrs) -> str:
280 (
281 fgcolor,
282 bgcolor,
283 bold,
284 underline,
285 strike,
286 italic,
287 blink,
288 reverse,
289 hidden,
290 ) = attrs
291 parts: list[str] = []
293 parts.extend(self._colors_to_code(fgcolor or "", bgcolor or ""))
295 if bold:
296 parts.append("1")
297 if italic:
298 parts.append("3")
299 if blink:
300 parts.append("5")
301 if underline:
302 parts.append("4")
303 if reverse:
304 parts.append("7")
305 if hidden:
306 parts.append("8")
307 if strike:
308 parts.append("9")
310 if parts:
311 result = "\x1b[0;" + ";".join(parts) + "m"
312 else:
313 result = "\x1b[0m"
315 self[attrs] = result
316 return result
318 def _color_name_to_rgb(self, color: str) -> tuple[int, int, int]:
319 "Turn 'ffffff', into (0xff, 0xff, 0xff)."
320 try:
321 rgb = int(color, 16)
322 except ValueError:
323 raise
324 else:
325 r = (rgb >> 16) & 0xFF
326 g = (rgb >> 8) & 0xFF
327 b = rgb & 0xFF
328 return r, g, b
330 def _colors_to_code(self, fg_color: str, bg_color: str) -> Iterable[str]:
331 """
332 Return a tuple with the vt100 values that represent this color.
333 """
334 # When requesting ANSI colors only, and both fg/bg color were converted
335 # to ANSI, ensure that the foreground and background color are not the
336 # same. (Unless they were explicitly defined to be the same color.)
337 fg_ansi = ""
339 def get(color: str, bg: bool) -> list[int]:
340 nonlocal fg_ansi
342 table = BG_ANSI_COLORS if bg else FG_ANSI_COLORS
344 if not color or self.color_depth == ColorDepth.DEPTH_1_BIT:
345 return []
347 # 16 ANSI colors. (Given by name.)
348 elif color in table:
349 return [table[color]]
351 # RGB colors. (Defined as 'ffffff'.)
352 else:
353 try:
354 rgb = self._color_name_to_rgb(color)
355 except ValueError:
356 return []
358 # When only 16 colors are supported, use that.
359 if self.color_depth == ColorDepth.DEPTH_4_BIT:
360 if bg: # Background.
361 if fg_color != bg_color:
362 exclude = [fg_ansi]
363 else:
364 exclude = []
365 code, name = _16_bg_colors.get_code(rgb, exclude=exclude)
366 return [code]
367 else: # Foreground.
368 code, name = _16_fg_colors.get_code(rgb)
369 fg_ansi = name
370 return [code]
372 # True colors. (Only when this feature is enabled.)
373 elif self.color_depth == ColorDepth.DEPTH_24_BIT:
374 r, g, b = rgb
375 return [(48 if bg else 38), 2, r, g, b]
377 # 256 RGB colors.
378 else:
379 return [(48 if bg else 38), 5, _256_colors[rgb]]
381 result: list[int] = []
382 result.extend(get(fg_color, False))
383 result.extend(get(bg_color, True))
385 return map(str, result)
388def _get_size(fileno: int) -> tuple[int, int]:
389 """
390 Get the size of this pseudo terminal.
392 :param fileno: stdout.fileno()
393 :returns: A (rows, cols) tuple.
394 """
395 size = os.get_terminal_size(fileno)
396 return size.lines, size.columns
399class Vt100_Output(Output):
400 """
401 :param get_size: A callable which returns the `Size` of the output terminal.
402 :param stdout: Any object with has a `write` and `flush` method + an 'encoding' property.
403 :param term: The terminal environment variable. (xterm, xterm-256color, linux, ...)
404 :param enable_cpr: When `True` (the default), send "cursor position
405 request" escape sequences to the output in order to detect the cursor
406 position. That way, we can properly determine how much space there is
407 available for the UI (especially for drop down menus) to render. The
408 `Renderer` will still try to figure out whether the current terminal
409 does respond to CPR escapes. When `False`, never attempt to send CPR
410 requests.
411 """
413 # For the error messages. Only display "Output is not a terminal" once per
414 # file descriptor.
415 _fds_not_a_terminal: set[int] = set()
417 def __init__(
418 self,
419 stdout: TextIO,
420 get_size: Callable[[], Size],
421 term: str | None = None,
422 default_color_depth: ColorDepth | None = None,
423 enable_bell: bool = True,
424 enable_cpr: bool = True,
425 ) -> None:
426 assert all(hasattr(stdout, a) for a in ("write", "flush"))
428 self._buffer: list[str] = []
429 self.stdout: TextIO = stdout
430 self.default_color_depth = default_color_depth
431 self._get_size = get_size
432 self.term = term
433 self.enable_bell = enable_bell
434 self.enable_cpr = enable_cpr
436 # Cache for escape codes.
437 self._escape_code_caches: dict[ColorDepth, _EscapeCodeCache] = {
438 ColorDepth.DEPTH_1_BIT: _EscapeCodeCache(ColorDepth.DEPTH_1_BIT),
439 ColorDepth.DEPTH_4_BIT: _EscapeCodeCache(ColorDepth.DEPTH_4_BIT),
440 ColorDepth.DEPTH_8_BIT: _EscapeCodeCache(ColorDepth.DEPTH_8_BIT),
441 ColorDepth.DEPTH_24_BIT: _EscapeCodeCache(ColorDepth.DEPTH_24_BIT),
442 }
444 # Keep track of whether the cursor shape was ever changed.
445 # (We don't restore the cursor shape if it was never changed - by
446 # default, we don't change them.)
447 self._cursor_shape_changed = False
449 @classmethod
450 def from_pty(
451 cls,
452 stdout: TextIO,
453 term: str | None = None,
454 default_color_depth: ColorDepth | None = None,
455 enable_bell: bool = True,
456 ) -> Vt100_Output:
457 """
458 Create an Output class from a pseudo terminal.
459 (This will take the dimensions by reading the pseudo
460 terminal attributes.)
461 """
462 fd: int | None
463 # Normally, this requires a real TTY device, but people instantiate
464 # this class often during unit tests as well. For convenience, we print
465 # an error message, use standard dimensions, and go on.
466 try:
467 fd = stdout.fileno()
468 except io.UnsupportedOperation:
469 fd = None
471 if not stdout.isatty() and (fd is None or fd not in cls._fds_not_a_terminal):
472 msg = "Warning: Output is not a terminal (fd=%r).\n"
473 sys.stderr.write(msg % fd)
474 sys.stderr.flush()
475 if fd is not None:
476 cls._fds_not_a_terminal.add(fd)
478 def get_size() -> Size:
479 # If terminal (incorrectly) reports its size as 0, pick a
480 # reasonable default. See
481 # https://github.com/ipython/ipython/issues/10071
482 rows, columns = (None, None)
484 # It is possible that `stdout` is no longer a TTY device at this
485 # point. In that case we get an `OSError` in the ioctl call in
486 # `get_size`. See:
487 # https://github.com/prompt-toolkit/python-prompt-toolkit/pull/1021
488 try:
489 rows, columns = _get_size(stdout.fileno())
490 except OSError:
491 pass
492 return Size(rows=rows or 24, columns=columns or 80)
494 return cls(
495 stdout,
496 get_size,
497 term=term,
498 default_color_depth=default_color_depth,
499 enable_bell=enable_bell,
500 )
502 def get_size(self) -> Size:
503 return self._get_size()
505 def fileno(self) -> int:
506 "Return file descriptor."
507 return self.stdout.fileno()
509 def encoding(self) -> str:
510 "Return encoding used for stdout."
511 return self.stdout.encoding
513 def write_raw(self, data: str) -> None:
514 """
515 Write raw data to output.
516 """
517 self._buffer.append(data)
519 def write(self, data: str) -> None:
520 """
521 Write text to output.
522 (Removes vt100 escape codes. -- used for safely writing text.)
523 """
524 self._buffer.append(data.replace("\x1b", "?"))
526 def set_title(self, title: str) -> None:
527 """
528 Set terminal title.
529 """
530 if self.term not in (
531 "linux",
532 "eterm-color",
533 ): # Not supported by the Linux console.
534 self.write_raw(
535 "\x1b]2;%s\x07" % title.replace("\x1b", "").replace("\x07", "")
536 )
538 def clear_title(self) -> None:
539 self.set_title("")
541 def erase_screen(self) -> None:
542 """
543 Erases the screen with the background colour and moves the cursor to
544 home.
545 """
546 self.write_raw("\x1b[2J")
548 def enter_alternate_screen(self) -> None:
549 self.write_raw("\x1b[?1049h\x1b[H")
551 def quit_alternate_screen(self) -> None:
552 self.write_raw("\x1b[?1049l")
554 def enable_mouse_support(self) -> None:
555 self.write_raw("\x1b[?1000h")
557 # Enable mouse-drag support.
558 self.write_raw("\x1b[?1003h")
560 # Enable urxvt Mouse mode. (For terminals that understand this.)
561 self.write_raw("\x1b[?1015h")
563 # Also enable Xterm SGR mouse mode. (For terminals that understand this.)
564 self.write_raw("\x1b[?1006h")
566 # Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr
567 # extensions.
569 def disable_mouse_support(self) -> None:
570 self.write_raw("\x1b[?1000l")
571 self.write_raw("\x1b[?1015l")
572 self.write_raw("\x1b[?1006l")
573 self.write_raw("\x1b[?1003l")
575 def erase_end_of_line(self) -> None:
576 """
577 Erases from the current cursor position to the end of the current line.
578 """
579 self.write_raw("\x1b[K")
581 def erase_down(self) -> None:
582 """
583 Erases the screen from the current line down to the bottom of the
584 screen.
585 """
586 self.write_raw("\x1b[J")
588 def reset_attributes(self) -> None:
589 self.write_raw("\x1b[0m")
591 def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None:
592 """
593 Create new style and output.
595 :param attrs: `Attrs` instance.
596 """
597 # Get current depth.
598 escape_code_cache = self._escape_code_caches[color_depth]
600 # Write escape character.
601 self.write_raw(escape_code_cache[attrs])
603 def disable_autowrap(self) -> None:
604 self.write_raw("\x1b[?7l")
606 def enable_autowrap(self) -> None:
607 self.write_raw("\x1b[?7h")
609 def enable_bracketed_paste(self) -> None:
610 self.write_raw("\x1b[?2004h")
612 def disable_bracketed_paste(self) -> None:
613 self.write_raw("\x1b[?2004l")
615 def reset_cursor_key_mode(self) -> None:
616 """
617 For vt100 only.
618 Put the terminal in cursor mode (instead of application mode).
619 """
620 # Put the terminal in cursor mode. (Instead of application mode.)
621 self.write_raw("\x1b[?1l")
623 def cursor_goto(self, row: int = 0, column: int = 0) -> None:
624 """
625 Move cursor position.
626 """
627 self.write_raw("\x1b[%i;%iH" % (row, column))
629 def cursor_up(self, amount: int) -> None:
630 if amount == 0:
631 pass
632 elif amount == 1:
633 self.write_raw("\x1b[A")
634 else:
635 self.write_raw("\x1b[%iA" % amount)
637 def cursor_down(self, amount: int) -> None:
638 if amount == 0:
639 pass
640 elif amount == 1:
641 # Note: Not the same as '\n', '\n' can cause the window content to
642 # scroll.
643 self.write_raw("\x1b[B")
644 else:
645 self.write_raw("\x1b[%iB" % amount)
647 def cursor_forward(self, amount: int) -> None:
648 if amount == 0:
649 pass
650 elif amount == 1:
651 self.write_raw("\x1b[C")
652 else:
653 self.write_raw("\x1b[%iC" % amount)
655 def cursor_backward(self, amount: int) -> None:
656 if amount == 0:
657 pass
658 elif amount == 1:
659 self.write_raw("\b") # '\x1b[D'
660 else:
661 self.write_raw("\x1b[%iD" % amount)
663 def hide_cursor(self) -> None:
664 self.write_raw("\x1b[?25l")
666 def show_cursor(self) -> None:
667 self.write_raw("\x1b[?12l\x1b[?25h") # Stop blinking cursor and show.
669 def set_cursor_shape(self, cursor_shape: CursorShape) -> None:
670 if cursor_shape == CursorShape._NEVER_CHANGE:
671 return
673 self._cursor_shape_changed = True
674 self.write_raw(
675 {
676 CursorShape.BLOCK: "\x1b[2 q",
677 CursorShape.BEAM: "\x1b[6 q",
678 CursorShape.UNDERLINE: "\x1b[4 q",
679 CursorShape.BLINKING_BLOCK: "\x1b[1 q",
680 CursorShape.BLINKING_BEAM: "\x1b[5 q",
681 CursorShape.BLINKING_UNDERLINE: "\x1b[3 q",
682 }.get(cursor_shape, "")
683 )
685 def reset_cursor_shape(self) -> None:
686 "Reset cursor shape."
687 # (Only reset cursor shape, if we ever changed it.)
688 if self._cursor_shape_changed:
689 self._cursor_shape_changed = False
691 # Reset cursor shape.
692 self.write_raw("\x1b[0 q")
694 def flush(self) -> None:
695 """
696 Write to output stream and flush.
697 """
698 if not self._buffer:
699 return
701 data = "".join(self._buffer)
702 self._buffer = []
704 flush_stdout(self.stdout, data)
706 def ask_for_cpr(self) -> None:
707 """
708 Asks for a cursor position report (CPR).
709 """
710 self.write_raw("\x1b[6n")
711 self.flush()
713 @property
714 def responds_to_cpr(self) -> bool:
715 if not self.enable_cpr:
716 return False
718 # When the input is a tty, we assume that CPR is supported.
719 # It's not when the input is piped from Pexpect.
720 if os.environ.get("PROMPT_TOOLKIT_NO_CPR", "") == "1":
721 return False
723 if is_dumb_terminal(self.term):
724 return False
725 try:
726 return self.stdout.isatty()
727 except ValueError:
728 return False # ValueError: I/O operation on closed file
730 def bell(self) -> None:
731 "Sound bell."
732 if self.enable_bell:
733 self.write_raw("\a")
734 self.flush()
736 def get_default_color_depth(self) -> ColorDepth:
737 """
738 Return the default color depth for a vt100 terminal, according to the
739 our term value.
741 We prefer 256 colors almost always, because this is what most terminals
742 support these days, and is a good default.
743 """
744 if self.default_color_depth is not None:
745 return self.default_color_depth
747 term = self.term
749 if term is None:
750 return ColorDepth.DEFAULT
752 if is_dumb_terminal(term):
753 return ColorDepth.DEPTH_1_BIT
755 if term in ("linux", "eterm-color"):
756 return ColorDepth.DEPTH_4_BIT
758 return ColorDepth.DEFAULT