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