Preserve existing panes when applying a preset
Previously: clicking 1 / 2H / 3H / 2V / 2×2 in the titlebar replaced the whole tree with brand-new empty leaves, killing every shell — and the only safeguard was a window.confirm() that's easy to miss-click. The user lost work whenever they reached for a preset. New behaviour via `reshapeToPreset`: - The preset's shape is built fresh (1, 2, 3, or 4 slots), then existing leaves are spliced into those slots in DFS order. Their id / distro / cwd / label / broadcast all carry over, so the same PaneId is still mapped — the PTY keeps running. - If the preset has MORE slots than existing leaves (e.g. 1 pane → 2×2), the extra slots stay as fresh empty leaves and new shells spawn there. No prompt — pure additive change. - If the preset has FEWER slots than existing leaves (e.g. 8 panes → 2×2), the overflow leaves are returned in `dropped`. We confirm with the user, and if they accept, kill those PTYs explicitly. Tradeoff: split ratios reset to 0.5 (the whole point of "apply preset" is to use its layout). That's an acceptable cost. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2a0c096095
commit
8c3af8f9ee
2 changed files with 68 additions and 10 deletions
36
src/App.tsx
36
src/App.tsx
|
|
@ -22,6 +22,7 @@ import {
|
||||||
changeLabel,
|
changeLabel,
|
||||||
toggleBroadcast as toggleBroadcastInTree,
|
toggleBroadcast as toggleBroadcastInTree,
|
||||||
setAllBroadcast,
|
setAllBroadcast,
|
||||||
|
reshapeToPreset,
|
||||||
serialize,
|
serialize,
|
||||||
deserialize,
|
deserialize,
|
||||||
presetSingle,
|
presetSingle,
|
||||||
|
|
@ -264,18 +265,33 @@ export default function App() {
|
||||||
|
|
||||||
const applyPreset = useCallback(
|
const applyPreset = useCallback(
|
||||||
(make: (d: { distro?: string }) => TreeNode) => {
|
(make: (d: { distro?: string }) => TreeNode) => {
|
||||||
const count = leafCount(tree);
|
const { tree: nextTree, dropped } = reshapeToPreset(tree, make, {
|
||||||
if (
|
distro: defaultDistro,
|
||||||
count > 1 &&
|
});
|
||||||
!window.confirm(
|
|
||||||
`Replace current layout (${count} panes)? This kills all open shells.`,
|
if (dropped.length > 0) {
|
||||||
)
|
const ok = window.confirm(
|
||||||
) {
|
`This preset has fewer slots than your current ${leafCount(tree)} panes. ${dropped.length} pane${dropped.length === 1 ? "" : "s"} will be closed (their shells will be killed). Continue?`,
|
||||||
return;
|
);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
for (const id of dropped) {
|
||||||
|
const paneId = paneIdByLeafRef.current.get(id);
|
||||||
|
if (paneId != null) {
|
||||||
|
void killPane(paneId).catch((e) =>
|
||||||
|
console.warn("killPane failed:", e),
|
||||||
|
);
|
||||||
|
paneIdByLeafRef.current.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (activeLeafId && dropped.includes(activeLeafId)) {
|
||||||
|
setActiveLeafId(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setTree(make({ distro: defaultDistro }));
|
|
||||||
|
setTree(nextTree);
|
||||||
},
|
},
|
||||||
[tree, defaultDistro],
|
[tree, defaultDistro, activeLeafId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const paletteLeaves = useMemo<LeafNode[]>(
|
const paletteLeaves = useMemo<LeafNode[]>(
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,48 @@ export function toggleBroadcast(root: TreeNode, leafId: NodeId): TreeNode {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reshape the tree into the structure produced by `preset`, but PRESERVE
|
||||||
|
* existing leaves (and their PTYs) by copying their id/distro/cwd/label/
|
||||||
|
* broadcast into the preset's slots, in DFS order.
|
||||||
|
*
|
||||||
|
* - If the preset has more slots than existing leaves, the extra slots stay
|
||||||
|
* as their freshly-created (empty) leaves — those panes will spawn new
|
||||||
|
* shells.
|
||||||
|
* - If the preset has fewer slots than existing leaves, the overflow leaves
|
||||||
|
* are returned in `dropped` so the caller can kill their PTYs.
|
||||||
|
*
|
||||||
|
* Split ratios reset to the preset's defaults (0.5 — the user's previous
|
||||||
|
* resize work is discarded; that's the point of "apply preset").
|
||||||
|
*/
|
||||||
|
export function reshapeToPreset(
|
||||||
|
existing: TreeNode,
|
||||||
|
preset: (d: LeafDefaults) => TreeNode,
|
||||||
|
defaults: LeafDefaults,
|
||||||
|
): { tree: TreeNode; dropped: NodeId[] } {
|
||||||
|
const existingLeaves = Array.from(walkLeaves(existing));
|
||||||
|
const tree = preset(defaults);
|
||||||
|
const slots = Array.from(walkLeaves(tree));
|
||||||
|
const dropped: NodeId[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < slots.length; i++) {
|
||||||
|
const src = existingLeaves[i];
|
||||||
|
if (!src) break;
|
||||||
|
const slot = slots[i];
|
||||||
|
slot.id = src.id;
|
||||||
|
if (src.distro !== undefined) slot.distro = src.distro;
|
||||||
|
if (src.cwd !== undefined) slot.cwd = src.cwd;
|
||||||
|
if (src.label !== undefined) slot.label = src.label;
|
||||||
|
if (src.broadcast !== undefined) slot.broadcast = src.broadcast;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = slots.length; i < existingLeaves.length; i++) {
|
||||||
|
dropped.push(existingLeaves[i].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tree, dropped };
|
||||||
|
}
|
||||||
|
|
||||||
/** Force every leaf's broadcast flag to `on`. Returns the same root reference
|
/** Force every leaf's broadcast flag to `on`. Returns the same root reference
|
||||||
* when nothing actually changed, so callers can skip a state update if so. */
|
* when nothing actually changed, so callers can skip a state update if so. */
|
||||||
export function setAllBroadcast(root: TreeNode, on: boolean): TreeNode {
|
export function setAllBroadcast(root: TreeNode, on: boolean): TreeNode {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue