Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
206 lines
6.6 KiB
TypeScript
206 lines
6.6 KiB
TypeScript
export type SupportedPlatform = 'darwin' | 'win32' | 'linux' | (string & {});
|
|
|
|
// True monospace CJK fonts only. Proportional fonts (PingFang SC,
|
|
// Microsoft YaHei UI, Hiragino Sans GB) render at non-2x widths in a
|
|
// terminal grid — including them here visibly broke alignment for users
|
|
// whose primary font lacked CJK glyphs. They are intentionally absent.
|
|
const CJK_SYSTEM_FALLBACK_FONTS = [
|
|
'"Sarasa Mono SC"',
|
|
'"Sarasa Mono TC"',
|
|
'"Maple Mono CN"',
|
|
'"LXGW WenKai Mono"',
|
|
'"Noto Sans Mono CJK SC"',
|
|
'"Source Han Mono SC"',
|
|
'"NSimSun"',
|
|
'"SimSun"',
|
|
];
|
|
|
|
export const CJK_SYSTEM_FALLBACK_STACK = CJK_SYSTEM_FALLBACK_FONTS.join(', ');
|
|
|
|
const NERD_FONT_FALLBACK_FONTS = [
|
|
'"Symbols Nerd Font Mono"',
|
|
'"Symbols Nerd Font"',
|
|
];
|
|
|
|
// Per-OS default CJK font when user hasn't explicitly set fallbackFont
|
|
// AND the current Latin font has no recommended pairing.
|
|
// All choices are TRUE monospace fonts that keep the terminal grid
|
|
// aligned. macOS has no system-installed monospace CJK font, so we
|
|
// reference Sarasa Mono SC which netcatty bundles as a webfont.
|
|
export function getDefaultCjkFallback(platform: SupportedPlatform): string {
|
|
if (platform === 'win32') return 'SimSun';
|
|
if (platform === 'darwin') return 'Sarasa Mono SC';
|
|
return 'Noto Sans Mono CJK SC';
|
|
}
|
|
|
|
// Every entry must point at a TRUE monospace CJK font. Sarasa Mono SC
|
|
// is the safest universal choice because netcatty bundles it via
|
|
// @font-face, so it works even on machines without other CJK monospace
|
|
// fonts installed.
|
|
const PER_FONT_CJK_PAIRING: Record<string, string> = {
|
|
'fira-code': 'Sarasa Mono SC',
|
|
'fira-mono': 'Sarasa Mono SC',
|
|
'jetbrains-mono': 'Sarasa Mono SC',
|
|
'cascadia-code': 'Sarasa Mono SC',
|
|
'cascadia-mono': 'Sarasa Mono SC',
|
|
'source-code-pro': 'Source Han Mono SC',
|
|
'ibm-plex-mono': 'Sarasa Mono SC',
|
|
'iosevka': 'Sarasa Mono SC',
|
|
'ioskeley-mono': 'Sarasa Mono SC',
|
|
'mononoki': 'Sarasa Mono SC',
|
|
'menlo': 'Sarasa Mono SC',
|
|
'monaco': 'Sarasa Mono SC',
|
|
'consolas': 'Sarasa Mono SC',
|
|
'courier-new': 'Sarasa Mono SC',
|
|
'dejavu-sans-mono':'Noto Sans Mono CJK SC',
|
|
'liberation-mono': 'Noto Sans Mono CJK SC',
|
|
'inconsolata': 'Noto Sans Mono CJK SC',
|
|
'victor-mono': 'Sarasa Mono SC',
|
|
'roboto-mono': 'Noto Sans Mono CJK SC',
|
|
'space-mono': 'Sarasa Mono SC',
|
|
'hack': 'Sarasa Mono SC',
|
|
'ubuntu-mono': 'Noto Sans Mono CJK SC',
|
|
'go-mono': 'Sarasa Mono SC',
|
|
};
|
|
|
|
export function getRecommendedCjkFor(
|
|
latinFontId: string,
|
|
platform: SupportedPlatform,
|
|
): string | null {
|
|
void platform;
|
|
return PER_FONT_CJK_PAIRING[latinFontId] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Split a CSS font-family list on commas that are OUTSIDE quoted family
|
|
* names. CSS permits commas inside quoted family names (e.g.
|
|
* `"Foo, Inc. Mono"`); a naive `string.split(',')` would tokenize that
|
|
* into broken pieces like `"Foo` and `Inc. Mono"`. Exported so other
|
|
* font-parsing call sites (extractPrimaryFamily, etc.) share the same
|
|
* rules.
|
|
*/
|
|
export function splitFontFamilyList(css: string): string[] {
|
|
const tokens: string[] = [];
|
|
let buf = '';
|
|
let quote: '"' | "'" | null = null;
|
|
for (let i = 0; i < css.length; i++) {
|
|
const c = css[i];
|
|
if (quote) {
|
|
buf += c;
|
|
if (c === '\\' && i + 1 < css.length) {
|
|
buf += css[++i];
|
|
continue;
|
|
}
|
|
if (c === quote) quote = null;
|
|
continue;
|
|
}
|
|
if (c === '"' || c === "'") {
|
|
buf += c;
|
|
quote = c;
|
|
continue;
|
|
}
|
|
if (c === ',') {
|
|
const trimmed = buf.trim();
|
|
if (trimmed) tokens.push(trimmed);
|
|
buf = '';
|
|
continue;
|
|
}
|
|
buf += c;
|
|
}
|
|
const tail = buf.trim();
|
|
if (tail) tokens.push(tail);
|
|
return tokens;
|
|
}
|
|
|
|
const CSS_GENERIC_FONT_FAMILIES = new Set([
|
|
'serif',
|
|
'sans-serif',
|
|
'monospace',
|
|
'cursive',
|
|
'fantasy',
|
|
'system-ui',
|
|
'ui-serif',
|
|
'ui-sans-serif',
|
|
'ui-monospace',
|
|
'ui-rounded',
|
|
'math',
|
|
'emoji',
|
|
'fangsong',
|
|
]);
|
|
|
|
function quoteIfNeeded(family: string): string {
|
|
const trimmed = family.trim();
|
|
if (!trimmed) return '';
|
|
if (CSS_GENERIC_FONT_FAMILIES.has(trimmed.toLowerCase())) return trimmed;
|
|
if (
|
|
(trimmed.startsWith('"') && trimmed.endsWith('"'))
|
|
|| (trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
) return trimmed;
|
|
const escaped = trimmed.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
return `"${escaped}"`;
|
|
}
|
|
|
|
interface ComposeArgs {
|
|
primaryFamily: string;
|
|
userFallback: string;
|
|
latinFontId: string;
|
|
platform: SupportedPlatform;
|
|
}
|
|
|
|
export function composeFontFamilyStack(args: ComposeArgs): string {
|
|
const { primaryFamily, userFallback, latinFontId, platform } = args;
|
|
|
|
const userFallbackQuoted = userFallback.trim() ? quoteIfNeeded(userFallback) : null;
|
|
|
|
const recommended = userFallbackQuoted
|
|
? null
|
|
: (getRecommendedCjkFor(latinFontId, platform) ?? getDefaultCjkFallback(platform));
|
|
const recommendedQuoted = recommended ? quoteIfNeeded(recommended) : null;
|
|
|
|
const seen = new Set<string>();
|
|
const pieces: string[] = [];
|
|
const push = (item: string | null | undefined) => {
|
|
if (!item) return;
|
|
const key = item.toLowerCase();
|
|
if (seen.has(key)) return;
|
|
seen.add(key);
|
|
pieces.push(item);
|
|
};
|
|
|
|
// Quote-aware split so a family name like `"Foo, Inc. Mono"` keeps
|
|
// its comma intact instead of being shredded into `"Foo` / `Inc. Mono"`.
|
|
for (const p of splitFontFamilyList(primaryFamily)) {
|
|
if (p.toLowerCase() === 'monospace') continue;
|
|
push(p);
|
|
}
|
|
|
|
// Latin-only fallback (bundled via @fontsource/jetbrains-mono in
|
|
// index.tsx). Catches Latin glyphs when the primary font isn't
|
|
// installed without intercepting CJK glyphs the way the bare
|
|
// `monospace` generic would on macOS Chrome (where the generic
|
|
// monospace pulls in PingFang via system CJK fallback, masking the
|
|
// user's CJK font choice).
|
|
//
|
|
// Per-glyph CSS fallback then behaves as intended:
|
|
// - Latin chars: primary (if installed) → JetBrains Mono. Cells
|
|
// stay aligned because JetBrains Mono is true monospace.
|
|
// - CJK chars: primary (no) → JetBrains Mono (no CJK glyphs) →
|
|
// user-chosen CJK font (or per-Latin-font recommendation) →
|
|
// system CJK stack.
|
|
// - Nerd PUA: all of the above (none have PUA) → Nerd Font stack.
|
|
push('"JetBrains Mono"');
|
|
|
|
push(userFallbackQuoted);
|
|
push(recommendedQuoted);
|
|
|
|
for (const sys of CJK_SYSTEM_FALLBACK_FONTS) push(sys);
|
|
for (const nerd of NERD_FONT_FALLBACK_FONTS) push(nerd);
|
|
|
|
// Final safety net only — should rarely be reached because JetBrains
|
|
// Mono covers Latin and the CJK stack covers Chinese glyphs. Kept
|
|
// for the edge case where bundled fonts fail to load.
|
|
push('monospace');
|
|
|
|
return pieces.join(', ');
|
|
}
|