Files
codegraph/src/ui/glyphs.ts
T
30421953ac fix(ui): consistent frame glyphs on Windows — agree with clack, keep raw path ASCII (#1307)
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>
2026-07-16 14:52:11 -05:00

139 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
}