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