From f48cfbfbb4b14061c163988a84da727e1383a4b0 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Fri, 3 Jul 2026 06:34:11 -0700 Subject: [PATCH 1/2] fix: detect color support for xterm-* TERM values (xterm-color, xterm-kitty) `ColorSupport.from_env()` matched the `xterm` TERM value with an exact equality check (`value == 'xterm'`), so any decorated variant such as `xterm-color`, `xterm-16color`, `xterm-kitty` or `xterm-ghostty` fell through to `NONE` and rendered without any color at all. This contradicted three things: - the method's own docstring, which states "If they contain `xterm`, we will enable 16 color support"; - the sibling branch immediately above, which already uses containment (`'256' in value`); and - `is_ansi_terminal()`, whose `ANSI_TERM_RE` matches `^xterm` as a prefix, so `is_ansi_terminal('xterm-color')` was True while `from_env()` reported no color support for the same terminal. Switch the check to `'xterm' in value` so every `xterm-*` variant gets at least 16-color support. The `256` branch runs first, so `xterm-256color` still resolves to `XTERM_256`. Add `test_color_support_from_env_term` asserting the mapping for bare `xterm`, the previously-broken `xterm-*` variants, and the `256` cases. It fails on the four variant cases before the fix and passes after. --- progressbar/env.py | 2 +- tests/test_color.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/progressbar/env.py b/progressbar/env.py index 14d92df..1356178 100644 --- a/progressbar/env.py +++ b/progressbar/env.py @@ -90,7 +90,7 @@ def from_env(cls) -> ColorSupport: break elif '256' in value: support = max(cls.XTERM_256, support) - elif value == 'xterm': + elif 'xterm' in value: support = max(cls.XTERM, support) elif env_flag(variable, default=False): # Generic truthy flags such as `FORCE_COLOR=1` enable diff --git a/tests/test_color.py b/tests/test_color.py index 3c0f5fb..26f84ca 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -97,6 +97,32 @@ 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), + ('xterm-kitty', env.ColorSupport.XTERM), + ('xterm-ghostty', 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), + ], +) +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', [ From ad6501a7bc981bf12f7f26dfb6414a6acc5df766 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 6 Jul 2026 18:15:36 +0200 Subject: [PATCH 2/2] feat(env): generalize TERM color detection beyond xterm variants Builds on the xterm-* fix: recognize every ANSI_TERM_RE terminal (screen, tmux, konsole, rxvt, linux, ...) as 16-color -- the same pattern is_ansi_terminal() already trusts -- and treat TERM names that are themselves truecolor terminals (xterm-kitty, xterm-ghostty) as 24-bit even when COLORTERM is stripped (ssh, sudo). COLOR_SUPPORT is now computed after the patterns it reads are defined. --- progressbar/env.py | 27 ++++++++++++++++++++++----- tests/test_color.py | 14 ++++++++++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/progressbar/env.py b/progressbar/env.py index 6300514..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 'xterm' in value: + 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 c67f624..bcb3071 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -105,11 +105,21 @@ def test_color_support_from_env(monkeypatch, variable, value) -> None: ('xterm', env.ColorSupport.XTERM), ('xterm-color', env.ColorSupport.XTERM), ('xterm-16color', env.ColorSupport.XTERM), - ('xterm-kitty', env.ColorSupport.XTERM), - ('xterm-ghostty', 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: