Skip to content

Commit ead727c

Browse files
BridgeARMylesBorins
authored andcommitted
tty: add getColorDepth function
Right now it is very difficult to determine if a terminal supports colors or not. This function adds this functionality by detecting environment variables and checking process. Backport-PR-URL: #19230 PR-URL: #17615 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 5aa3a2d commit ead727c

File tree

6 files changed

+175
-8
lines changed

6 files changed

+175
-8
lines changed

doc/api/assert.md

+3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ AssertionError [ERR_ASSERTION]: Input A expected to deepStrictEqual input B:
6565
]
6666
```
6767

68+
To deactivate the colors, use the `NODE_DISABLE_COLORS` environment variable.
69+
Please note that this will also deactivate the colors in the REPL.
70+
6871
## Legacy mode
6972

7073
> Stability: 0 - Deprecated: Use strict mode instead.

doc/api/tty.md

+26
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,32 @@ added: v0.7.7
121121
A `number` specifying the number of rows the TTY currently has. This property
122122
is updated whenever the `'resize'` event is emitted.
123123

124+
### writeStream.getColorDepth([env])
125+
<!-- YAML
126+
added: REPLACEME
127+
-->
128+
129+
* `env` {object} A object containing the environment variables to check.
130+
Defaults to `process.env`.
131+
* Returns: {number}
132+
133+
Returns:
134+
* 1 for 2,
135+
* 4 for 16,
136+
* 8 for 256,
137+
* 24 for 16,777,216
138+
colors supported.
139+
140+
Use this to determine what colors the terminal supports. Due to the nature of
141+
colors in terminals it is possible to either have false positives or false
142+
negatives. It depends on process information and the environment variables that
143+
may lie about what terminal is used.
144+
To enforce a specific behavior without relying on `process.env` it is possible
145+
to pass in an object with different settings.
146+
147+
Use the `NODE_DISABLE_COLORS` environment variable to enforce this function to
148+
always return 1.
149+
124150
## tty.isatty(fd)
125151
<!-- YAML
126152
added: v0.5.8

lib/internal/errors.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
const kCode = Symbol('code');
1414
const messages = new Map();
1515

16+
var green = '';
17+
var red = '';
18+
var white = '';
19+
1620
const {
1721
UV_EAI_MEMORY,
1822
UV_EAI_NODATA,
@@ -90,7 +94,7 @@ function createErrDiff(actual, expected, operator) {
9094
const expectedLines = util
9195
.inspect(expected, { compact: false }).split('\n');
9296
const msg = `Input A expected to ${operator} input B:\n` +
93-
'\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m';
97+
`${green}+ expected${white} ${red}- actual${white}`;
9498
const skippedMsg = ' ... Lines skipped';
9599

96100
// Remove all ending lines that match (this optimizes the output for
@@ -136,7 +140,7 @@ function createErrDiff(actual, expected, operator) {
136140
printedLines++;
137141
}
138142
lastPos = i;
139-
other += `\n\u001b[32m+\u001b[39m ${expectedLines[i]}`;
143+
other += `\n${green}+${white} ${expectedLines[i]}`;
140144
printedLines++;
141145
// Only extra actual lines exist
142146
} else if (expectedLines.length < i + 1) {
@@ -152,7 +156,7 @@ function createErrDiff(actual, expected, operator) {
152156
printedLines++;
153157
}
154158
lastPos = i;
155-
res += `\n\u001b[31m-\u001b[39m ${actualLines[i]}`;
159+
res += `\n${red}-${white} ${actualLines[i]}`;
156160
printedLines++;
157161
// Lines diverge
158162
} else if (actualLines[i] !== expectedLines[i]) {
@@ -168,8 +172,8 @@ function createErrDiff(actual, expected, operator) {
168172
printedLines++;
169173
}
170174
lastPos = i;
171-
res += `\n\u001b[31m-\u001b[39m ${actualLines[i]}`;
172-
other += `\n\u001b[32m+\u001b[39m ${expectedLines[i]}`;
175+
res += `\n${red}-${white} ${actualLines[i]}`;
176+
other += `\n${green}+${white} ${expectedLines[i]}`;
173177
printedLines += 2;
174178
// Lines are identical
175179
} else {
@@ -205,6 +209,13 @@ class AssertionError extends Error {
205209
if (message != null) {
206210
super(message);
207211
} else {
212+
if (util_ === null &&
213+
process.stdout.isTTY &&
214+
process.stdout.getColorDepth() !== 1) {
215+
green = '\u001b[32m';
216+
white = '\u001b[39m';
217+
red = '\u001b[31m';
218+
}
208219
const util = lazyUtil();
209220
if (actual && actual.stack && actual instanceof Error)
210221
actual = `${actual.name}: ${actual.message}`;

lib/tty.js

+72
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ const net = require('net');
2626
const { TTY, isTTY } = process.binding('tty_wrap');
2727
const errors = require('internal/errors');
2828
const readline = require('readline');
29+
const { release } = require('os');
30+
31+
const OSRelease = release().split('.');
32+
33+
const COLORS_2 = 1;
34+
const COLORS_16 = 4;
35+
const COLORS_256 = 8;
36+
const COLORS_16m = 24;
2937

3038
function isatty(fd) {
3139
return Number.isInteger(fd) && fd >= 0 && isTTY(fd);
@@ -90,6 +98,70 @@ inherits(WriteStream, net.Socket);
9098

9199
WriteStream.prototype.isTTY = true;
92100

101+
WriteStream.prototype.getColorDepth = function(env = process.env) {
102+
if (env.NODE_DISABLE_COLORS || env.TERM === 'dumb' && !env.COLORTERM) {
103+
return COLORS_2;
104+
}
105+
106+
if (process.platform === 'win32') {
107+
// Windows 10 build 10586 is the first Windows release that supports 256
108+
// colors. Windows 10 build 14931 is the first release that supports
109+
// 16m/TrueColor.
110+
if (+OSRelease[0] >= 10) {
111+
const build = +OSRelease[2];
112+
if (build >= 14931)
113+
return COLORS_16m;
114+
if (build >= 10586)
115+
return COLORS_256;
116+
}
117+
118+
return COLORS_16;
119+
}
120+
121+
if (env.TMUX) {
122+
return COLORS_256;
123+
}
124+
125+
if (env.CI) {
126+
if ('TRAVIS' in env || 'CIRCLECI' in env || 'APPVEYOR' in env ||
127+
'GITLAB_CI' in env || env.CI_NAME === 'codeship') {
128+
return COLORS_256;
129+
}
130+
return COLORS_2;
131+
}
132+
133+
if ('TEAMCITY_VERSION' in env) {
134+
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ?
135+
COLORS_16 : COLORS_2;
136+
}
137+
138+
switch (env.TERM_PROGRAM) {
139+
case 'iTerm.app':
140+
if (!env.TERM_PROGRAM_VERSION ||
141+
/^[0-2]\./.test(env.TERM_PROGRAM_VERSION)) {
142+
return COLORS_256;
143+
}
144+
return COLORS_16m;
145+
case 'HyperTerm':
146+
case 'Hyper':
147+
case 'MacTerm':
148+
return COLORS_16m;
149+
case 'Apple_Terminal':
150+
return COLORS_256;
151+
}
152+
153+
if (env.TERM) {
154+
if (/^xterm-256/.test(env.TERM))
155+
return COLORS_256;
156+
if (/^screen|^xterm|^vt100|color|ansi|cygwin|linux/i.test(env.TERM))
157+
return COLORS_16;
158+
}
159+
160+
if (env.COLORTERM)
161+
return COLORS_16;
162+
163+
return COLORS_2;
164+
};
93165

94166
WriteStream.prototype._refreshSize = function() {
95167
var oldCols = this.columns;

test/parallel/test-assert.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -763,10 +763,13 @@ common.expectsError(
763763
);
764764

765765
// Test error diffs
766+
const colors = process.stdout.isTTY && process.stdout.getColorDepth() > 1;
766767
const start = 'Input A expected to deepStrictEqual input B:';
767-
const actExp = '\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m';
768-
const plus = '\u001b[32m+\u001b[39m';
769-
const minus = '\u001b[31m-\u001b[39m';
768+
const actExp = colors ?
769+
'\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m' :
770+
'+ expected - actual';
771+
const plus = colors ? '\u001b[32m+\u001b[39m' : '+';
772+
const minus = colors ? '\u001b[31m-\u001b[39m' : '-';
770773
let message = [
771774
start,
772775
`${actExp} ... Lines skipped`,
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert').strict;
5+
/* eslint-disable no-restricted-properties */
6+
const { openSync } = require('fs');
7+
const tty = require('tty');
8+
9+
const { WriteStream } = require('tty');
10+
11+
// Do our best to grab a tty fd.
12+
function getTTYfd() {
13+
const ttyFd = [0, 1, 2, 4, 5].find(tty.isatty);
14+
if (ttyFd === undefined) {
15+
try {
16+
return openSync('/dev/tty');
17+
} catch (e) {
18+
// There aren't any tty fd's available to use.
19+
return -1;
20+
}
21+
}
22+
return ttyFd;
23+
}
24+
25+
const fd = getTTYfd();
26+
27+
// Give up if we did not find a tty
28+
if (fd === -1)
29+
common.skip();
30+
31+
const writeStream = new WriteStream(fd);
32+
33+
let depth = writeStream.getColorDepth();
34+
35+
assert.equal(typeof depth, 'number');
36+
assert(depth >= 1 && depth <= 24);
37+
38+
// If the terminal does not support colors, skip the rest
39+
if (depth === 1)
40+
common.skip();
41+
42+
assert.notEqual(writeStream.getColorDepth({ TERM: 'dumb' }), depth);
43+
44+
// Deactivate colors
45+
const tmp = process.env.NODE_DISABLE_COLORS;
46+
process.env.NODE_DISABLE_COLORS = 1;
47+
48+
depth = writeStream.getColorDepth();
49+
50+
assert.equal(depth, 1);
51+
52+
process.env.NODE_DISABLE_COLORS = tmp;

0 commit comments

Comments
 (0)