mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
feat: shell controller, TabStage, layout and theme plugins with repair view
This commit is contained in:
1 parent
aff29a9581
commit
86161d66e2
120 files changed
+23693
-3381
No files matched your search
@@ -0,0 +1,165 @@
|
||||
import { useEffect } from "react";
|
||||
import type { WorkspacePayload } from "@/types/ui-types";
|
||||
import { createId } from "@/lib/create-id";
|
||||
import { rightDockableIds } from "@/sidebar/rail-items";
|
||||
import {
|
||||
buildLayoutPayload,
|
||||
resolveLayoutSplits,
|
||||
resolveLayoutTabTarget,
|
||||
snapshotData,
|
||||
} from "@/shell/shell-layout";
|
||||
import { addSplitTab, makeSplitTab } from "@/shell/split/split-tabs";
|
||||
import { listPanes } from "@/shell/split/split-tree";
|
||||
import { setShellLayoutProvider } from "@/plugin-host/shell-bridge";
|
||||
import { useSplitLabel, type TabsState } from "./use-tabs";
|
||||
import type { ShellRegions } from "./types";
|
||||
|
||||
/** Saving and applying a whole arrangement, for workspaces and plugins. */
|
||||
export function useWorkspaceLayout(
|
||||
tabsState: TabsState,
|
||||
regions: ShellRegions,
|
||||
) {
|
||||
const splitLabel = useSplitLabel();
|
||||
const {
|
||||
tabs,
|
||||
activeTabId,
|
||||
tabsReady,
|
||||
tabsRef,
|
||||
hostsRef,
|
||||
setTabs,
|
||||
setActiveTabId,
|
||||
suppressPaneTargetRef,
|
||||
pendingPaneTargetRef,
|
||||
} = tabsState;
|
||||
|
||||
function buildWorkspacePayload(): WorkspacePayload {
|
||||
return buildLayoutPayload({
|
||||
tabs,
|
||||
activeTabId,
|
||||
sidebar: {
|
||||
left: {
|
||||
view: regions.left.view,
|
||||
open: regions.left.open,
|
||||
width: regions.left.width,
|
||||
},
|
||||
right: {
|
||||
view: regions.right.view,
|
||||
open: regions.right.view !== null,
|
||||
width: regions.right.width,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function applyLayout(
|
||||
payload: WorkspacePayload,
|
||||
name: string,
|
||||
): Promise<{ skipped: string[] }> {
|
||||
// Tear down the current arrangement the same way an individual tab close does.
|
||||
for (const tab of [...tabsRef.current]) {
|
||||
tabsState.doCloseTab(tab.id);
|
||||
}
|
||||
// Tabs reopened here must not be pulled into a pane of the old layout.
|
||||
suppressPaneTargetRef.current = true;
|
||||
pendingPaneTargetRef.current = null;
|
||||
try {
|
||||
const slotIdToNewTabId = new Map<string, string>();
|
||||
const skippedTabs: string[] = [];
|
||||
|
||||
for (const snapshot of payload.tabs) {
|
||||
const target = resolveLayoutTabTarget(snapshot, hostsRef.current);
|
||||
|
||||
if (target.kind === "skip") {
|
||||
skippedTabs.push(snapshot.hostNameSnapshot || snapshot.label);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target.kind === "singleton") {
|
||||
const newTabId = tabsState.openSingletonTab(
|
||||
snapshot.type,
|
||||
undefined,
|
||||
target.host,
|
||||
snapshotData(snapshot),
|
||||
);
|
||||
slotIdToNewTabId.set(snapshot.slotId, newTabId ?? snapshot.type);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target.kind === "host") {
|
||||
const newTabId = tabsState.openTab(
|
||||
target.host,
|
||||
snapshot.type,
|
||||
{
|
||||
instanceId: createId(),
|
||||
restoredSessionId: null,
|
||||
savedLabel: snapshot.customLabel ?? snapshot.label,
|
||||
},
|
||||
{ data: snapshotData(snapshot) },
|
||||
);
|
||||
slotIdToNewTabId.set(snapshot.slotId, newTabId);
|
||||
}
|
||||
}
|
||||
|
||||
const splitIdBySlotId = new Map<string, string>();
|
||||
const splitIdByTabId = new Map<string, string>();
|
||||
let firstSplitId: string | null = null;
|
||||
resolveLayoutSplits(payload, slotIdToNewTabId).forEach((saved, index) => {
|
||||
const splitTab = makeSplitTab(
|
||||
createId(),
|
||||
saved.label || name || splitLabel(index + 1),
|
||||
saved.split,
|
||||
);
|
||||
if (saved.slotId) splitIdBySlotId.set(saved.slotId, splitTab.id);
|
||||
for (const pane of listPanes(saved.split.root)) {
|
||||
if (pane.tabId) splitIdByTabId.set(pane.tabId, splitTab.id);
|
||||
}
|
||||
firstSplitId ??= splitTab.id;
|
||||
setTabs((prev) => addSplitTab(prev, splitTab));
|
||||
});
|
||||
|
||||
// A tab that now sits in a split is shown through its split.
|
||||
const activeSlot = payload.activeSlotId;
|
||||
const activeTabInLayout = activeSlot
|
||||
? slotIdToNewTabId.get(activeSlot)
|
||||
: undefined;
|
||||
const activeId =
|
||||
(activeSlot ? splitIdBySlotId.get(activeSlot) : undefined) ??
|
||||
(activeTabInLayout
|
||||
? (splitIdByTabId.get(activeTabInLayout) ?? activeTabInLayout)
|
||||
: undefined) ??
|
||||
firstSplitId ??
|
||||
"dashboard";
|
||||
setActiveTabId(activeId);
|
||||
|
||||
// Older payloads predate the sidebar field, so leave the docks alone then.
|
||||
const sidebar = payload.sidebar;
|
||||
if (sidebar) {
|
||||
if (sidebar.left.view) {
|
||||
regions.left.setView(
|
||||
sidebar.left.view === "split-screen" ? "hosts" : sidebar.left.view,
|
||||
);
|
||||
}
|
||||
regions.left.setOpen(sidebar.left.open);
|
||||
if (sidebar.left.width) regions.left.setWidth(sidebar.left.width);
|
||||
|
||||
const right = sidebar.right.open ? sidebar.right.view : null;
|
||||
regions.right.setView(
|
||||
right && rightDockableIds().includes(right) ? right : null,
|
||||
);
|
||||
if (sidebar.right.width) regions.right.setWidth(sidebar.right.width);
|
||||
}
|
||||
|
||||
return { skipped: skippedTabs };
|
||||
} finally {
|
||||
suppressPaneTargetRef.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setShellLayoutProvider({
|
||||
getLayout: () => (tabsReady ? buildWorkspacePayload() : null),
|
||||
applyLayout: (layout, options) =>
|
||||
applyLayout(layout as WorkspacePayload, options?.name ?? ""),
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user