codegraph's glyphs were ASCII on every Windows console while @clack/prompts drew its Unicode frame around them, so one index block mixed `|` and `│` rails (#398). supportsUnicode() now mirrors the is-unicode-supported detection clack bundles (Windows Terminal, VS Code, ConEmu/Cmder, Alacritty, xterm-256color, JetBrains, CI), so both systems always pick the same glyph family. The shimmer worker's raw fs.writeSync(1) bytes still decode through the console codepage (OEM codepages mojibake UTF-8 even under Windows Terminal — the #168 regression to avoid), so: - the raw path gets its own supportsUnicodeRawWrites() that stays ASCII on win32 unless CODEGRAPH_UNICODE=1, and - the persistent "phase done" lines move from the worker to the parent, written via process.stdout (wide-char console API, codepage-immune) at phase transitions — the main thread is alive there, it's delivering the progress callback. Only transient, self-erasing animation frames remain on the raw path, so ASCII never lands in scrollback. Fixes #398 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
139 lines
4.5 KiB
TypeScript
139 lines
4.5 KiB
TypeScript
/**
|
||
* Glyph selection for CLI output.
|
||
*
|
||
* On Windows, console output is interpreted via the active output
|
||
* codepage. PowerShell 5.1 and cmd.exe in legacy conhost default to
|
||
* OEM codepages (CP437, CP936, ...), so UTF-8 bytes written to the
|
||
* console render as mojibake (see #168). The shimmer worker is hit
|
||
* hardest because it uses `fs.writeSync(1, ...)` (raw bytes, no
|
||
* TTY-aware encoding conversion) to keep animation smooth while the
|
||
* main thread is blocked in SQLite. To stay readable everywhere, we
|
||
* fall back to ASCII glyphs whenever the terminal is not known to
|
||
* handle UTF-8.
|
||
*
|
||
* The Windows branch must agree with @clack/prompts (which bundles
|
||
* `is-unicode-supported`): clack draws the outer `┌ │ └` frame around
|
||
* init/index/sync output, and if it decides Unicode while we decide
|
||
* ASCII, one block mixes `│` and `|` rails (#398). The terminals the
|
||
* list recognizes (Windows Terminal, VS Code, ConEmu/Cmder, Alacritty,
|
||
* JetBrains, Terminus, CI log viewers) all run with a UTF-8-capable
|
||
* output path, so the raw-byte shimmer writes render correctly there
|
||
* too; unrecognized Windows consoles keep the safe ASCII fallback —
|
||
* and clack falls back to ASCII in those as well, so output stays
|
||
* consistent in both directions.
|
||
*
|
||
* Detection:
|
||
* - `CODEGRAPH_ASCII=1` -> ASCII (escape hatch for any terminal)
|
||
* - `CODEGRAPH_UNICODE=1` -> Unicode (opt-in on any terminal)
|
||
* - Windows -> mirror is-unicode-supported (see above)
|
||
* - Linux kernel console (`TERM=linux`) -> ASCII
|
||
* - Everything else -> Unicode
|
||
*/
|
||
|
||
export function supportsUnicode(): boolean {
|
||
if (process.env.CODEGRAPH_ASCII === '1') return false;
|
||
if (process.env.CODEGRAPH_UNICODE === '1') return true;
|
||
if (process.platform === 'win32') {
|
||
const env = process.env;
|
||
return Boolean(
|
||
env.CI ||
|
||
env.WT_SESSION || // Windows Terminal
|
||
env.TERMINUS_SUBLIME ||
|
||
env.ConEmuTask === '{cmd::Cmder}' || // ConEmu and cmder
|
||
env.TERM_PROGRAM === 'Terminus-Sublime' ||
|
||
env.TERM_PROGRAM === 'vscode' ||
|
||
env.TERM === 'xterm-256color' ||
|
||
env.TERM === 'alacritty' ||
|
||
env.TERMINAL_EMULATOR === 'JetBrains-JediTerm'
|
||
);
|
||
}
|
||
return process.env.TERM !== 'linux';
|
||
}
|
||
|
||
export interface Glyphs {
|
||
ok: string;
|
||
err: string;
|
||
info: string;
|
||
warn: string;
|
||
spinner: string[];
|
||
barFilled: string;
|
||
barEmpty: string;
|
||
rail: string;
|
||
phaseDone: string;
|
||
dash: string;
|
||
hLine: string;
|
||
treeBranch: string;
|
||
treeLast: string;
|
||
treePipe: string;
|
||
}
|
||
|
||
export const UNICODE_GLYPHS: Glyphs = {
|
||
ok: '✓',
|
||
err: '✗',
|
||
info: 'ℹ',
|
||
warn: '⚠',
|
||
spinner: ['·', '✢', '✳', '✶', '✻', '✽'],
|
||
barFilled: '█',
|
||
barEmpty: '░',
|
||
rail: '│',
|
||
phaseDone: '◆',
|
||
dash: '—',
|
||
hLine: '─',
|
||
treeBranch: '├── ',
|
||
treeLast: '└── ',
|
||
treePipe: '│ ',
|
||
};
|
||
|
||
export const ASCII_GLYPHS: Glyphs = {
|
||
ok: '[OK]',
|
||
err: '[ERR]',
|
||
info: '[i]',
|
||
warn: '[!]',
|
||
spinner: ['.', '*', '+', 'x', 'o', 'O'],
|
||
barFilled: '#',
|
||
barEmpty: '-',
|
||
rail: '|',
|
||
phaseDone: '*',
|
||
dash: '-',
|
||
hLine: '-',
|
||
treeBranch: '|-- ',
|
||
treeLast: '`-- ',
|
||
treePipe: '| ',
|
||
};
|
||
|
||
let cached: Glyphs | null = null;
|
||
|
||
export function getGlyphs(): Glyphs {
|
||
if (cached === null) {
|
||
cached = supportsUnicode() ? UNICODE_GLYPHS : ASCII_GLYPHS;
|
||
}
|
||
return cached;
|
||
}
|
||
|
||
/**
|
||
* Unicode support for the RAW console write path — `fs.writeSync(1, ...)`,
|
||
* used only by the shimmer worker's transient animation frames. Raw bytes
|
||
* bypass Node's TTY-aware conversion and get decoded by the ACTIVE CONSOLE
|
||
* CODEPAGE on Windows; OEM codepages (CP437, CP936, ...) mojibake UTF-8
|
||
* there even inside Windows Terminal, whose ConPTY still decodes app output
|
||
* with the session codepage (#168). So the raw path stays ASCII on every
|
||
* Windows terminal unless the user opts in via CODEGRAPH_UNICODE=1 —
|
||
* independent of `supportsUnicode()`, which governs the codepage-immune
|
||
* main-thread writes (`process.stdout` uses the wide-char console API).
|
||
*/
|
||
export function supportsUnicodeRawWrites(): boolean {
|
||
if (process.env.CODEGRAPH_ASCII === '1') return false;
|
||
if (process.env.CODEGRAPH_UNICODE === '1') return true;
|
||
if (process.platform === 'win32') return false;
|
||
return process.env.TERM !== 'linux';
|
||
}
|
||
|
||
export function getRawWriteGlyphs(): Glyphs {
|
||
return supportsUnicodeRawWrites() ? UNICODE_GLYPHS : ASCII_GLYPHS;
|
||
}
|
||
|
||
/** Reset the cached glyph set. Test-only; production code should call `getGlyphs()`. */
|
||
export function _resetGlyphsCache(): void {
|
||
cached = null;
|
||
}
|