glyphs.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. /**
  2. * Glyph selection for CLI output.
  3. *
  4. * On Windows, console output is interpreted via the active output
  5. * codepage. PowerShell 5.1 and cmd.exe in legacy conhost default to
  6. * OEM codepages (CP437, CP936, ...), so UTF-8 bytes written to the
  7. * console render as mojibake (see #168). The shimmer worker is hit
  8. * hardest because it uses `fs.writeSync(1, ...)` (raw bytes, no
  9. * TTY-aware encoding conversion) to keep animation smooth while the
  10. * main thread is blocked in SQLite. To stay readable everywhere, we
  11. * fall back to ASCII glyphs whenever the terminal is not known to
  12. * handle UTF-8.
  13. *
  14. * The Windows branch must agree with @clack/prompts (which bundles
  15. * `is-unicode-supported`): clack draws the outer `┌ │ └` frame around
  16. * init/index/sync output, and if it decides Unicode while we decide
  17. * ASCII, one block mixes `│` and `|` rails (#398). The terminals the
  18. * list recognizes (Windows Terminal, VS Code, ConEmu/Cmder, Alacritty,
  19. * JetBrains, Terminus, CI log viewers) all run with a UTF-8-capable
  20. * output path, so the raw-byte shimmer writes render correctly there
  21. * too; unrecognized Windows consoles keep the safe ASCII fallback —
  22. * and clack falls back to ASCII in those as well, so output stays
  23. * consistent in both directions.
  24. *
  25. * Detection:
  26. * - `CODEGRAPH_ASCII=1` -> ASCII (escape hatch for any terminal)
  27. * - `CODEGRAPH_UNICODE=1` -> Unicode (opt-in on any terminal)
  28. * - Windows -> mirror is-unicode-supported (see above)
  29. * - Linux kernel console (`TERM=linux`) -> ASCII
  30. * - Everything else -> Unicode
  31. */
  32. export function supportsUnicode(): boolean {
  33. if (process.env.CODEGRAPH_ASCII === '1') return false;
  34. if (process.env.CODEGRAPH_UNICODE === '1') return true;
  35. if (process.platform === 'win32') {
  36. const env = process.env;
  37. return Boolean(
  38. env.CI ||
  39. env.WT_SESSION || // Windows Terminal
  40. env.TERMINUS_SUBLIME ||
  41. env.ConEmuTask === '{cmd::Cmder}' || // ConEmu and cmder
  42. env.TERM_PROGRAM === 'Terminus-Sublime' ||
  43. env.TERM_PROGRAM === 'vscode' ||
  44. env.TERM === 'xterm-256color' ||
  45. env.TERM === 'alacritty' ||
  46. env.TERMINAL_EMULATOR === 'JetBrains-JediTerm'
  47. );
  48. }
  49. return process.env.TERM !== 'linux';
  50. }
  51. export interface Glyphs {
  52. ok: string;
  53. err: string;
  54. info: string;
  55. warn: string;
  56. spinner: string[];
  57. barFilled: string;
  58. barEmpty: string;
  59. rail: string;
  60. phaseDone: string;
  61. dash: string;
  62. hLine: string;
  63. treeBranch: string;
  64. treeLast: string;
  65. treePipe: string;
  66. }
  67. export const UNICODE_GLYPHS: Glyphs = {
  68. ok: '✓',
  69. err: '✗',
  70. info: 'ℹ',
  71. warn: '⚠',
  72. spinner: ['·', '✢', '✳', '✶', '✻', '✽'],
  73. barFilled: '█',
  74. barEmpty: '░',
  75. rail: '│',
  76. phaseDone: '◆',
  77. dash: '—',
  78. hLine: '─',
  79. treeBranch: '├── ',
  80. treeLast: '└── ',
  81. treePipe: '│ ',
  82. };
  83. export const ASCII_GLYPHS: Glyphs = {
  84. ok: '[OK]',
  85. err: '[ERR]',
  86. info: '[i]',
  87. warn: '[!]',
  88. spinner: ['.', '*', '+', 'x', 'o', 'O'],
  89. barFilled: '#',
  90. barEmpty: '-',
  91. rail: '|',
  92. phaseDone: '*',
  93. dash: '-',
  94. hLine: '-',
  95. treeBranch: '|-- ',
  96. treeLast: '`-- ',
  97. treePipe: '| ',
  98. };
  99. let cached: Glyphs | null = null;
  100. export function getGlyphs(): Glyphs {
  101. if (cached === null) {
  102. cached = supportsUnicode() ? UNICODE_GLYPHS : ASCII_GLYPHS;
  103. }
  104. return cached;
  105. }
  106. /**
  107. * Unicode support for the RAW console write path — `fs.writeSync(1, ...)`,
  108. * used only by the shimmer worker's transient animation frames. Raw bytes
  109. * bypass Node's TTY-aware conversion and get decoded by the ACTIVE CONSOLE
  110. * CODEPAGE on Windows; OEM codepages (CP437, CP936, ...) mojibake UTF-8
  111. * there even inside Windows Terminal, whose ConPTY still decodes app output
  112. * with the session codepage (#168). So the raw path stays ASCII on every
  113. * Windows terminal unless the user opts in via CODEGRAPH_UNICODE=1 —
  114. * independent of `supportsUnicode()`, which governs the codepage-immune
  115. * main-thread writes (`process.stdout` uses the wide-char console API).
  116. */
  117. export function supportsUnicodeRawWrites(): boolean {
  118. if (process.env.CODEGRAPH_ASCII === '1') return false;
  119. if (process.env.CODEGRAPH_UNICODE === '1') return true;
  120. if (process.platform === 'win32') return false;
  121. return process.env.TERM !== 'linux';
  122. }
  123. export function getRawWriteGlyphs(): Glyphs {
  124. return supportsUnicodeRawWrites() ? UNICODE_GLYPHS : ASCII_GLYPHS;
  125. }
  126. /** Reset the cached glyph set. Test-only; production code should call `getGlyphs()`. */
  127. export function _resetGlyphsCache(): void {
  128. cached = null;
  129. }