feat(app): terminal font and xterm theme from daemon config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 09:43:38 +07:00
parent 0f28be1300
commit 61c69adb17
3 changed files with 55 additions and 9 deletions
+42 -3
View File
@@ -9,15 +9,35 @@ import { registerSearch, unregisterSearch } from "./searchRegistry";
const decoder = new TextDecoder();
const encoder = new TextEncoder();
export function TerminalView({ surfaceId }: { surfaceId: string }) {
function xtermTheme(p: Record<string, string>) {
return {
background: p["bg-panel"],
foreground: p["text-primary"],
cursor: p["text-primary"],
selectionBackground: p["search-match"],
};
}
export function TerminalView({ surfaceId, font, palette }: { surfaceId: string; font: { family: string; size: number } | null; palette: Record<string, string> | null }) {
const ref = useRef<HTMLDivElement>(null);
const termRef = useRef<Terminal | null>(null);
const fitRef = useRef<FitAddon | null>(null);
useEffect(() => {
if (!ref.current) return;
// allowProposedApi is required by the search addon: its match decorations
// call registerMarker/registerDecoration (proposed API). Without it findNext
// throws and the scrollback search counter never updates.
const term = new Terminal({ fontFamily: "'JetBrains Mono Variable', 'JetBrains Mono', monospace", fontSize: 13, convertEol: false, scrollback: 10000, allowProposedApi: true });
const term = new Terminal({
fontFamily: font ? `'${font.family}', monospace` : "'JetBrains Mono Variable', 'JetBrains Mono', monospace",
fontSize: font?.size ?? 13,
convertEol: false,
scrollback: 10000,
allowProposedApi: true,
theme: palette ? xtermTheme(palette) : undefined,
});
termRef.current = term;
try {
term.loadAddon(new WebglAddon());
} catch {
@@ -31,6 +51,7 @@ export function TerminalView({ surfaceId }: { surfaceId: string }) {
const fit = new FitAddon();
term.loadAddon(fit);
fitRef.current = fit;
// Fit the grid to the container and tell the daemon the new size. Coalesced
// through rAF so a burst of resize callbacks yields one resize per frame.
@@ -76,8 +97,26 @@ export function TerminalView({ surfaceId }: { surfaceId: string }) {
void detachSurface(surfaceId);
unregisterSearch(surfaceId);
term.dispose();
termRef.current = null;
fitRef.current = null;
};
}, [surfaceId]);
}, [surfaceId]); // eslint-disable-line react-hooks/exhaustive-deps
// Live re-apply font and theme when config changes without remounting.
// palette is a new object each render so we depend on a stable key instead.
const paletteKey = palette
? `${palette["bg-panel"]}|${palette["text-primary"]}|${palette["search-match"]}`
: null;
useEffect(() => {
const t = termRef.current;
if (!t) return;
if (font) {
t.options.fontFamily = `'${font.family}', monospace`;
t.options.fontSize = font.size;
}
if (palette) t.options.theme = xtermTheme(palette);
requestAnimationFrame(() => { try { fitRef.current?.fit(); } catch { /* ignore */ } });
}, [font?.family, font?.size, paletteKey]); // eslint-disable-line react-hooks/exhaustive-deps
return <div ref={ref} style={{ width: "100%", height: "100%" }} />;
}