Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions progressbar/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ def from_env(cls) -> ColorSupport:
"""Get the color support from the environment.

If any of the environment variables contain `24bit` or `truecolor`,
we will enable true color/24 bit support. If they contain `256`, we
will enable 256 color/8 bit support. If they contain `xterm`, we will
enable 16 color support. Otherwise, we will assume no color support.
we will enable true color/24 bit support. A `TERM` that is itself a
truecolor terminal (see `TRUECOLOR_TERMS`) also enables 24 bit
support. If they contain `256`, we will enable 256 color/8 bit
support. If they match a known ANSI terminal (see `ANSI_TERM_RE`,
e.g. `xterm-color`, `screen`, `tmux`, `konsole`, `rxvt`, `linux`), we
will enable 16 color support. Otherwise, we assume no color support.

If `JUPYTER_COLUMNS` or `JUPYTER_LINES` or `JPY_PARENT_PID` is set, we
will assume true color support.
Expand Down Expand Up @@ -115,9 +118,15 @@ def _from_term_variables(
# Truecolor support, we don't need to check anything else.
support = cls.XTERM_TRUECOLOR
break
elif value in TRUECOLOR_TERMS:
# A TERM name that itself guarantees a 24-bit terminal.
support = max(cls.XTERM_TRUECOLOR, support)
elif '256' in value:
support = max(cls.XTERM_256, support)
elif value == 'xterm':
elif ANSI_TERM_RE.match(value):
# Any recognized ANSI terminal (xterm-color, screen, tmux,
# konsole, rxvt, linux, ...) advertises at least 16 colors,
# matching is_ansi_terminal()'s use of the same pattern.
support = max(cls.XTERM, support)
elif env_flag(variable, default=False):
return cls.XTERM_TRUECOLOR
Expand Down Expand Up @@ -200,7 +209,6 @@ def is_terminal(
or os.environ.get('JUPYTER_LINES')
or os.environ.get('JPY_PARENT_PID')
)
COLOR_SUPPORT = ColorSupport.from_env()
ANSI_TERMS = (
'([xe]|bv)term',
'(sco)?ansi',
Expand All @@ -215,3 +223,12 @@ def is_terminal(
ANSI_TERM_RE: re.Pattern[str] = re.compile(
f'^({"|".join(ANSI_TERMS)})', re.IGNORECASE
)

#: TERM values that on their own guarantee a truecolor-capable terminal, so
#: 24-bit color still engages when ``COLORTERM`` is stripped (e.g. over ssh
#: or sudo). Limited to names that *are* the terminal; generic values such as
#: ``xterm-256color`` are used by plenty of 256-only emulators.
TRUECOLOR_TERMS: frozenset[str] = frozenset({'xterm-kitty', 'xterm-ghostty'})

# Defined after ANSI_TERM_RE / TRUECOLOR_TERMS because from_env() reads them.
COLOR_SUPPORT = ColorSupport.from_env()
36 changes: 36 additions & 0 deletions tests/test_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,42 @@ def test_color_support_from_env(monkeypatch, variable, value) -> None:
env.ColorSupport.from_env()


@pytest.mark.parametrize(
('term', 'expected'),
[
# Bare ``xterm`` and any ``xterm-*`` variant advertise (at least) 16
# color support, matching the documented "if they contain ``xterm``"
# behaviour and ``is_ansi_terminal``'s ``^xterm`` prefix match.
('xterm', env.ColorSupport.XTERM),
('xterm-color', env.ColorSupport.XTERM),
('xterm-16color', env.ColorSupport.XTERM),
# Every other ANSI_TERM_RE terminal is 16-color too, not NONE.
('screen', env.ColorSupport.XTERM),
('tmux', env.ColorSupport.XTERM),
('konsole', env.ColorSupport.XTERM),
('rxvt-unicode', env.ColorSupport.XTERM),
('linux', env.ColorSupport.XTERM),
# A ``256`` anywhere in the value still wins over plain xterm.
('xterm-256color', env.ColorSupport.XTERM_256),
('screen-256color', env.ColorSupport.XTERM_256),
# TERM names that are themselves truecolor terminals engage 24-bit
# color even when COLORTERM is stripped (ssh, sudo).
('xterm-kitty', env.ColorSupport.XTERM_TRUECOLOR),
('xterm-ghostty', env.ColorSupport.XTERM_TRUECOLOR),
# Unknown terminals still mean no detected support.
('dumb', env.ColorSupport.NONE),
],
)
def test_color_support_from_env_term(monkeypatch, term, expected) -> None:
if os.name == 'nt':
# Windows has special handling so we need to disable that to make the
# tests work properly
monkeypatch.setattr(os, 'name', 'posix')

monkeypatch.setenv('TERM', term)
assert env.ColorSupport.from_env() == expected


@pytest.mark.parametrize(
'variable',
[
Expand Down
Loading