mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: load plugin frontends at runtime and remove plugin ids from the shell
This commit is contained in:
1 parent
49f0286a14
commit
e88d5a52e3
721 files changed
+54265
-44416
No files matched your search
@@ -0,0 +1,175 @@
|
||||
import type {
|
||||
Host,
|
||||
Tab,
|
||||
WorkspacePayload,
|
||||
WorkspaceTabSnapshot,
|
||||
} from "@/types/ui-types";
|
||||
import { getTabType } from "./tab-registry";
|
||||
|
||||
/**
|
||||
* Serializing and restoring the shell's arrangement: which tabs are open,
|
||||
* how they are split, and what the docks show. This is the shell's own
|
||||
* state, so it lives here; the workspaces plugin only stores and names these
|
||||
* snapshots, through app.tabs.getLayout and applyLayout.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Core tab types worth saving in a layout. "dashboard" is left out on
|
||||
* purpose: the shell always keeps one alive as the fallback tab, so it is
|
||||
* not a meaningful part of an arrangement.
|
||||
*/
|
||||
const CORE_CAPTURABLE = new Set([
|
||||
"files",
|
||||
"tunnel",
|
||||
"tmux_monitor",
|
||||
"serial",
|
||||
"homepage",
|
||||
"termix-id",
|
||||
"session-logs",
|
||||
"snippets",
|
||||
"macros",
|
||||
"history",
|
||||
"ssh-tools",
|
||||
]);
|
||||
|
||||
/** Core types reopened as singletons, with an optional preselected host. */
|
||||
const CORE_SINGLETON = new Set([
|
||||
"tunnel",
|
||||
"tmux_monitor",
|
||||
"homepage",
|
||||
"termix-id",
|
||||
"session-logs",
|
||||
"snippets",
|
||||
"macros",
|
||||
"history",
|
||||
"ssh-tools",
|
||||
]);
|
||||
|
||||
export function isCapturableTabType(type: string): boolean {
|
||||
if (CORE_CAPTURABLE.has(type)) return true;
|
||||
const def = getTabType(type);
|
||||
return !!def && def.inLayouts !== false;
|
||||
}
|
||||
|
||||
function opensAsSingleton(type: string): boolean {
|
||||
if (CORE_SINGLETON.has(type)) return true;
|
||||
const def = getTabType(type);
|
||||
return !!def && (!!def.singleton || !!def.hostless);
|
||||
}
|
||||
|
||||
function createSlotId(): string {
|
||||
if (typeof globalThis.crypto?.randomUUID === "function") {
|
||||
return globalThis.crypto.randomUUID();
|
||||
}
|
||||
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps live tabs to snapshots, with a fresh slotId per tab. slotId is a
|
||||
* stable key within the saved payload, distinct from Tab.id, which is
|
||||
* regenerated every time a tab opens.
|
||||
*/
|
||||
export function buildLayoutTabSnapshots(
|
||||
tabs: Tab[],
|
||||
genSlotId: () => string = createSlotId,
|
||||
): { snapshots: WorkspaceTabSnapshot[]; slotIdByTabId: Map<string, string> } {
|
||||
const capturable = tabs.filter((tab) => isCapturableTabType(tab.type));
|
||||
const slotIdByTabId = new Map(capturable.map((tab) => [tab.id, genSlotId()]));
|
||||
|
||||
const snapshots: WorkspaceTabSnapshot[] = capturable.map((tab) => ({
|
||||
slotId: slotIdByTabId.get(tab.id)!,
|
||||
type: tab.type,
|
||||
hostSyncId: tab.type === "serial" ? null : (tab.host?.syncId ?? null),
|
||||
hostNameSnapshot: tab.host?.name ?? null,
|
||||
label: tab.label,
|
||||
customLabel: tab.customLabel,
|
||||
initialFilePath: tab.initialFilePath,
|
||||
initialPath: tab.initialPath,
|
||||
data: tab.data,
|
||||
serialConfig: tab.type === "serial" ? tab.serialConfig : undefined,
|
||||
}));
|
||||
|
||||
return { snapshots, slotIdByTabId };
|
||||
}
|
||||
|
||||
/** Remaps a slotId-keyed array (paneTabIds shape) to live tab ids. */
|
||||
export function remapSlotIds(
|
||||
slotIds: (string | null)[],
|
||||
slotIdToTabId: Map<string, string>,
|
||||
): (string | null)[] {
|
||||
return slotIds.map((slotId) =>
|
||||
slotId != null ? (slotIdToTabId.get(slotId) ?? null) : null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The tab's plugin payload. Payloads saved before tabs carried `data` stored
|
||||
* the fleet a fleet-inventory tab showed as a field of its own.
|
||||
*/
|
||||
export function snapshotData(
|
||||
snapshot: WorkspaceTabSnapshot,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (snapshot.data) return snapshot.data;
|
||||
return snapshot.fleetId !== undefined
|
||||
? { fleetId: snapshot.fleetId }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* How to reopen one saved tab. "singleton" types go through
|
||||
* openSingletonTab with an optional host; "host" types need a resolved host
|
||||
* to open at all.
|
||||
*/
|
||||
export function resolveLayoutTabTarget(
|
||||
snapshot: WorkspaceTabSnapshot,
|
||||
allHosts: Host[],
|
||||
):
|
||||
| { kind: "serial" }
|
||||
| { kind: "singleton"; host?: Host }
|
||||
| { kind: "host"; host: Host }
|
||||
| { kind: "skip" } {
|
||||
if (snapshot.type === "serial") {
|
||||
return snapshot.serialConfig ? { kind: "serial" } : { kind: "skip" };
|
||||
}
|
||||
|
||||
let host: Host | undefined;
|
||||
if (snapshot.hostSyncId) {
|
||||
host = allHosts.find((h) => h.syncId === snapshot.hostSyncId);
|
||||
if (!host) return { kind: "skip" };
|
||||
}
|
||||
|
||||
if (opensAsSingleton(snapshot.type)) {
|
||||
return { kind: "singleton", host };
|
||||
}
|
||||
|
||||
return host ? { kind: "host", host } : { kind: "skip" };
|
||||
}
|
||||
|
||||
export function buildLayoutPayload(input: {
|
||||
tabs: Tab[];
|
||||
activeTabId: string;
|
||||
splitMode: WorkspacePayload["splitMode"];
|
||||
paneTabIds: (string | null)[];
|
||||
rowSizes: number[];
|
||||
rowColSizes: number[][];
|
||||
genSlotId?: () => string;
|
||||
sidebar?: WorkspacePayload["sidebar"];
|
||||
}): WorkspacePayload {
|
||||
const { snapshots, slotIdByTabId } = buildLayoutTabSnapshots(
|
||||
input.tabs,
|
||||
input.genSlotId,
|
||||
);
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
tabs: snapshots,
|
||||
activeSlotId: slotIdByTabId.get(input.activeTabId) ?? null,
|
||||
splitMode: input.splitMode,
|
||||
paneTabIds: input.paneTabIds.map((tabId) =>
|
||||
tabId != null ? (slotIdByTabId.get(tabId) ?? null) : null,
|
||||
),
|
||||
rowSizes: input.rowSizes,
|
||||
rowColSizes: input.rowColSizes,
|
||||
...(input.sidebar ? { sidebar: input.sidebar } : {}),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user