Add M3: APPDATA persistence + presets + per-pane distro/label
Backend:
- save_workspace / load_workspace Tauri commands writing to
%APPDATA%\com.megaproxy.tiletopia\workspace.json with atomic
tmp+rename. Path from app.path().app_config_dir() (no dirs crate).
Layout helpers:
- tree.ts: changeDistro (with id swap to force XtermPane remount via
{#key}), changeLabel, presetSingle / TwoColumns / ThreeColumns /
TwoRows / TwoByTwo.
- New ops.ts with PaneOps interface bundling split / close /
setDistro / setLabel / distros, drilled through Pane chain
instead of individual callbacks.
UI:
- LeafPane: in-toolbar editable label (click to rename, Enter
saves, Esc cancels) and distro chip popover. Picking a different
distro respawns the pane.
- App.svelte: migrated from localStorage to APPDATA via the new
Tauri commands, debounced 500ms. One-time localStorage migration
on boot. Split inherits parent's distro+cwd. Titlebar preset
buttons with confirm when replacing >1 pane.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1869d08181
commit
64b90ebddb
10 changed files with 434 additions and 74 deletions
141
src/App.svelte
141
src/App.svelte
|
|
@ -1,7 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { listDistros } from "./ipc";
|
||||
import { listDistros, saveWorkspace, loadWorkspace } from "./ipc";
|
||||
import Pane from "./lib/layout/Pane.svelte";
|
||||
import type { PaneOps } from "./lib/layout/ops";
|
||||
import {
|
||||
type TreeNode,
|
||||
type NodeId,
|
||||
|
|
@ -9,12 +10,20 @@
|
|||
newLeaf,
|
||||
splitLeaf,
|
||||
closeLeaf,
|
||||
findLeaf,
|
||||
leafCount,
|
||||
changeDistro,
|
||||
changeLabel,
|
||||
serialize,
|
||||
deserialize,
|
||||
presetSingle,
|
||||
presetTwoColumns,
|
||||
presetThreeColumns,
|
||||
presetTwoRows,
|
||||
presetTwoByTwo,
|
||||
} from "./lib/layout/tree";
|
||||
|
||||
const STORAGE_KEY = "tiletopia.tree.v1";
|
||||
const LEGACY_STORAGE_KEY = "tiletopia.tree.v1";
|
||||
|
||||
let distros = $state<string[]>([]);
|
||||
let defaultDistro = $state<string | undefined>(undefined);
|
||||
|
|
@ -26,14 +35,36 @@
|
|||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Restore saved layout, if any.
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
const t = deserialize(saved);
|
||||
if (t) tree = t;
|
||||
// 1. Try the new APPDATA persistence.
|
||||
let loaded: TreeNode | null = null;
|
||||
try {
|
||||
const json = await loadWorkspace();
|
||||
if (json) loaded = deserialize(json);
|
||||
} catch (e) {
|
||||
console.warn("loadWorkspace failed:", e);
|
||||
}
|
||||
|
||||
// Resolve default distro.
|
||||
// 2. Fall back to the M2 localStorage layout (one-time migration).
|
||||
if (!loaded) {
|
||||
try {
|
||||
const legacy = localStorage.getItem(LEGACY_STORAGE_KEY);
|
||||
if (legacy) {
|
||||
loaded = deserialize(legacy);
|
||||
if (loaded) {
|
||||
// Promote to APPDATA so it survives future loads even without
|
||||
// localStorage. Fire-and-forget; debounced save will catch it too.
|
||||
void saveWorkspace(legacy);
|
||||
}
|
||||
localStorage.removeItem(LEGACY_STORAGE_KEY);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("legacy localStorage migration failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (loaded) tree = loaded;
|
||||
|
||||
// 3. Resolve default distro.
|
||||
try {
|
||||
distros = await listDistros();
|
||||
defaultDistro = distros.find(isInteractiveDistro) ?? distros[0];
|
||||
|
|
@ -41,8 +72,8 @@
|
|||
console.warn("list_distros failed:", 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.
|
||||
// 4. Backfill distro on any leaves that lack one (handles first launch
|
||||
// and trees saved before defaults were resolved).
|
||||
if (defaultDistro) backfillDistro(tree, defaultDistro);
|
||||
|
||||
ready = true;
|
||||
|
|
@ -57,19 +88,31 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Auto-save on every change. $effect re-runs whenever the proxied tree
|
||||
// mutates anywhere (deep reactivity).
|
||||
// ---- debounced auto-save -------------------------------------------------
|
||||
let saveTimer: number | null = null;
|
||||
const SAVE_DEBOUNCE_MS = 500;
|
||||
|
||||
$effect(() => {
|
||||
if (!ready) return;
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, serialize(tree));
|
||||
} catch (e) {
|
||||
console.warn("localStorage save failed:", e);
|
||||
}
|
||||
const json = serialize(tree);
|
||||
if (saveTimer != null) clearTimeout(saveTimer);
|
||||
saveTimer = window.setTimeout(() => {
|
||||
saveTimer = null;
|
||||
saveWorkspace(json).catch((e) =>
|
||||
console.warn("saveWorkspace failed:", e),
|
||||
);
|
||||
}, SAVE_DEBOUNCE_MS);
|
||||
});
|
||||
|
||||
// ---- pane ops ------------------------------------------------------------
|
||||
function handleSplit(leafId: NodeId, orientation: Orientation) {
|
||||
tree = splitLeaf(tree, leafId, orientation, { distro: defaultDistro });
|
||||
// Inherit distro + cwd from the parent leaf so split-from-project
|
||||
// keeps both panes in the same context.
|
||||
const parent = findLeaf(tree, leafId);
|
||||
const inherit = parent
|
||||
? { distro: parent.distro ?? defaultDistro, cwd: parent.cwd }
|
||||
: { distro: defaultDistro };
|
||||
tree = splitLeaf(tree, leafId, orientation, inherit);
|
||||
}
|
||||
|
||||
function handleClose(leafId: NodeId) {
|
||||
|
|
@ -77,11 +120,29 @@
|
|||
tree = next ?? newLeaf({ distro: defaultDistro });
|
||||
}
|
||||
|
||||
function resetLayout() {
|
||||
if (!confirm("Replace current layout with a single pane? This kills all open shells.")) {
|
||||
function handleSetDistro(leafId: NodeId, distro: string) {
|
||||
tree = changeDistro(tree, leafId, distro);
|
||||
}
|
||||
|
||||
function handleSetLabel(leafId: NodeId, label: string | undefined) {
|
||||
tree = changeLabel(tree, leafId, label);
|
||||
}
|
||||
|
||||
const ops: PaneOps = $derived({
|
||||
split: handleSplit,
|
||||
close: handleClose,
|
||||
setDistro: handleSetDistro,
|
||||
setLabel: handleSetLabel,
|
||||
distros,
|
||||
});
|
||||
|
||||
// ---- preset layouts ------------------------------------------------------
|
||||
function applyPreset(make: (d: { distro?: string }) => TreeNode) {
|
||||
const count = leafCount(tree);
|
||||
if (count > 1 && !confirm(`Replace current layout (${count} panes)? This kills all open shells.`)) {
|
||||
return;
|
||||
}
|
||||
tree = newLeaf({ distro: defaultDistro });
|
||||
tree = make({ distro: defaultDistro });
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -105,28 +166,34 @@
|
|||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="presets">
|
||||
<span class="muted">layout:</span>
|
||||
<button class="preset-btn" title="Single pane" onclick={() => applyPreset(presetSingle)}>1</button>
|
||||
<button class="preset-btn" title="Two columns" onclick={() => applyPreset(presetTwoColumns)}>2H</button>
|
||||
<button class="preset-btn" title="Three columns" onclick={() => applyPreset(presetThreeColumns)}>3H</button>
|
||||
<button class="preset-btn" title="Two rows" onclick={() => applyPreset(presetTwoRows)}>2V</button>
|
||||
<button class="preset-btn" title="2 × 2 grid" onclick={() => applyPreset(presetTwoByTwo)}>2×2</button>
|
||||
</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 ready}
|
||||
<Pane node={tree} onSplit={handleSplit} onClose={handleClose} />
|
||||
<Pane node={tree} {ops} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.distros {
|
||||
.distros, .presets {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
.distro-btn {
|
||||
.distro-btn, .preset-btn {
|
||||
font: inherit;
|
||||
font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace;
|
||||
font-size: 11px;
|
||||
|
|
@ -137,7 +204,7 @@
|
|||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.distro-btn:hover {
|
||||
.distro-btn:hover, .preset-btn:hover {
|
||||
background: #2a2a2a;
|
||||
color: #ddd;
|
||||
}
|
||||
|
|
@ -146,6 +213,10 @@
|
|||
color: #cce6ff;
|
||||
border-color: #2a5a8c;
|
||||
}
|
||||
.preset-btn {
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
.muted {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
|
|
@ -156,18 +227,4 @@
|
|||
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