Skip to content

Commit 8d70003

Browse files
committed
fix(color-support): treat TERM=dumb as no color before applying COLORTERM, #49
1 parent 678da3a commit 8d70003

14 files changed

Lines changed: 310 additions & 70 deletions

File tree

‎.github/workflows/test.yml‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
fail-fast: false # doesn't stop on first failure
5151
matrix:
5252
os: [ubuntu-latest, windows-latest]
53-
node-version: [ 18, 20, 22, 24 ]
53+
node-version: [ 18, 20, 22, 24, 26 ]
5454
include:
5555
- os: ubuntu-latest
5656
node-version: *coverage-node-version
@@ -76,6 +76,9 @@ jobs:
7676
- name: Install dependencies (without rebuilding)
7777
run: npm install --ignore-scripts
7878

79+
- name: Run color support checker
80+
run: npm run color-checker
81+
7982
- name: Run tests
8083
if: matrix.coverage != true
8184
run: npx vitest
@@ -145,7 +148,7 @@ jobs:
145148
strategy:
146149
fail-fast: false
147150
matrix:
148-
deno-version: [ 2.0.x, 2.1.x, 2.2.x ]
151+
deno-version: [ 2.3.x, 2.9.x ]
149152

150153
steps:
151154
- name: Checkout repository

‎CHANGELOG.md‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,44 @@
11
# Changelog
22

3+
## 4.4.0 (2026-09-13)
4+
5+
- fix(color-support): treat `TERM=dumb` as no color before applying `COLORTERM`, #49.
6+
7+
`COLORTERM` is a color level hint.
8+
When `TERM=dumb`, Ansis now disables ANSI color output even if color environment variables such as `COLORTERM=truecolor` were inherited from a parent process (e.g. in Emacs M-x compile).
9+
10+
`FORCE_COLOR` keeps the highest priority and can still explicitly enable colors.
11+
12+
**Auto-detection behavior changes after the fix**
13+
14+
| Use case | v4.3.1 | v4.4.0 | Notes |
15+
|----------|-------|--------------|-------|
16+
| `TERM=dumb` | ✅ may allow colors * | ❌ no color | `TERM=dumb` now has higher priority in `autoDetectLevel`. |
17+
| `TERM=dumb` + `COLORTERM=truecolor` | ✅ allow colors | ❌ no color | `COLORTERM` no longer overrides `TERM=dumb`. |
18+
| `TERM=dumb` + CI | ✅ allow colors | ❌ no color | CI detection runs after `TERM=dumb`. |
19+
| `TERM=dumb` + PM2 | ✅ may allow colors * | ❌ no color | PM2 detection runs after `TERM=dumb`. |
20+
| `TERM=dumb` + Next.js runtime | ✅ may allow colors * | ❌ no color | Next.js detection runs after `TERM=dumb`. |
21+
| `COLORTERM` without `TERM=dumb` | ✅ allow colors | ✅ allow colors | Unchanged. |
22+
| CI without `TERM=dumb` | ✅ allow colors | ✅ allow colors | Unchanged. |
23+
| PM2 without `TERM=dumb` | ✅ allow colors | ✅ allow colors | Unchanged. |
24+
| Next.js without `TERM=dumb` | ✅ allow colors | ✅ allow colors | Unchanged. |
25+
26+
`*` - Depends on additional conditions.
27+
28+
29+
- test(color-support): add regression coverage for `TERM=dumb` with inherited `COLORTERM=truecolor`
30+
31+
The tests also cover PM2 and Next.js edge behavior where `stdout.isTTY` is not exposed.
32+
33+
- test: add a manual color support checker
34+
35+
Run `npm run color-checker` to print the environment values used by color detection and visually inspect ANSI 16, ANSI 256, and Truecolor rendering.
36+
37+
- test(deno): run CI against Deno 2.3+ and the current LTS
38+
39+
Deno 2.0-2.2 are no longer tested in the CI matrix because they cannot use the same `deno.lock` v5 format as Deno 2.3+.
40+
Runtime compatibility with Deno 2.0-2.2 remains supported.
41+
342
## 4.3.1 (2026-05-31)
443

544
- fix: remove closing `reset` ANSI sequence

‎README.md‎

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -425,40 +425,44 @@ Ansis detects color support from the runtime environment in this order:
425425
1. Chromium browser-like runtimes
426426
- detected first -> truecolor
427427

428-
2. `COLORTERM` (some terminals set it even when output is not TTY)
428+
2. Dumb terminal
429+
- `TERM=dumb` -> no colors
430+
431+
3. `COLORTERM` (some terminals set it even when output is not TTY)
432+
- used as a hint, not as a replacement for `FORCE_COLOR`
429433
- `truecolor` or `24bit` -> truecolor
430434
- `ansi256` -> 256 colors
431435
- `ansi` -> 16 colors
432436

