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:
+5
-2
@@ -7,7 +7,7 @@ import { Wizard } from "./Wizard";
|
|||||||
import { ConfirmDelete } from "./ConfirmDelete";
|
import { ConfirmDelete } from "./ConfirmDelete";
|
||||||
import { EventCenter } from "./EventCenter";
|
import { EventCenter } from "./EventCenter";
|
||||||
import { maybeNotify } from "./notify";
|
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 { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent, setWorkspaceMeta, focusSurface, getEventLog, markEventsRead, getHealth, closeWorkspaceCmd, getConfig } from "./socketBridge";
|
||||||
import type { EventRecord, DaemonHealth, ConfigView } from "./socketBridge";
|
import type { EventRecord, DaemonHealth, ConfigView } from "./socketBridge";
|
||||||
import { leafIds } from "./layoutTypes";
|
import { leafIds } from "./layoutTypes";
|
||||||
@@ -136,6 +136,9 @@ export function App() {
|
|||||||
const effectiveFocus = focusedId && leaves.includes(focusedId) ? focusedId : leaves[0] ?? null;
|
const effectiveFocus = focusedId && leaves.includes(focusedId) ? focusedId : leaves[0] ?? null;
|
||||||
effectiveFocusRef.current = effectiveFocus;
|
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) {
|
function selectWorkspace(id: string) {
|
||||||
setActiveId(id);
|
setActiveId(id);
|
||||||
setFocusedId(null);
|
setFocusedId(null);
|
||||||
@@ -153,7 +156,7 @@ export function App() {
|
|||||||
)}
|
)}
|
||||||
<div style={{ flex: 1, minHeight: 0, position: "relative" }}>
|
<div style={{ flex: 1, minHeight: 0, position: "relative" }}>
|
||||||
{active
|
{active
|
||||||
? <LayoutEngine workspaceId={active.id} layout={active.layout} running={running} states={states} surfaces={active.surfaces} focusedId={effectiveFocus} onFocus={setFocusedId} zoomed={active.zoomed} searchSurfaceId={searchSurfaceId} searchNonce={searchNonce} onCloseSearch={() => setSearchSurfaceId(null)} />
|
? <LayoutEngine workspaceId={active.id} layout={active.layout} running={running} states={states} surfaces={active.surfaces} focusedId={effectiveFocus} onFocus={setFocusedId} zoomed={active.zoomed} searchSurfaceId={searchSurfaceId} searchNonce={searchNonce} onCloseSearch={() => setSearchSurfaceId(null)} font={termFont} palette={termPalette} />
|
||||||
: <div style={{ color: COLORS.textMuted, padding: 24 }}>No workspace — create one to begin.</div>}
|
: <div style={{ color: COLORS.textMuted, padding: 24 }}>No workspace — create one to begin.</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ interface Props {
|
|||||||
searchSurfaceId: string | null;
|
searchSurfaceId: string | null;
|
||||||
searchNonce: number;
|
searchNonce: number;
|
||||||
onCloseSearch: () => void;
|
onCloseSearch: () => void;
|
||||||
|
font: { family: string; size: number } | null;
|
||||||
|
palette: Record<string, string> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Edge = "left" | "right" | "top" | "bottom";
|
type Edge = "left" | "right" | "top" | "bottom";
|
||||||
@@ -40,7 +42,7 @@ function shortPath(cwd: string): string {
|
|||||||
return leaf ? `~/${leaf}` : cwd;
|
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
|
// Panel drag-to-reorder. Implemented with raw pointer events rather than the
|
||||||
// HTML5 drag API, which is unreliable in the macOS WKWebView Tauri uses.
|
// HTML5 drag API, which is unreliable in the macOS WKWebView Tauri uses.
|
||||||
const [drop, setDrop] = useState<DropTarget | null>(null);
|
const [drop, setDrop] = useState<DropTarget | null>(null);
|
||||||
@@ -78,7 +80,7 @@ export function LayoutEngine({ workspaceId, layout, running, states, surfaces, f
|
|||||||
if (!layout) {
|
if (!layout) {
|
||||||
return <div style={{ color: COLORS.textMuted, padding: 24 }}>Empty workspace — apply a preset to add panels.</div>;
|
return <div style={{ color: COLORS.textMuted, padding: 24 }}>Empty workspace — apply a preset to add panels.</div>;
|
||||||
}
|
}
|
||||||
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) {
|
if (zoomed) {
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%", height: "100%", padding: 12, boxSizing: "border-box" }}>
|
<div style={{ width: "100%", height: "100%", padding: 12, boxSizing: "border-box" }}>
|
||||||
@@ -103,6 +105,8 @@ interface NodeProps {
|
|||||||
searchSurfaceId: string | null;
|
searchSurfaceId: string | null;
|
||||||
searchNonce: number;
|
searchNonce: number;
|
||||||
onCloseSearch: () => void;
|
onCloseSearch: () => void;
|
||||||
|
font: { family: string; size: number } | null;
|
||||||
|
palette: Record<string, string> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Node({ node, path, ...rest }: NodeProps) {
|
function Node({ node, path, ...rest }: NodeProps) {
|
||||||
@@ -112,7 +116,7 @@ function Node({ node, path, ...rest }: NodeProps) {
|
|||||||
return <SplitView split={node.split} path={path} {...rest} />;
|
return <SplitView split={node.split} path={path} {...rest} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag, searchSurfaceId, searchNonce, onCloseSearch }: Omit<NodeProps, "node" | "path"> & { id: string }) {
|
function Leaf({ id, workspaceId, running, states, surfaces, focusedId, onFocus, zoomed, drop, onStartPanelDrag, searchSurfaceId, searchNonce, onCloseSearch, font, palette }: Omit<NodeProps, "node" | "path"> & { id: string }) {
|
||||||
const focused = focusedId === id;
|
const focused = focusedId === id;
|
||||||
const dropEdge = drop && drop.id === id ? drop.edge : null;
|
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); }} />}
|
onMouseDown={(e) => { e.stopPropagation(); onFocus(id); void setZoom(workspaceId, id); }} />}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minHeight: 0 }}>
|
<div style={{ flex: 1, minHeight: 0 }}>
|
||||||
<TerminalView key={id} surfaceId={id} />
|
<TerminalView key={id} surfaceId={id} font={font} palette={palette} />
|
||||||
</div>
|
</div>
|
||||||
{searchSurfaceId === id && (
|
{searchSurfaceId === id && (
|
||||||
<SearchBar surfaceId={id} reopenNonce={searchNonce} onClose={onCloseSearch} />
|
<SearchBar surfaceId={id} reopenNonce={searchNonce} onClose={onCloseSearch} />
|
||||||
|
|||||||
@@ -9,15 +9,35 @@ import { registerSearch, unregisterSearch } from "./searchRegistry";
|
|||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
const encoder = new TextEncoder();
|
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 ref = useRef<HTMLDivElement>(null);
|
||||||
|
const termRef = useRef<Terminal | null>(null);
|
||||||
|
const fitRef = useRef<FitAddon | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
// allowProposedApi is required by the search addon: its match decorations
|
// allowProposedApi is required by the search addon: its match decorations
|
||||||
// call registerMarker/registerDecoration (proposed API). Without it findNext
|
// call registerMarker/registerDecoration (proposed API). Without it findNext
|
||||||
// throws and the scrollback search counter never updates.
|
// 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 {
|
try {
|
||||||
term.loadAddon(new WebglAddon());
|
term.loadAddon(new WebglAddon());
|
||||||
} catch {
|
} catch {
|
||||||
@@ -31,6 +51,7 @@ export function TerminalView({ surfaceId }: { surfaceId: string }) {
|
|||||||
|
|
||||||
const fit = new FitAddon();
|
const fit = new FitAddon();
|
||||||
term.loadAddon(fit);
|
term.loadAddon(fit);
|
||||||
|
fitRef.current = fit;
|
||||||
|
|
||||||
// Fit the grid to the container and tell the daemon the new size. Coalesced
|
// 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.
|
// 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);
|
void detachSurface(surfaceId);
|
||||||
unregisterSearch(surfaceId);
|
unregisterSearch(surfaceId);
|
||||||
term.dispose();
|
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%" }} />;
|
return <div ref={ref} style={{ width: "100%", height: "100%" }} />;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user