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, reverse) 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 ) = attrs
281 parts: list[str] = []
282
283 parts.extend(self._colors_to_code(fgcolor or "", bgcolor or ""))
284
285 if bold:
286 parts.append("1")
287 if italic:
288 parts.append("3")
289 if blink:
290 parts.append("5")
291 if underline:
292 parts.append("4")
293 if reverse:
294 parts.append("7")
295 if hidden:
296 parts.append("8")
297 if strike:
298 parts.append("9")
299
300 if parts:
301 result = "\x1b[0;" + ";".join(parts) + "m"
302 else:
303 result = "\x1b[0m"
304
305 self[attrs] = result
306 return result
307
308 def _color_name_to_rgb(self, color: str) -> tuple[int, int, int]:
309 "Turn 'ffffff', into (0xff, 0xff, 0xff)."
310 try:
311 rgb = int(color, 16)
312 except ValueError:
313 raise
314 else:
315 r = (rgb >> 16) & 0xFF
316 g = (rgb >> 8) & 0xFF
317 b = rgb & 0xFF
318 return r, g, b
319
320 def _colors_to_code(self, fg_color: str, bg_color: str) -> Iterable[str]:
321 """
322 Return a tuple with the vt100 values that represent this color.
323 """
324 # When requesting ANSI colors only, and both fg/bg color were converted
325 # to ANSI, ensure that the foreground and background color are not the
326 # same. (Unless they were explicitly defined to be the same color.)
327 fg_ansi = ""
328
329 def get(color: str, bg: bool) -> list[int]:
330 nonlocal fg_ansi
331
332 table = BG_ANSI_COLORS if bg else FG_ANSI_COLORS
333
334 if not color or self.color_depth == ColorDepth.DEPTH_1_BIT:
335 return []
336
337 # 16 ANSI colors. (Given by name.)
338 elif color in table:
339 return [table[color]]
340
341 # RGB colors. (Defined as 'ffffff'.)
342 else:
343 try:
344 rgb = self._color_name_to_rgb(color)
345 except ValueError:
346 return []
347
348 # When only 16 colors are supported, use that.
349 if self.color_depth == ColorDepth.DEPTH_4_BIT:
350 if bg: # Background.
351 if fg_color != bg_color:
352 exclude = [fg_ansi]
353 else:
354 exclude = []
355 code, name = _16_bg_colors.get_code(rgb, exclude=exclude)
356 return [code]
357 else: # Foreground.
358 code, name = _16_fg_colors.get_code(rgb)
359 fg_ansi = name
360 return [code]
361
362 # True colors. (Only when this feature is enabled.)
363 elif self.color_depth == ColorDepth.DEPTH_24_BIT:
364 r, g, b = rgb
365 return [(48 if bg else 38), 2, r, g, b]
366
367 # 256 RGB colors.
368 else:
369 return [(48 if bg else 38), 5, _256_colors[rgb]]
370
371 result: list[int] = []
372 result.extend(get(fg_color, False))
373 result.extend(get(bg_color, True))
374
375 return map(str, result)
376
377
378def _get_size(fileno: int) -> tuple[int, int]:
379 """
380 Get the size of this pseudo terminal.
381
382 :param fileno: stdout.fileno()
383 :returns: A (rows, cols) tuple.
384 """
385 size = os.get_terminal_size(fileno)
386 return size.lines, size.columns
387
388
389class Vt100_Output(Output):
390 """
391 :param get_size: A callable which returns the `Size` of the output terminal.
392 :param stdout: Any object with has a `write` and `flush` method + an 'encoding' property.
393 :param term: The terminal environment variable. (xterm, xterm-256color, linux, ...)
394 :param enable_cpr: When `True` (the default), send "cursor position
395 request" escape sequences to the output in order to detect the cursor
396 position. That way, we can properly determine how much space there is
397 available for the UI (especially for drop down menus) to render. The
398 `Renderer` will still try to figure out whether the current terminal
399 does respond to CPR escapes. When `False`, never attempt to send CPR
400 requests.
401 """
402
403 # For the error messages. Only display "Output is not a terminal" once per
404 # file descriptor.
405 _fds_not_a_terminal: set[int] = set()
406
407 def __init__(
408 self,
409 stdout: TextIO,
410 get_size: Callable[[], Size],
411 term: str | None = None,
412 default_color_depth: ColorDepth | None = None,
413 enable_bell: bool = True,
414 enable_cpr: bool = True,
415 ) -> None:
416 assert all(hasattr(stdout, a) for a in ("write", "flush"))
417
418 self._buffer: list[str] = []
419 self.stdout: TextIO = stdout
420 self.default_color_depth = default_color_depth
421 self._get_size = get_size
422 self.term = term
423 self.enable_bell = enable_bell
424 self.enable_cpr = enable_cpr
425
426 # Cache for escape codes.
427 self._escape_code_caches: dict[ColorDepth, _EscapeCodeCache] = {
428 ColorDepth.DEPTH_1_BIT: _EscapeCodeCache(ColorDepth.DEPTH_1_BIT),
429 ColorDepth.DEPTH_4_BIT: _EscapeCodeCache(ColorDepth.DEPTH_4_BIT),
430 ColorDepth.DEPTH_8_BIT: _EscapeCodeCache(ColorDepth.DEPTH_8_BIT),
431 ColorDepth.DEPTH_24_BIT: _EscapeCodeCache(ColorDepth.DEPTH_24_BIT),
432 }
433
434 # Keep track of whether the cursor shape was ever changed.
435 # (We don't restore the cursor shape if it was never changed - by
436 # default, we don't change them.)
437 self._cursor_shape_changed = False
438
439 # Don't hide/show the cursor when this was already done.
440 # (`None` means that we don't know whether the cursor is visible or
441 # not.)
442 self._cursor_visible: bool | None = None
443
444 @classmethod
445 def from_pty(
446 cls,
447 stdout: TextIO,
448 term: str | None = None,
449 default_color_depth: ColorDepth | None = None,
450 enable_bell: bool = True,
451 ) -> Vt100_Output:
452 """
453 Create an Output class from a pseudo terminal.
454 (This will take the dimensions by reading the pseudo
455 terminal attributes.)
456 """
457 fd: int | None
458 # Normally, this requires a real TTY device, but people instantiate
459 # this class often during unit tests as well. For convenience, we print
460 # an error message, use standard dimensions, and go on.
461 try:
462 fd = stdout.fileno()
463 except io.UnsupportedOperation:
464 fd = None
465
466 if not stdout.isatty() and (fd is None or fd not in cls._fds_not_a_terminal):
467 msg = "Warning: Output is not a terminal (fd=%r).\n"
468 sys.stderr.write(msg % fd)
469 sys.stderr.flush()
470 if fd is not None:
471 cls._fds_not_a_terminal.add(fd)
472
473 def get_size() -> Size:
474 # If terminal (incorrectly) reports its size as 0, pick a
475 # reasonable default. See
476 # https://github.com/ipython/ipython/issues/10071
477 rows, columns = (None, None)
478
479 # It is possible that `stdout` is no longer a TTY device at this
480 # point. In that case we get an `OSError` in the ioctl call in
481 # `get_size`. See:
482 # https://github.com/prompt-toolkit/python-prompt-toolkit/pull/1021
483 try:
484 rows, columns = _get_size(stdout.fileno())
485 except OSError:
486 pass
487 return Size(rows=rows or 24, columns=columns or 80)
488
489 return cls(
490 stdout,
491 get_size,
492 term=term,
493 default_color_depth=default_color_depth,
494 enable_bell=enable_bell,
495 )
496
497 def get_size(self) -> Size:
498 return self._get_size()
499
500 def fileno(self) -> int:
501 "Return file descriptor."
502 return self.stdout.fileno()
503
504 def encoding(self) -> str:
505 "Return encoding used for stdout."
506 return self.stdout.encoding
507
508 def write_raw(self, data: str) -> None:
509 """
510 Write raw data to output.
511 """
512 self._buffer.append(data)
513
514 def write(self, data: str) -> None:
515 """
516 Write text to output.
517 (Removes vt100 escape codes. -- used for safely writing text.)
518 """
519 self._buffer.append(data.replace("\x1b", "?"))
520
521 def set_title(self, title: str) -> None:
522 """
523 Set terminal title.
524 """
525 if self.term not in (
526 "linux",
527 "eterm-color",
528 ): # Not supported by the Linux console.
529 self.write_raw(
530 "\x1b]2;{}\x07".format(title.replace("\x1b", "").replace("\x07", ""))
531 )
532
533 def clear_title(self) -> None:
534 self.set_title("")
535
536 def erase_screen(self) -> None:
537 """
538 Erases the screen with the background color and moves the cursor to
539 home.
540 """
541 self.write_raw("\x1b[2J")
542
543 def enter_alternate_screen(self) -> None:
544 self.write_raw("\x1b[?1049h\x1b[H")
545
546 def quit_alternate_screen(self) -> None:
547 self.write_raw("\x1b[?1049l")
548
549 def enable_mouse_support(self) -> None:
550 self.write_raw("\x1b[?1000h")
551
552 # Enable mouse-drag support.
553 self.write_raw("\x1b[?1003h")
554
555 # Enable urxvt Mouse mode. (For terminals that understand this.)
556 self.write_raw("\x1b[?1015h")
557
558 # Also enable Xterm SGR mouse mode. (For terminals that understand this.)
559 self.write_raw("\x1b[?1006h")
560
561 # Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr
562 # extensions.
563
564 def disable_mouse_support(self) -> None:
565 self.write_raw("\x1b[?1000l")
566 self.write_raw("\x1b[?1015l")
567 self.write_raw("\x1b[?1006l")
568 self.write_raw("\x1b[?1003l")
569
570 def erase_end_of_line(self) -> None:
571 """
572 Erases from the current cursor position to the end of the current line.
573 """
574 self.write_raw("\x1b[K")
575
576 def erase_down(self) -> None:
577 """
578 Erases the screen from the current line down to the bottom of the
579 screen.
580 """
581 self.write_raw("\x1b[J")
582
583 def reset_attributes(self) -> None:
584 self.write_raw("\x1b[0m")
585
586 def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None:
587 """
588 Create new style and output.
589
590 :param attrs: `Attrs` instance.
591 """
592 # Get current depth.
593 escape_code_cache = self._escape_code_caches[color_depth]
594
595 # Write escape character.
596 self.write_raw(escape_code_cache[attrs])
597
598 def disable_autowrap(self) -> None:
599 self.write_raw("\x1b[?7l")
600
601 def enable_autowrap(self) -> None:
602 self.write_raw("\x1b[?7h")
603
604 def enable_bracketed_paste(self) -> None:
605 self.write_raw("\x1b[?2004h")
606
607 def disable_bracketed_paste(self) -> None:
608 self.write_raw("\x1b[?2004l")
609
610 def reset_cursor_key_mode(self) -> None:
611 """
612 For vt100 only.
613 Put the terminal in cursor mode (instead of application mode).
614 """
615 # Put the terminal in cursor mode. (Instead of application mode.)
616 self.write_raw("\x1b[?1l")
617
618 def cursor_goto(self, row: int = 0, column: int = 0) -> None:
619 """
620 Move cursor position.
621 """
622 self.write_raw("\x1b[%i;%iH" % (row, column))
623
624 def cursor_up(self, amount: int) -> None:
625 if amount == 0:
626 pass
627 elif amount == 1:
628 self.write_raw("\x1b[A")
629 else:
630 self.write_raw("\x1b[%iA" % amount)
631
632 def cursor_down(self, amount: int) -> None:
633 if amount == 0:
634 pass
635 elif amount == 1:
636 # Note: Not the same as '\n', '\n' can cause the window content to
637 # scroll.
638 self.write_raw("\x1b[B")
639 else:
640 self.write_raw("\x1b[%iB" % amount)
641
642 def cursor_forward(self, amount: int) -> None:
643 if amount == 0:
644 pass
645 elif amount == 1:
646 self.write_raw("\x1b[C")
647 else:
648 self.write_raw("\x1b[%iC" % amount)
649
650 def cursor_backward(self, amount: int) -> None:
651 if amount == 0:
652 pass
653 elif amount == 1:
654 self.write_raw("\b") # '\x1b[D'
655 else:
656 self.write_raw("\x1b[%iD" % amount)
657
658 def hide_cursor(self) -> None:
659 if self._cursor_visible in (True, None):
660 self._cursor_visible = False
661 self.write_raw("\x1b[?25l")
662
663 def show_cursor(self) -> None:
664 if self._cursor_visible in (False, None):
665 self._cursor_visible = True
666 self.write_raw("\x1b[?12l\x1b[?25h") # Stop blinking cursor and show.
667
668 def set_cursor_shape(self, cursor_shape: CursorShape) -> None:
669 if cursor_shape == CursorShape._NEVER_CHANGE:
670 return
671
672 self._cursor_shape_changed = True
673 self.write_raw(
674 {
675 CursorShape.BLOCK: "\x1b[2 q",
676 CursorShape.BEAM: "\x1b[6 q",
677 CursorShape.UNDERLINE: "\x1b[4 q",
678 CursorShape.BLINKING_BLOCK: "\x1b[1 q",
679 CursorShape.BLINKING_BEAM: "\x1b[5 q",
680 CursorShape.BLINKING_UNDERLINE: "\x1b[3 q",
681 }.get(cursor_shape, "")
682 )
683
684 def reset_cursor_shape(self) -> None:
685 "Reset cursor shape."
686 # (Only reset cursor shape, if we ever changed it.)
687 if self._cursor_shape_changed:
688 self._cursor_shape_changed = False
689
690 # Reset cursor shape.
691 self.write_raw("\x1b[0 q")
692
693 def flush(self) -> None:
694 """
695 Write to output stream and flush.
696 """
697 if not self._buffer:
698 return
699
700 data = "".join(self._buffer)
701 self._buffer = []
702
703 flush_stdout(self.stdout, data)
704
705 def ask_for_cpr(self) -> None:
706 """
707 Asks for a cursor position report (CPR).
708 """
709 self.write_raw("\x1b[6n")
710 self.flush()
711
712 @property
713 def responds_to_cpr(self) -> bool:
714 if not self.enable_cpr:
715 return False
716
717 # When the input is a tty, we assume that CPR is supported.
718 # It's not when the input is piped from Pexpect.
719 if os.environ.get("PROMPT_TOOLKIT_NO_CPR", "") == "1":
720 return False
721
722 if is_dumb_terminal(self.term):
723 return False
724 try:
725 return self.stdout.isatty()
726 except ValueError:
727 return False # ValueError: I/O operation on closed file
728
729 def bell(self) -> None:
730 "Sound bell."
731 if self.enable_bell:
732 self.write_raw("\a")
733 self.flush()
734
735 def get_default_color_depth(self) -> ColorDepth:
736 """
737 Return the default color depth for a vt100 terminal, according to the
738 our term value.
739
740 We prefer 256 colors almost always, because this is what most terminals
741 support these days, and is a good default.
742 """
743 if self.default_color_depth is not None:
744 return self.default_color_depth
745
746 term = self.term
747
748 if term is None:
749 return ColorDepth.DEFAULT
750
751 if is_dumb_terminal(term):
752 return ColorDepth.DEPTH_1_BIT
753
754 if term in ("linux", "eterm-color"):
755 return ColorDepth.DEPTH_4_BIT
756
757 return ColorDepth.DEFAULT