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:
@@ -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%" }} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user