source dump of claude code
at main 231 lines 7.6 kB view raw
1import chalk from 'chalk' 2import type { Color, TextStyles } from './styles.js' 3 4/** 5 * xterm.js (VS Code, Cursor, code-server, Coder) has supported truecolor 6 * since 2017, but code-server/Coder containers often don't set 7 * COLORTERM=truecolor. chalk's supports-color doesn't recognize 8 * TERM_PROGRAM=vscode (it only knows iTerm.app/Apple_Terminal), so it falls 9 * through to the -256color regex → level 2. At level 2, chalk.rgb() 10 * downgrades to the nearest 6×6×6 cube color: rgb(215,119,87) (Claude 11 * orange) → idx 174 rgb(215,135,135) — washed-out salmon. 12 * 13 * Gated on level === 2 (not < 3) to respect NO_COLOR / FORCE_COLOR=0 — 14 * those yield level 0 and are an explicit "no colors" request. Desktop VS 15 * Code sets COLORTERM=truecolor itself, so this is a no-op there (already 3). 16 * 17 * Must run BEFORE the tmux clamp — if tmux is running inside a VS Code 18 * terminal, tmux's passthrough limitation wins and we want level 2. 19 */ 20function boostChalkLevelForXtermJs(): boolean { 21 if (process.env.TERM_PROGRAM === 'vscode' && chalk.level === 2) { 22 chalk.level = 3 23 return true 24 } 25 return false 26} 27 28/** 29 * tmux parses truecolor SGR (\e[48;2;r;g;bm) into its cell buffer correctly, 30 * but its client-side emitter only re-emits truecolor to the outer terminal if 31 * the outer terminal advertises Tc/RGB capability (via terminal-overrides). 32 * Default tmux config doesn't set this, so tmux emits the cell to iTerm2/etc 33 * WITHOUT the bg sequence — outer terminal's buffer has bg=default → black on 34 * dark profiles. Clamping to level 2 makes chalk emit 256-color (\e[48;5;Nm), 35 * which tmux passes through cleanly. grey93 (255) is visually identical to 36 * rgb(240,240,240). 37 * 38 * Users who HAVE set `terminal-overrides ,*:Tc` get a technically-unnecessary 39 * downgrade, but the visual difference is imperceptible. Querying 40 * `tmux show -gv terminal-overrides` to detect this would add a subprocess on 41 * startup — not worth it. 42 * 43 * $TMUX is a pty-lifecycle env var set by tmux itself; it never comes from 44 * globalSettings.env, so reading it here is correct. chalk is a singleton, so 45 * this clamps ALL truecolor output (fg+bg+hex) across the entire app. 46 */ 47function clampChalkLevelForTmux(): boolean { 48 // bg.ts sets terminal-overrides :Tc before attach, so truecolor passes 49 // through — skip the clamp. General escape hatch for anyone who's 50 // configured their tmux correctly. 51 if (process.env.CLAUDE_CODE_TMUX_TRUECOLOR) return false 52 if (process.env.TMUX && chalk.level > 2) { 53 chalk.level = 2 54 return true 55 } 56 return false 57} 58// Computed once at module load — terminal/tmux environment doesn't change mid-session. 59// Order matters: boost first so the tmux clamp can re-clamp if tmux is running 60// inside a VS Code terminal. Exported for debugging — tree-shaken if unused. 61export const CHALK_BOOSTED_FOR_XTERMJS = boostChalkLevelForXtermJs() 62export const CHALK_CLAMPED_FOR_TMUX = clampChalkLevelForTmux() 63 64export type ColorType = 'foreground' | 'background' 65 66const RGB_REGEX = /^rgb\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)$/ 67const ANSI_REGEX = /^ansi256\(\s?(\d+)\s?\)$/ 68 69export const colorize = ( 70 str: string, 71 color: string | undefined, 72 type: ColorType, 73): string => { 74 if (!color) { 75 return str 76 } 77 78 if (color.startsWith('ansi:')) { 79 const value = color.substring('ansi:'.length) 80 switch (value) { 81 case 'black': 82 return type === 'foreground' ? chalk.black(str) : chalk.bgBlack(str) 83 case 'red': 84 return type === 'foreground' ? chalk.red(str) : chalk.bgRed(str) 85 case 'green': 86 return type === 'foreground' ? chalk.green(str) : chalk.bgGreen(str) 87 case 'yellow': 88 return type === 'foreground' ? chalk.yellow(str) : chalk.bgYellow(str) 89 case 'blue': 90 return type === 'foreground' ? chalk.blue(str) : chalk.bgBlue(str) 91 case 'magenta': 92 return type === 'foreground' ? chalk.magenta(str) : chalk.bgMagenta(str) 93 case 'cyan': 94 return type === 'foreground' ? chalk.cyan(str) : chalk.bgCyan(str) 95 case 'white': 96 return type === 'foreground' ? chalk.white(str) : chalk.bgWhite(str) 97 case 'blackBright': 98 return type === 'foreground' 99 ? chalk.blackBright(str) 100 : chalk.bgBlackBright(str) 101 case 'redBright': 102 return type === 'foreground' 103 ? chalk.redBright(str) 104 : chalk.bgRedBright(str) 105 case 'greenBright': 106 return type === 'foreground' 107 ? chalk.greenBright(str) 108 : chalk.bgGreenBright(str) 109 case 'yellowBright': 110 return type === 'foreground' 111 ? chalk.yellowBright(str) 112 : chalk.bgYellowBright(str) 113 case 'blueBright': 114 return type === 'foreground' 115 ? chalk.blueBright(str) 116 : chalk.bgBlueBright(str) 117 case 'magentaBright': 118 return type === 'foreground' 119 ? chalk.magentaBright(str) 120 : chalk.bgMagentaBright(str) 121 case 'cyanBright': 122 return type === 'foreground' 123 ? chalk.cyanBright(str) 124 : chalk.bgCyanBright(str) 125 case 'whiteBright': 126 return type === 'foreground' 127 ? chalk.whiteBright(str) 128 : chalk.bgWhiteBright(str) 129 } 130 } 131 132 if (color.startsWith('#')) { 133 return type === 'foreground' 134 ? chalk.hex(color)(str) 135 : chalk.bgHex(color)(str) 136 } 137 138 if (color.startsWith('ansi256')) { 139 const matches = ANSI_REGEX.exec(color) 140 141 if (!matches) { 142 return str 143 } 144 145 const value = Number(matches[1]) 146 147 return type === 'foreground' 148 ? chalk.ansi256(value)(str) 149 : chalk.bgAnsi256(value)(str) 150 } 151 152 if (color.startsWith('rgb')) { 153 const matches = RGB_REGEX.exec(color) 154 155 if (!matches) { 156 return str 157 } 158 159 const firstValue = Number(matches[1]) 160 const secondValue = Number(matches[2]) 161 const thirdValue = Number(matches[3]) 162 163 return type === 'foreground' 164 ? chalk.rgb(firstValue, secondValue, thirdValue)(str) 165 : chalk.bgRgb(firstValue, secondValue, thirdValue)(str) 166 } 167 168 return str 169} 170 171/** 172 * Apply TextStyles to a string using chalk. 173 * This is the inverse of parsing ANSI codes - we generate them from structured styles. 174 * Theme resolution happens at component layer, not here. 175 */ 176export function applyTextStyles(text: string, styles: TextStyles): string { 177 let result = text 178 179 // Apply styles in reverse order of desired nesting. 180 // chalk wraps text so later calls become outer wrappers. 181 // Desired order (outermost to innermost): 182 // background > foreground > text modifiers 183 // So we apply: text modifiers first, then foreground, then background last. 184 185 if (styles.inverse) { 186 result = chalk.inverse(result) 187 } 188 189 if (styles.strikethrough) { 190 result = chalk.strikethrough(result) 191 } 192 193 if (styles.underline) { 194 result = chalk.underline(result) 195 } 196 197 if (styles.italic) { 198 result = chalk.italic(result) 199 } 200 201 if (styles.bold) { 202 result = chalk.bold(result) 203 } 204 205 if (styles.dim) { 206 result = chalk.dim(result) 207 } 208 209 if (styles.color) { 210 // Color is now always a raw color value (theme resolution happens at component layer) 211 result = colorize(result, styles.color, 'foreground') 212 } 213 214 if (styles.backgroundColor) { 215 // backgroundColor is now always a raw color value 216 result = colorize(result, styles.backgroundColor, 'background') 217 } 218 219 return result 220} 221 222/** 223 * Apply a raw color value to text. 224 * Theme resolution should happen at component layer, not here. 225 */ 226export function applyColor(text: string, color: Color | undefined): string { 227 if (!color) { 228 return text 229 } 230 return colorize(text, color, 'foreground') 231}