Add M2 splits-tree layout
- src/lib/layout/tree.ts: pure helpers + types (newLeaf, splitLeaf, closeLeaf, replaceById, serialize/deserialize with shape-checking). - SplitNode.svelte: flex container with pointer-captured gutter drag. - LeafPane.svelte: per-pane toolbar (split-right ⇥, split-down ⇣, close ×) over the existing XtermPane. - Pane.svelte: recursive dispatcher between SplitNode and LeafPane, keyed on leaf.id so swaps unmount XtermPane cleanly (kills PTY). - App.svelte: tree-as-state with split/close handlers, auto-save to localStorage on every \$effect tick. Titlebar shows clickable distro buttons setting the default for new panes; existing panes keep theirs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9beab64e00
commit
efcdf6a9ce
7 changed files with 531 additions and 59 deletions
137
src/App.svelte
137
src/App.svelte
|
|
@ -1,35 +1,87 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import XtermPane from "./components/XtermPane.svelte";
|
||||
import { listDistros } from "./ipc";
|
||||
import Pane from "./lib/layout/Pane.svelte";
|
||||
import {
|
||||
type TreeNode,
|
||||
type NodeId,
|
||||
type Orientation,
|
||||
newLeaf,
|
||||
splitLeaf,
|
||||
closeLeaf,
|
||||
leafCount,
|
||||
serialize,
|
||||
deserialize,
|
||||
} from "./lib/layout/tree";
|
||||
|
||||
const STORAGE_KEY = "tiletopia.tree.v1";
|
||||
|
||||
let distros = $state<string[]>([]);
|
||||
let selected = $state<string | undefined>(undefined);
|
||||
let status = $state("starting…");
|
||||
let statusOk = $state(true);
|
||||
let loadError = $state<string | null>(null);
|
||||
let defaultDistro = $state<string | undefined>(undefined);
|
||||
let ready = $state(false);
|
||||
let tree = $state<TreeNode>(newLeaf());
|
||||
|
||||
function isInteractiveDistro(name: string): boolean {
|
||||
return !name.toLowerCase().startsWith("docker-desktop");
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Restore saved layout, if any.
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
const t = deserialize(saved);
|
||||
if (t) tree = t;
|
||||
}
|
||||
|
||||
// Resolve default distro.
|
||||
try {
|
||||
const d = await listDistros();
|
||||
console.log("listDistros() returned:", d);
|
||||
distros = d;
|
||||
// Pick fresh every mount (HMR can preserve $state across reloads).
|
||||
selected = d.find(isInteractiveDistro) ?? d[0] ?? undefined;
|
||||
console.log("default selected:", selected);
|
||||
distros = await listDistros();
|
||||
defaultDistro = distros.find(isInteractiveDistro) ?? distros[0];
|
||||
} catch (e) {
|
||||
console.warn("list_distros failed:", e);
|
||||
loadError = String(e);
|
||||
}
|
||||
|
||||
// If any leaf in the (possibly-restored) tree has no distro, fill in the default.
|
||||
// Handles first launch and trees saved before defaults were set.
|
||||
if (defaultDistro) backfillDistro(tree, defaultDistro);
|
||||
|
||||
ready = true;
|
||||
});
|
||||
|
||||
function backfillDistro(node: TreeNode, fallback: string) {
|
||||
if (node.kind === "leaf") {
|
||||
if (!node.distro) node.distro = fallback;
|
||||
} else {
|
||||
backfillDistro(node.a, fallback);
|
||||
backfillDistro(node.b, fallback);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-save on every change. $effect re-runs whenever the proxied tree
|
||||
// mutates anywhere (deep reactivity).
|
||||
$effect(() => {
|
||||
if (!ready) return;
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, serialize(tree));
|
||||
} catch (e) {
|
||||
console.warn("localStorage save failed:", e);
|
||||
}
|
||||
});
|
||||
|
||||
function pick(d: string) {
|
||||
console.log("user picked distro:", d);
|
||||
selected = d;
|
||||
function handleSplit(leafId: NodeId, orientation: Orientation) {
|
||||
tree = splitLeaf(tree, leafId, orientation, { distro: defaultDistro });
|
||||
}
|
||||
|
||||
function handleClose(leafId: NodeId) {
|
||||
const next = closeLeaf(tree, leafId);
|
||||
tree = next ?? newLeaf({ distro: defaultDistro });
|
||||
}
|
||||
|
||||
function resetLayout() {
|
||||
if (!confirm("Replace current layout with a single pane? This kills all open shells.")) {
|
||||
return;
|
||||
}
|
||||
tree = newLeaf({ distro: defaultDistro });
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -41,34 +93,29 @@
|
|||
{#if distros.length === 0}
|
||||
<span class="muted">no distros enumerated</span>
|
||||
{:else}
|
||||
<span class="muted">default:</span>
|
||||
{#each distros as d}
|
||||
<button
|
||||
class="distro-btn"
|
||||
class:active={d === selected}
|
||||
onclick={() => pick(d)}
|
||||
>
|
||||
{d}
|
||||
</button>
|
||||
class:active={d === defaultDistro}
|
||||
onclick={() => (defaultDistro = d)}
|
||||
title="Set default distro for new panes"
|
||||
>{d}</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="status {statusOk ? 'ok' : 'err'}">{status}</span>
|
||||
<span class="layout-info">
|
||||
{leafCount(tree)} pane{leafCount(tree) === 1 ? "" : "s"}
|
||||
</span>
|
||||
<button class="reset-btn" onclick={resetLayout} title="Reset to a single pane">
|
||||
Reset
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="pane-wrap">
|
||||
{#if loadError}
|
||||
<pre class="err-pre">listDistros failed: {loadError}</pre>
|
||||
{:else if selected !== undefined || distros.length === 0}
|
||||
{#key selected}
|
||||
<XtermPane
|
||||
distro={selected}
|
||||
onStatus={(msg, ok) => {
|
||||
status = msg;
|
||||
statusOk = ok;
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{#if ready}
|
||||
<Pane node={tree} onSplit={handleSplit} onClose={handleClose} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -103,10 +150,24 @@
|
|||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
.err-pre {
|
||||
color: #d66;
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
.layout-info {
|
||||
margin-left: auto;
|
||||
font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace;
|
||||
color: #777;
|
||||
font-size: 11px;
|
||||
}
|
||||
.reset-btn {
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
background: #2a2a2a;
|
||||
color: #aaa;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 3px;
|
||||
padding: 2px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.reset-btn:hover {
|
||||
background: #3a3a3a;
|
||||
color: #ddd;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue