Files
NetMesh/infrastructure/config/fonts.ts

179 lines
11 KiB
TypeScript
Raw Normal View History

/**
* Terminal Fonts Configuration
*
* `family` is the raw CSS font-family string for the Latin glyphs only.
* CJK and icon fallbacks are composed at runtime by composeFontFamilyStack()
* in cjkFonts.ts, which lets users pick the CJK font independently or have
* one chosen automatically per Latin font.
*/
export interface TerminalFont {
id: string;
name: string;
family: string;
description: string;
category: 'monospace' | 'proportional';
}
const BASE_TERMINAL_FONTS: TerminalFont[] = [
// Existing Latin monospace fonts (ids unchanged for sync compatibility)
{ id: 'menlo', name: 'Menlo', family: 'Menlo, monospace', description: 'macOS system font, clean and professional', category: 'monospace' },
{ id: 'monaco', name: 'Monaco', family: 'Monaco, monospace', description: 'Classic monospace, excellent readability', category: 'monospace' },
{ id: 'consolas', name: 'Consolas', family: 'Consolas, monospace', description: 'Windows-style monospace, clear and compact', category: 'monospace' },
{ id: 'courier-new', name: 'Courier New', family: '"Courier New", monospace', description: 'Classic typewriter style, universal support', category: 'monospace' },
{ id: 'source-code-pro', name: 'Source Code Pro', family: '"Source Code Pro", monospace', description: "Adobe's professional programming font", category: 'monospace' },
{ id: 'fira-code', name: 'Fira Code', family: '"Fira Code", monospace', description: 'Monospace font with programming ligatures', category: 'monospace' },
{ id: 'fira-mono', name: 'Fira Mono', family: '"Fira Mono", monospace', description: 'Clean monospace without ligatures', category: 'monospace' },
{ id: 'inconsolata', name: 'Inconsolata', family: 'Inconsolata, monospace', description: 'Elegant and readable monospace font', category: 'monospace' },
{ id: 'dejavu-sans-mono', name: 'DejaVu Sans Mono', family: '"DejaVu Sans Mono", monospace', description: 'Wide character support, very readable', category: 'monospace' },
{ id: 'liberation-mono', name: 'Liberation Mono', family: '"Liberation Mono", monospace', description: 'Open source monospace font, Courier alternative', category: 'monospace' },
{ id: 'jetbrains-mono', name: 'JetBrains Mono', family: '"JetBrains Mono", monospace', description: 'Professional font designed for IDEs', category: 'monospace' },
{ id: 'victor-mono', name: 'Victor Mono', family: '"Victor Mono", monospace', description: 'Stylish monospace with italic support', category: 'monospace' },
{ id: 'cascadia-code', name: 'Cascadia Code', family: '"Cascadia Code", monospace', description: "Microsoft's modern monospace font", category: 'monospace' },
{ id: 'cascadia-mono', name: 'Cascadia Mono', family: '"Cascadia Mono", monospace', description: 'Cascadia without ligatures', category: 'monospace' },
{ id: 'droid-sans-mono', name: 'Droid Sans Mono', family: '"Droid Sans Mono", monospace', description: "Google's Droid monospace font", category: 'monospace' },
{ id: 'ubuntu-mono', name: 'Ubuntu Mono', family: '"Ubuntu Mono", monospace', description: "Ubuntu's official monospace font", category: 'monospace' },
{ id: 'roboto-mono', name: 'Roboto Mono', family: '"Roboto Mono", monospace', description: "Google's Roboto monospace variant", category: 'monospace' },
{ id: 'ibm-plex-mono', name: 'IBM Plex Mono', family: '"IBM Plex Mono", monospace', description: "IBM's professional monospace font", category: 'monospace' },
{ id: 'space-mono', name: 'Space Mono', family: '"Space Mono", monospace', description: 'Geometric monospace with strong personality', category: 'monospace' },
{ id: 'input-mono', name: 'Input Mono', family: '"Input Mono", monospace', description: 'Designed specifically for coding', category: 'monospace' },
{ id: 'hack', name: 'Hack', family: 'Hack, monospace', description: 'Designed for source code, excellent in terminals', category: 'monospace' },
{ id: 'anonymous-pro', name: 'Anonymous Pro', family: '"Anonymous Pro", monospace', description: 'Designed for coding and terminal use', category: 'monospace' },
{ id: 'programmer-fonts', name: 'Programmer Fonts', family: '"Programmer Fonts", monospace', description: 'Optimized for programming with clear glyphs', category: 'monospace' },
{ id: 'pt-mono', name: 'PT Mono', family: '"PT Mono", monospace', description: "ParaType's monospace font", category: 'monospace' },
{ id: 'iosevka', name: 'Iosevka', family: 'Iosevka, monospace', description: 'Highly customizable monospace font', category: 'monospace' },
{ id: 'ioskeley-mono', name: 'Ioskeley Mono', family: '"Ioskeley Mono", monospace', description: 'Iosevka variant mimicking Berkeley Mono style', category: 'monospace' },
{ id: 'mononoki', name: 'Mononoki', family: 'Mononoki, monospace', description: 'Crisp and clear monospace with ligatures', category: 'monospace' },
{ id: 'go-mono', name: 'Go Mono', family: '"Go Mono", monospace', description: "Google Go's monospace font", category: 'monospace' },
{ id: 'overpass-mono', name: 'Overpass Mono', family: '"Overpass Mono", monospace', description: 'Open source monospace with good coverage', category: 'monospace' },
// True monospace CJK-coverage fonts only. PingFang SC and Microsoft
// YaHei UI (the OS system fonts) are deliberately omitted — they are
// proportional sans-serif designs whose Latin glyphs render with
// variable widths and whose CJK glyphs don't fit a terminal's 2x cell
// grid. Picking one as the primary font produced visibly bloated
// spacing for ASCII characters in #931.
{ id: 'sarasa-mono-sc', name: 'Sarasa Mono SC', family: '"Sarasa Mono SC", monospace', description: 'Iosevka + Source Han Sans (Simplified Chinese), 2:1 monospace', category: 'monospace' },
{ id: 'sarasa-mono-tc', name: 'Sarasa Mono TC', family: '"Sarasa Mono TC", monospace', description: 'Iosevka + Source Han Sans (Traditional Chinese), 2:1 monospace', category: 'monospace' },
{ id: 'maple-mono-cn', name: 'Maple Mono CN', family: '"Maple Mono CN", monospace', description: 'Maple Mono with unified Latin + Simplified Chinese metrics', category: 'monospace' },
{ id: 'lxgw-wenkai-mono', name: 'LXGW WenKai Mono', family: '"LXGW WenKai Mono", monospace', description: 'Monospace Kaishu (regular-script) derived from Fontworks Klee One', category: 'monospace' },
];
export const TERMINAL_FONTS: TerminalFont[] = BASE_TERMINAL_FONTS;
export const DEFAULT_FONT_SIZE = 14;
export const MIN_FONT_SIZE = 10;
export const MAX_FONT_SIZE = 32;
export type FontPlatform = 'darwin' | 'win32' | 'linux' | (string & {});
/**
* The default terminal font id for a given OS. Chosen so the primary
* Latin glyphs render with a font that ships with the OS avoiding the
* cold-start cell-misalignment in #1647, where a bundled webfont
* (JetBrains Mono, font-display: swap) swapped in *after* xterm measured
* the cell grid and left ASCII garbled until a manual window resize.
*
* - darwin: Menlo is a macOS system font (instant, no swap).
* - win32: Consolas ships with every Windows since Vista.
* - linux: DejaVu Sans Mono is the most widely pre-installed Linux
* monospace; if a given distro lacks it, the runtime font-ready
* remeasure (useTerminalEffects) still recovers any swap.
*
* Unknown platforms fall back to the macOS default.
*/
export function getDefaultTerminalFontIdForPlatform(platform: FontPlatform): string {
if (platform === 'win32') return 'consolas';
if (platform === 'linux') return 'dejavu-sans-mono';
return 'menlo';
}
/**
* Normalize a `navigator.platform` string to the coarse OS buckets used
* by getDefaultTerminalFontIdForPlatform. Mirrors the detection already
* used for the CJK fallback stack in Terminal.tsx (Mac/Win regex on
* navigator.platform); anything else including Linux is `linux`.
*/
export function detectFontPlatform(navigatorPlatform: string): FontPlatform {
if (/Win/i.test(navigatorPlatform)) return 'win32';
if (/Mac|iPhone|iPad|iPod/i.test(navigatorPlatform)) return 'darwin';
return 'linux';
}
/**
* The stored sentinel meaning "follow the OS default font". It is
* persisted and cloud-synced as-is (platform-neutral), so each device
* resolves it to its own locally-installed default at render time
* unlike a concrete platform id, which would leak one OS's font to
* another via sync and reintroduce #1647 on the receiving machine.
*/
export const TERMINAL_FONT_AUTO = 'auto';
/**
* Resolve a stored terminal font id to a concrete font id. The `auto`
* sentinel (and any empty/nullish value) resolves to the per-platform
* default; an explicit user choice is returned unchanged.
*/
export function resolveTerminalFontFamilyId(
id: string | null | undefined,
navigatorPlatform: string,
): string {
if (id && id !== TERMINAL_FONT_AUTO) return id;
return getDefaultTerminalFontIdForPlatform(detectFontPlatform(navigatorPlatform));
}
/**
* Whether two terminal font ids resolve to the same concrete font on this
* platform. Font pickers display the resolved concrete font for an `auto`
* value, so a click on the shown default arrives as that concrete id while
* the stored value is still `auto`; comparing raw would treat it as a
* change and pin a per-OS font (which then syncs across devices). Comparing
* resolved ids makes that click a correct no-op.
*/
export function isSameResolvedTerminalFont(
a: string | null | undefined,
b: string | null | undefined,
navigatorPlatform: string,
): boolean {
return (
resolveTerminalFontFamilyId(a, navigatorPlatform) ===
resolveTerminalFontFamilyId(b, navigatorPlatform)
);
}
// Font ids that earlier versions of netcatty exposed in the primary font
// dropdown but that are proportional (non-monospace) and produce broken
// cell-grid alignment when used as a terminal font. Reads should migrate
// these to a sane default.
const DEPRECATED_PRIMARY_FONT_IDS = new Set<string>([
'pingfang-sc',
'microsoft-yahei',
'comic-sans-ms',
]);
export function isDeprecatedPrimaryFontId(fontId: string | null | undefined): boolean {
return !!fontId && DEPRECATED_PRIMARY_FONT_IDS.has(fontId);
}
/**
* In-place migration for any object carrying `fontFamily` /
* `fontFamilyOverride` (Host, GroupConfig). When the saved id is one
* we've since removed from TERMINAL_FONTS, drop the override so the
* record inherits the global default rather than silently rendering
* "fallback to fonts[0]" while still claiming an override is active.
*
* Returns the (possibly new) value to assign back. Caller decides
* whether to mutate or copy; both are safe with this shape.
*/
export function migrateDeprecatedFontOverride<
T extends { fontFamily?: string; fontFamilyOverride?: boolean },
>(record: T): T {
if (!isDeprecatedPrimaryFontId(record.fontFamily)) return record;
const next = { ...record };
delete next.fontFamily;
if (next.fontFamilyOverride === true) {
next.fontFamilyOverride = false;
}
return next;
}