Per-pane and global terminal zoom via keyboard
Each leaf now carries an optional fontSizeOffset, persisted in workspace.json alongside everything else. Ctrl+= / Ctrl+- / Ctrl+0 adjust the active pane; adding Shift escalates to every pane (the mirror of the broadcast Shift+Alt convention, with shift alone since the keys are otherwise unused). Bindings match on e.code so layouts that don't have "=" / "-" / "0" in the same spot still work. XtermPane gained a fontSize prop. A secondary effect reacts to changes: set term.options.fontSize, fit() to recompute cols/rows for the new cell size, refresh(), then resizePane so bash redraws the prompt at the right width. No remount, so PTY + scrollback survive zoom changes. The new tree helpers (resolveFontSize / adjustFontSize / adjustAllFontSizes) are metadata-only — they don't swap leaf ids, so nothing respawns. reshapeToPreset also carries the offset across when splicing existing leaves into a new layout. 12 new vitest cases pin those invariants plus the clamp and reset-to-default behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8f9667b218
commit
aab36afce4
6 changed files with 237 additions and 11 deletions
|
|
@ -52,8 +52,12 @@ interface XtermPaneProps {
|
|||
onFocus?: () => void;
|
||||
/** Increment to refocus the terminal programmatically (palette etc.). */
|
||||
focusTrigger?: number;
|
||||
/** Absolute font size in px. Changes are applied live (fit + PTY resize). */
|
||||
fontSize?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_XTERM_FONT_SIZE = 13;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -67,8 +71,16 @@ export default function XtermPane({
|
|||
onDataReceived,
|
||||
onFocus,
|
||||
focusTrigger = 0,
|
||||
fontSize,
|
||||
}: XtermPaneProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const termRef = useRef<Terminal | null>(null);
|
||||
const fitRef = useRef<FitAddon | null>(null);
|
||||
const paneIdRef = useRef<PaneId | null>(null);
|
||||
// Stash the most recent `fontSize` prop so the mount effect can pick
|
||||
// up the initial value without re-running when it changes (the secondary
|
||||
// effect below handles dynamic updates).
|
||||
const initialFontSizeRef = useRef(fontSize);
|
||||
|
||||
// Stable refs for callbacks so the mount effect doesn't need to re-run when
|
||||
// parents pass new inline functions, while still always calling the latest version.
|
||||
|
|
@ -93,7 +105,7 @@ export default function XtermPane({
|
|||
|
||||
let term: Terminal | null = new Terminal({
|
||||
fontFamily: '"Cascadia Mono", "JetBrains Mono", "Consolas", monospace',
|
||||
fontSize: 13,
|
||||
fontSize: initialFontSizeRef.current ?? DEFAULT_XTERM_FONT_SIZE,
|
||||
cursorBlink: true,
|
||||
theme: {
|
||||
background: "#0c0c0c",
|
||||
|
|
@ -103,8 +115,10 @@ export default function XtermPane({
|
|||
convertEol: false,
|
||||
allowProposedApi: true,
|
||||
});
|
||||
termRef.current = term;
|
||||
|
||||
const fit = new FitAddon();
|
||||
fitRef.current = fit;
|
||||
term.loadAddon(fit);
|
||||
term.open(container);
|
||||
|
||||
|
|
@ -127,6 +141,7 @@ export default function XtermPane({
|
|||
void killPane(paneId);
|
||||
return;
|
||||
}
|
||||
paneIdRef.current = paneId;
|
||||
onStatusRef.current?.(`pane ${paneId} alive`, true);
|
||||
onSpawnRef.current?.(paneId);
|
||||
} catch (e) {
|
||||
|
|
@ -219,6 +234,9 @@ export default function XtermPane({
|
|||
if (paneId != null) void killPane(paneId);
|
||||
term?.dispose();
|
||||
term = null;
|
||||
termRef.current = null;
|
||||
fitRef.current = null;
|
||||
paneIdRef.current = null;
|
||||
};
|
||||
// distro/cwd are only used at spawn time; intentionally omitted from deps
|
||||
// so remounting doesn't happen if a parent re-renders with the same values.
|
||||
|
|
@ -228,13 +246,6 @@ export default function XtermPane({
|
|||
// -------------------------------------------------------------------------
|
||||
// focusTrigger: programmatic refocus from parent (palette navigation etc.)
|
||||
// -------------------------------------------------------------------------
|
||||
const termRef = useRef<Terminal | null>(null);
|
||||
|
||||
// Keep termRef in sync via a second effect that runs after mount.
|
||||
// We can't easily share the Terminal instance across the two effects without
|
||||
// a ref, so we store it on termRef inside the mount effect instead.
|
||||
// Actually, let's just wire focusTrigger by querying the textarea directly —
|
||||
// that avoids the cross-effect coupling problem entirely.
|
||||
useEffect(() => {
|
||||
if (focusTrigger > 0 && containerRef.current) {
|
||||
const ta = containerRef.current.querySelector<HTMLTextAreaElement>(
|
||||
|
|
@ -244,8 +255,31 @@ export default function XtermPane({
|
|||
}
|
||||
}, [focusTrigger]);
|
||||
|
||||
// Suppress unused ref warning
|
||||
void termRef;
|
||||
// -------------------------------------------------------------------------
|
||||
// Live font-size changes (Ctrl+Shift+= / - / 0).
|
||||
//
|
||||
// Setting term.options.fontSize re-rasterises glyphs immediately, but the
|
||||
// cols/rows the terminal thinks it has are still based on the OLD cell
|
||||
// size — so we have to fit() to recompute, refresh() to repaint, then
|
||||
// ship the new dimensions to the PTY so bash redraws the prompt at the
|
||||
// right width.
|
||||
// -------------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
const term = termRef.current;
|
||||
const fit = fitRef.current;
|
||||
if (!term || !fit) return;
|
||||
const target = fontSize ?? DEFAULT_XTERM_FONT_SIZE;
|
||||
if (term.options.fontSize === target) return;
|
||||
try {
|
||||
term.options.fontSize = target;
|
||||
fit.fit();
|
||||
term.refresh(0, term.rows - 1);
|
||||
const paneId = paneIdRef.current;
|
||||
if (paneId != null) void resizePane(paneId, term.cols, term.rows);
|
||||
} catch (e) {
|
||||
console.warn("font-size apply failed", e);
|
||||
}
|
||||
}, [fontSize]);
|
||||
|
||||
return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue