FN-6659: keep xterm font metrics symbols-free
Keep terminal wide-glyph fallback out of xterm's measured font path while preserving Nerd Font symbols in DOM rendering. - Split terminal font resolution into symbols-free xterm font families and scoped glyph fallback families. - Apply the glyph fallback CSS variable to both TerminalModal and SessionTerminal surfaces. - Update regression coverage and the documented mobile xterm wide-glyph recurrence analysis. Files changed: .changeset/fn-6659-terminal-render.md | 5 +++ .../xterm-symbols-nerd-font-unicode-range.md | 22 +++++++----- .../dashboard/app/__tests__/terminal-input.test.ts | 42 +++++++++++++--------- .../dashboard/app/components/SessionTerminal.css | 8 +++++ .../dashboard/app/components/SessionTerminal.tsx | 21 +++++++++-- .../dashboard/app/components/TerminalModal.css | 8 +++++ .../dashboard/app/components/TerminalModal.tsx | 13 ++++++- .../__tests__/SessionTerminal.mobile.test.tsx | 6 ++-- .../components/__tests__/SessionTerminal.test.tsx | 14 ++++---- .../components/__tests__/TerminalModal.test.tsx | 9 +++-- .../utils/__tests__/terminalPreferences.test.ts | 2 +- .../dashboard/app/utils/terminalPreferences.ts | 16 +++++---- 12 files changed, 117 insertions(+), 49 deletions(-) Fusion-Task-Id: FN-6659 Fusion-Task-Lineage: e29a709d-9d9d-4a90-9616-8f08c41b126d
This commit is contained in:
5
.changeset/fn-6659-terminal-render.md
Normal file
5
.changeset/fn-6659-terminal-render.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Fix mobile terminal font measurement by keeping the symbols-only Nerd Font out of xterm's measured ASCII font stack while retaining a scoped DOM glyph fallback.
|
||||
@@ -10,7 +10,7 @@ symptoms:
|
||||
- "Terminal glyphs render with oversized inter-character spacing after the symbols font loads"
|
||||
- "Mobile DOM/canvas xterm output wraps after very few columns even for ASCII commands"
|
||||
- "Powerline prompt glyphs are needed, but ASCII must measure against a real monospace text font"
|
||||
root_cause: symbols_only_font_face_without_unicode_range_or_symbols_first_stack_participated_in_ascii_cell_measurement
|
||||
root_cause: symbols_only_font_face_participated_in_ios_xterm_ascii_cell_measurement_even_when_unicode_range_scoped
|
||||
resolution_type: code_fix
|
||||
severity: high
|
||||
related_components:
|
||||
@@ -21,6 +21,8 @@ related_components:
|
||||
- FN-6390
|
||||
- FN-6424
|
||||
- FN-6603
|
||||
- FN-6638
|
||||
- FN-6659
|
||||
tags:
|
||||
- xterm
|
||||
- font-loading
|
||||
@@ -34,16 +36,19 @@ tags:
|
||||
|
||||
## Problem
|
||||
|
||||
A symbols-only Nerd Font can corrupt xterm.js cell measurement when it appears first in the terminal `fontFamily` stack. FN-6390 correctly added an async post-font-load remeasure, but FN-6424 found the recurrence: the browser could still measure ASCII cells against `SymbolsNerdFontMono` after `font-display: swap`, producing huge gaps such as `p n p m b u i l d` on mobile.
|
||||
A symbols-only Nerd Font can corrupt xterm.js cell measurement when it participates in the terminal `fontFamily` stack. FN-6390 correctly added an async post-font-load remeasure, but FN-6424 found the recurrence: the browser could still measure ASCII cells against `SymbolsNerdFontMono` after `font-display: swap`, producing huge gaps such as `p n p m b u i l d` on mobile.
|
||||
|
||||
FN-6603 found the third recurrence: the FN-6390 remeasure and FN-6424 `unicode-range` were both present, but the shared terminal preference stack still listed the symbols face first. Mobile WebKit/xterm canvas measurement could still use that first face for cell metrics while actual ASCII glyph rendering fell through to a later monospace font. The visible symptom was the same wide-cell layout (`A G E N T S . m d`) with intact powerline glyphs.
|
||||
|
||||
FN-6638 then added a `text-size-adjust: 100%` pin plus best-effort `document.fonts` settlement and unconditional xterm option reapply/fit/refresh. That recurrence's diagnostic measured `66.76px for AGENTS.md` across symbols-first, symbols-last, and system-mono stacks and was initially read as "font-stack ordering is inert." FN-6659 corrected that reading: all three diagnostic stacks were still symbols-inclusive because every preset appended `"Fusion Terminal Nerd Font Symbols"`, and that symbols face was the only bundled/loaded terminal `@font-face`. Playwright/desktop WebKit emulation and the unfinished real-iOS acceptance gate let four blind fixes ship despite the real iOS Safari symptom remaining.
|
||||
|
||||
## Solution
|
||||
|
||||
Keep the symbols font available for powerline/Nerd-Font codepoints, but apply both guards:
|
||||
Keep the symbols font available for powerline/Nerd-Font codepoints, but do not let it participate in xterm's measured `fontFamily` option:
|
||||
|
||||
1. Scope its `@font-face` with `unicode-range` so printable ASCII is never resolved through that family during normal glyph fallback.
|
||||
2. Keep real monospace text faces before the symbols family in every xterm `fontFamily` preset. The symbols family should be a fallback, not the first measurement candidate, because xterm's DOM/canvas metrics path is less reliable than normal DOM text fallback on mobile WebKit.
|
||||
2. Keep `XTERM_FONT_FAMILY` and every terminal preset symbols-free. `TerminalModal` and `SessionTerminal` must pass only real text monospace stacks to `new Terminal(...)`, remeasure, and live-preference updates.
|
||||
3. If a DOM-renderer symbols fallback is needed, attach it through a separate scoped CSS variable/rule for `.xterm-rows span` (for example `--terminal-glyph-font-family`) rather than the xterm option that drives ASCII cell measurement. Do not re-tune ordering: FN-6659 showed symbols-last was still unsafe on real iOS because the symbols face's mere presence polluted the measured shorthand.
|
||||
|
||||
Use the standard Symbols Nerd Font ranges, including powerline and private-use blocks, for example:
|
||||
|
||||
@@ -56,7 +61,7 @@ Use the standard Symbols Nerd Font ranges, including powerline and private-use b
|
||||
}
|
||||
```
|
||||
|
||||
Do not replace this with fixed `letterSpacing`, hardcoded column counts, or by removing the async remeasure. xterm should still refit after web fonts load; the font face and stack ordering together must prevent symbols-only metrics from applying to ASCII.
|
||||
Do not replace this with fixed `letterSpacing`, hardcoded column counts, or by removing the async remeasure. xterm should still refit after web fonts load; the measured xterm font stack must stay symbols-free so symbols-only metrics cannot apply to ASCII on real iOS Safari.
|
||||
|
||||
## Regression coverage
|
||||
|
||||
@@ -65,6 +70,7 @@ Automated jsdom tests cannot validate font advance widths, so cover the enforcea
|
||||
- Parse emitted/app CSS and assert the terminal symbols `@font-face` has a `unicode-range`.
|
||||
- Assert the range contains required Nerd-Font/powerline blocks such as `U+E0A0-E0D7`, `U+E700-E8EF`, and `U+F0001-F1AF0`.
|
||||
- Assert no range overlaps printable ASCII (`U+0020-007E`).
|
||||
- Assert the shared default stack and every terminal font preset place a real text monospace face before `"Fusion Terminal Nerd Font Symbols"`.
|
||||
- Check every xterm consumer: `TerminalModal` and `SessionTerminal` both use `resolveTerminalFontFamily()`, so both are affected by stack ordering and both need component-level coverage that the stack passed to `new Terminal(...)` is measurement-safe.
|
||||
- Verify in a mobile/touch browser path that ASCII output renders tightly while the powerline glyph still renders for the default `nerd-font` and `system-mono` presets.
|
||||
- Assert the shared default stack and every terminal font preset do **not** include `"Fusion Terminal Nerd Font Symbols"` in the xterm-measured family.
|
||||
- Assert the retained symbols-rendering mechanism is separate from xterm measurement (for example CSS rules using `--terminal-glyph-font-family` on DOM row spans).
|
||||
- Check every xterm consumer: `TerminalModal` and `SessionTerminal` both use `resolveTerminalFontFamily()`, so both need component-level coverage that the stack passed to `new Terminal(...)`, remeasure, and live preference updates is symbols-free.
|
||||
- Verify on a real iOS Safari device/cloud path (not Playwright/desktop WebKit emulation) that ASCII output renders tightly while the powerline glyph still renders for the default `nerd-font` and `system-mono` presets on both `TerminalModal` and `SessionTerminal`.
|
||||
|
||||
@@ -26,6 +26,18 @@ function findSessionTerminalTextSizingRule(): string {
|
||||
return match?.[1] ?? "";
|
||||
}
|
||||
|
||||
function findTerminalGlyphFallbackRule(): string {
|
||||
const match = css.match(/\.terminal-xterm\s+\.xterm-rows\s+span\s*\{([^}]*)\}/);
|
||||
return match?.[1] ?? "";
|
||||
}
|
||||
|
||||
function findSessionTerminalGlyphFallbackRule(): string {
|
||||
const match = css.match(
|
||||
/\.cli-session-terminal__viewport\s+\.xterm-rows\s+span\s*\{([^}]*)\}/,
|
||||
);
|
||||
return match?.[1] ?? "";
|
||||
}
|
||||
|
||||
function expectTextSizeAdjustPinned(ruleBody: string): void {
|
||||
expect(ruleBody).not.toBe("");
|
||||
expect(ruleBody).toMatch(/-webkit-text-size-adjust\s*:\s*100%\s*;/);
|
||||
@@ -103,6 +115,11 @@ describe("terminal helper textarea CSS contract", () => {
|
||||
it("pins iOS text-size adjustment on the SessionTerminal xterm viewport", () => {
|
||||
expectTextSizeAdjustPinned(findSessionTerminalTextSizingRule());
|
||||
});
|
||||
|
||||
it("keeps a DOM glyph fallback mechanism outside xterm measurement options", () => {
|
||||
expect(findTerminalGlyphFallbackRule()).toMatch(/--terminal-glyph-font-family/);
|
||||
expect(findSessionTerminalGlyphFallbackRule()).toMatch(/--terminal-glyph-font-family/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("FN-6424 terminal symbols font CSS contract", () => {
|
||||
@@ -118,7 +135,7 @@ describe("FN-6424 terminal symbols font CSS contract", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("FN-6603 terminal font stack measurement contract", () => {
|
||||
describe("FN-6659 terminal font stack measurement contract", () => {
|
||||
const symbolsFamily = '"Fusion Terminal Nerd Font Symbols"';
|
||||
|
||||
function splitFontFamilies(stack: string): string[] {
|
||||
@@ -128,28 +145,19 @@ describe("FN-6603 terminal font stack measurement contract", () => {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
it("keeps the default symbols fallback after real monospace text fonts", () => {
|
||||
it("keeps the default xterm measurement family free of the symbols face", () => {
|
||||
const families = splitFontFamilies(XTERM_FONT_FAMILY);
|
||||
const symbolsIndex = families.indexOf(symbolsFamily);
|
||||
const firstTextFontIndex = families.findIndex((family) => family !== symbolsFamily);
|
||||
|
||||
expect(symbolsIndex).toBeGreaterThan(-1);
|
||||
expect(firstTextFontIndex).toBeGreaterThan(-1);
|
||||
expect(symbolsIndex).toBeGreaterThan(firstTextFontIndex);
|
||||
expect(families).not.toContain(symbolsFamily);
|
||||
expect(families.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("gives every terminal font preset a measurement-safe text face before symbols", () => {
|
||||
it("keeps every terminal preset free of the symbols face xterm measures", () => {
|
||||
for (const preset of TERMINAL_FONT_FAMILY_PRESETS) {
|
||||
const families = splitFontFamilies(preset.css);
|
||||
const symbolsIndex = families.indexOf(symbolsFamily);
|
||||
const firstTextFontIndex = families.findIndex((family) => family !== symbolsFamily);
|
||||
|
||||
expect(firstTextFontIndex, `${preset.id} has a text font`).toBeGreaterThan(-1);
|
||||
if (symbolsIndex >= 0) {
|
||||
expect(symbolsIndex, `${preset.id} symbols fallback order`).toBeGreaterThan(
|
||||
firstTextFontIndex,
|
||||
);
|
||||
}
|
||||
expect(families, `${preset.id} xterm measurement stack`).not.toContain(symbolsFamily);
|
||||
expect(families.length, `${preset.id} has a text font`).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -132,6 +132,14 @@ SessionTerminal hosts the same xterm DOM/canvas measurement subtree as TerminalM
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:Terminal 2026-06-18-15:45:
|
||||
Recurrence #5 requires the attach terminal to mirror TerminalModal: xterm receives a symbols-free measured family, and only DOM row spans receive the optional glyph fallback. This keeps the real-iOS WebKit measurement invariant shared across DOM/canvas fallback and desktop WebGL while avoiding a SessionTerminal-only recurrence.
|
||||
*/
|
||||
.cli-session-terminal__viewport .xterm-rows span {
|
||||
font-family: var(--terminal-glyph-font-family, inherit) !important;
|
||||
}
|
||||
|
||||
.cli-session-terminal__advance-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./SessionTerminal.css";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Terminal as TerminalIcon, ShieldAlert, Settings, Eye } from "lucide-react";
|
||||
import type { Terminal as XTerm, ITerminalAddon } from "@xterm/xterm";
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
TERMINAL_PREFERENCES_KEY,
|
||||
readTerminalPreferences,
|
||||
resolveTerminalFontFamily,
|
||||
resolveTerminalGlyphFontFamily,
|
||||
waitForTerminalFontMetrics,
|
||||
} from "../utils/terminalPreferences";
|
||||
|
||||
@@ -263,6 +264,10 @@ export function SessionTerminal({
|
||||
|
||||
const terminalPreferences = readTerminalPreferences();
|
||||
terminal.options.fontFamily = resolveTerminalFontFamily(terminalPreferences.fontFamily);
|
||||
containerRef.current?.style.setProperty(
|
||||
"--terminal-glyph-font-family",
|
||||
resolveTerminalGlyphFontFamily(terminalPreferences.fontFamily),
|
||||
);
|
||||
terminal.options.fontSize = terminalPreferences.fontSize;
|
||||
terminal.options.cursorStyle = terminalPreferences.cursorStyle;
|
||||
terminal.options.cursorBlink = terminalPreferences.cursorBlink && !readOnly && mode === "live";
|
||||
@@ -342,10 +347,14 @@ export function SessionTerminal({
|
||||
|
||||
const terminalPreferences = readTerminalPreferences();
|
||||
const resolvedFontFamily = resolveTerminalFontFamily(terminalPreferences.fontFamily);
|
||||
containerRef.current.style.setProperty(
|
||||
"--terminal-glyph-font-family",
|
||||
resolveTerminalGlyphFontFamily(terminalPreferences.fontFamily),
|
||||
);
|
||||
|
||||
/*
|
||||
FNXC:Terminal 2026-06-17-18:25:
|
||||
SessionTerminal shares the FN-6603 wide-cell hazard because it passes the same resolved font stack to xterm's mobile DOM/canvas renderer. The shared terminalPreferences stack keeps real monospace faces before the symbols fallback so this attach surface inherits the durable cell-measurement fix instead of relying on a separate SessionTerminal-only font path.
|
||||
FNXC:Terminal 2026-06-18-15:42:
|
||||
SessionTerminal shares TerminalModal's recurrence #5 root cause: FN-6638's 66.76px diagnostic compared only symbols-inclusive stacks, so real iOS Safari still let the loaded symbols @font-face pollute xterm's ASCII measurement. Pass only the symbols-free resolved family to xterm on this attach surface too; DOM glyph fallback is scoped to the viewport CSS variable and never to the xterm font option used by DOM/canvas measurement or desktop WebGL.
|
||||
|
||||
FNXC:Terminal 2026-06-17-00:50:
|
||||
SessionTerminal consumes the shared localStorage terminal preferences for parity with TerminalModal, but replay safety still owns input posture: cursor blink is the user preference AND-gated by !readOnly && mode === "live" so read-only, idle, and ended sessions never blink.
|
||||
@@ -546,6 +555,11 @@ export function SessionTerminal({
|
||||
|
||||
const elevated = Boolean(posture?.elevated);
|
||||
const flagSummary = posture?.elevatedFlags?.join(", ");
|
||||
const terminalGlyphStyle = {
|
||||
"--terminal-glyph-font-family": resolveTerminalGlyphFontFamily(
|
||||
readTerminalPreferences().fontFamily,
|
||||
),
|
||||
} as CSSProperties;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -627,6 +641,7 @@ export function SessionTerminal({
|
||||
className="cli-session-terminal__viewport"
|
||||
ref={containerRef}
|
||||
data-testid="cli-terminal-viewport"
|
||||
style={terminalGlyphStyle}
|
||||
/>
|
||||
|
||||
{showConfirmAdvance && !advanceDismissed && (
|
||||
|
||||
@@ -555,6 +555,14 @@ Real iOS Safari recurrence #4 kept wide ASCII cells even after unicode-range sco
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:Terminal 2026-06-18-15:44:
|
||||
FN-6659 keeps the loaded symbols @font-face out of xterm's measured font option because every FN-6638 diagnostic stack that measured 66.76px still included that face. Limit the symbols fallback to DOM renderer row spans via a CSS custom property so ASCII measurement, fit, refresh, and WebGL/canvas option paths stay symbols-free while powerline glyphs can still resolve when xterm emits DOM text nodes.
|
||||
*/
|
||||
.terminal-xterm .xterm-rows span {
|
||||
font-family: var(--terminal-glyph-font-family, inherit) !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* xterm fit may apply inline pixel heights on the `.xterm` root after
|
||||
* row/line-height recomputation (e.g. after font-size changes). Keep the root
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "./TerminalModal.css";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useState, useEffect, useRef, useCallback, type CSSProperties } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getErrorMessage } from "@fusion/core";
|
||||
import {
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
clampTerminalFontSize,
|
||||
readTerminalPreferences,
|
||||
resolveTerminalFontFamily,
|
||||
resolveTerminalGlyphFontFamily,
|
||||
waitForTerminalFontMetrics,
|
||||
writeTerminalPreferences,
|
||||
type TerminalPreferences,
|
||||
@@ -243,6 +244,15 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
);
|
||||
const fontSize = terminalPreferences.fontSize;
|
||||
const resolvedFontFamily = resolveTerminalFontFamily(terminalPreferences.fontFamily);
|
||||
/*
|
||||
FNXC:Terminal 2026-06-18-15:40:
|
||||
TerminalModal must pass a symbols-free family to xterm so iOS WebKit measures ASCII cells against real monospace metrics. Keep the symbols fallback only in a scoped DOM glyph CSS variable; this preserves powerline glyph availability for DOM rows without reintroducing the loaded symbols @font-face into xterm's measurement, fit, or WebGL/canvas option path.
|
||||
*/
|
||||
const terminalGlyphStyle = {
|
||||
"--terminal-glyph-font-family": resolveTerminalGlyphFontFamily(
|
||||
terminalPreferences.fontFamily,
|
||||
),
|
||||
} as CSSProperties;
|
||||
const [showShortcuts, setShowShortcuts] = useState(false);
|
||||
const [showPreferences, setShowPreferences] = useState(false);
|
||||
const [stickyModifier, setStickyModifier] = useState<null | "ctrl" | "alt">(null);
|
||||
@@ -1507,6 +1517,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
ref={terminalRef}
|
||||
className="terminal-xterm"
|
||||
data-testid="terminal-xterm"
|
||||
style={terminalGlyphStyle}
|
||||
onPointerDown={handleTerminalGestureFocus}
|
||||
onTouchStart={handleTerminalGestureFocus}
|
||||
/>
|
||||
|
||||
@@ -108,10 +108,8 @@ function splitFontFamilies(stack: string): string[] {
|
||||
|
||||
function expectMeasurementSafeFontStack(stack: string): void {
|
||||
const families = splitFontFamilies(stack);
|
||||
const symbolsIndex = families.indexOf(TERMINAL_SYMBOLS_FONT_FAMILY);
|
||||
const firstTextIndex = families.findIndex((family) => family !== TERMINAL_SYMBOLS_FONT_FAMILY);
|
||||
expect(firstTextIndex).toBeGreaterThan(-1);
|
||||
expect(symbolsIndex).toBeGreaterThan(firstTextIndex);
|
||||
expect(families.length).toBeGreaterThan(0);
|
||||
expect(families).not.toContain(TERMINAL_SYMBOLS_FONT_FAMILY);
|
||||
}
|
||||
|
||||
/** Pull the parsed input frames a WS has sent. */
|
||||
|
||||
@@ -70,10 +70,8 @@ function splitFontFamilies(stack: string): string[] {
|
||||
|
||||
function expectMeasurementSafeFontStack(stack: string): void {
|
||||
const families = splitFontFamilies(stack);
|
||||
const symbolsIndex = families.indexOf(TERMINAL_SYMBOLS_FONT_FAMILY);
|
||||
const firstTextIndex = families.findIndex((family) => family !== TERMINAL_SYMBOLS_FONT_FAMILY);
|
||||
expect(firstTextIndex).toBeGreaterThan(-1);
|
||||
expect(symbolsIndex).toBeGreaterThan(firstTextIndex);
|
||||
expect(families.length).toBeGreaterThan(0);
|
||||
expect(families).not.toContain(TERMINAL_SYMBOLS_FONT_FAMILY);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -140,7 +138,7 @@ describe("SessionTerminal", () => {
|
||||
await waitFor(() => expect(FakeWS.instances.length).toBe(1));
|
||||
expect(Terminal).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fontFamily: expect.stringContaining("Fusion Terminal Nerd Font Symbols"),
|
||||
fontFamily: resolveTerminalFontFamily("nerd-font"),
|
||||
fontSize: DEFAULT_TERMINAL_PREFERENCES.fontSize,
|
||||
cursorStyle: DEFAULT_TERMINAL_PREFERENCES.cursorStyle,
|
||||
cursorBlink: DEFAULT_TERMINAL_PREFERENCES.cursorBlink,
|
||||
@@ -175,7 +173,8 @@ describe("SessionTerminal", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(FakeWS.instances.length).toBe(1);
|
||||
expect(load).toHaveBeenCalledWith(
|
||||
expect(load).toHaveBeenCalledWith(expect.stringContaining("MesloLGS NF"));
|
||||
expect(load).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("Fusion Terminal Nerd Font Symbols"),
|
||||
);
|
||||
});
|
||||
@@ -228,12 +227,13 @@ describe("SessionTerminal", () => {
|
||||
await waitFor(() => expect(FakeWS.instances.length).toBe(1));
|
||||
expect(Terminal).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fontFamily: expect.stringContaining("Fusion Terminal Nerd Font Symbols"),
|
||||
fontFamily: resolveTerminalFontFamily("nerd-font"),
|
||||
fontSize: DEFAULT_TERMINAL_PREFERENCES.fontSize,
|
||||
cursorStyle: DEFAULT_TERMINAL_PREFERENCES.cursorStyle,
|
||||
cursorBlink: true,
|
||||
}),
|
||||
);
|
||||
expectMeasurementSafeFontStack(mockTerm.options.fontFamily as string);
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -4849,7 +4849,8 @@ describe("TerminalModal — xterm focus initialization (FN-1602)", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockTerminalInstance.open).toHaveBeenCalled();
|
||||
expect(load).toHaveBeenCalledWith(
|
||||
expect(load).toHaveBeenCalledWith(expect.stringContaining("MesloLGS NF"));
|
||||
expect(load).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("Fusion Terminal Nerd Font Symbols"),
|
||||
);
|
||||
});
|
||||
@@ -4886,7 +4887,8 @@ describe("TerminalModal — xterm focus initialization (FN-1602)", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockTerminalInstance.open).toHaveBeenCalled();
|
||||
expect(load).toHaveBeenCalledWith(
|
||||
expect(load).toHaveBeenCalledWith(expect.stringContaining("MesloLGS NF"));
|
||||
expect(load).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("Fusion Terminal Nerd Font Symbols"),
|
||||
);
|
||||
});
|
||||
@@ -4898,6 +4900,9 @@ describe("TerminalModal — xterm focus initialization (FN-1602)", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockTerminalInstance.options.fontFamily).toBe(XTERM_FONT_FAMILY);
|
||||
expect(mockTerminalInstance.options.fontFamily).not.toContain(
|
||||
"Fusion Terminal Nerd Font Symbols",
|
||||
);
|
||||
expect(mockTerminalInstance.options.fontSize).toBe(DEFAULT_TERMINAL_PREFERENCES.fontSize);
|
||||
expect(mockFitAddonFit.mock.calls.length).toBeGreaterThan(fitCallBaseline);
|
||||
expect(mockResize).toHaveBeenCalledWith(
|
||||
|
||||
@@ -111,7 +111,7 @@ describe("terminalPreferences", () => {
|
||||
|
||||
expect(load).toHaveBeenCalledWith(expect.stringContaining("MesloLGS NF"));
|
||||
expect(load).toHaveBeenCalledWith("12px \"MesloLGS NF\"");
|
||||
expect(load).toHaveBeenCalledWith("12px \"Fusion Terminal Nerd Font Symbols\"");
|
||||
expect(load).not.toHaveBeenCalledWith("12px \"Fusion Terminal Nerd Font Symbols\"");
|
||||
expect(readyAwaited).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,11 +7,11 @@ export const MAX_TERMINAL_FONT_SIZE = 32;
|
||||
export const TERMINAL_SYMBOLS_FONT_FAMILY = '"Fusion Terminal Nerd Font Symbols"';
|
||||
|
||||
/*
|
||||
FNXC:Terminal 2026-06-17-18:12:
|
||||
Mobile WebKit can render ASCII through a later text fallback while xterm's canvas/DOM cell-measurement probe still binds metrics from the first listed symbols-only face. Keep real monospace text faces first for stable cell widths across mobile DOM/canvas and desktop WebGL renderers, then use the unicode-range-scoped symbols face as a fallback for powerline/Nerd-Font codepoints in every preset.
|
||||
FNXC:Terminal 2026-06-18-15:38:
|
||||
FN-6659 recurrence #5 showed the FN-6638 66.76px diagnostic compared only symbols-inclusive stacks: symbols-first, symbols-last, and system-mono all still contained the loaded unicode-range symbols @font-face. Real iOS Safari therefore implicated the symbols face's mere presence in xterm's measured font shorthand, not stack order. Keep xterm's measured family symbols-free for every preset and both terminal surfaces; a separate DOM glyph CSS layer may append the symbols face where it does not feed xterm's ASCII cell measurement.
|
||||
*/
|
||||
export const XTERM_FONT_FAMILY =
|
||||
`"MesloLGS NF", "MesloLGM Nerd Font", "JetBrainsMono Nerd Font", "FiraCode Nerd Font", "Hack Nerd Font", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace, ${TERMINAL_SYMBOLS_FONT_FAMILY}`;
|
||||
'"MesloLGS NF", "MesloLGM Nerd Font", "JetBrainsMono Nerd Font", "FiraCode Nerd Font", "Hack Nerd Font", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace';
|
||||
|
||||
export const TERMINAL_FONT_FAMILY_PRESETS = [
|
||||
{
|
||||
@@ -22,17 +22,17 @@ export const TERMINAL_FONT_FAMILY_PRESETS = [
|
||||
{
|
||||
id: "system-mono",
|
||||
label: "System monospace",
|
||||
css: `ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace, ${TERMINAL_SYMBOLS_FONT_FAMILY}`,
|
||||
css: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
||||
},
|
||||
{
|
||||
id: "jetbrains-mono",
|
||||
label: "JetBrains Mono",
|
||||
css: `"JetBrains Mono", "JetBrainsMono Nerd Font", ui-monospace, SFMono-Regular, monospace, ${TERMINAL_SYMBOLS_FONT_FAMILY}`,
|
||||
css: '"JetBrains Mono", "JetBrainsMono Nerd Font", ui-monospace, SFMono-Regular, monospace',
|
||||
},
|
||||
{
|
||||
id: "fira-code",
|
||||
label: "Fira Code",
|
||||
css: `"Fira Code", "FiraCode Nerd Font", ui-monospace, SFMono-Regular, monospace, ${TERMINAL_SYMBOLS_FONT_FAMILY}`,
|
||||
css: '"Fira Code", "FiraCode Nerd Font", ui-monospace, SFMono-Regular, monospace',
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -71,6 +71,10 @@ export function resolveTerminalFontFamily(fontFamily: TerminalFontFamily): strin
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveTerminalGlyphFontFamily(fontFamily: TerminalFontFamily): string {
|
||||
return `${resolveTerminalFontFamily(fontFamily)}, ${TERMINAL_SYMBOLS_FONT_FAMILY}`;
|
||||
}
|
||||
|
||||
const CSS_GENERIC_FONT_FAMILIES = new Set([
|
||||
"serif",
|
||||
"sans-serif",
|
||||
|
||||
Reference in New Issue
Block a user