diff --git a/src/App.tsx b/src/App.tsx index 7a4b5ef..d917f79 100644 --- a/src/App.tsx +++ b/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( diff --git a/src/lib/layout/tree.ts b/src/lib/layout/tree.ts index b0d805b..38bfbc8 100644 --- a/src/lib/layout/tree.ts +++ b/src/lib/layout/tree.ts @@ -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 {