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
|
|
@ -11,6 +11,12 @@ import {
|
|||
changeDistro,
|
||||
changeLabel,
|
||||
toggleBroadcast,
|
||||
adjustFontSize,
|
||||
adjustAllFontSizes,
|
||||
resolveFontSize,
|
||||
DEFAULT_FONT_SIZE,
|
||||
MIN_FONT_SIZE,
|
||||
MAX_FONT_SIZE,
|
||||
serialize,
|
||||
deserialize,
|
||||
presetSingle,
|
||||
|
|
@ -298,6 +304,101 @@ describe("toggleBroadcast", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("resolveFontSize", () => {
|
||||
it("returns the default when offset is undefined or 0", () => {
|
||||
expect(resolveFontSize(undefined)).toBe(DEFAULT_FONT_SIZE);
|
||||
expect(resolveFontSize(0)).toBe(DEFAULT_FONT_SIZE);
|
||||
});
|
||||
|
||||
it("clamps to [MIN_FONT_SIZE, MAX_FONT_SIZE]", () => {
|
||||
expect(resolveFontSize(-9999)).toBe(MIN_FONT_SIZE);
|
||||
expect(resolveFontSize(9999)).toBe(MAX_FONT_SIZE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("adjustFontSize", () => {
|
||||
it("bumps a leaf's offset by delta", () => {
|
||||
const leaf = newLeaf();
|
||||
const next = adjustFontSize(leaf, leaf.id, 2) as LeafNode;
|
||||
expect(next.fontSizeOffset).toBe(2);
|
||||
});
|
||||
|
||||
it("MUST NOT swap the leaf id (metadata-only — pane should not respawn)", () => {
|
||||
const leaf = newLeaf();
|
||||
const next = adjustFontSize(leaf, leaf.id, 1) as LeafNode;
|
||||
expect(next.id).toBe(leaf.id);
|
||||
});
|
||||
|
||||
it("clamps the offset so the resolved font size stays within bounds", () => {
|
||||
const leaf = newLeaf();
|
||||
const bigUp = adjustFontSize(leaf, leaf.id, 999) as LeafNode;
|
||||
expect(resolveFontSize(bigUp.fontSizeOffset)).toBe(MAX_FONT_SIZE);
|
||||
const bigDown = adjustFontSize(leaf, leaf.id, -999) as LeafNode;
|
||||
expect(resolveFontSize(bigDown.fontSizeOffset)).toBe(MIN_FONT_SIZE);
|
||||
});
|
||||
|
||||
it("strips the offset field entirely when the result is 0", () => {
|
||||
const leaf = newLeaf({ fontSizeOffset: 1 });
|
||||
const next = adjustFontSize(leaf, leaf.id, -1) as LeafNode;
|
||||
expect(next.fontSizeOffset).toBeUndefined();
|
||||
expect("fontSizeOffset" in next).toBe(false);
|
||||
});
|
||||
|
||||
it("delta=null resets to default", () => {
|
||||
const leaf = newLeaf({ fontSizeOffset: 5 });
|
||||
const next = adjustFontSize(leaf, leaf.id, null) as LeafNode;
|
||||
expect(next.fontSizeOffset).toBeUndefined();
|
||||
});
|
||||
|
||||
it("only touches the targeted leaf", () => {
|
||||
const target = newLeaf({ label: "a" });
|
||||
const sibling = newLeaf({ label: "b", fontSizeOffset: 3 });
|
||||
const root = newSplit("h", target, sibling);
|
||||
const next = adjustFontSize(root, target.id, 2) as SplitNode;
|
||||
expect((next.a as LeafNode).fontSizeOffset).toBe(2);
|
||||
expect((next.b as LeafNode).fontSizeOffset).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("adjustAllFontSizes", () => {
|
||||
it("shifts every leaf by the same delta and preserves independence", () => {
|
||||
const a = newLeaf({ fontSizeOffset: 0 });
|
||||
const b = newLeaf({ fontSizeOffset: 2 });
|
||||
const c = newLeaf({ fontSizeOffset: -1 });
|
||||
const root = newSplit("h", a, newSplit("v", b, c));
|
||||
const next = adjustAllFontSizes(root, 1);
|
||||
const offsets = Array.from(walkLeaves(next)).map((l) => l.fontSizeOffset ?? 0);
|
||||
expect(offsets).toEqual([1, 3, 0]);
|
||||
});
|
||||
|
||||
it("delta=null resets every leaf to default", () => {
|
||||
const a = newLeaf({ fontSizeOffset: 4 });
|
||||
const b = newLeaf({ fontSizeOffset: -3 });
|
||||
const root = newSplit("h", a, b);
|
||||
const next = adjustAllFontSizes(root, null);
|
||||
for (const leaf of walkLeaves(next)) {
|
||||
expect(leaf.fontSizeOffset).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("MUST NOT swap any leaf id", () => {
|
||||
const a = newLeaf({ fontSizeOffset: 1 });
|
||||
const b = newLeaf();
|
||||
const root = newSplit("h", a, b);
|
||||
const idsBefore = leafIds(root);
|
||||
const next = adjustAllFontSizes(root, 1);
|
||||
expect(leafIds(next)).toEqual(idsBefore);
|
||||
});
|
||||
|
||||
it("returns the same root reference when nothing changes (e.g. all at min, delta < 0)", () => {
|
||||
const minOffset = MIN_FONT_SIZE - DEFAULT_FONT_SIZE;
|
||||
const a = newLeaf({ fontSizeOffset: minOffset });
|
||||
const b = newLeaf({ fontSizeOffset: minOffset });
|
||||
const root = newSplit("h", a, b);
|
||||
expect(adjustAllFontSizes(root, -1)).toBe(root);
|
||||
});
|
||||
});
|
||||
|
||||
describe("presets", () => {
|
||||
it("presetSingle returns a single leaf with the provided distro", () => {
|
||||
const t = presetSingle({ distro: "Ubuntu" });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue