From cb3f2308e17cd2878d2722db7762bdc725e9ff48 Mon Sep 17 00:00:00 2001 From: Josh Junon Date: Tue, 20 Jun 2017 10:02:09 -0700 Subject: [PATCH] Add RGB (256/Truecolor) support (#140) --- index.js | 68 +++++++++++++++++++++++++++++++++++++++++++--------- package.json | 4 ++-- readme.md | 59 +++++++++++++++++++++++++++++++++++++++++---- test.js | 60 ++++++++++++++++++++++++++++++++-------------- 4 files changed, 155 insertions(+), 36 deletions(-) diff --git a/index.js b/index.js index ab458f7..351253f 100644 --- a/index.js +++ b/index.js @@ -6,9 +6,14 @@ var supportsColor = require('supports-color'); var defineProps = Object.defineProperties; var isSimpleWindowsTerm = process.platform === 'win32' && !/^xterm/i.test(process.env.TERM); +// supportsColor.level -> ansiStyles.color[name] mapping +var levelMapping = ['ansi', 'ansi', 'ansi256', 'ansi16m']; +// color-convert models to exclude from the Chalk API due to conflicts and such. +var skipModels = ['gray']; + function Chalk(options) { - // detect mode if not set manually - this.enabled = !options || options.enabled === undefined ? supportsColor : options.enabled; + // detect level if not set manually + this.level = !options || options.level === undefined ? supportsColor.level : options.level; } // use bright blue on Windows as the normal blue color is illegible @@ -23,7 +28,45 @@ Object.keys(ansiStyles).forEach(function (key) { styles[key] = { get: function () { - return build.call(this, this._styles ? this._styles.concat(key) : [key]); + var codes = ansiStyles[key]; + return build.call(this, this._styles ? this._styles.concat(codes) : [codes], key); + } + }; +}); + +ansiStyles.color.closeRe = new RegExp(escapeStringRegexp(ansiStyles.color.close), 'g'); +Object.keys(ansiStyles.color.ansi).forEach(function (model) { + if (skipModels.indexOf(model) !== -1) { + return; + } + + styles[model] = { + get: function () { + var level = this.level; + return function () { + var open = ansiStyles.color[levelMapping[level]][model].apply(null, arguments); + var codes = {open: open, close: ansiStyles.color.close, closeRe: ansiStyles.color.closeRe}; + return build.call(this, this._styles ? this._styles.concat(codes) : [codes], model); + }; + } + }; +}); + +ansiStyles.bgColor.closeRe = new RegExp(escapeStringRegexp(ansiStyles.bgColor.close), 'g'); +Object.keys(ansiStyles.bgColor.ansi).forEach(function (model) { + if (skipModels.indexOf(model) !== -1) { + return; + } + + var bgModel = 'bg' + model.charAt(0).toUpperCase() + model.substring(1); + styles[bgModel] = { + get: function () { + var level = this.level; + return function () { + var open = ansiStyles.bgColor[levelMapping[level]][model].apply(null, arguments); + var codes = {open: open, close: ansiStyles.bgColor.close, closeRe: ansiStyles.bgColor.closeRe}; + return build.call(this, this._styles ? this._styles.concat(codes) : [codes], model); + }; } }; }); @@ -31,7 +74,7 @@ Object.keys(ansiStyles).forEach(function (key) { // eslint-disable-next-line func-names var proto = defineProps(function chalk() {}, styles); -function build(_styles) { +function build(_styles, key) { var builder = function () { return applyStyle.apply(builder, arguments); }; @@ -40,16 +83,19 @@ function build(_styles) { builder._styles = _styles; - Object.defineProperty(builder, 'enabled', { + Object.defineProperty(builder, 'level', { enumerable: true, get: function () { - return self.enabled; + return self.level; }, - set: function (v) { - self.enabled = v; + set: function (level) { + self.level = level; } }); + // see below for fix regarding invisible grey/dim combination on windows. + builder.hasGrey = this.hasGrey || key === 'gray' || key === 'grey'; + // __proto__ is used because we must return a function, but there is // no way to create a function with a different prototype. /* eslint-disable no-proto */ @@ -71,7 +117,7 @@ function applyStyle() { } } - if (!this.enabled || !str) { + if (!this.level || !str) { return str; } @@ -82,12 +128,12 @@ function applyStyle() { // see https://github.com/chalk/chalk/issues/58 // If we're on Windows and we're dealing with a gray color, temporarily make 'dim' a noop. var originalDim = ansiStyles.dim.open; - if (isSimpleWindowsTerm && (nestedStyles.indexOf('gray') !== -1 || nestedStyles.indexOf('grey') !== -1)) { + if (isSimpleWindowsTerm && this.hasGrey) { ansiStyles.dim.open = ''; } while (i--) { - var code = ansiStyles[nestedStyles[i]]; + var code = nestedStyles[i]; // Replace any instances already present with a re-opening code // otherwise only the part of the string until said closing code diff --git a/package.json b/package.json index 7008e0f..f8d2344 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,9 @@ "text" ], "dependencies": { - "ansi-styles": "^2.1.0", + "ansi-styles": "^3.0.0", "escape-string-regexp": "^1.0.2", - "supports-color": "^3.1.2" + "supports-color": "^3.2.3" }, "devDependencies": { "coveralls": "^2.11.2", diff --git a/readme.md b/readme.md index 3785043..a6d5544 100644 --- a/readme.md +++ b/readme.md @@ -76,14 +76,23 @@ CPU: ${chalk.red('90%')} RAM: ${chalk.green('40%')} DISK: ${chalk.yellow('70%')} `); + +// Use RGB colors in terminal emulators that support it. +log(chalk.keyword('orange')('Yay for orange colored text!')); +log(chalk.rgb(123, 45, 67).underline('Underlined reddish color')); +log(chalk.hex('#DEADED').bold('Bold gray!')); ``` Easily define your own themes. ```js const chalk = require('chalk'); + const error = chalk.bold.red; +const warning = chalk.keyword('orange'); + console.log(error('Error!')); +console.log(warning('Warning!')); ``` Take advantage of console.log [string substitution](http://nodejs.org/docs/latest/api/console.html#console_console_log_data). @@ -105,16 +114,23 @@ Chain [styles](#styles) and call the last one as a method with a string argument Multiple arguments will be separated by space. -### chalk.enabled +### chalk.level -Color support is automatically detected, but you can override it by setting the `enabled` property. You should however only do this in your own code as it applies globally to all chalk consumers. +Color support is automatically detected, but you can override it by setting the `level` property. You should however only do this in your own code as it applies globally to all chalk consumers. If you need to change this in a reusable module create a new instance: ```js -const ctx = new chalk.constructor({enabled: false}); +const ctx = new chalk.constructor({level: 0}); ``` +Levels are as follows: + +0. All colors disabled +1. Basic color support (16 colors) +2. 256 color support +3. RGB/Truecolor support (16 million colors) + ### chalk.supportsColor Detect whether the terminal [supports color](https://github.com/chalk/supports-color). Used internally and handled for you, but exposed for convenience. @@ -174,10 +190,42 @@ console.log(chalk.styles.red.open + 'Hello' + chalk.styles.red.close); - `bgWhite` -## 256-colors +## 256/16 million (Truecolor) color support -Chalk does not support anything other than the base eight colors, which guarantees it will work on all terminals and systems. Some terminals, specifically `xterm` compliant ones, will support the full range of 8-bit colors. For this the lower level [ansi-256-colors](https://github.com/jbnicolai/ansi-256-colors) package can be used. +Chalk supports 256 colors and, when manually specified, [Truecolor (16 million colors)](https://gist.github.com/XVilka/8346728) on all supported terminal emulators. +Colors are downsampled from 16 million RGB values to an ANSI color format that is supported by the terminal emulator (or by specifying {level: n} as a chalk option). For example, Chalk configured to run at level 1 (basic color support) will downsample an RGB value of #FF0000 (red) to 31 (ANSI escape for red). + +Some examples: + +- `chalk.hex('#DEADED').underline('Hello, world!')` +- `chalk.keyword('orange')('Some orange text')` +- `chalk.rgb(15, 100, 204).inverse('Hello!')` + +Background versions of these models are prefixed with `bg` and the first level of the module capitalized (e.g. `keyword` for foreground colors and `bgKeyword` for background colors). + +- `chalk.bgHex('#DEADED').underline('Hello, world!')` +- `chalk.bgKeyword('orange')('Some orange text')` +- `chalk.bgRgb(15, 100, 204).inverse('Hello!')` + +As of this writing, these are the supported color models that are exposed in Chalk: + +- `rgb` - e.g. `chalk.rgb(255, 136, 0).bold('Orange!')` +- `hex` - e.g. `chalk.hex('#ff8800').bold('Orange!')` +- `keyword` (CSS keywords) - e.g. `chalk.keyword('orange').bold('Orange!')` +- `hsl` - e.g. `chalk.hsl(32, 100, 50).bold('Orange!')` +- `hsv` +- `hwb` +- `cmyk` +- `xyz` +- `lab` +- `lch` +- `ansi16` +- `ansi256` +- `hcg` +- `apple` (see [qix-/color-convert#30](https://github.com/Qix-/color-convert/issues/30)) + +For a complete list of color models, see [`color-convert`'s list of conversions](https://github.com/Qix-/color-convert/blob/master/conversions.js). ## Windows @@ -194,6 +242,7 @@ If you're on Windows, do yourself a favor and use [`cmder`](http://cmder.net/) i - [ansi-regex](https://github.com/chalk/ansi-regex) - Regular expression for matching ANSI escape codes - [wrap-ansi](https://github.com/chalk/wrap-ansi) - Wordwrap a string with ANSI escape codes - [slice-ansi](https://github.com/chalk/slice-ansi) - Slice a string with ANSI escape codes +- [color-convert](https://github.com/qix-/color-convert) - Converts colors between different models ## License diff --git a/test.js b/test.js index b802684..144efaf 100644 --- a/test.js +++ b/test.js @@ -72,6 +72,26 @@ describe('chalk', function () { it('line breaks should open and close colors', function () { assert.equal(chalk.grey('hello\nworld'), '\u001b[90mhello\u001b[39m\n\u001b[90mworld\u001b[39m'); }); + + it('should properly convert RGB to 16 colors on basic color terminals', function () { + assert.equal(new chalk.constructor({level: 1}).hex('#FF0000')('hello'), '\u001b[91mhello\u001b[39m'); + assert.equal(new chalk.constructor({level: 1}).bgHex('#FF0000')('hello'), '\u001b[101mhello\u001b[49m'); + }); + + it('should properly convert RGB to 256 colors on basic color terminals', function () { + assert.equal(new chalk.constructor({level: 2}).hex('#FF0000')('hello'), '\u001b[38;5;196mhello\u001b[39m'); + assert.equal(new chalk.constructor({level: 2}).bgHex('#FF0000')('hello'), '\u001b[48;5;196mhello\u001b[49m'); + }); + + it('should properly convert RGB to 256 colors on basic color terminals', function () { + assert.equal(new chalk.constructor({level: 3}).hex('#FF0000')('hello'), '\u001b[38;2;255;0;0mhello\u001b[39m'); + assert.equal(new chalk.constructor({level: 3}).bgHex('#FF0000')('hello'), '\u001b[48;2;255;0;0mhello\u001b[49m'); + }); + + it('should not emit RGB codes if level is 0', function () { + assert.equal(new chalk.constructor({level: 0}).hex('#FF0000')('hello'), 'hello'); + assert.equal(new chalk.constructor({level: 0}).bgHex('#FF0000')('hello'), 'hello'); + }); }); describe('chalk on windows', function () { @@ -132,39 +152,43 @@ describe('chalk on windows', function () { }); }); -describe('chalk.enabled', function () { +describe('chalk.level', function () { it('should not output colors when manually disabled', function () { - chalk.enabled = false; + var oldLevel = chalk.level; + chalk.level = 0; assert.equal(chalk.red('foo'), 'foo'); - chalk.enabled = true; + chalk.level = oldLevel; }); it('should enable/disable colors based on overall chalk enabled property, not individual instances', function () { - chalk.enabled = true; + var oldLevel = chalk.level; + chalk.level = 1; var red = chalk.red; - assert.equal(red.enabled, true); - chalk.enabled = false; - assert.equal(red.enabled, chalk.enabled); - chalk.enabled = true; + assert.equal(red.level, 1); + chalk.level = 0; + assert.equal(red.level, chalk.level); + chalk.level = oldLevel; }); it('should propagate enable/disable changes from child colors', function () { - chalk.enabled = true; + var oldLevel = chalk.level; + chalk.level = 1; var red = chalk.red; - assert.equal(red.enabled, true); - assert.equal(chalk.enabled, true); - red.enabled = false; - assert.equal(red.enabled, false); - assert.equal(chalk.enabled, false); - chalk.enabled = true; - assert.equal(red.enabled, true); - assert.equal(chalk.enabled, true); + assert.equal(red.level, 1); + assert.equal(chalk.level, 1); + red.level = 0; + assert.equal(red.level, 0); + assert.equal(chalk.level, 0); + chalk.level = 1; + assert.equal(red.level, 1); + assert.equal(chalk.level, 1); + chalk.level = oldLevel; }); }); describe('chalk.constructor', function () { it('should create a isolated context where colors can be disabled', function () { - var ctx = new chalk.constructor({enabled: false}); + var ctx = new chalk.constructor({level: 0}); assert.equal(ctx.red('foo'), 'foo'); assert.equal(chalk.red('foo'), '\u001b[31mfoo\u001b[39m'); });