Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for 24bit TrueColor #30

Merged
merged 1 commit into from
Apr 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/plenty-ways-carry.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;
Comment on lines +56 to +59

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't realize that, TIL!

} 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