Files
Termix/src/ui/shell/tab-registry.ts
T

156 lines
4.8 KiB
TypeScript

import type { ComponentType, Ref } from "react";
import type { HostDraft } from "@termix/plugin-sdk/frontend";
import type { Host, Tab } from "@/types/ui-types";
import { createRegistry } from "@/lib/registry";
import { viewOwner } from "@/plugin-host/view-ownership";
/**
* The shell callbacks a registered tab may use. Same bag every tab gets, so
* a plugin tab can do anything a core tab can without new positional props.
*/
export interface TabShellCallbacks {
openTab: (
host: Host | null,
type: string,
options?: {
label?: string;
forceNewTab?: boolean;
data?: Record<string, unknown>;
},
) => void;
openSingletonTab: (
type: string,
options?: { label?: string; data?: Record<string, unknown> },
) => void;
closeTab: (tabId: string) => void;
renameTab: (tabId: string, label: string) => void;
openRailView: (id: string) => void;
/** Closes a rail view wherever it is shown, e.g. when its feature goes away. */
closeRailView: (id: string) => void;
/** Opens the host editor for a new host with these fields filled in. */
openHostEditor?: (draft?: HostDraft) => void;
/** Saves a quick-connect tab's host. Core terminal only. */
saveQuickConnect?: (tab: Tab, host: Host) => Promise<void>;
}
export interface TabRenderProps {
tab: Tab;
host?: Host;
/** The host in the shape SSH-facing components take. */
sshHost?: Record<string, unknown>;
label: string;
isVisible: boolean;
isFocusedPane: boolean;
inSplit?: boolean;
handleRef: Ref<unknown>;
shell: TabShellCallbacks;
}
export interface StandaloneViewProps {
hostId?: string;
view: string;
params: URLSearchParams;
}
export interface TabTypeDef {
/** The tab type. */
id: string;
pluginId?: string;
component: ComponentType<TabRenderProps>;
icon?: ComponentType<{ className?: string }>;
titleKey?: string;
requiresHost?: boolean;
noHostMessageKey?: string;
persistent?: boolean;
singleton?: boolean;
session?: boolean;
hostless?: boolean;
restore?: (host: Host) => boolean;
activityTypes?: string[];
standalone?: ComponentType<StandaloneViewProps>;
standaloneViews?: string[];
panelFrame?: boolean;
/** False keeps this tab out of saved layouts and workspaces. */
inLayouts?: boolean;
/** Panels that type into a terminal act on the last one of these focused. */
commandTarget?: boolean;
/** Paints its own background; its frame stays transparent. */
ownBackground?: boolean;
/** Every open is a new tab. */
multiInstance?: boolean;
preload?: () => Promise<unknown>;
}
const registry = createRegistry<TabTypeDef>();
export const registerTabType = registry.register;
export const getTabType = registry.get;
export const listTabTypes = registry.list;
export const useTabTypes = registry.useList;
export const resetTabTypes = registry.reset;
/** Core tab types that are saved and reopened after login. */
const CORE_PERSISTENT = new Set<string>();
/** Core tab types that hold a live session (close confirm, refresh). */
const CORE_SESSION = new Set<string>();
/** Core tab types that restore without a host. */
const CORE_HOSTLESS = new Set(["dashboard"]);
/**
* A plugin tab type nobody registered right now: its plugin is off, failed or
* still loading. It keeps the behaviour it had, so a saved tab survives the
* plugin being disabled instead of being dropped by the next sync.
*/
function isOwnedButAbsent(type: string): boolean {
return !registry.get(type) && !!viewOwner("tab", type);
}
export function isPersistentTabType(type: string): boolean {
if (CORE_PERSISTENT.has(type)) return true;
const def = registry.get(type);
if (def) return !!def.persistent;
return isOwnedButAbsent(type);
}
export function isSessionTabType(type: string): boolean {
return CORE_SESSION.has(type) || !!registry.get(type)?.session;
}
function isHostlessTabType(type: string): boolean {
return CORE_HOSTLESS.has(type) || !!registry.get(type)?.hostless;
}
/** Whether a saved tab of this type may be restored for this host. */
export function canRestoreTabType(
type: string,
host: Host | undefined,
): boolean {
const def = registry.get(type);
if (!host) return isHostlessTabType(type) || isOwnedButAbsent(type);
return def?.restore ? def.restore(host) : true;
}
/** The tab type a recent-activity entry opens, if a registered tab claims it. */
export function tabTypeForActivity(
activityType: string,
): TabTypeDef | undefined {
return registry
.list()
.find(
(def) =>
def.id === activityType || def.activityTypes?.includes(activityType),
);
}
/** The registered tab serving a `?view=` full-screen link. */
export function standaloneViewFor(view: string): TabTypeDef | undefined {
return registry
.list()
.find(
(def) =>
def.standalone &&
(def.id === view || def.standaloneViews?.includes(view)),
);
}