diff --git a/progressbar/env.py b/progressbar/env.py index be4f47d..2c92b6e 100644 --- a/progressbar/env.py +++ b/progressbar/env.py @@ -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. @@ -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 @@ -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', @@ -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() diff --git a/tests/test_color.py b/tests/test_color.py index 1690553..bcb3071 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -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', [