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