Skip to content

Commit 343acf6

Browse files
BridgeARMayaLekova
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. PR-URL: nodejs#17615 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent efb4954 commit 343acf6

File tree

6 files changed

+176
-9
lines changed

6 files changed

+176
-9
lines changed

doc/api/assert.md

Lines changed: 3 additions & 0 deletions
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

Lines changed: 26 additions & 0 deletions
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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ const kCode = Symbol('code');
1414
const kInfo = Symbol('info');
1515
const messages = new Map();
1616

17+
var green = '';
18+
var red = '';
19+
var white = '';
20+
1721
const { errmap } = process.binding('uv');
1822
const { kMaxLength } = process.binding('buffer');
1923
const { defineProperty } = Object;
@@ -143,7 +147,7 @@ function createErrDiff(actual, expected, operator) {
143147
const expectedLines = util
144148
.inspect(expected, { compact: false }).split('\n');
145149
const msg = `Input A expected to ${operator} input B:\n` +
146-
'\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m';
150+
`${green}+ expected${white} ${red}- actual${white}`;
147151
const skippedMsg = ' ... Lines skipped';
148152

149153
// Remove all ending lines that match (this optimizes the output for
@@ -189,7 +193,7 @@ function createErrDiff(actual, expected, operator) {
189193
printedLines++;
190194
}
191195
lastPos = i;
192-
other += `\n\u001b[32m+\u001b[39m ${expectedLines[i]}`;
196+
other += `\n${green}+${white} ${expectedLines[i]}`;
193197
printedLines++;
194198
// Only extra actual lines exist
195199
} else if (expectedLines.length < i + 1) {
@@ -205,7 +209,7 @@ function createErrDiff(actual, expected, operator) {
205209
printedLines++;
206210
}
207211
lastPos = i;
208-
res += `\n\u001b[31m-\u001b[39m ${actualLines[i]}`;
212+
res += `\n${red}-${white} ${actualLines[i]}`;
209213
printedLines++;
210214
// Lines diverge
211215
} else if (actualLines[i] !== expectedLines[i]) {
@@ -221,8 +225,8 @@ function createErrDiff(actual, expected, operator) {
221225
printedLines++;
222226
}
223227
lastPos = i;
224-
res += `\n\u001b[31m-\u001b[39m ${actualLines[i]}`;
225-
other += `\n\u001b[32m+\u001b[39m ${expectedLines[i]}`;
228+
res += `\n${red}-${white} ${actualLines[i]}`;
229+
other += `\n${green}+${white} ${expectedLines[i]}`;
226230
printedLines += 2;
227231
// Lines are identical
228232
} else {
@@ -258,7 +262,14 @@ class AssertionError extends Error {
258262
if (message != null) {
259263
super(message);
260264
} else {
261-
if (util === null) util = require('util');
265+
if (util === null) {
266+
util = require('util');
267+
if (process.stdout.isTTY && process.stdout.getColorDepth() !== 1) {
268+
green = '\u001b[32m';
269+
white = '\u001b[39m';
270+
red = '\u001b[31m';
271+
}
272+
}
262273

263274
if (actual && actual.stack && actual instanceof Error)
264275
actual = `${actual.name}: ${actual.message}`;

lib/tty.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ const { inherits } = util;
2828
const errnoException = util._errnoException;
2929
const errors = require('internal/errors');
3030
const readline = require('readline');
31+
const { release } = require('os');
32+
33+
const OSRelease = release().split('.');
34+
35+
const COLORS_2 = 1;
36+
const COLORS_16 = 4;
37+
const COLORS_256 = 8;
38+
const COLORS_16m = 24;
3139

3240
function isatty(fd) {
3341
return Number.isInteger(fd) && fd >= 0 && isTTY(fd);
@@ -104,6 +112,70 @@ inherits(WriteStream, net.Socket);
104112

105113
WriteStream.prototype.isTTY = true;
106114

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

108180
WriteStream.prototype._refreshSize = function() {
109181
var oldCols = this.columns;

test/parallel/test-assert.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -788,10 +788,13 @@ common.expectsError(
788788
Error.stackTraceLimit = tmpLimit;
789789

790790
// Test error diffs
791+
const colors = process.stdout.isTTY && process.stdout.getColorDepth() > 1;
791792
const start = 'Input A expected to deepStrictEqual input B:';
792-
const actExp = '\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m';
793-
const plus = '\u001b[32m+\u001b[39m';
794-
const minus = '\u001b[31m-\u001b[39m';
793+
const actExp = colors ?
794+
'\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m' :
795+
'+ expected - actual';
796+
const plus = colors ? '\u001b[32m+\u001b[39m' : '+';
797+
const minus = colors ? '\u001b[31m-\u001b[39m' : '-';
795798
let message = [
796799
start,
797800
`${actExp} ... Lines skipped`,
Lines changed: 52 additions & 0 deletions
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)