feat: bundled Nerd Font symbols fallback + per-device terminal font setting

Shell prompts using Nerd Font glyphs (powerline, p10k/starship folder and
git icons) rendered as missing-glyph boxes: the built-in xterm stack has no
private-use-area symbols, and phones have no Nerd Fonts installed at all.

- Bundle Symbols Nerd Font Mono (icons-only, MIT, 1.2MB woff2) served from
  fonts/ and appended to the terminal stack before monospace — browsers fall
  back per glyph, so icons render everywhere while text stays in the text
  fonts. font-display: block + preload keep tofu out of xterm's glyph atlas.
- New per-device terminalFontFamily setting (App Settings > Terminal &
  Input > Font): prepended to the built-in stack, never a replacement, so
  the symbols fallback and final monospace always survive. Applied live on
  save (refit + echo-overlay refreshFont, mirroring setFontSize).
- Single source for both xterm surfaces: TERMINAL_FONT_DEFAULT_STACK +
  resolveTerminalFontFamily() in constants.js, unit-tested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjnbbZRBuvR5E3SDouwXr9
This commit is contained in:
Tobias Weber
2026-08-19 00:45:19 +02:00
co-authored by Claude Fable 5
parent 09bf00c815
commit 108c00e78d
10 changed files with 176 additions and 2 deletions
+17 -1
View File
@@ -225,7 +225,7 @@ Object.assign(CodemanApp.prototype, {
this.terminal = new Terminal({
theme: { ...window.codemanCurrentXtermTheme() },
fontFamily: '"Fira Code", "Cascadia Code", "JetBrains Mono", "SF Mono", Monaco, monospace',
fontFamily: window.CodemanTerminalFont.resolve(this.loadAppSettingsFromStorage?.().terminalFontFamily),
// Use smaller font on mobile to fit more columns (prevents wrapping of Claude's status line)
fontSize: MobileDetection.getDeviceType() === 'mobile' ? 10 : 14,
lineHeight: 1.2,
@@ -4122,6 +4122,22 @@ Object.assign(CodemanApp.prototype, {
this._predictiveEcho?.refreshFont();
},
/**
* Apply the per-device `terminalFontFamily` setting to the live terminal.
* The custom family is resolved against the built-in stack (constants.js),
* so passing '' / undefined restores the default. Mirrors setFontSize():
* refit for the new cell metrics, then refresh the echo overlays' cached
* font so predictions keep landing on the right cells.
*/
applyTerminalFontFamily(custom) {
const resolved = window.CodemanTerminalFont.resolve(custom);
if (!this.terminal || this.terminal.options.fontFamily === resolved) return;
this.terminal.options.fontFamily = resolved;
this.fitAddon?.fit();
this._localEchoOverlay?.refreshFont();
this._predictiveEcho?.refreshFont();
},
loadFontSize() {
const saved = localStorage.getItem('codeman-font-size');
if (saved) {