Skip to content

Commit

Permalink
Add support for 24bit TrueColor
Browse files Browse the repository at this point in the history
  • Loading branch information
marvinhagemeister committed Apr 22, 2023
1 parent a81083d commit 737ef18
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 7 deletions.
7 changes: 7 additions & 0 deletions .changeset/plenty-ways-carry.md
@@ -0,0 +1,7 @@
---
'kolorist': minor
---

Add support for 24bit TrueColor detection.

This is supported in nearly every modern terminals these days. The exception to that is the built in Terminal app on macOS and CI systems. TrueColor values are automatically converted to Ansi 256 when TrueColor isn't supported, but Ansi 256 is. The only case where I found that in practice was with Terminal.app on macOS which only supports Ansi 256.
52 changes: 51 additions & 1 deletion src/index.test.ts
Expand Up @@ -41,7 +41,18 @@ describe('colors', () => {

it('should print demo', () => {
const strs = Object.keys(k)
.filter(key => !['options', 'ansi256', 'ansi256Bg', 'link', 'SupportLevel'].includes(key))
.filter(
key =>
![
'options',
'ansi256',
'ansi256Bg',
'link',
'SupportLevel',
'trueColor',
'trueColorBg',
].includes(key)
)
.map(x => (k as any)[x]('foobar'));

console.log(columnize(strs, 16));
Expand Down Expand Up @@ -75,6 +86,45 @@ describe('colors', () => {
});
});

describe('TrueColor 24bit', () => {
beforeEach(() => {
k.options.supportLevel = k.SupportLevel.trueColor;
});

it('should print foreground', () => {
const str = k.trueColor(134, 239, 172)('foo');
console.log(str);
});

it('should print background', () => {
const str = k.trueColorBg(134, 239, 172)('foo');
console.log(str);
});

it('should mix with modifiers', () => {
console.log(k.dim(k.trueColor(134, 239, 172)('foo')));
console.log(k.dim(k.trueColorBg(134, 239, 172)(k.black('foo'))));
});

it('should be stripped', () => {
t.equal(k.stripColors(k.trueColor(134, 239, 172)('foo')), 'foo');
});

it('should be ignored if no terminal support', () => {
k.options.supportLevel = k.SupportLevel.ansi;
t.equal(
JSON.stringify(k.trueColor(134, 239, 172)('foo')),
JSON.stringify('foo')
);
});

it('should convert color space to ansi256 if possible', () => {
k.options.supportLevel = k.SupportLevel.ansi256;
t.equal(k.trueColor(134, 239, 172)('foo'), k.ansi256(157)('foo'));
t.equal(k.trueColorBg(134, 239, 172)('foo'), k.ansi256Bg(157)('foo'));
});
});

it('should toggle enabled or disabled', () => {
k.options.enabled = true;
t.equal(k.cyan('foo'), '\u001b[36mfoo\u001b[39m');
Expand Down
66 changes: 60 additions & 6 deletions src/index.ts
Expand Up @@ -14,6 +14,7 @@ export const enum SupportLevel {
none,
ansi,
ansi256,
trueColor,
}

/**
Expand All @@ -22,10 +23,15 @@ export const enum SupportLevel {
let supportLevel: SupportLevel = SupportLevel.none;

if (globalVar.process && globalVar.process.env && globalVar.process.stdout) {
const { FORCE_COLOR, NODE_DISABLE_COLORS, NO_COLOR, TERM } = globalVar.process.env;
const { FORCE_COLOR, NODE_DISABLE_COLORS, NO_COLOR, TERM, COLORTERM } =
globalVar.process.env;
if (NODE_DISABLE_COLORS || NO_COLOR || FORCE_COLOR === '0') {
enabled = false;
} else if (FORCE_COLOR === '1' || FORCE_COLOR === '2' || FORCE_COLOR === '3') {
} else if (
FORCE_COLOR === '1' ||
FORCE_COLOR === '2' ||
FORCE_COLOR === '3'
) {
enabled = true;
} else if (TERM === 'dumb') {
enabled = false;
Expand All @@ -47,10 +53,19 @@ if (globalVar.process && globalVar.process.env && globalVar.process.stdout) {
}

if (enabled) {
supportLevel =
TERM && TERM.endsWith('-256color')
? SupportLevel.ansi256
: SupportLevel.ansi;
// Windows supports 24bit True Colors since Windows 10 revision #14931,
// see https://devblogs.microsoft.com/commandline/24-bit-color-in-the-windows-console/
if (process.platform === 'win32') {
supportLevel = SupportLevel.trueColor;
} else {
if (COLORTERM && (COLORTERM === 'truecolor' || COLORTERM === '24bit')) {
supportLevel = SupportLevel.trueColor;
} else if (TERM && (TERM.endsWith('-256color') || TERM.endsWith('256'))) {
supportLevel = SupportLevel.ansi256;
} else {
supportLevel = SupportLevel.ansi;
}
}
}
}

Expand All @@ -75,6 +90,33 @@ function kolorist(
};
}

// Lower colors into 256 color space
// Taken from https://github.com/Qix-/color-convert/blob/3f0e0d4e92e235796ccb17f6e85c72094a651f49/conversions.js
// which is MIT licensed and copyright by Heather Arthur and Josh Junon
function rgbToAnsi256(r: number, g: number, b: number): number {
// We use the extended greyscale palette here, with the exception of
// black and white. normal palette only has 4 greyscale shades.
if (r >> 4 === g >> 4 && g >> 4 === b >> 4) {
if (r < 8) {
return 16;
}

if (r > 248) {
return 231;
}

return Math.round(((r - 8) / 247) * 24) + 232;
}

const ansi =
16 +
36 * Math.round((r / 255) * 5) +
6 * Math.round((g / 255) * 5) +
Math.round((b / 255) * 5);

return ansi;
}

export function stripColors(str: string | number) {
return ('' + str)
.replace(/\x1b\[[0-9;]+m/g, '')
Expand Down Expand Up @@ -135,6 +177,18 @@ export const ansi256 = (n: number) =>
export const ansi256Bg = (n: number) =>
kolorist('48;5;' + n, 0, SupportLevel.ansi256);

// TrueColor 24bit support
export const trueColor = (r: number, g: number, b: number) => {
return options.supportLevel === SupportLevel.ansi256
? ansi256(rgbToAnsi256(r, g, b))
: kolorist(`38;2;${r};${g};${b}`, 0, SupportLevel.trueColor);
};
export const trueColorBg = (r: number, g: number, b: number) => {
return options.supportLevel === SupportLevel.ansi256
? ansi256Bg(rgbToAnsi256(r, g, b))
: kolorist(`48;2;${r};${g};${b}`, 0, SupportLevel.trueColor);
};

// Links
const OSC = '\u001B]';
const BEL = '\u0007';
Expand Down

0 comments on commit 737ef18

Please sign in to comment.