Adds a new `useShellContext` hook with tests to the dashboard, integrated into `App.tsx` and `Header.tsx` to provide shell context plumbing throughout the UI. Fusion-Task-Id: FN-3406
185 lines
7.1 KiB
TypeScript
185 lines
7.1 KiB
TypeScript
import { app, type BrowserWindow, ipcMain, type Tray } from "electron";
|
|
import { setupAutoUpdater, showExportSettingsDialog, showImportSettingsDialog, type NormalizedDesktopRemoteLaunch } from "./native.js";
|
|
import { type EngineStatus, updateTrayStatus } from "./tray.js";
|
|
import {
|
|
applyDeleteProfile,
|
|
applySetActiveProfile,
|
|
buildSavedProfile,
|
|
getDesktopShellModeState,
|
|
readShellSettings,
|
|
writeShellSettings,
|
|
type DesktopShellMode,
|
|
type ShellConnectionProfile,
|
|
} from "./shell-settings.js";
|
|
import type { DesktopRuntimeStatus } from "./local-runtime.js";
|
|
|
|
interface ShellConnectionProfileInput {
|
|
id?: string;
|
|
name: string;
|
|
serverUrl: string;
|
|
authToken?: string | null;
|
|
}
|
|
|
|
interface ShellConnectionState {
|
|
host: "desktop-shell";
|
|
desktopModeState: {
|
|
isFirstRun: boolean;
|
|
desktopMode: DesktopShellMode | null;
|
|
};
|
|
desktopMode?: DesktopShellMode;
|
|
activeProfileId: string | null;
|
|
profiles: ShellConnectionProfile[];
|
|
localRuntime?: DesktopRuntimeStatus;
|
|
}
|
|
|
|
type DesktopLaunchMode = "choose" | "local" | "remote";
|
|
|
|
interface RegisterIpcOptions {
|
|
onDesktopModeChange?: (mode: DesktopShellMode) => Promise<void>;
|
|
onDesktopLaunchModeChange?: (mode: DesktopLaunchMode) => Promise<void>;
|
|
getRuntimeStatus?: () => DesktopRuntimeStatus;
|
|
startLocalRuntime?: () => Promise<DesktopRuntimeStatus>;
|
|
stopLocalRuntime?: () => Promise<DesktopRuntimeStatus>;
|
|
getServerPort?: () => number | undefined;
|
|
getDesktopLaunchMode?: () => DesktopLaunchMode;
|
|
getDesktopLaunchContext?: () => NormalizedDesktopRemoteLaunch | null;
|
|
}
|
|
|
|
function isDesktopLaunchMode(value: unknown): value is DesktopLaunchMode {
|
|
return value === "choose" || value === "local" || value === "remote";
|
|
}
|
|
|
|
function toShellState(
|
|
settings: Awaited<ReturnType<typeof readShellSettings>>,
|
|
runtimeStatus?: DesktopRuntimeStatus,
|
|
): ShellConnectionState {
|
|
return {
|
|
host: "desktop-shell",
|
|
desktopModeState: getDesktopShellModeState(settings),
|
|
desktopMode: settings.desktopMode ?? undefined,
|
|
activeProfileId: settings.activeProfileId,
|
|
profiles: settings.profiles,
|
|
localRuntime: runtimeStatus ?? { source: "none", state: "stopped" },
|
|
};
|
|
}
|
|
|
|
async function emitShellState(
|
|
mainWindow: BrowserWindow,
|
|
getRuntimeStatus?: () => DesktopRuntimeStatus,
|
|
): Promise<ShellConnectionState> {
|
|
const state = toShellState(await readShellSettings(), getRuntimeStatus?.());
|
|
mainWindow.webContents.send("shell:state", state);
|
|
return state;
|
|
}
|
|
|
|
export function registerIpcHandlers(mainWindow: BrowserWindow, tray: Tray, options: RegisterIpcOptions = {}): void {
|
|
ipcMain.handle("window:minimize", () => mainWindow.minimize());
|
|
ipcMain.handle("window:maximize", () => {
|
|
const isCurrentlyMaximized = mainWindow.isMaximized();
|
|
if (isCurrentlyMaximized) {
|
|
mainWindow.unmaximize();
|
|
return false;
|
|
}
|
|
mainWindow.maximize();
|
|
return true;
|
|
});
|
|
ipcMain.handle("window:close", () => mainWindow.close());
|
|
ipcMain.handle("window:isMaximized", () => mainWindow.isMaximized());
|
|
ipcMain.handle("platform:get", () => process.platform);
|
|
|
|
ipcMain.handle("app:getSystemInfo", () => ({
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
electronVersion: process.versions.electron,
|
|
nodeVersion: process.versions.node,
|
|
appVersion: app.getVersion(),
|
|
}));
|
|
|
|
ipcMain.handle("app:checkForUpdates", () => {
|
|
try {
|
|
setupAutoUpdater(mainWindow);
|
|
return { status: "checking" as const };
|
|
} catch (error) {
|
|
return { status: "error" as const, error: error instanceof Error ? error.message : String(error) };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("tray:updateStatus", (_event, status: EngineStatus) => updateTrayStatus(tray, status));
|
|
ipcMain.handle("native:showExportDialog", () => showExportSettingsDialog(mainWindow));
|
|
ipcMain.handle("native:showImportDialog", () => showImportSettingsDialog(mainWindow));
|
|
ipcMain.handle("app:getServerPort", () => options.getServerPort?.());
|
|
|
|
ipcMain.handle("desktopRuntime:getStatus", async () => options.getRuntimeStatus?.() ?? { source: "none", state: "stopped" });
|
|
ipcMain.handle("desktopRuntime:startLocal", async () => options.startLocalRuntime?.() ?? { source: "none", state: "stopped" });
|
|
ipcMain.handle("desktopRuntime:stopLocal", async () => options.stopLocalRuntime?.() ?? { source: "none", state: "stopped" });
|
|
ipcMain.handle("desktopLaunchMode:getMode", async () => options.getDesktopLaunchMode?.() ?? "choose");
|
|
ipcMain.handle("desktopLaunchMode:getContext", async () => options.getDesktopLaunchContext?.() ?? null);
|
|
ipcMain.handle("desktopLaunchMode:setMode", async (_event, mode: unknown) => {
|
|
if (!isDesktopLaunchMode(mode)) {
|
|
throw new Error("Invalid desktop launch mode");
|
|
}
|
|
|
|
if (options.onDesktopLaunchModeChange) {
|
|
await options.onDesktopLaunchModeChange(mode);
|
|
return options.getDesktopLaunchMode?.() ?? mode;
|
|
}
|
|
|
|
if ((mode === "local" || mode === "remote") && options.onDesktopModeChange) {
|
|
await options.onDesktopModeChange(mode);
|
|
return options.getDesktopLaunchMode?.() ?? mode;
|
|
}
|
|
|
|
return options.getDesktopLaunchMode?.() ?? mode;
|
|
});
|
|
|
|
ipcMain.handle("shell:getState", () => readShellSettings().then((settings) => toShellState(settings, options.getRuntimeStatus?.())));
|
|
ipcMain.handle("shell:listProfiles", async () => (await readShellSettings()).profiles);
|
|
|
|
ipcMain.handle("shell:saveProfile", async (_event, profile: ShellConnectionProfileInput) => {
|
|
const settings = await readShellSettings();
|
|
const nextProfile = buildSavedProfile(settings, profile);
|
|
const existing = settings.profiles.find((item) => item.id === nextProfile.id);
|
|
|
|
settings.profiles = existing
|
|
? settings.profiles.map((item) => (item.id === existing.id ? nextProfile : item))
|
|
: [...settings.profiles, nextProfile];
|
|
await writeShellSettings(settings);
|
|
await emitShellState(mainWindow, options.getRuntimeStatus);
|
|
return nextProfile;
|
|
});
|
|
|
|
ipcMain.handle("shell:deleteProfile", async (_event, profileId: string) => {
|
|
const settings = applyDeleteProfile(await readShellSettings(), profileId);
|
|
await writeShellSettings(settings);
|
|
await emitShellState(mainWindow, options.getRuntimeStatus);
|
|
});
|
|
|
|
ipcMain.handle("shell:setActiveProfile", async (_event, profileId: string | null) => {
|
|
const settings = applySetActiveProfile(await readShellSettings(), profileId);
|
|
await writeShellSettings(settings);
|
|
return emitShellState(mainWindow, options.getRuntimeStatus);
|
|
});
|
|
|
|
ipcMain.handle("shell:getDesktopModeState", async () => {
|
|
const settings = await readShellSettings();
|
|
return getDesktopShellModeState(settings);
|
|
});
|
|
|
|
ipcMain.handle("shell:setDesktopMode", async (_event, mode: DesktopShellMode) => {
|
|
const settings = await readShellSettings();
|
|
settings.desktopMode = mode;
|
|
settings.hasCompletedModeSelection = true;
|
|
await writeShellSettings(settings);
|
|
await options.onDesktopModeChange?.(mode);
|
|
return emitShellState(mainWindow, options.getRuntimeStatus);
|
|
});
|
|
|
|
ipcMain.handle("shell:startQrScan", async () => {
|
|
throw new Error("QR scanning is not available in desktop shell");
|
|
});
|
|
|
|
ipcMain.handle("shell:openConnectionManager", () => {
|
|
mainWindow.webContents.send("shell:open-connection-manager");
|
|
});
|
|
}
|