import { ContextMenuItemSchema, DesktopAppBrandingSchema, DesktopEnvironmentBootstrapSchema, DesktopThemeSchema, EDITORS, EditorId, PickedThemeFileSchema, PickFolderOptionsSchema, PRIMARY_LOCAL_ENVIRONMENT_ID, REMOTE_CAPABLE_EDITOR_IDS, SystemSettingsPaneSchema, type DesktopEnvironmentBootstrap, type PickedThemeFile, } from "@t3tools/contracts"; import { WORKSPACE_IMAGE_PREVIEW_EXTENSIONS } from "@t3tools/shared/filePreview"; import { isCommandAvailable } from "@t3tools/shared/shell"; import * as NodeOS from "node:os"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts"; import * as DesktopLocalEnvironmentAuth from "../../backend/DesktopLocalEnvironmentAuth.ts"; import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts"; import * as DesktopAppSettings from "../../settings/DesktopAppSettings.ts"; import * as DesktopWslBackend from "../../wsl/DesktopWslBackend.ts"; import * as DesktopWslEnvironment from "../../wsl/DesktopWslEnvironment.ts"; import * as ElectronApp from "../../electron/ElectronApp.ts"; import * as ElectronDialog from "../../electron/ElectronDialog.ts"; import * as ElectronMenu from "../../electron/ElectronMenu.ts"; import * as ElectronShell from "../../electron/ElectronShell.ts"; import * as ElectronTheme from "../../electron/ElectronTheme.ts"; import * as ElectronWindow from "../../electron/ElectronWindow.ts"; import * as IpcChannels from "../channels.ts"; import * as DesktopIpc from "../DesktopIpc.ts"; import { extractDistroFromUncPath, resolveWslPickFolderDefaultPath, wslUncPathToLinuxPath, } from "../../wsl/wslPathParsing.ts"; const ContextMenuPosition = Schema.Struct({ x: Schema.Number, y: Schema.Number, }); const ContextMenuInput = Schema.Struct({ items: Schema.Array(ContextMenuItemSchema), position: Schema.optionalKey(ContextMenuPosition), }); function toWebSocketBaseUrl(httpBaseUrl: URL): string { const url = new URL(httpBaseUrl.href); url.protocol = url.protocol === "https:" ? "wss:" : "ws:"; return url.href; } export const getAppBranding = DesktopIpc.makeSyncIpcMethod({ channel: IpcChannels.GET_APP_BRANDING_CHANNEL, result: Schema.NullOr(DesktopAppBrandingSchema), handler: Effect.fn("desktop.ipc.window.getAppBranding")(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; return environment.branding; }), }); export const getSystemLocale = DesktopIpc.makeSyncIpcMethod({ channel: IpcChannels.GET_SYSTEM_LOCALE_CHANNEL, result: Schema.String, handler: Effect.fn("desktop.ipc.window.getSystemLocale")(function* () { const electronApp = yield* ElectronApp.ElectronApp; return yield* electronApp.systemLocale; }), }); export const getWindowFullscreenState = DesktopIpc.makeSyncIpcMethod({ channel: IpcChannels.GET_WINDOW_FULLSCREEN_STATE_CHANNEL, result: Schema.Boolean, handler: Effect.fn("desktop.ipc.window.getWindowFullscreenState")(function* () { const electronWindow = yield* ElectronWindow.ElectronWindow; const window = yield* electronWindow.currentMainOrFirst; return Option.isSome(window) && window.value.isFullScreen(); }), }); export const getLocalEnvironmentBootstraps = DesktopIpc.makeSyncIpcMethod({ channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL, result: Schema.Array(DesktopEnvironmentBootstrapSchema), handler: Effect.fn("desktop.ipc.window.getLocalEnvironmentBootstraps")(function* () { const pool = yield* DesktopBackendPool.DesktopBackendPool; const instances = yield* pool.list; const bootstraps: DesktopEnvironmentBootstrap[] = []; for (const instance of instances) { const isPrimary = instance.id === PRIMARY_LOCAL_ENVIRONMENT_ID; const config = yield* instance.currentConfig; const snapshot = yield* instance.snapshot; // A secondary backend (e.g. a parallel WSL backend) that hasn't produced // a config yet (mid-registration, before its first start cycle) or that // is retrying a *transient* preflight failure (WSL VM still booting, a // not-yet-built linux server entry) is not listening on a port. We // surface it as a *pending* bootstrap (null endpoints, no token) so the // renderer can show a "Connecting…" indicator while it retries — null // endpoints keep the renderer from dialing the dead port, avoiding the // needless /api/auth/bootstrap/bearer error cycles a real endpoint would // trigger. if (Option.isNone(config) || Option.isSome(config.value.preflightFailure)) { // Skip the primary (same-origin, no "connecting" affordance) and skip a // secondary whose preflight failed *fatally* (no node, wrong version, // missing build tools): it has stopped retrying, so an indefinite // "Connecting…" would be misleading — its error is surfaced by the // WSL-state UI instead. const fatalPreflight = Option.isSome(config) && Option.isSome(config.value.preflightFailure) && config.value.preflightFailure.value.fatal; const stoppedPreflight = Option.isSome(config) && Option.isSome(config.value.preflightFailure) && (!snapshot.desiredRunning || !snapshot.restartScheduled); if (isPrimary || fatalPreflight || stoppedPreflight) continue; bootstraps.push({ id: instance.id, label: yield* instance.label, runningDistro: null, httpBaseUrl: null, wsBaseUrl: null, }); continue; } const { bootstrap, httpBaseUrl } = config.value; const runningDistro = config.value.runningDistro ?? null; bootstraps.push({ id: instance.id, label: runningDistro === null ? yield* instance.label : `WSL (${runningDistro})`, runningDistro, httpBaseUrl: httpBaseUrl.href, wsBaseUrl: toWebSocketBaseUrl(httpBaseUrl), ...(bootstrap.desktopBootstrapToken ? { bootstrapToken: bootstrap.desktopBootstrapToken } : {}), }); } return bootstraps; }), }); // Pull the distro selection out of a backend instance id like // "wsl:ubuntu". Returns null for "wsl:default", which is the sentinel // for "track the user's WSL default distro" and maps to the // wslEnv-derived default at picker time. function extractWslDistroFromEnvironmentId(envId: string): string | null { if (!envId.startsWith(DesktopWslBackend.WSL_INSTANCE_ID_PREFIX)) { return null; } const suffix = envId.slice(DesktopWslBackend.WSL_INSTANCE_ID_PREFIX.length); return suffix === "default" || suffix.length === 0 ? null : suffix; } export const getLocalEnvironmentBearerToken = DesktopIpc.makeIpcMethod({ channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BEARER_TOKEN_CHANNEL, payload: Schema.Void, result: Schema.String, handler: Effect.fn("desktop.ipc.window.getLocalEnvironmentBearerToken")(function* () { const localAuth = yield* DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth; return yield* localAuth.getBearerToken; }), }); export const pickFolder = DesktopIpc.makeIpcMethod({ channel: IpcChannels.PICK_FOLDER_CHANNEL, payload: Schema.UndefinedOr(PickFolderOptionsSchema), result: Schema.NullOr(Schema.String), handler: Effect.fn("desktop.ipc.window.pickFolder")(function* (options) { const dialog = yield* ElectronDialog.ElectronDialog; const electronWindow = yield* ElectronWindow.ElectronWindow; const environment = yield* DesktopEnvironment.DesktopEnvironment; const appSettings = yield* DesktopAppSettings.DesktopAppSettings; const wslEnvironment = yield* DesktopWslEnvironment.DesktopWslEnvironment; // Three picker modes: // - targetEnvironmentId omitted: default to the primary picker. Keeps // the historical behavior unchanged for users who never enabled the // WSL backend, and is what unfamiliar callers should get out of the // box. // - targetEnvironmentId starts with "wsl:": route to the WSL picker // using the distro encoded in the id (or the user's selected // wslDistro when the id is the "wsl:default" sentinel). // - anything else (incl. PRIMARY_LOCAL_ENVIRONMENT_ID): primary picker. const targetId = options?.targetEnvironmentId; const wslDistroFromTarget = targetId !== undefined && targetId.startsWith(DesktopWslBackend.WSL_INSTANCE_ID_PREFIX) ? extractWslDistroFromEnvironmentId(targetId) : null; const useWsl = targetId !== undefined && targetId !== PRIMARY_LOCAL_ENVIRONMENT_ID && targetId.startsWith(DesktopWslBackend.WSL_INSTANCE_ID_PREFIX); const settings = yield* appSettings.get; // Fall back to the persisted wslDistro when the id is the // "wsl:default" sentinel; the orchestrator uses the same fallback // for the actual backend. const wslDistro = useWsl ? (wslDistroFromTarget ?? settings.wslDistro) : null; const defaultPath = useWsl ? Option.fromNullishOr( resolveWslPickFolderDefaultPath( options, { distro: wslDistro }, yield* wslEnvironment.listDistros, Option.getOrNull(yield* wslEnvironment.getUserHome(wslDistro)), ), ) : environment.resolvePickFolderDefaultPath(options); const selectedPath = yield* dialog.pickFolder({ owner: yield* electronWindow.focusedMainOrFirst, defaultPath, }); if (Option.isNone(selectedPath)) { return null; } if (!useWsl) { return selectedPath.value; } const linuxUncPath = wslUncPathToLinuxPath(selectedPath.value); if (linuxUncPath !== null) { return linuxUncPath; } const converted = yield* wslEnvironment.windowsToWslPath( extractDistroFromUncPath(selectedPath.value) ?? wslDistro, selectedPath.value, ); return Option.getOrElse(converted, () => selectedPath.value); }), }); export const pickProjectFavicon = DesktopIpc.makeIpcMethod({ channel: IpcChannels.PICK_PROJECT_FAVICON_CHANNEL, payload: Schema.UndefinedOr(Schema.String), result: Schema.NullOr(Schema.String), handler: Effect.fn("desktop.ipc.window.pickProjectFavicon")(function* (initialPath) { const dialog = yield* ElectronDialog.ElectronDialog; const electronWindow = yield* ElectronWindow.ElectronWindow; const paths = yield* dialog.pickFiles({ owner: yield* electronWindow.focusedMainOrFirst, defaultPath: Option.fromNullishOr(initialPath), multiple: false, filters: [ { name: "Images", extensions: WORKSPACE_IMAGE_PREVIEW_EXTENSIONS.map((extension) => extension.slice(1)), }, ], }); return paths[0] ?? null; }), }); export const setTheme = DesktopIpc.makeIpcMethod({ channel: IpcChannels.SET_THEME_CHANNEL, payload: DesktopThemeSchema, result: Schema.Void, handler: Effect.fn("desktop.ipc.window.setTheme")(function* (theme) { const electronTheme = yield* ElectronTheme.ElectronTheme; yield* electronTheme.setSource(theme); }), }); export const showContextMenu = DesktopIpc.makeIpcMethod({ channel: IpcChannels.CONTEXT_MENU_CHANNEL, payload: ContextMenuInput, result: Schema.NullOr(Schema.String), handler: Effect.fn("desktop.ipc.window.showContextMenu")(function* (input) { const electronMenu = yield* ElectronMenu.ElectronMenu; const electronWindow = yield* ElectronWindow.ElectronWindow; const window = yield* electronWindow.focusedMainOrFirst; if (Option.isNone(window)) { return null; } const selectedItemId = yield* electronMenu.showContextMenu({ window: window.value, items: input.items, position: Option.fromNullishOr(input.position), }); return Option.getOrNull(selectedItemId); }), }); export const openExternal = DesktopIpc.makeIpcMethod({ channel: IpcChannels.OPEN_EXTERNAL_CHANNEL, payload: Schema.String, result: Schema.Boolean, handler: Effect.fn("desktop.ipc.window.openExternal")(function* (url) { const shell = yield* ElectronShell.ElectronShell; return yield* shell.openExternal(url); }), }); export const openSystemSettings = DesktopIpc.makeIpcMethod({ channel: IpcChannels.OPEN_SYSTEM_SETTINGS_CHANNEL, payload: SystemSettingsPaneSchema, result: Schema.Boolean, handler: Effect.fn("desktop.ipc.window.openSystemSettings")(function* (pane) { const shell = yield* ElectronShell.ElectronShell; return yield* shell.openSystemSettings(pane); }), }); export const probeRemoteEditors = DesktopIpc.makeIpcMethod({ channel: IpcChannels.PROBE_REMOTE_EDITORS_CHANNEL, payload: Schema.Undefined, result: Schema.Array(EditorId), // Probes THIS machine (where the renderer runs) for remote-capable editor // CLIs, unlike the server's probe which walks the environment host's PATH. // A Finder-launched app can miss PATH entries; an empty result makes the // renderer fall back to VS Code only, so that fails soft. handler: Effect.fn("desktop.ipc.window.probeRemoteEditors")(function* () { const available: Array = []; for (const editorId of REMOTE_CAPABLE_EDITOR_IDS) { const commands = EDITORS.find((editor) => editor.id === editorId)?.commands; if (!commands) continue; for (const command of commands) { if (yield* isCommandAvailable(command, { env: process.env })) { available.push(editorId); break; } } } return available; }), }); /** Theme files are a few KB; anything larger returns empty text and lets the * renderer reject it by size without the contents ever crossing the bridge. */ const PICKED_THEME_FILE_MAX_BYTES = 256 * 1024; export const pickThemeFiles = DesktopIpc.makeIpcMethod({ channel: IpcChannels.PICK_THEME_FILES_CHANNEL, payload: Schema.Undefined, result: Schema.NullOr(Schema.Array(PickedThemeFileSchema)), handler: Effect.fn("desktop.ipc.window.pickThemeFiles")(function* () { const dialog = yield* ElectronDialog.ElectronDialog; const electronWindow = yield* ElectronWindow.ElectronWindow; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; // The VS Code extensions directory is the same dotfolder on Windows, // macOS, and Linux; when it is missing the picker opens wherever the // platform would by default. const extensionsDir = path.join(NodeOS.homedir(), ".vscode", "extensions"); const defaultPath = yield* fileSystem .exists(extensionsDir) .pipe(Effect.orElseSucceed(() => false)); const paths = yield* dialog.pickFiles({ owner: yield* electronWindow.focusedMainOrFirst, defaultPath: defaultPath ? Option.some(extensionsDir) : Option.none(), filters: [{ name: "JSON", extensions: ["json"] }], multiple: true, }); if (paths.length === 0) { return null; } return yield* Effect.forEach(paths, (filePath) => { const name = path.basename(filePath); return Effect.gen(function* () { const info = yield* fileSystem.stat(filePath); const size = Number(info.size); if (size > PICKED_THEME_FILE_MAX_BYTES) { return { name, size, text: "" } satisfies PickedThemeFile; } const text = yield* fileSystem.readFileString(filePath); return { name, size, text } satisfies PickedThemeFile; }).pipe( // An unreadable file degrades to an entry the renderer reports. Effect.orElseSucceed((): PickedThemeFile => ({ name, size: 0, text: "" })), ); }); }), });