diff --git a/app/src/App.tsx b/app/src/App.tsx index feb9b08..07f592c 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -7,7 +7,7 @@ import { Wizard } from "./Wizard"; import { ConfirmDelete } from "./ConfirmDelete"; import { EventCenter } from "./EventCenter"; import { maybeNotify } from "./notify"; -import { COLORS, applyTheme } from "./theme"; +import { COLORS, applyTheme, resolvePalette } from "./theme"; import { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent, setWorkspaceMeta, focusSurface, getEventLog, markEventsRead, getHealth, closeWorkspaceCmd, getConfig } from "./socketBridge"; import type { EventRecord, DaemonHealth, ConfigView } from "./socketBridge"; import { leafIds } from "./layoutTypes"; @@ -136,6 +136,9 @@ export function App() { const effectiveFocus = focusedId && leaves.includes(focusedId) ? focusedId : leaves[0] ?? null; effectiveFocusRef.current = effectiveFocus; + const termFont = config ? { family: config.font_family, size: config.font_size } : null; + const termPalette = config ? resolvePalette(config.theme, config.accent) : null; + function selectWorkspace(id: string) { setActiveId(id); setFocusedId(null); @@ -153,7 +156,7 @@ export function App() { )}
{active - ? setSearchSurfaceId(null)} /> + ? setSearchSurfaceId(null)} font={termFont} palette={termPalette} /> :
No workspace — create one to begin.
}
diff --git a/app/src/LayoutEngine.tsx b/app/src/LayoutEngine.tsx index 21be0e3..74b6570 100644 --- a/app/src/LayoutEngine.tsx +++ b/app/src/LayoutEngine.tsx @@ -22,6 +22,8 @@ interface Props { searchSurfaceId: string | null; searchNonce: number; onCloseSearch: () => void; + font: { family: string; size: number } | null; + palette: Record | null; } type Edge = "left" | "right" | "top" | "bottom"; @@ -40,7 +42,7 @@ function shortPath(cwd: string): string { return leaf ? `~/${leaf}` : cwd; } -export function LayoutEngine({ workspaceId, layout, running, states, surfaces, focusedId, onFocus, zoomed, searchSurfaceId, searchNonce, onCloseSearch }: Props) { +export function LayoutEngine({ workspaceId, layout, running, states, surfaces, focusedId, onFocus, zoomed, searchSurfaceId, searchNonce, onCloseSearch, font, palette }: Props) { // Panel drag-to-reorder. Implemented with raw pointer events rather than the // HTML5 drag API, which is unreliable in the macOS WKWebView Tauri uses. const [drop, setDrop] = useState(null); @@ -78,7 +80,7 @@ export function LayoutEngine({ workspaceId, layout, running, states, surfaces, f if (!layout) { return
Empty workspace — apply a preset to add panels.
; } - const shared = { workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag: startPanelDrag, searchSurfaceId, searchNonce, onCloseSearch }; + const shared = { workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag: startPanelDrag, searchSurfaceId, searchNonce, onCloseSearch, font, palette }; if (zoomed) { return (
@@ -103,6 +105,8 @@ interface NodeProps { searchSurfaceId: string | null; searchNonce: number; onCloseSearch: () => void; + font: { family: string; size: number } | null; + palette: Record | null; } function Node({ node, path, ...rest }: NodeProps) { @@ -112,7 +116,7 @@ function Node({ node, path, ...rest }: NodeProps) { return ; } -function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag, searchSurfaceId, searchNonce, onCloseSearch }: Omit & { id: string }) { +function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag, searchSurfaceId, searchNonce, onCloseSearch, font, palette }: Omit & { id: string }) { const focused = focusedId === id; const dropEdge = drop && drop.id === id ? drop.edge : null; @@ -181,7 +185,7 @@ function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus, onMouseDown={(e) => { e.stopPropagation(); onFocus(id); void setZoom(workspaceId, id); }} />}
- +
{searchSurfaceId === id && ( diff --git a/app/src/TerminalView.tsx b/app/src/TerminalView.tsx index f16ebb6..35c0361 100644 --- a/app/src/TerminalView.tsx +++ b/app/src/TerminalView.tsx @@ -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) { + 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 | null }) { const ref = useRef(null); + const termRef = useRef(null); + const fitRef = useRef(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
; }