Skip to content

Instantly share code, notes, and snippets.

@panoply
Created August 4, 2025 22:54
Show Gist options
  • Save panoply/3d562195d9c91fa3991c65fca2d9af81 to your computer and use it in GitHub Desktop.
Save panoply/3d562195d9c91fa3991c65fca2d9af81 to your computer and use it in GitHub Desktop.
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
/* 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