433-
3. CI environment (not TTY)
437+
4. CI environment (not TTY)
434438
- GitHub Actions -> truecolor
435439
- other CI environments -> 16 colors
436440

437-
4. Terminal
441+
5. Terminal
438442
- no TTY -> no colors
439-
- `TERM=dumb` -> no colors
440443
- `PM2` and `Next.js` non-TTY runtimes -> color output
441444

442-
5. Windows
445+
6. Windows
443446
- Windows terminals since Windows 10 build 14931 (released 2016) -> truecolor
444447

445-
6. 256-color terminals
448+
7. 256-color terminals
446449
- known 256-color terminals -> 256 colors
447450

448-
7. Fallback
451+
8. Fallback
449452
- unknown terminals -> 16 colors
450453

451454
<details>
452455
<summary>Supported terminals and CI environments</summary>
453456

454457
| Terminal | ANSI 16<br>colors | ANSI 256<br>colors | True<br>Color | env.<br>TERM | env.<br>COLORTERM | Specifically ENV variables |
455458
|:---------------------------------|-------------------|:-------------------|:--------------|:---------------:|:-----------------:|:---------------------------------------|
456-
| Azure CI | ✅ | ❌ | ❌ | dumb | | TF_BUILD<br>AGENT_NAME |
457-
| GitHub CI | ✅ | ✅ | ✅ | dumb | | CI, GITHUB_ACTIONS |
458-
| GitTea CI | ✅ | ✅ | ✅ | dumb | | CI, GITEA_ACTIONS |
459-
| GitLab CI | ✅ | ❌ | ❌ | dumb | | CI, GITLAB_CI |
460-
| Travis CI | ✅ | ❌ | ❌ | dumb | | TRAVIS |
461-
| PM2<br>not isTTY | ✅[^1] | ✅[^1] | ✅[^1] | dumb | | PM2_HOME<br>pm_id |
459+
| Azure CI | ✅ | ❌ | ❌ | | | TF_BUILD<br>AGENT_NAME |
460+
| GitHub CI | ✅ | ✅ | ✅ | | | CI, GITHUB_ACTIONS |
461+
| GitTea CI | ✅ | ✅ | ✅ | | | CI, GITEA_ACTIONS |
462+
| GitLab CI | ✅ | ❌ | ❌ | | | CI, GITLAB_CI |
463+
| Travis CI | ✅ | ❌ | ❌ | | | TRAVIS |
464+
| PM2<br>not isTTY | ✅[^1] | ✅[^1] | ✅[^1] | | | PM2_HOME<br>pm_id |
465+
| Next.js<br>edge runtime | ✅[^1] | ✅[^1] | ✅[^1] | | | NEXT_RUNTIME='edge'<br>NEXT_RUNTIME='experimental-edge' |
462466
| JetBrains TeamCity<br>>=2020.1.1 | ✅ | ✅ | ❌ | | | TEAMCITY_VERSION |
463467
| JetBrains IDEA | ✅ | ✅ | ✅ | xterm-256color | | TERMINAL_EMULATOR='JetBrains-JediTerm' |
464468
| VS Code | ✅ | ✅ | ✅ | xterm-256color | truecolor | |
@@ -469,7 +473,7 @@ Ansis detects color support from the runtime environment in this order:
469473
| Kitty | ✅ | ✅ | ✅ | xterm-kitty | truecolor | |
470474
| KDE Konsole | ✅ | ✅ | ✅ | xterm-256color | truecolor | |
471475

472-
- ^1 Colors supported depends on actual terminal.
476+
- ^1 Colors supported depends on actual output destination. `TERM=dumb` disables colors before PM2 and Next.js detection.
473477
- ^2: The Windows terminal supports true color since Windows 10 revision 14931 (2016-09-21).
474478

475479
See also:
@@ -533,7 +537,8 @@ Force or override color support via environment variable (see [force-color.org](
533537
<a name="using-env-colorterm"></a>
534538
#### `COLORTERM`
535539

536-
Hint the auto-detected color level using terminal emulator conventions:
540+
`COLORTERM` provides a color capability hint for auto-detection, including in CI and non-TTY environments.
541+
It is not a replacement for `FORCE_COLOR`.
537542

538543
| Value | Level |
539544
|------------------------|-----------------------:|
@@ -858,4 +863,4 @@ If you find this useful, please ⭐️ the repo.
858863

859864
[styleText]: https://nodejs.org/api/util.html#utilstyletextformat-text-options
860865

861-
[styleText-mods]: https://nodejs.org/api/util.html#modifiers
866+
[styleText-mods]: https://nodejs.org/api/util.html#modifiers

‎deno.lock‎

Lines changed: 22 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ansis",
3-
"version": "4.3.1",
3+
"version": "4.4.0",
44
"description": "A small and fast ANSI color library",
55
"keywords": [
66
"ansi",
@@ -48,6 +48,7 @@
4848
"build": "rollup -c",
4949
"postinstall": "npm run build && npm i ansis -D",
5050
"demo": "node --experimental-modules ./examples/index.js",
51+
"color-checker": "npm run color-checker --prefix ./test/manual/color-checker",
5152
"bench": "(cd ./bench/ && npm install); node ./bench/index.js",
5253
"bench:truecolor": "(cd ./bench/ && npm install); node bench/truecolor.bench.js",
5354
"compare": "(cd ./compare/ && npm install); node ./compare/index.js",
@@ -86,7 +87,7 @@
8687
"@rollup/plugin-terser": "^0.4.4",
8788
"@types/node": "^22.15.17",
8889
"@vitest/coverage-v8": "^3.1.1",
89-
"ansis": "file:dist/node14",
90+
"ansis": "^4.3.1",
9091
"css-color-names": "1.0.1",
9192
"esbuild": "0.25.2",
9293
"prettier": "3.5.3",

‎package.npm-node10.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name":"ansis",
3-
"version":"4.3.1-node10",
3+
"version":"4.4.0-node10",
44
"description":"ANSI color lib",
55
"keywords":["ansi","colors","cli"],
66
"license":"ISC",

‎package.npm-node14.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name":"ansis",
3-
"version":"4.3.1",
3+
"version":"4.4.0",
44
"description":"ANSI color lib",
55
"keywords":["ansi","colors","cli"],
66
"license":"ISC",

‎src/color-support.js‎

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { LEVEL_UNDEFINED, LEVEL_BW, LEVEL_16COLORS, LEVEL_256COLORS, LEVEL_TRUECOLOR } from './color-levels.js';
22
import { keys, SEPARATOR } from './constants.js';
33

4-
// Optimisation: declare variables here for more compact code shape after compilation
4+
// Optimisation: declare variables here for more compact code shape after Terser compilation
55
let term;
66

77
/**
@@ -21,64 +21,69 @@ let term;
2121
* @return {number}
2222
*/
2323
let autoDetectLevel = (proc, env, envKeys) => {
24-
// Optimisation: The Terser inlines a function at use place, so we can split the logic on small functions.
24+
// Optimisation: The Terser inlines a function at use place, so in the source we can split the logic on small functions.
25+
26+
let isWin = () => proc.platform === 'win32';
27+
28+
// In Node.js, `process.stdout.isTTY` is `true` for TTY streams and `undefined` for non-TTY output.
29+
// Other runtimes may not expose `isTTY`.
30+
let isTTY = () => proc.stdout?.isTTY;
31+
32+
// Detect CI environments.
33+
// Most CI tools set the `CI` ENV variable:
34+
// Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari, etc.
35+
// https://github.com/watson/ci-info/blob/master/vendors.json
36+
let isCI = () => env.CI;
2537

2638
// PM2 does not set process.stdout.isTTY, but color output may still be supported, depends on the actual terminal.
2739
// PM2 always sets PM2_HOME to a non-empty (truthy) value when running in either fork or cluster mode.
2840
let detectPM2 = () => env.PM2_HOME;
2941

30-
// In the Next.js `edge` runtime, process.stdout is undefined, but colored output is still supported.
42+
// In the Next.js `edge` runtime, process.stdout is undefined, but the output destination may still support colors.
3143
// Runtime values that support colors: `nodejs`, `edge`, `experimental-edge`.
3244
let detectNextJs = () => /edge/.test(env.NEXT_RUNTIME);
3345

34-
// Size optimization: intentionally returns a falsy/truthy value instead of a boolean.
35-
let isTTY = () => detectPM2() || detectNextJs() || proc.stdout?.isTTY;
36-
37-
let isWin = () => proc.platform === 'win32';
46+
term = env.TERM;
3847

3948
// Note: the order of checks is important!
4049

41-
// 1) Detect terminals supporting TrueColor by COLORTERM
50+
// 1) A dumb terminal is not expected to render ANSI escape sequences (e.g. in Emacs M-x compile).
51+
// TERM=dumb takes precedence over COLORTERM, CI detection, and platform-specific defaults.
52+
if (term === 'dumb') return LEVEL_BW;
53+
54+
// 2) Detect color support using the COLORTERM hint.
4255
// Most modern terminals use `TERM=xterm-256color` with `COLORTERM=truecolor`.
43-
// COLORTERM values: `truecolor` or `24bit`, `ansi256`, `ansi`
56+
// COLORTERM values: `truecolor` or `24bit`, `ansi256`, `ansi`.
4457
// Terminals that set COLORTERM=truecolor: iTerm, VSCode, `xterm-kitty`, KDE Konsole.
45-
4658
let level = {
4759
'24bit': LEVEL_TRUECOLOR,
4860
truecolor: LEVEL_TRUECOLOR,
4961
ansi256: LEVEL_256COLORS,
5062
ansi: LEVEL_16COLORS,
5163
}[env.COLORTERM];
5264

53-
term = env.TERM;
54-
5565
if (level) return level;
5666

57-
// 2) Detect color support in CI.
58-
// Note: CI environments are not TTY and often advertise themselves as `dumb` terminals.
59-
60-
// CI tools
61-
// https://github.com/watson/ci-info/blob/master/vendors.json
62-
if (env.CI) {
67+
// 3) Detect color support in CI.
68+
// CI environments can render ANSI colors even when TTY is undefined.
69+
if (isCI()) {
6370
// CI supports truecolor: GITHUB_ACTIONS
6471
if (/,GITHUB/.test(envKeys)) return LEVEL_TRUECOLOR;
6572

66-
// others CI supports only 16 colors, e.g. when env contains:
67-
// - CI_NAME === codeship | sourcehut
73+
// Default to 16 colors for other CI environments, including those identified by environment variables:
74+
// - CI_NAME: codeship | sourcehut
6875
// - GITLAB_CI | CIRCLECI | TRAVIS | APPVEYOR | BUILDKITE | DRONE | BITBUCKET_BUILD_NUMBER | AZURE_HTTP_USER_AGENT
6976

7077
return LEVEL_16COLORS;
7178
}
7279

73-
// 3) Detect unknown output or colors are not supported
74-
if (!isTTY() || term === 'dumb') return LEVEL_BW;
80+
// 4) Detect unknown output environments.
81+
if (!(isTTY() || detectPM2() || detectNextJs())) return LEVEL_BW;
7582

76-
// 4) Truecolor support starts from Windows 10 build 14931 (2016-09-21), today we assume modern Windows is used
83+
// 5) Truecolor support since Windows 10 build 14931 (2016-09-21), today we assume modern Windows is used.
7784
if (isWin()) return LEVEL_TRUECOLOR;
7885

79-
// 5) Detect terminals supporting 256 colors
80-
81-
// Note: check for 256 colors after ENV variables such as TERM, COLORTERM.
86+
// 6) Detect 256-color support from TERM after COLORTERM and CI.
8287
// Terminals, that support 256 colors:
8388
// - screen-256color
8489
// - xterm-256color
@@ -90,16 +95,16 @@ let autoDetectLevel = (proc, env, envKeys) => {
9095
// - ansi-256color
9196
if (/-256/.test(term)) return LEVEL_256COLORS;
9297

93-
// 6) Defaults, 16-color output for unknown terminals,
94-
// as all known terminals supporting 256 colors or truecolor have already been detected above.
95-
// To enable truecolor in unknown terminals, set the `COLORTERM=24bit` environment variable.
98+
// 7) Default to 16-color for the other supported output environments.
99+
// No previous check identified 256-color or truecolor support.
100+
// To enable truecolor in unknown terminals, set COLORTERM=24bit.
96101

97102
// Known terminals supporting 16 colors:
98103
// - xterm
99104
// - xterm-color
100105
// - screen-color
101106
// - ansi, ansi-x3.64, ansi.sysk
102-
// - linux - Linux virtual console (tty1, tty2, SSH, etc.)
107+
// - linux - Linux virtual console (tty1, tty2, etc.)
103108
// - tmux - Terminal emulator
104109
// - tmux - Terminal tmux installed on macOS has `tmux-256color` name
105110
// - cygwin - Cygwin terminal
@@ -123,13 +128,13 @@ export const getLevel = (thisRef) => {
123128
let colorLevel = LEVEL_BW;
124129

125130
try {
126-
// keys(env) triggers a Deno permission request; throws if access is denied
127-
// stringify environment variable keys to check for specific ones using a RegExp
131+
// keys(env) triggers a Deno permission request. Throws if access is denied.
132+
// Stringify environment variable keys to check for specific ones using a RegExp.
128133
let envKeys = SEPARATOR + keys(env).join(SEPARATOR);
129134
colorLevel = autoDetectLevel(proc, env, envKeys);
130135
} catch (error) {
131-
// if the permission is not granted, environment variables have no effect, even variables like FORCE_COLOR will be ignored
132-
// env now points to a new empty object to avoid Deno requests for every env access in code below
136+
// If the permission is not granted, environment variables have no effect, even variables like FORCE_COLOR will be ignored.
137+
// `env` now points to a new empty object to avoid Deno requests for every env access in code below.
133138
env = {};
134139
}
135140

0 commit comments

Comments
 (0)