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: https://github.com/nodejs/node/pull/17615
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
Ruben Bridgewater 2018-01-16 13:20:13 +01:00
parent 19bff313be
commit bb9cedb0f0
No known key found for this signature in database
GPG Key ID: F07496B3EB3C1762
6 changed files with 176 additions and 9 deletions

View File

@ -65,6 +65,9 @@ AssertionError [ERR_ASSERTION]: Input A expected to deepStrictEqual input B:
]
```
To deactivate the colors, use the `NODE_DISABLE_COLORS` environment variable.
Please note that this will also deactivate the colors in the REPL.
## Legacy mode
> Stability: 0 - Deprecated: Use strict mode instead.

View File

@ -121,6 +121,32 @@ added: v0.7.7
A `number` specifying the number of rows the TTY currently has. This property
is updated whenever the `'resize'` event is emitted.
### writeStream.getColorDepth([env])
<!-- YAML
added: REPLACEME
-->
* `env` {object} A object containing the environment variables to check.
Defaults to `process.env`.
* Returns: {number}
Returns:
* 1 for 2,
* 4 for 16,
* 8 for 256,
* 24 for 16,777,216
colors supported.
Use this to determine what colors the terminal supports. Due to the nature of
colors in terminals it is possible to either have false positives or false
negatives. It depends on process information and the environment variables that
may lie about what terminal is used.
To enforce a specific behavior without relying on `process.env` it is possible
to pass in an object with different settings.
Use the `NODE_DISABLE_COLORS` environment variable to enforce this function to
always return 1.
## tty.isatty(fd)
<!-- YAML
added: v0.5.8

View File

@ -14,6 +14,10 @@ const kCode = Symbol('code');
const kInfo = Symbol('info');
const messages = new Map();
var green = '';
var red = '';
var white = '';
const { errmap } = process.binding('uv');
const { kMaxLength } = process.binding('buffer');
const { defineProperty } = Object;
@ -143,7 +147,7 @@ function createErrDiff(actual, expected, operator) {
const expectedLines = util
.inspect(expected, { compact: false }).split('\n');
const msg = `Input A expected to ${operator} input B:\n` +
'\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m';
`${green}+ expected${white} ${red}- actual${white}`;
const skippedMsg = ' ... Lines skipped';
// Remove all ending lines that match (this optimizes the output for
@ -189,7 +193,7 @@ function createErrDiff(actual, expected, operator) {
printedLines++;
}
lastPos = i;
other += `\n\u001b[32m+\u001b[39m ${expectedLines[i]}`;
other += `\n${green}+${white} ${expectedLines[i]}`;
printedLines++;
// Only extra actual lines exist
} else if (expectedLines.length < i + 1) {
@ -205,7 +209,7 @@ function createErrDiff(actual, expected, operator) {
printedLines++;
}
lastPos = i;
res += `\n\u001b[31m-\u001b[39m ${actualLines[i]}`;
res += `\n${red}-${white} ${actualLines[i]}`;
printedLines++;
// Lines diverge
} else if (actualLines[i] !== expectedLines[i]) {
@ -221,8 +225,8 @@ function createErrDiff(actual, expected, operator) {
printedLines++;
}
lastPos = i;
res += `\n\u001b[31m-\u001b[39m ${actualLines[i]}`;
other += `\n\u001b[32m+\u001b[39m ${expectedLines[i]}`;
res += `\n${red}-${white} ${actualLines[i]}`;
other += `\n${green}+${white} ${expectedLines[i]}`;
printedLines += 2;
// Lines are identical
} else {
@ -258,7 +262,14 @@ class AssertionError extends Error {
if (message != null) {
super(message);
} else {
if (util === null) util = require('util');
if (util === null) {
util = require('util');
if (process.stdout.isTTY && process.stdout.getColorDepth() !== 1) {
green = '\u001b[32m';
white = '\u001b[39m';
red = '\u001b[31m';
}
}
if (actual && actual.stack && actual instanceof Error)
actual = `${actual.name}: ${actual.message}`;

View File

@ -28,6 +28,14 @@ const { inherits } = util;
const errnoException = util._errnoException;
const errors = require('internal/errors');
const readline = require('readline');
const { release } = require('os');
const OSRelease = release().split('.');
const COLORS_2 = 1;
const COLORS_16 = 4;
const COLORS_256 = 8;
const COLORS_16m = 24;
function isatty(fd) {
return Number.isInteger(fd) && fd >= 0 && isTTY(fd);
@ -104,6 +112,70 @@ inherits(WriteStream, net.Socket);
WriteStream.prototype.isTTY = true;
WriteStream.prototype.getColorDepth = function(env = process.env) {
if (env.NODE_DISABLE_COLORS || env.TERM === 'dumb' && !env.COLORTERM) {
return COLORS_2;
}
if (process.platform === 'win32') {
// Windows 10 build 10586 is the first Windows release that supports 256
// colors. Windows 10 build 14931 is the first release that supports
// 16m/TrueColor.
if (+OSRelease[0] >= 10) {
const build = +OSRelease[2];
if (build >= 14931)
return COLORS_16m;
if (build >= 10586)
return COLORS_256;
}
return COLORS_16;
}
if (env.TMUX) {
return COLORS_256;
}
if (env.CI) {
if ('TRAVIS' in env || 'CIRCLECI' in env || 'APPVEYOR' in env ||
'GITLAB_CI' in env || env.CI_NAME === 'codeship') {
return COLORS_256;
}
return COLORS_2;
}
if ('TEAMCITY_VERSION' in env) {
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ?
COLORS_16 : COLORS_2;
}
switch (env.TERM_PROGRAM) {
case 'iTerm.app':
if (!env.TERM_PROGRAM_VERSION ||
/^[0-2]\./.test(env.TERM_PROGRAM_VERSION)) {
return COLORS_256;
}
return COLORS_16m;
case 'HyperTerm':
case 'Hyper':
case 'MacTerm':
return COLORS_16m;
case 'Apple_Terminal':
return COLORS_256;
}
if (env.TERM) {
if (/^xterm-256/.test(env.TERM))
return COLORS_256;
if (/^screen|^xterm|^vt100|color|ansi|cygwin|linux/i.test(env.TERM))
return COLORS_16;
}
if (env.COLORTERM)
return COLORS_16;
return COLORS_2;
};
WriteStream.prototype._refreshSize = function() {
var oldCols = this.columns;

View File

@ -788,10 +788,13 @@ common.expectsError(
Error.stackTraceLimit = tmpLimit;
// Test error diffs
const colors = process.stdout.isTTY && process.stdout.getColorDepth() > 1;
const start = 'Input A expected to deepStrictEqual input B:';
const actExp = '\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m';
const plus = '\u001b[32m+\u001b[39m';
const minus = '\u001b[31m-\u001b[39m';
const actExp = colors ?
'\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m' :
'+ expected - actual';
const plus = colors ? '\u001b[32m+\u001b[39m' : '+';
const minus = colors ? '\u001b[31m-\u001b[39m' : '-';
let message = [
start,
`${actExp} ... Lines skipped`,

View File

@ -0,0 +1,52 @@
'use strict';
const common = require('../common');
const assert = require('assert').strict;
/* eslint-disable no-restricted-properties */
const { openSync } = require('fs');
const tty = require('tty');
const { WriteStream } = require('tty');
// Do our best to grab a tty fd.
function getTTYfd() {
const ttyFd = [0, 1, 2, 4, 5].find(tty.isatty);
if (ttyFd === undefined) {
try {
return openSync('/dev/tty');
} catch (e) {
// There aren't any tty fd's available to use.
return -1;
}
}
return ttyFd;
}
const fd = getTTYfd();
// Give up if we did not find a tty
if (fd === -1)
common.skip();
const writeStream = new WriteStream(fd);
let depth = writeStream.getColorDepth();
assert.equal(typeof depth, 'number');
assert(depth >= 1 && depth <= 24);
// If the terminal does not support colors, skip the rest
if (depth === 1)
common.skip();
assert.notEqual(writeStream.getColorDepth({ TERM: 'dumb' }), depth);
// Deactivate colors
const tmp = process.env.NODE_DISABLE_COLORS;
process.env.NODE_DISABLE_COLORS = 1;
depth = writeStream.getColorDepth();
assert.equal(depth, 1);
process.env.NODE_DISABLE_COLORS = tmp;