Created
August 4, 2025 22:54
-
-
Save panoply/3d562195d9c91fa3991c65fca2d9af81 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const EMPTY_STRING = '' | |
const separator = ',' | |
// color spaces | |
export const SPACE_UNDEFINED = -1 | |
export const SPACE_BW = 0 | |
export const SPACE_16COLORS = 1 | |
export const SPACE_256COLORS = 2 | |
export const SPACE_TRUECOLOR = 3 | |
// color space descriptions used in the benchmark | |
export const colorSpaces = { | |
[SPACE_BW]: 'black and white', | |
[SPACE_16COLORS]: '16 colors', | |
[SPACE_256COLORS]: '256 colors', | |
[SPACE_TRUECOLOR]: 'truecolor' | |
} | |
const styles = {} | |
const colorSpace = getColorSpace() | |
const hasColors = colorSpace > SPACE_BW | |
const mono = { open: EMPTY_STRING, close: EMPTY_STRING } | |
const monoFn = () => mono | |
const esc = hasColors ? (open, close) => ({ open: `\u001b[${open}m`, close: `\u001b[${close}m` }) : monoFn | |
const closeCode = 39 | |
const bgCloseCode = 49 | |
const bgOffset = 10 | |
const createRgb16Fn = (offset, closeCode) => (r, g, b) => esc(rgbToAnsi16(r, g, b) + offset, closeCode) | |
const createRgb256Fn = (fn) => (r, g, b) => fn(rgbToAnsi256(r, g, b)) | |
const createHexFn = (fn) => (hex) => fn(...hexToRgb(hex)) | |
// defaults, truecolor | |
let fnRgb = (r, g, b) => esc(`38;2;${r};${g};${b}`, closeCode) | |
let fnBgRgb = (r, g, b) => esc(`48;2;${r};${g};${b}`, bgCloseCode) | |
let fnAnsi256 = (code) => esc(`38;5;${code}`, closeCode) | |
let fnBgAnsi256 = (code) => esc(`48;5;${code}`, bgCloseCode) | |
if (colorSpace === SPACE_256COLORS) { | |
fnRgb = createRgb256Fn(fnAnsi256) | |
fnBgRgb = createRgb256Fn(fnBgAnsi256) | |
} else if (colorSpace === SPACE_16COLORS) { | |
fnRgb = createRgb16Fn(0, closeCode) | |
fnBgRgb = createRgb16Fn(bgOffset, bgCloseCode) | |
fnAnsi256 = (code) => esc(ansi256To16(code), closeCode) | |
fnBgAnsi256 = (code) => esc(ansi256To16(code) + bgOffset, bgCloseCode) | |
} | |
const styleData = { | |
// color functions | |
ansi256: fnAnsi256, // alias for compatibility with chalk | |
bgAnsi256: fnBgAnsi256, // alias for compatibility with chalk | |
fg: fnAnsi256, | |
bg: fnBgAnsi256, | |
rgb: fnRgb, | |
bgRgb: fnBgRgb, | |
hex: createHexFn(fnRgb), | |
bgHex: createHexFn(fnBgRgb), | |
// styles | |
visible: mono, | |
reset: esc(0, 0), | |
bold: esc(1, 22), | |
dim: esc(2, 22), | |
italic: esc(3, 23), | |
underline: esc(4, 24), | |
inverse: esc(7, 27), | |
hidden: esc(8, 28), | |
strikethrough: esc(9, 29) | |
} | |
// generate ANSI 16 colors dynamically to reduce the code | |
let bright = 'Bright' | |
let code = 30 | |
let bgName | |
let stylePrototype | |
let LF = '\n' | |
/** | |
* Convert hex color string to RGB values. | |
*/ | |
export function hexToRgb(value) { | |
let [, color] = /([a-f\d]{3,6})/i.exec(value) || [] | |
let len = color ? color.length : 0 | |
if (len === 3) { | |
// let [r, g, b] = color; color = r + r + g + g + b + b; // this is a bit slower than direct access by index | |
color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2] | |
} else if (len !== 6) { // Fix: Changed 6 ^ len to len !== 6 for clarity | |
return [0, 0, 0] | |
} | |
let decimal = parseInt(color, 16) | |
return [decimal >> 16 & 255, decimal >> 8 & 255, decimal & 255] | |
} | |
/** | |
* Convert RGB values to approximate code of ANSI 256 colors. | |
*/ | |
export function rgbToAnsi256(r, g, b) { | |
// greyscale | |
if (r === g && g === b) { | |
if (r < 8) return 16 | |
if (r > 248) return 231 | |
return Math.round(((r - 8) / 247) * 24) + 232 | |
} | |
return 16 | |
// r / 255 * 5 => r / 51 | |
+ (36 * Math.round(r / 51)) | |
+ (6 * Math.round(g / 51)) | |
+ Math.round(b / 51) | |
} | |
/** | |
* Convert ANSI 256 color code to approximate code of ANSI 16 colors. | |
*/ | |
export function ansi256To16(code) { | |
let r | |
, g | |
, b | |
, value | |
, remainder | |
if (code < 8) return 30 + code | |
if (code < 16) return 90 + (code - 8) | |
if (code >= 232) { | |
// greyscale | |
r = g = b = (((code - 232) * 10) + 8) / 255 | |
} else { | |
code -= 16 | |
remainder = code % 36 | |
r = (code / 36 | 0) / 5 | |
g = (remainder / 6 | 0) / 5 | |
b = (remainder % 6) / 5 | |
} | |
value = Math.max(r, g, b) * 2 | |
return value | |
? 30 + (Math.round(b) << 2 | Math.round(g) << 1 | Math.round(r)) + (value !== 2 ? 60 : 0) | |
: 30 | |
} | |
export function rgbToAnsi16(r, g, b) { | |
return ansi256To16(rgbToAnsi256(r, g, b)) | |
} | |
/** | |
* Detect color space. | |
* | |
* Truecolor is supported by: | |
* - some CI (e.g. GitHub CI) | |
* - Windows (since Windows 10 revision 14931) | |
* - iTerm, VSCode, JetBrains-JediTerm | |
* - xterm-kitty | |
*/ | |
function detectColorSpace(env, isTTY, isWin) { | |
// Use `echo $TERM` command to display terminal name in `env.TERM`. | |
let term = env.TERM || '' | |
let envKeys = separator + Object.keys(env).join(separator) | |
let colorspace = { | |
'24bit': SPACE_TRUECOLOR, | |
truecolor: SPACE_TRUECOLOR, | |
ansi256: SPACE_256COLORS, | |
ansi: SPACE_16COLORS | |
}[env.COLORTERM] | |
if (colorspace !== undefined) return colorspace | |
// Azure DevOps CI | |
if (env.TF_BUILD) return SPACE_16COLORS | |
// JetBrains | |
if (/,TEAMCI/.test(envKeys)) return SPACE_256COLORS | |
// CI tools | |
if (env.CI) { | |
// CI supports truecolor: GITHUB_ACTIONS, GITEA_ACTIONS | |
if (/,GIT(HUB|EA)/.test(envKeys)) return SPACE_TRUECOLOR | |
return SPACE_16COLORS | |
} | |
if (!isTTY || /-mono|dumb/i.test(term)) return SPACE_BW | |
if (isWin) return SPACE_TRUECOLOR | |
if (/term-(kit|dir)/.test(term)) return SPACE_TRUECOLOR | |
if (env.TERMINAL_EMULATOR && env.TERMINAL_EMULATOR.includes('JediTerm')) return SPACE_TRUECOLOR | |
// - screen-256color | |
// - xterm-256color | |
// - rxvt-256color | |
// - putty-256color | |
// - mintty-256color | |
// - linux-256color | |
// - tmux-256color | |
// - ansi-256color | |
if (/-256/.test(term)) return SPACE_256COLORS | |
if (/scr|xterm|tty|ansi|color|[nm]ux|vt|cyg/.test(term)) return SPACE_16COLORS | |
return SPACE_TRUECOLOR | |
} | |
/** | |
* Get the color space supported by the current environment. | |
*/ | |
export function getColorSpace(x) { | |
let _this = x || globalThis | |
let proc = _this.process || {} | |
let argv = proc.argv || [] | |
let colorSpace = SPACE_UNDEFINED | |
let env = proc.env || {} | |
let hasFlag = (regex) => argv.some((value) => regex.test(value)) | |
// Note: In Deno 2.0+, the `process` is available globally | |
let isDeno = !!_this.Deno | |
if (isDeno) { | |
try { | |
Object.keys(env) | |
} catch { | |
env = {} | |
colorSpace = SPACE_BW | |
} | |
} | |
const isPM2 = !!env.PM2_HOME && !!env.pm_id | |
const nextRuntime = env.NEXT_RUNTIME | |
const hasNextEdgeRuntime = nextRuntime && nextRuntime.includes('edge') | |
let isTTY = isPM2 || hasNextEdgeRuntime || !!(proc.stdout && proc.stdout.isTTY) | |
// - https://force-color.org | |
// - https://nodejs.org/api/tty.html#writestreamhascolorscount-env | |
// - https://nodejs.org/api/cli.html#force_color1-2-3 | |
const FORCE_COLOR = 'FORCE_COLOR' | |
const forceColorValue = env[FORCE_COLOR] | |
const forceColorNum = parseInt(forceColorValue) | |
const forceColor = isNaN(forceColorNum) ? forceColorValue === 'false' ? 0 : SPACE_UNDEFINED : forceColorNum | |
const isForceEnabled = (FORCE_COLOR in env && forceColor !== 0) || hasFlag(/^-{1,2}color=?(true|always)?$/) | |
if (isForceEnabled) colorSpace = forceColor | |
if (colorSpace < SPACE_BW) colorSpace = detectColorSpace(env, isTTY, proc.platform === 'win32') | |
if (forceColor === 0 || !!env.NO_COLOR || hasFlag(/^-{1,2}(no-color|color=(false|never))$/)) return SPACE_BW | |
const hasChrome = _this.window && _this.window.chrome | |
return (isForceEnabled && colorSpace === SPACE_BW) || !!hasChrome ? SPACE_TRUECOLOR : colorSpace | |
} | |
'black,red,green,yellow,blue,magenta,cyan,white'.split(separator).map((name) => { | |
bgName = 'bg' + name[0].toUpperCase() + name.slice(1) | |
styleData[name] = esc(code, closeCode) | |
styleData[name + bright] = esc(code + 60, closeCode) | |
styleData[bgName] = esc(code + bgOffset, bgCloseCode) | |
styleData[bgName + bright] = esc(code + 70, bgCloseCode) | |
code++ | |
}) | |
styleData.grey = styleData.gray = esc(90, closeCode) | |
styleData.bgGrey = styleData.bgGray = esc(100, bgCloseCode) | |
/** | |
* Create a style function with ANSI codes | |
*/ | |
function createStyle({ _p: props }, { open, close }) { | |
const fn = (arg, ...values) => { | |
// API rule: if the argument is one of '', undefined or null, then return empty string | |
if (!arg) { | |
if (open && open === close) return open | |
if (arg === null || arg === undefined || arg === EMPTY_STRING) return EMPTY_STRING | |
} | |
let output = arg.raw | |
? String.raw({ raw: arg }, ...values) | |
: EMPTY_STRING + arg | |
let props = fn._p | |
let { _a: openStack, _b: closeStack } = props | |
if (output.includes('\u001b')) { // Fix: Changed '' to '\u001b' | |
while (props) { | |
let search = props.close | |
let replacement = props.open | |
let searchLength = search.length | |
let result = EMPTY_STRING | |
let lastPos = 0 | |
let pos | |
if (searchLength) { | |
while ((pos = output.indexOf(search, lastPos)) !== -1) { | |
result += output.slice(lastPos, pos) + replacement | |
lastPos = pos + searchLength | |
} | |
output = result + output.slice(lastPos) | |
} | |
props = props._p | |
} | |
} | |
if (output.includes(LF)) { | |
output = output.replace(/(\r?\n)/g, closeStack + '$1' + openStack) | |
} | |
return openStack + output + closeStack | |
} | |
let openStack = open | |
let closeStack = close | |
if (props) { | |
openStack = props._a + open | |
closeStack = close + props._b | |
} | |
Object.setPrototypeOf(fn, stylePrototype) | |
fn._p = { open, close, _a: openStack, _b: closeStack, _p: props } | |
fn.open = openStack | |
fn.close = closeStack | |
return fn | |
} | |
/** | |
* Create ANSI instance | |
*/ | |
const ANSI = function() { | |
let _self = { | |
ANSI: ANSI, | |
isSupported: () => hasColors, | |
strip: (str) => str.replace(/\u001b[[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, EMPTY_STRING), | |
extend(colors) { | |
for (let name in colors) { | |
let color = colors[name] | |
// can be: f - function, s - string, o - object | |
let type = (typeof color)[0] | |
// detect whether the value is object {open, close} or hex string | |
// type === 'string' for extend colors, e.g. ANSI.extend({ pink: '#FF75D1' }) | |
let styleProps = type === 's' ? fnRgb(...hexToRgb(color)) : color | |
// type === 'function' | |
if (type === 'f') { | |
styles[name] = { | |
get() { | |
return (...args) => createStyle(this, color(...args)) | |
} | |
} | |
} else { | |
styles[name] = { | |
get() { | |
let style = createStyle(this, styleProps) | |
Object.defineProperty(this, name, { value: style }) | |
return style | |
} | |
} | |
} | |
} | |
stylePrototype = Object.create({}, styles) | |
Object.setPrototypeOf(_self, stylePrototype) | |
return _self | |
} | |
} | |
return _self.extend(styleData) | |
} | |
const ansi = new ANSI() | |
export default ansi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-console */ | |
import c from './cli.js' | |
// Print a test pattern with all available colors and styles | |
function printTestPattern() { | |
console.log(c.gray('\n--------------------------------------')) | |
console.log(c.bold.blue('TEST COLOR PATTERNS')) | |
console.log(c.gray('--------------------------------------\n')) | |
// Test basic colors | |
console.log(c.bold.blue('Basic Colors:')) | |
console.log(c.black('■') + ' Black ') | |
console.log(c.red('■ Red')) | |
console.log(c.green('■ Green')) | |
console.log(c.yellow('■ Yellow')) | |
console.log(c.blue('■ Blue') + ' ') | |
console.log(c.magenta('■ Magenta')) | |
console.log(c.cyan('■ Cyan')) | |
console.log(c.white('■ White')) | |
// Test bright colors | |
console.log(c.bold.blue('\nBright Colors:')) | |
console.log(c.blackBright('■ BlackBright') + ' ' + c.redBright('■ RedBright')) | |
console.log(c.greenBright('■ GreenBright') + ' ' + c.yellowBright('■ YellowBright')) | |
console.log(c.blueBright('■ BlueBright') + ' ' + c.magentaBright('■ MagentaBright')) | |
console.log(c.cyanBright('■ CyanBright') + ' ' + c.whiteBright('■ WhiteBright')) | |
// Test background colors | |
console.log(c.bold.blue('\nBackground Colors:')) | |
console.log(c.bgBlack(' Black ') + ' ' + c.bgRed(' Red ') + ' ' + c.bgGreen(' Green ')) | |
console.log(c.bgYellow(' Yellow ') + ' ' + c.bgBlue(' Blue ') + ' ' + c.bgMagenta(' Magenta ')) | |
console.log(c.bgCyan(' Cyan ') + ' ' + c.bgWhite(' White ')) | |
// Test styles | |
console.log(c.bold.blue('\nText Styles:')) | |
console.log(c.bold('Bold') + ' ' + c.dim('Dim') + ' ' + c.italic('Italic')) | |
console.log(c.underline('Underline') + ' ' + c.inverse('Inverse') + ' ' + c.strikethrough('Strikethrough')) | |
// Test RGB and HEX colors | |
console.log(c.bold.blue('\nRGB and HEX Colors:')) | |
// RGB rainbow | |
const rainbowColors = [ | |
[255, 0, 0], // Red | |
[255, 127, 0], // Orange | |
[255, 255, 0], // Yellow | |
[0, 255, 0], // Green | |
[0, 0, 255], // Blue | |
[75, 0, 130], // Indigo | |
[148, 0, 211] // Violet | |
] | |
let rainbow = '' | |
for (const [r, g, b] of rainbowColors) { | |
rainbow += c.rgb(r, g, b)('■') | |
} | |
console.log(c.bold.blue('RGB Rainbow: ') + rainbow) | |
// HEX colors | |
console.log(c.bold.blue('HEX Colors: ') + | |
c.hex('#FF5733')('■ #FF5733') + ' ' + | |
c.hex('#33FF57')('■ #33FF57') + ' ' + | |
c.hex('#3357FF')('■ #3357FF') | |
) | |
// Test nesting styles | |
console.log(c.bold.blue('\nNested Styles:')) | |
console.log(c.red('Red ' + c.bold('Bold Red ' + c.underline('Bold Red Underline') + ' Bold Red') + ' Red')) | |
// Test RGB background | |
console.log(c.bold.blue('\nRGB Background:')) | |
console.log(c.bgRgb(255, 105, 180)(' Hot Pink Background ')) | |
// Test HEX background | |
console.log(c.bold.blue('\nHEX Background:')) | |
console.log(c.bgHex('#1E90FF')(' Dodger Blue Background ')) | |
// Test custom colors (extension) | |
console.log('\nCustom Colors (Extension):') | |
const extendedc = c.extend({ | |
hotPink: '#FF69B4', | |
skyBlue: '#87CEEB', | |
limeGreen: '#32CD32' | |
}) | |
console.log(extendedc.hotPink('Hot Pink') + ' ' + | |
extendedc.skyBlue('Sky Blue') + ' ' + | |
extendedc.limeGreen('Lime Green')) | |
// Support detection | |
console.log('\nSystem Info:') | |
console.log(`Color Support: ${c.isSupported() ? 'Yes' : 'No'}`) | |
} | |
// Test with a multiline string | |
function testMultilineString() { | |
console.log(c.bold.blue('\nMultiline String Test:')) | |
const multiline = c.green(`This is a multiline string | |
with green text that spans | |
multiple lines. | |
The color should continue on each line.`) | |
console.log(multiline) | |
} | |
// Test stripping c codes | |
function testStripping() { | |
console.log(c.gray('\n--------------------------------------')) | |
console.log(c.bold.blue('TEST CODE STRIPPING')) | |
console.log(c.gray('--------------------------------------\n')) | |
const coloredText = c.red('This text is red') + ' ' + | |
c.blue('This text is blue') + ' ' + | |
c.green('This text is green') | |
console.log('Original: ' + coloredText) | |
console.log('Stripped: ' + c.strip(coloredText)) | |
} | |
// Test template literals | |
function testTemplateLiterals() { | |
console.log('\nTemplate Literals Test:') | |
const name = 'User' | |
const age = 30 | |
const message = c.cyan`Hello, ${name}! You are ${age} years old.` | |
console.log(message) | |
} | |
// Run all demonstrations | |
function runDemo() { | |
printTestPattern() | |
testMultilineString() | |
testStripping() | |
testTemplateLiterals() | |
console.log('\nIntegration test completed.') | |
} | |
runDemo() | |
process.exit(0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment