import * as Clock from "effect/Clock";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Ref from "effect/Ref"; import * as Electron from "electron"; import { type DesktopSnapShotEvent, DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts"; import * as DesktopAssets from "../app/DesktopAssets.ts";import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";import { makeComponentLogger } from "../app/DesktopObservability.ts";import * as ElectronMenu from "../electron/ElectronMenu.ts";import { getDesktopUrl } from "../electron/ElectronProtocol.ts";import * as ElectronShell from "../electron/ElectronShell.ts";import * as ElectronTheme from "../electron/ElectronTheme.ts";import * as ElectronWindow from "../electron/ElectronWindow.ts";import { MENU_ACTION_CHANNEL, QUIT_SHORTCUT_CHANNEL, SNAP_SHOT_EVENT_CHANNEL, WINDOW_FULLSCREEN_STATE_CHANNEL,} from "../ipc/channels.ts";import * as PreviewManager from "../preview/Manager.ts";import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";import * as DesktopClientSettings from "../settings/DesktopClientSettings.ts";import * as ElectronApp from "../electron/ElectronApp.ts";import { makeQuitShortcutHandler } from "./QuitHold.ts"; const TITLEBAR_HEIGHT = 40;const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linuxconst TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937";const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc";const MAIN_WINDOW_BOUNDS_PERSIST_DEBOUNCE_MS = 500;const DEVELOPMENT_LOAD_RETRY_DELAYS_MS = [100, 250, 500, 1_000, 2_000] as const;// Renderer crash (usually V8 OOM on long sessions) recovery: reload after a// short delay, at most MAX_ATTEMPTS times per rolling WINDOW so a renderer// that dies on boot cannot reload-loop forever.const RENDERER_RECOVERY_RELOAD_DELAY_MS = 500;const RENDERER_RECOVERY_MAX_ATTEMPTS = 3;const RENDERER_RECOVERY_WINDOW_MS = 60_000;const DEVELOPMENT_RETRYABLE_LOAD_ERROR_CODES = new Set([ -2, // ERR_FAILED -7, // ERR_TIMED_OUT -9, // ERR_UNEXPECTED (custom protocol handler rejected) -102, // ERR_CONNECTION_REFUSED -105, // ERR_NAME_NOT_RESOLVED -106, // ERR_INTERNET_DISCONNECTED -118, // ERR_CONNECTION_TIMED_OUT]); type WindowTitleBarOptions = Pick< Electron.BrowserWindowConstructorOptions, "titleBarOverlay" | "titleBarStyle" | "trafficLightPosition">; type DesktopWindowRuntimeServices = | DesktopEnvironment.DesktopEnvironment | DesktopAssets.DesktopAssets | DesktopAppSettings.DesktopAppSettings | DesktopClientSettings.DesktopClientSettings | ElectronApp.ElectronApp | ElectronMenu.ElectronMenu | ElectronShell.ElectronShell | ElectronTheme.ElectronTheme | ElectronWindow.ElectronWindow | PreviewManager.PreviewManager; export type DesktopWindowError = | ElectronWindow.ElectronWindowCreateError | PreviewManager.PreviewManagerError; export type MainWindowZoomDirection = "in" | "out" | "reset"; export class DesktopWindow extends Context.Service< DesktopWindow, { readonly createMain: Effect.Effect<Electron.BrowserWindow, DesktopWindowError>; readonly ensureMain: Effect.Effect<Electron.BrowserWindow, DesktopWindowError>; readonly revealOrCreateMain: Effect.Effect<Electron.BrowserWindow, DesktopWindowError>; readonly activate: Effect.Effect<void, DesktopWindowError>; readonly createMainIfBackendReady: Effect.Effect<void, DesktopWindowError>; // Show a lightweight "Connecting to WSL" splash window immediately (wsl-only // mode), before the WSL backend that serves the renderer is ready. It is // dismissed automatically once the real main window reveals. readonly showConnectingSplash: Effect.Effect<void>; // Marks the primary backend as ready so `createMainIfBackendReady` and the // macOS "activate without windows" path may open the real main window. The // renderer now always loads the local client URL (getDesktopUrl) and connects // to the backend through the connection layer, so the reported httpBaseUrl is // no longer used to point the window at the backend — it is kept only for the // readiness log and to preserve the callback contract the backend pool drives. readonly handleBackendReady: (httpBaseUrl: URL) => Effect.Effect<void, DesktopWindowError>; // Called when the backend transitions back to "not ready" (clean stop, // restart, crash). Clears the latch that lets `activate` auto-create a // window so a "macOS dock click" while the backend is down doesn't // produce a stranded window pointing at nothing. readonly handleBackendNotReady: Effect.Effect<void>; readonly flushMainWindowBounds: Effect.Effect<void>; readonly prepareCaptureReveal: Effect.Effect<void>; readonly dispatchMenuAction: ( action: string, options?: { readonly reveal?: boolean }, ) => Effect.Effect<void, DesktopWindowError>; /** * Push a capture lifecycle event to the renderer. Only `started` reveals the * window; the rest must not interrupt the app the user has switched to. */ readonly dispatchSnapShotEvent: ( event: DesktopSnapShotEvent, ) => Effect.Effect<void, DesktopWindowError>; // Zooms the main window's own webContents. The Electron `zoomIn`/`zoomOut` // menu roles act on whichever webContents has keyboard focus, so with an // embedded preview WebContentsView (or DevTools) focused they zoom the // guest page instead of the app UI. The menu routes here to always target // the main window. readonly zoomMain: (direction: MainWindowZoomDirection) => Effect.Effect<void>; readonly syncAppearance: Effect.Effect<void>; }>()("@t3tools/desktop/window/DesktopWindow") {} const { logInfo: logWindowInfo, logWarning: logWindowWarning } = makeComponentLogger("desktop-window"); function getIconOption( iconPaths: DesktopAssets.DesktopIconPaths, platform: NodeJS.Platform,): { icon: string } | Record<string, never> { if (platform === "darwin") return {}; // macOS uses .icns from app bundle const ext = platform === "win32" ? "ico" : "png"; return Option.match(iconPaths[ext], { onNone: () => ({}), onSome: (icon) => ({ icon }), });} function getInitialWindowBackgroundColor(shouldUseDarkColors: boolean): string { return shouldUseDarkColors ? "#0a0a0a" : "#ffffff";} type DisplayBounds = Pick<Electron.Rectangle, "x" | "y" | "width" | "height">; function windowFitsWithinDisplay( windowBounds: DesktopAppSettings.DesktopWindowBounds, displayBounds: DisplayBounds,): boolean { return ( windowBounds.x >= displayBounds.x && windowBounds.y >= displayBounds.y && windowBounds.x + windowBounds.width <= displayBounds.x + displayBounds.width && windowBounds.y + windowBounds.height <= displayBounds.y + displayBounds.height );} function windowBoundsEqual( left: DesktopAppSettings.DesktopWindowBounds, right: DesktopAppSettings.DesktopWindowBounds,): boolean { return ( left.x === right.x && left.y === right.y && left.width === right.width && left.height === right.height );} export function resolveInitialMainWindowBounds( persistedBounds: DesktopAppSettings.DesktopWindowBounds | null, displays: readonly DisplayBounds[],): DesktopAppSettings.DesktopWindowBounds | typeof DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE { if ( persistedBounds !== null && displays.some((display) => windowFitsWithinDisplay(persistedBounds, display)) ) { return persistedBounds; } return DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE;} // A self-contained "Connecting to WSL" splash, shown immediately in wsl-only// mode while the WSL backend (which serves the renderer) cold-boots. Inlined as// a data URL so it needs no bundled asset and no backend — pure CSS, no JS.function buildConnectingSplashDataUrl(shouldUseDarkColors: boolean): string { const background = getInitialWindowBackgroundColor(shouldUseDarkColors); const label = shouldUseDarkColors ? "#9ca3af" : "#6b7280"; const accent = shouldUseDarkColors ? "#f8fafc" : "#1f2937"; const track = shouldUseDarkColors ? "rgba(248,250,252,0.18)" : "rgba(31,41,55,0.18)"; const html = `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><style>html,body{margin:0;height:100%}body{background:${background};color:${label};font-family:system-ui,-apple-system,'Segoe UI',sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;-webkit-user-select:none;user-select:none;-webkit-app-region:drag}.spinner{width:26px;height:26px;border:3px solid ${track};border-top-color:${accent};border-radius:50%;animation:spin .8s linear infinite}.label{font-size:13px}@keyframes spin{to{transform:rotate(360deg)}}</style></head><body><div class="spinner"></div><div class="label">Connecting to WSL…</div></body></html>`; return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;} export function isSameOriginRendererNavigation(input: { readonly applicationUrl: string; readonly navigationUrl: string;}): boolean { try { return new URL(input.applicationUrl).origin === new URL(input.navigationUrl).origin; } catch { return false; }} export function isRetryableDevelopmentRendererLoadFailure(input: { readonly applicationUrl: string; readonly errorCode: number; readonly isMainFrame: boolean; readonly validatedUrl: string;}): boolean { return ( input.isMainFrame && DEVELOPMENT_RETRYABLE_LOAD_ERROR_CODES.has(input.errorCode) && isSameOriginRendererNavigation({ applicationUrl: input.applicationUrl, navigationUrl: input.validatedUrl, }) );} export function concealPendingQuitWindow( window: Pick< Electron.BrowserWindow, "isDestroyed" | "isFullScreen" | "setFullScreen" | "setOpacity" >,): void { if (window.isDestroyed()) return; if (window.isFullScreen()) { window.setFullScreen(false); } // Electron implements window opacity on macOS and Windows. Linux keeps the // release-gated quit behavior but cannot make the pending window disappear. window.setOpacity(0);} function getWindowTitleBarOptions( shouldUseDarkColors: boolean, platform: NodeJS.Platform,): WindowTitleBarOptions { if (platform === "darwin") { return { titleBarStyle: "hiddenInset", trafficLightPosition: { x: 16, y: 18 }, }; } return { titleBarStyle: "hidden", titleBarOverlay: { color: TITLEBAR_COLOR, height: TITLEBAR_HEIGHT, symbolColor: shouldUseDarkColors ? TITLEBAR_DARK_SYMBOL_COLOR : TITLEBAR_LIGHT_SYMBOL_COLOR, }, };} function syncWindowAppearance( window: Electron.BrowserWindow, shouldUseDarkColors: boolean, platform: NodeJS.Platform,): Effect.Effect<void> { return Effect.sync(() => { if (window.isDestroyed()) { return; } window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors)); const { titleBarOverlay } = getWindowTitleBarOptions(shouldUseDarkColors, platform); if (typeof titleBarOverlay === "object") { window.setTitleBarOverlay(titleBarOverlay); } });} type RevealSubscription = (listener: () => void) => void; function bindFirstRevealTrigger( subscribers: readonly RevealSubscription[], reveal: () => void,): void { let revealed = false; const fire = () => { if (revealed) return; revealed = true; reveal(); }; for (const subscribe of subscribers) { subscribe(fire); }} /** @public Service construction is part of the canonical Effect module API. */export const make = Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const assets = yield* DesktopAssets.DesktopAssets; const electronMenu = yield* ElectronMenu.ElectronMenu; const electronShell = yield* ElectronShell.ElectronShell; const electronTheme = yield* ElectronTheme.ElectronTheme; const electronWindow = yield* ElectronWindow.ElectronWindow; const previewManager = yield* PreviewManager.PreviewManager; const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings; const clientSettings = yield* DesktopClientSettings.DesktopClientSettings; const electronApp = yield* ElectronApp.ElectronApp; // Window-side latch for the primary backend's readiness. Set by // handleBackendReady (driven by the pool's onReady callback), cleared // by handleBackendNotReady (driven by onShutdown). Only consumed by // createMainIfBackendReady, which gates the post-readiness window // open in development and the macOS "activate without windows" path. const backendReadyRef = yield* Ref.make(false); // The transient "Connecting to WSL" splash window, tracked separately so it // is never mistaken for the real main window. const splashWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()); const context = yield* Effect.context<DesktopWindowRuntimeServices>(); const runFork = Effect.runForkWith(context); const runPromise = Effect.runPromiseWith(context); let flushMainWindowBounds: Effect.Effect<void> = Effect.void; const dismissConnectingSplash = Effect.gen(function* () { const splash = yield* Ref.getAndSet(splashWindowRef, Option.none()); if (Option.isSome(splash) && !splash.value.isDestroyed()) { splash.value.close(); } }); // currentMainOrFirst / focusedMainOrFirst fall back to "any first window", // which during WSL-only boot is the connecting splash. The splash is never // registered via setMain, so it must be treated as "no real main window" -- // otherwise ensureMain/activate/dispatchMenuAction latch onto it and never // open (or retry) the real main. That is the failure the pool's swallowed // post-readiness window-open error would otherwise strand the user in: // splash up, backend ready, no main, and activation only re-reveals splash. const withoutSplash = (window: Option.Option<Electron.BrowserWindow>) => Ref.get(splashWindowRef).pipe( Effect.map((splash) => Option.isSome(splash) && Option.isSome(window) && window.value === splash.value ? Option.none<Electron.BrowserWindow>() : window, ), ); const currentMainWindow = electronWindow.currentMainOrFirst.pipe(Effect.flatMap(withoutSplash)); const focusedMainWindow = electronWindow.focusedMainOrFirst.pipe(Effect.flatMap(withoutSplash)); const createWindow = Effect.fn("desktop.window.createWindow")(function* (): Effect.fn.Return< Electron.BrowserWindow, DesktopWindowError > { yield* previewManager.getBrowserSession(); const applicationUrl = getDesktopUrl(environment.isDevelopment); const iconPaths = yield* assets.iconPaths; const iconOption = getIconOption(iconPaths, environment.platform); const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors; const persistedSettings = yield* desktopSettings.get; const persistedBounds = persistedSettings.mainWindowBounds; const displayBoundsResult = yield* Effect.sync(() => { try { return { _tag: "Success" as const, bounds: Electron.screen.getAllDisplays().map((display) => display.bounds), }; } catch (cause) { return { _tag: "Failure" as const, cause }; } }); const displayBounds = displayBoundsResult._tag === "Success" ? displayBoundsResult.bounds : yield* logWindowWarning("failed to read connected displays; using defaults", { cause: displayBoundsResult.cause, }).pipe(Effect.as<readonly Electron.Rectangle[]>([])); const initialBounds = resolveInitialMainWindowBounds(persistedBounds, displayBounds); const restoredPersistedBounds = persistedBounds !== null && initialBounds === persistedBounds; if (persistedBounds !== null && initialBounds === DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE) { yield* logWindowWarning("saved main window bounds could not be restored; using defaults"); } const window = yield* electronWindow.create({ ...initialBounds, minWidth: 840, minHeight: 620, show: false, autoHideMenuBar: true, ...(environment.platform === "darwin" ? { disableAutoHideCursor: true } : {}), backgroundColor: getInitialWindowBackgroundColor(shouldUseDarkColors), ...iconOption, title: environment.displayName, ...getWindowTitleBarOptions(shouldUseDarkColors, environment.platform), webPreferences: { preload: environment.preloadPath, // The window boots hidden (show: false until ready-to-show), and // Chromium throttles hidden renderers: timers coalesce and rAF stops, // which stalls first paint. Boot unthrottled; the first-reveal trigger // re-enables throttling so a hidden or minimized window goes back to // being cheap after it has been shown once. backgroundThrottling: false, contextIsolation: true, nodeIntegration: false, sandbox: true, webviewTag: true, }, }); if (environment.platform === "darwin") { window.setAutoHideCursor(false); } let boundsPersistFiber: Fiber.Fiber<void, never> | undefined; let pendingBoundsPersistFiber: Fiber.Fiber<void, never> | undefined; let boundsPersistenceEnabled = persistedBounds === null || restoredPersistedBounds; const readPersistableBounds = (): DesktopAppSettings.DesktopWindowBounds | null => { if (window.isDestroyed()) { return null; } const bounds = window.isFullScreen() || window.isMaximized() || window.isMinimized() ? window.getNormalBounds() : window.getBounds(); return DesktopAppSettings.normalizeMainWindowBounds({ x: Math.round(bounds.x), y: Math.round(bounds.y), width: Math.round(bounds.width), height: Math.round(bounds.height), }); }; const fallbackWindowBounds = boundsPersistenceEnabled ? null : readPersistableBounds(); const fallbackWindowMaximized = persistedSettings.mainWindowMaximized; const persistCurrentBounds = (): Fiber.Fiber<void, never> | undefined => { if (!boundsPersistenceEnabled) { return pendingBoundsPersistFiber; } const bounds = readPersistableBounds(); if (bounds === null) { return pendingBoundsPersistFiber; } pendingBoundsPersistFiber = runFork( desktopSettings.setMainWindowBounds(bounds, window.isMaximized()).pipe( Effect.asVoid, Effect.catch((error) => logWindowWarning("failed to persist main window bounds", { message: error.message, }), ), ), ); return pendingBoundsPersistFiber; }; const scheduleBoundsPersist = () => { if (!boundsPersistenceEnabled) { const currentBounds = readPersistableBounds(); if ( currentBounds === null || (fallbackWindowBounds !== null && windowBoundsEqual(currentBounds, fallbackWindowBounds) && window.isMaximized() === fallbackWindowMaximized) ) { return; } } boundsPersistenceEnabled = true; if (boundsPersistFiber !== undefined) { const fiber = boundsPersistFiber; boundsPersistFiber = undefined; runFork(Fiber.interrupt(fiber)); } boundsPersistFiber = runFork( Effect.sleep(MAIN_WINDOW_BOUNDS_PERSIST_DEBOUNCE_MS).pipe( Effect.andThen( Effect.sync(() => { boundsPersistFiber = undefined; void persistCurrentBounds(); }), ), ), ); }; const clearBoundsPersist = () => { if (boundsPersistFiber === undefined) { return; } const fiber = boundsPersistFiber; boundsPersistFiber = undefined; runFork(Fiber.interrupt(fiber)); }; const flushBoundsPersist = Effect.sync(() => { clearBoundsPersist(); return persistCurrentBounds(); }).pipe( Effect.flatMap((fiber) => fiber === undefined ? Effect.void : Fiber.join(fiber).pipe(Effect.asVoid), ), ); flushMainWindowBounds = flushBoundsPersist; yield* previewManager.setMainWindow(window); window.webContents.on("will-attach-webview", (event, webPreferences, params) => { if ( typeof params.partition !== "string" || !previewManager.isBrowserPartition(params.partition) ) { event.preventDefault(); return;Lines 1–500 of 1001. Load more
import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; import * as Electron from "electron"; import { type DesktopSnapShotEvent, DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts"; import * as DesktopAssets from "../app/DesktopAssets.ts"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; import { makeComponentLogger } from "../app/DesktopObservability.ts"; import * as ElectronMenu from "../electron/ElectronMenu.ts"; import { getDesktopUrl } from "../electron/ElectronProtocol.ts"; import * as ElectronShell from "../electron/ElectronShell.ts"; import * as ElectronTheme from "../electron/ElectronTheme.ts"; import * as ElectronWindow from "../electron/ElectronWindow.ts"; import { MENU_ACTION_CHANNEL, QUIT_SHORTCUT_CHANNEL, SNAP_SHOT_EVENT_CHANNEL, WINDOW_FULLSCREEN_STATE_CHANNEL, } from "../ipc/channels.ts"; import * as PreviewManager from "../preview/Manager.ts"; import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; import * as DesktopClientSettings from "../settings/DesktopClientSettings.ts"; import * as ElectronApp from "../electron/ElectronApp.ts"; import { makeQuitShortcutHandler } from "./QuitHold.ts"; const TITLEBAR_HEIGHT = 40; const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux const TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937"; const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc"; const MAIN_WINDOW_BOUNDS_PERSIST_DEBOUNCE_MS = 500; const DEVELOPMENT_LOAD_RETRY_DELAYS_MS = [100, 250, 500, 1_000, 2_000] as const; // Renderer crash (usually V8 OOM on long sessions) recovery: reload after a // short delay, at most MAX_ATTEMPTS times per rolling WINDOW so a renderer // that dies on boot cannot reload-loop forever. const RENDERER_RECOVERY_RELOAD_DELAY_MS = 500; const RENDERER_RECOVERY_MAX_ATTEMPTS = 3; const RENDERER_RECOVERY_WINDOW_MS = 60_000; const DEVELOPMENT_RETRYABLE_LOAD_ERROR_CODES = new Set([ -2, // ERR_FAILED -7, // ERR_TIMED_OUT -9, // ERR_UNEXPECTED (custom protocol handler rejected) -102, // ERR_CONNECTION_REFUSED -105, // ERR_NAME_NOT_RESOLVED -106, // ERR_INTERNET_DISCONNECTED -118, // ERR_CONNECTION_TIMED_OUT ]); type WindowTitleBarOptions = Pick< Electron.BrowserWindowConstructorOptions, "titleBarOverlay" | "titleBarStyle" | "trafficLightPosition" >; type DesktopWindowRuntimeServices = | DesktopEnvironment.DesktopEnvironment | DesktopAssets.DesktopAssets | DesktopAppSettings.DesktopAppSettings | DesktopClientSettings.DesktopClientSettings | ElectronApp.ElectronApp | ElectronMenu.ElectronMenu | ElectronShell.ElectronShell | ElectronTheme.ElectronTheme | ElectronWindow.ElectronWindow | PreviewManager.PreviewManager; export type DesktopWindowError = | ElectronWindow.ElectronWindowCreateError | PreviewManager.PreviewManagerError; export type MainWindowZoomDirection = "in" | "out" | "reset"; export class DesktopWindow extends Context.Service< DesktopWindow, { readonly createMain: Effect.Effect<Electron.BrowserWindow, DesktopWindowError>; readonly ensureMain: Effect.Effect<Electron.BrowserWindow, DesktopWindowError>; readonly revealOrCreateMain: Effect.Effect<Electron.BrowserWindow, DesktopWindowError>; readonly activate: Effect.Effect<void, DesktopWindowError>; readonly createMainIfBackendReady: Effect.Effect<void, DesktopWindowError>; // Show a lightweight "Connecting to WSL" splash window immediately (wsl-only // mode), before the WSL backend that serves the renderer is ready. It is // dismissed automatically once the real main window reveals. readonly showConnectingSplash: Effect.Effect<void>; // Marks the primary backend as ready so `createMainIfBackendReady` and the // macOS "activate without windows" path may open the real main window. The // renderer now always loads the local client URL (getDesktopUrl) and connects // to the backend through the connection layer, so the reported httpBaseUrl is // no longer used to point the window at the backend — it is kept only for the // readiness log and to preserve the callback contract the backend pool drives. readonly handleBackendReady: (httpBaseUrl: URL) => Effect.Effect<void, DesktopWindowError>; // Called when the backend transitions back to "not ready" (clean stop, // restart, crash). Clears the latch that lets `activate` auto-create a // window so a "macOS dock click" while the backend is down doesn't // produce a stranded window pointing at nothing. readonly handleBackendNotReady: Effect.Effect<void>; readonly flushMainWindowBounds: Effect.Effect<void>; readonly prepareCaptureReveal: Effect.Effect<void>; readonly dispatchMenuAction: ( action: string, options?: { readonly reveal?: boolean }, ) => Effect.Effect<void, DesktopWindowError>; /** * Push a capture lifecycle event to the renderer. Only `started` reveals the * window; the rest must not interrupt the app the user has switched to. */ readonly dispatchSnapShotEvent: ( event: DesktopSnapShotEvent, ) => Effect.Effect<void, DesktopWindowError>; // Zooms the main window's own webContents. The Electron `zoomIn`/`zoomOut` // menu roles act on whichever webContents has keyboard focus, so with an // embedded preview WebContentsView (or DevTools) focused they zoom the // guest page instead of the app UI. The menu routes here to always target // the main window. readonly zoomMain: (direction: MainWindowZoomDirection) => Effect.Effect<void>; readonly syncAppearance: Effect.Effect<void>; } >()("@t3tools/desktop/window/DesktopWindow") {} const { logInfo: logWindowInfo, logWarning: logWindowWarning } = makeComponentLogger("desktop-window"); function getIconOption( iconPaths: DesktopAssets.DesktopIconPaths, platform: NodeJS.Platform, ): { icon: string } | Record<string, never> { if (platform === "darwin") return {}; // macOS uses .icns from app bundle const ext = platform === "win32" ? "ico" : "png"; return Option.match(iconPaths[ext], { onNone: () => ({}), onSome: (icon) => ({ icon }), }); } function getInitialWindowBackgroundColor(shouldUseDarkColors: boolean): string { return shouldUseDarkColors ? "#0a0a0a" : "#ffffff"; } type DisplayBounds = Pick<Electron.Rectangle, "x" | "y" | "width" | "height">; function windowFitsWithinDisplay( windowBounds: DesktopAppSettings.DesktopWindowBounds, displayBounds: DisplayBounds, ): boolean { return ( windowBounds.x >= displayBounds.x && windowBounds.y >= displayBounds.y && windowBounds.x + windowBounds.width <= displayBounds.x + displayBounds.width && windowBounds.y + windowBounds.height <= displayBounds.y + displayBounds.height ); } function windowBoundsEqual( left: DesktopAppSettings.DesktopWindowBounds, right: DesktopAppSettings.DesktopWindowBounds, ): boolean { return ( left.x === right.x && left.y === right.y && left.width === right.width && left.height === right.height ); } export function resolveInitialMainWindowBounds( persistedBounds: DesktopAppSettings.DesktopWindowBounds | null, displays: readonly DisplayBounds[], ): DesktopAppSettings.DesktopWindowBounds | typeof DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE { if ( persistedBounds !== null && displays.some((display) => windowFitsWithinDisplay(persistedBounds, display)) ) { return persistedBounds; } return DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE; } // A self-contained "Connecting to WSL" splash, shown immediately in wsl-only // mode while the WSL backend (which serves the renderer) cold-boots. Inlined as // a data URL so it needs no bundled asset and no backend — pure CSS, no JS. function buildConnectingSplashDataUrl(shouldUseDarkColors: boolean): string { const background = getInitialWindowBackgroundColor(shouldUseDarkColors); const label = shouldUseDarkColors ? "#9ca3af" : "#6b7280"; const accent = shouldUseDarkColors ? "#f8fafc" : "#1f2937"; const track = shouldUseDarkColors ? "rgba(248,250,252,0.18)" : "rgba(31,41,55,0.18)"; const html = `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><style>html,body{margin:0;height:100%}body{background:${background};color:${label};font-family:system-ui,-apple-system,'Segoe UI',sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;-webkit-user-select:none;user-select:none;-webkit-app-region:drag}.spinner{width:26px;height:26px;border:3px solid ${track};border-top-color:${accent};border-radius:50%;animation:spin .8s linear infinite}.label{font-size:13px}@keyframes spin{to{transform:rotate(360deg)}}</style></head><body><div class="spinner"></div><div class="label">Connecting to WSL…</div></body></html>`; return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; } export function isSameOriginRendererNavigation(input: { readonly applicationUrl: string; readonly navigationUrl: string; }): boolean { try { return new URL(input.applicationUrl).origin === new URL(input.navigationUrl).origin; } catch { return false; } } export function isRetryableDevelopmentRendererLoadFailure(input: { readonly applicationUrl: string; readonly errorCode: number; readonly isMainFrame: boolean; readonly validatedUrl: string; }): boolean { return ( input.isMainFrame && DEVELOPMENT_RETRYABLE_LOAD_ERROR_CODES.has(input.errorCode) && isSameOriginRendererNavigation({ applicationUrl: input.applicationUrl, navigationUrl: input.validatedUrl, }) ); } export function concealPendingQuitWindow( window: Pick< Electron.BrowserWindow, "isDestroyed" | "isFullScreen" | "setFullScreen" | "setOpacity" >, ): void { if (window.isDestroyed()) return; if (window.isFullScreen()) { window.setFullScreen(false); } // Electron implements window opacity on macOS and Windows. Linux keeps the // release-gated quit behavior but cannot make the pending window disappear. window.setOpacity(0); } function getWindowTitleBarOptions( shouldUseDarkColors: boolean, platform: NodeJS.Platform, ): WindowTitleBarOptions { if (platform === "darwin") { return { titleBarStyle: "hiddenInset", trafficLightPosition: { x: 16, y: 18 }, }; } return { titleBarStyle: "hidden", titleBarOverlay: { color: TITLEBAR_COLOR, height: TITLEBAR_HEIGHT, symbolColor: shouldUseDarkColors ? TITLEBAR_DARK_SYMBOL_COLOR : TITLEBAR_LIGHT_SYMBOL_COLOR, }, }; } function syncWindowAppearance( window: Electron.BrowserWindow, shouldUseDarkColors: boolean, platform: NodeJS.Platform, ): Effect.Effect<void> { return Effect.sync(() => { if (window.isDestroyed()) { return; } window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors)); const { titleBarOverlay } = getWindowTitleBarOptions(shouldUseDarkColors, platform); if (typeof titleBarOverlay === "object") { window.setTitleBarOverlay(titleBarOverlay); } }); } type RevealSubscription = (listener: () => void) => void; function bindFirstRevealTrigger( subscribers: readonly RevealSubscription[], reveal: () => void, ): void { let revealed = false; const fire = () => { if (revealed) return; revealed = true; reveal(); }; for (const subscribe of subscribers) { subscribe(fire); } } /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const assets = yield* DesktopAssets.DesktopAssets; const electronMenu = yield* ElectronMenu.ElectronMenu; const electronShell = yield* ElectronShell.ElectronShell; const electronTheme = yield* ElectronTheme.ElectronTheme; const electronWindow = yield* ElectronWindow.ElectronWindow; const previewManager = yield* PreviewManager.PreviewManager; const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings; const clientSettings = yield* DesktopClientSettings.DesktopClientSettings; const electronApp = yield* ElectronApp.ElectronApp; // Window-side latch for the primary backend's readiness. Set by // handleBackendReady (driven by the pool's onReady callback), cleared // by handleBackendNotReady (driven by onShutdown). Only consumed by // createMainIfBackendReady, which gates the post-readiness window // open in development and the macOS "activate without windows" path. const backendReadyRef = yield* Ref.make(false); // The transient "Connecting to WSL" splash window, tracked separately so it // is never mistaken for the real main window. const splashWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()); const context = yield* Effect.context<DesktopWindowRuntimeServices>(); const runFork = Effect.runForkWith(context); const runPromise = Effect.runPromiseWith(context); let flushMainWindowBounds: Effect.Effect<void> = Effect.void; const dismissConnectingSplash = Effect.gen(function* () { const splash = yield* Ref.getAndSet(splashWindowRef, Option.none()); if (Option.isSome(splash) && !splash.value.isDestroyed()) { splash.value.close(); } }); // currentMainOrFirst / focusedMainOrFirst fall back to "any first window", // which during WSL-only boot is the connecting splash. The splash is never // registered via setMain, so it must be treated as "no real main window" -- // otherwise ensureMain/activate/dispatchMenuAction latch onto it and never // open (or retry) the real main. That is the failure the pool's swallowed // post-readiness window-open error would otherwise strand the user in: // splash up, backend ready, no main, and activation only re-reveals splash. const withoutSplash = (window: Option.Option<Electron.BrowserWindow>) => Ref.get(splashWindowRef).pipe( Effect.map((splash) => Option.isSome(splash) && Option.isSome(window) && window.value === splash.value ? Option.none<Electron.BrowserWindow>() : window, ), ); const currentMainWindow = electronWindow.currentMainOrFirst.pipe(Effect.flatMap(withoutSplash)); const focusedMainWindow = electronWindow.focusedMainOrFirst.pipe(Effect.flatMap(withoutSplash)); const createWindow = Effect.fn("desktop.window.createWindow")(function* (): Effect.fn.Return< Electron.BrowserWindow, DesktopWindowError > { yield* previewManager.getBrowserSession(); const applicationUrl = getDesktopUrl(environment.isDevelopment); const iconPaths = yield* assets.iconPaths; const iconOption = getIconOption(iconPaths, environment.platform); const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors; const persistedSettings = yield* desktopSettings.get; const persistedBounds = persistedSettings.mainWindowBounds; const displayBoundsResult = yield* Effect.sync(() => { try { return { _tag: "Success" as const, bounds: Electron.screen.getAllDisplays().map((display) => display.bounds), }; } catch (cause) { return { _tag: "Failure" as const, cause }; } }); const displayBounds = displayBoundsResult._tag === "Success" ? displayBoundsResult.bounds : yield* logWindowWarning("failed to read connected displays; using defaults", { cause: displayBoundsResult.cause, }).pipe(Effect.as<readonly Electron.Rectangle[]>([])); const initialBounds = resolveInitialMainWindowBounds(persistedBounds, displayBounds); const restoredPersistedBounds = persistedBounds !== null && initialBounds === persistedBounds; if (persistedBounds !== null && initialBounds === DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE) { yield* logWindowWarning("saved main window bounds could not be restored; using defaults"); } const window = yield* electronWindow.create({ ...initialBounds, minWidth: 840, minHeight: 620, show: false, autoHideMenuBar: true, ...(environment.platform === "darwin" ? { disableAutoHideCursor: true } : {}), backgroundColor: getInitialWindowBackgroundColor(shouldUseDarkColors), ...iconOption, title: environment.displayName, ...getWindowTitleBarOptions(shouldUseDarkColors, environment.platform), webPreferences: { preload: environment.preloadPath, // The window boots hidden (show: false until ready-to-show), and // Chromium throttles hidden renderers: timers coalesce and rAF stops, // which stalls first paint. Boot unthrottled; the first-reveal trigger // re-enables throttling so a hidden or minimized window goes back to // being cheap after it has been shown once. backgroundThrottling: false, contextIsolation: true, nodeIntegration: false, sandbox: true, webviewTag: true, }, }); if (environment.platform === "darwin") { window.setAutoHideCursor(false); } let boundsPersistFiber: Fiber.Fiber<void, never> | undefined; let pendingBoundsPersistFiber: Fiber.Fiber<void, never> | undefined; let boundsPersistenceEnabled = persistedBounds === null || restoredPersistedBounds; const readPersistableBounds = (): DesktopAppSettings.DesktopWindowBounds | null => { if (window.isDestroyed()) { return null; } const bounds = window.isFullScreen() || window.isMaximized() || window.isMinimized() ? window.getNormalBounds() : window.getBounds(); return DesktopAppSettings.normalizeMainWindowBounds({ x: Math.round(bounds.x), y: Math.round(bounds.y), width: Math.round(bounds.width), height: Math.round(bounds.height), }); }; const fallbackWindowBounds = boundsPersistenceEnabled ? null : readPersistableBounds(); const fallbackWindowMaximized = persistedSettings.mainWindowMaximized; const persistCurrentBounds = (): Fiber.Fiber<void, never> | undefined => { if (!boundsPersistenceEnabled) { return pendingBoundsPersistFiber; } const bounds = readPersistableBounds(); if (bounds === null) { return pendingBoundsPersistFiber; } pendingBoundsPersistFiber = runFork( desktopSettings.setMainWindowBounds(bounds, window.isMaximized()).pipe( Effect.asVoid, Effect.catch((error) => logWindowWarning("failed to persist main window bounds", { message: error.message, }), ), ), ); return pendingBoundsPersistFiber; }; const scheduleBoundsPersist = () => { if (!boundsPersistenceEnabled) { const currentBounds = readPersistableBounds(); if ( currentBounds === null || (fallbackWindowBounds !== null && windowBoundsEqual(currentBounds, fallbackWindowBounds) && window.isMaximized() === fallbackWindowMaximized) ) { return; } } boundsPersistenceEnabled = true; if (boundsPersistFiber !== undefined) { const fiber = boundsPersistFiber; boundsPersistFiber = undefined; runFork(Fiber.interrupt(fiber)); } boundsPersistFiber = runFork( Effect.sleep(MAIN_WINDOW_BOUNDS_PERSIST_DEBOUNCE_MS).pipe( Effect.andThen( Effect.sync(() => { boundsPersistFiber = undefined; void persistCurrentBounds(); }), ), ), ); }; const clearBoundsPersist = () => { if (boundsPersistFiber === undefined) { return; } const fiber = boundsPersistFiber; boundsPersistFiber = undefined; runFork(Fiber.interrupt(fiber)); }; const flushBoundsPersist = Effect.sync(() => { clearBoundsPersist(); return persistCurrentBounds(); }).pipe( Effect.flatMap((fiber) => fiber === undefined ? Effect.void : Fiber.join(fiber).pipe(Effect.asVoid), ), ); flushMainWindowBounds = flushBoundsPersist; yield* previewManager.setMainWindow(window); window.webContents.on("will-attach-webview", (event, webPreferences, params) => { if ( typeof params.partition !== "string" || !previewManager.isBrowserPartition(params.partition) ) { event.preventDefault(); return;