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,
|
||||
toggleBroadcast as toggleBroadcastInTree,
|
||||
setAllBroadcast,
|
||||
reshapeToPreset,
|
||||
serialize,
|
||||
deserialize,
|
||||
presetSingle,
|
||||
|
|
@ -264,18 +265,33 @@ export default function App() {
|
|||
|
||||
const applyPreset = useCallback(
|
||||
(make: (d: { distro?: string }) => TreeNode) => {
|
||||
const count = leafCount(tree);
|
||||
if (
|
||||
count > 1 &&
|
||||
!window.confirm(
|
||||
`Replace current layout (${count} panes)? This kills all open shells.`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
const { tree: nextTree, dropped } = reshapeToPreset(tree, make, {
|
||||
distro: defaultDistro,
|
||||
});
|
||||
|
||||
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?`,
|
||||
);
|
||||
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[]>(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
* when nothing actually changed, so callers can skip a state update if so. */
|
||||
export function setAllBroadcast(root: TreeNode, on: boolean): TreeNode {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue