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 type * as Electron from "electron"; 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, WINDOW_FULLSCREEN_STATE_CHANNEL } from "../ipc/channels.ts";import * as PreviewManager from "../preview/Manager.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 DEVELOPMENT_LOAD_RETRY_DELAYS_MS = [100, 250, 500, 1_000, 2_000] as const;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 | ElectronMenu.ElectronMenu | ElectronShell.ElectronShell | ElectronTheme.ElectronTheme | ElectronWindow.ElectronWindow | PreviewManager.PreviewManager; export type DesktopWindowError = | ElectronWindow.ElectronWindowCreateError | PreviewManager.PreviewManagerError; 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 dispatchMenuAction: (action: string) => Effect.Effect<void, DesktopWindowError>; 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";} // 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, }) );} 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); }} 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; // 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); 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 window = yield* electronWindow.create({ width: 1100, height: 780, 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, contextIsolation: true, nodeIntegration: false, sandbox: true, webviewTag: true, }, }); if (environment.platform === "darwin") { window.setAutoHideCursor(false); } 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; } webPreferences.sandbox = true; webPreferences.nodeIntegration = false; webPreferences.nodeIntegrationInSubFrames = false; webPreferences.contextIsolation = false; }); window.webContents.on("context-menu", (event, params) => { event.preventDefault(); const menuTemplate: Electron.MenuItemConstructorOptions[] = []; if (params.misspelledWord) { for (const suggestion of params.dictionarySuggestions.slice(0, 5)) { menuTemplate.push({ label: suggestion, click: () => window.webContents.replaceMisspelling(suggestion), }); } if (params.dictionarySuggestions.length === 0) { menuTemplate.push({ label: "No suggestions", enabled: false }); } menuTemplate.push({ type: "separator" }); } if (Option.isSome(ElectronShell.parseSafeExternalUrl(params.linkURL))) { menuTemplate.push( { label: "Copy Link", click: () => { void runPromise(electronShell.copyText(params.linkURL)); }, }, { type: "separator" }, ); } if (params.mediaType === "image") { menuTemplate.push({ label: "Copy Image", click: () => window.webContents.copyImageAt(params.x, params.y), }); menuTemplate.push({ type: "separator" }); } menuTemplate.push( { role: "cut", enabled: params.editFlags.canCut }, { role: "copy", enabled: params.editFlags.canCopy }, { role: "paste", enabled: params.editFlags.canPaste }, { role: "selectAll", enabled: params.editFlags.canSelectAll }, ); void runPromise(electronMenu.popupTemplate({ window, template: menuTemplate })); }); window.webContents.setWindowOpenHandler(({ url }) => { if (Option.isSome(ElectronShell.parseSafeExternalUrl(url))) { void runPromise(electronShell.openExternal(url)); } return { action: "deny" }; }); window.webContents.on("will-navigate", (event, url) => { if ( isSameOriginRendererNavigation({ applicationUrl, navigationUrl: url, }) ) { return; } event.preventDefault(); if (Option.isSome(ElectronShell.parseSafeExternalUrl(url))) { void runPromise(electronShell.openExternal(url)); } }); window.on("page-title-updated", (event) => { event.preventDefault(); window.setTitle(environment.displayName); }); if (environment.platform === "darwin") { window.on("enter-full-screen", () => { window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, true); }); window.on("leave-full-screen", () => { window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, false); }); } let developmentLoadRetryIndex = 0; let developmentLoadRetryFiber: Fiber.Fiber<void, never> | undefined; const clearDevelopmentLoadRetry = () => { if (developmentLoadRetryFiber === undefined) { return; } const retryFiber = developmentLoadRetryFiber; developmentLoadRetryFiber = undefined; runFork(Fiber.interrupt(retryFiber)); }; const loadApplication = () => { if (window.isDestroyed()) { return; } void window.loadURL(applicationUrl).catch(() => undefined); }; const scheduleDevelopmentLoadRetry = () => { if (developmentLoadRetryFiber !== undefined || window.isDestroyed()) { return undefined; } const retryIndex = Math.min( developmentLoadRetryIndex, DEVELOPMENT_LOAD_RETRY_DELAYS_MS.length - 1, ); const retryInMs = DEVELOPMENT_LOAD_RETRY_DELAYS_MS[retryIndex] ?? 2_000; developmentLoadRetryIndex += 1; developmentLoadRetryFiber = runFork( Effect.sleep(retryInMs).pipe( Effect.andThen( Effect.sync(() => { developmentLoadRetryFiber = undefined; if (!window.isDestroyed()) { loadApplication(); } }), ), ), ); return retryInMs; }; window.webContents.on("did-finish-load", () => { if ( environment.isDevelopment && !isSameOriginRendererNavigation({ applicationUrl, navigationUrl: window.webContents.getURL(), }) ) { return; } clearDevelopmentLoadRetry(); developmentLoadRetryIndex = 0; window.setTitle(environment.displayName); }); window.webContents.on( "did-fail-load", (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { if (!isMainFrame) { return; } const retryInMs = environment.isDevelopment && isRetryableDevelopmentRendererLoadFailure({ applicationUrl, errorCode, isMainFrame, validatedUrl: validatedURL, }) ? scheduleDevelopmentLoadRetry() : undefined; void runPromise( logWindowWarning("main window failed to load", { errorCode, errorDescription, url: validatedURL, ...(retryInMs === undefined ? {} : { retryInMs }), }), ); }, ); window.webContents.on("render-process-gone", (_event, details) => { void runPromise( logWindowWarning("main window render process gone", { reason: details.reason, exitCode: details.exitCode, }), ); }); const revealSubscribers: RevealSubscription[] = [(fire) => window.once("ready-to-show", fire)]; if (environment.platform === "linux") { revealSubscribers.push((fire) => window.webContents.once("did-finish-load", fire)); } bindFirstRevealTrigger(revealSubscribers, () => { // Reveal the real window, then close the connecting splash (if any) so the // two don't overlap and there's no blank gap between them. void runPromise(Effect.andThen(electronWindow.reveal(window), dismissConnectingSplash)); }); loadApplication(); if (environment.isDevelopment) { window.webContents.openDevTools({ mode: "detach" }); } window.on("closed", () => { clearDevelopmentLoadRetry(); void runPromise(electronWindow.clearMain(Option.some(window))); }); return window; }); const createMain = Effect.gen(function* () { const window = yield* createWindow(); yield* electronWindow.setMain(window); yield* logWindowInfo("main window created"); return window; }).pipe(Effect.withSpan("desktop.window.createMain")); const ensureMain = Effect.gen(function* () { const existingWindow = yield* currentMainWindow; if (Option.isSome(existingWindow)) {Lines 1–500 of 631. Load more
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 type * as Electron from "electron"; 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, WINDOW_FULLSCREEN_STATE_CHANNEL } from "../ipc/channels.ts"; import * as PreviewManager from "../preview/Manager.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 DEVELOPMENT_LOAD_RETRY_DELAYS_MS = [100, 250, 500, 1_000, 2_000] as const; 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 | ElectronMenu.ElectronMenu | ElectronShell.ElectronShell | ElectronTheme.ElectronTheme | ElectronWindow.ElectronWindow | PreviewManager.PreviewManager; export type DesktopWindowError = | ElectronWindow.ElectronWindowCreateError | PreviewManager.PreviewManagerError; 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 dispatchMenuAction: (action: string) => Effect.Effect<void, DesktopWindowError>; 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"; } // 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, }) ); } 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); } } 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; // 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); 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 window = yield* electronWindow.create({ width: 1100, height: 780, 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, contextIsolation: true, nodeIntegration: false, sandbox: true, webviewTag: true, }, }); if (environment.platform === "darwin") { window.setAutoHideCursor(false); } 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; } webPreferences.sandbox = true; webPreferences.nodeIntegration = false; webPreferences.nodeIntegrationInSubFrames = false; webPreferences.contextIsolation = false; }); window.webContents.on("context-menu", (event, params) => { event.preventDefault(); const menuTemplate: Electron.MenuItemConstructorOptions[] = []; if (params.misspelledWord) { for (const suggestion of params.dictionarySuggestions.slice(0, 5)) { menuTemplate.push({ label: suggestion, click: () => window.webContents.replaceMisspelling(suggestion), }); } if (params.dictionarySuggestions.length === 0) { menuTemplate.push({ label: "No suggestions", enabled: false }); } menuTemplate.push({ type: "separator" }); } if (Option.isSome(ElectronShell.parseSafeExternalUrl(params.linkURL))) { menuTemplate.push( { label: "Copy Link", click: () => { void runPromise(electronShell.copyText(params.linkURL)); }, }, { type: "separator" }, ); } if (params.mediaType === "image") { menuTemplate.push({ label: "Copy Image", click: () => window.webContents.copyImageAt(params.x, params.y), }); menuTemplate.push({ type: "separator" }); } menuTemplate.push( { role: "cut", enabled: params.editFlags.canCut }, { role: "copy", enabled: params.editFlags.canCopy }, { role: "paste", enabled: params.editFlags.canPaste }, { role: "selectAll", enabled: params.editFlags.canSelectAll }, ); void runPromise(electronMenu.popupTemplate({ window, template: menuTemplate })); }); window.webContents.setWindowOpenHandler(({ url }) => { if (Option.isSome(ElectronShell.parseSafeExternalUrl(url))) { void runPromise(electronShell.openExternal(url)); } return { action: "deny" }; }); window.webContents.on("will-navigate", (event, url) => { if ( isSameOriginRendererNavigation({ applicationUrl, navigationUrl: url, }) ) { return; } event.preventDefault(); if (Option.isSome(ElectronShell.parseSafeExternalUrl(url))) { void runPromise(electronShell.openExternal(url)); } }); window.on("page-title-updated", (event) => { event.preventDefault(); window.setTitle(environment.displayName); }); if (environment.platform === "darwin") { window.on("enter-full-screen", () => { window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, true); }); window.on("leave-full-screen", () => { window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, false); }); } let developmentLoadRetryIndex = 0; let developmentLoadRetryFiber: Fiber.Fiber<void, never> | undefined; const clearDevelopmentLoadRetry = () => { if (developmentLoadRetryFiber === undefined) { return; } const retryFiber = developmentLoadRetryFiber; developmentLoadRetryFiber = undefined; runFork(Fiber.interrupt(retryFiber)); }; const loadApplication = () => { if (window.isDestroyed()) { return; } void window.loadURL(applicationUrl).catch(() => undefined); }; const scheduleDevelopmentLoadRetry = () => { if (developmentLoadRetryFiber !== undefined || window.isDestroyed()) { return undefined; } const retryIndex = Math.min( developmentLoadRetryIndex, DEVELOPMENT_LOAD_RETRY_DELAYS_MS.length - 1, ); const retryInMs = DEVELOPMENT_LOAD_RETRY_DELAYS_MS[retryIndex] ?? 2_000; developmentLoadRetryIndex += 1; developmentLoadRetryFiber = runFork( Effect.sleep(retryInMs).pipe( Effect.andThen( Effect.sync(() => { developmentLoadRetryFiber = undefined; if (!window.isDestroyed()) { loadApplication(); } }), ), ), ); return retryInMs; }; window.webContents.on("did-finish-load", () => { if ( environment.isDevelopment && !isSameOriginRendererNavigation({ applicationUrl, navigationUrl: window.webContents.getURL(), }) ) { return; } clearDevelopmentLoadRetry(); developmentLoadRetryIndex = 0; window.setTitle(environment.displayName); }); window.webContents.on( "did-fail-load", (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { if (!isMainFrame) { return; } const retryInMs = environment.isDevelopment && isRetryableDevelopmentRendererLoadFailure({ applicationUrl, errorCode, isMainFrame, validatedUrl: validatedURL, }) ? scheduleDevelopmentLoadRetry() : undefined; void runPromise( logWindowWarning("main window failed to load", { errorCode, errorDescription, url: validatedURL, ...(retryInMs === undefined ? {} : { retryInMs }), }), ); }, ); window.webContents.on("render-process-gone", (_event, details) => { void runPromise( logWindowWarning("main window render process gone", { reason: details.reason, exitCode: details.exitCode, }), ); }); const revealSubscribers: RevealSubscription[] = [(fire) => window.once("ready-to-show", fire)]; if (environment.platform === "linux") { revealSubscribers.push((fire) => window.webContents.once("did-finish-load", fire)); } bindFirstRevealTrigger(revealSubscribers, () => { // Reveal the real window, then close the connecting splash (if any) so the // two don't overlap and there's no blank gap between them. void runPromise(Effect.andThen(electronWindow.reveal(window), dismissConnectingSplash)); }); loadApplication(); if (environment.isDevelopment) { window.webContents.openDevTools({ mode: "detach" }); } window.on("closed", () => { clearDevelopmentLoadRetry(); void runPromise(electronWindow.clearMain(Option.some(window))); }); return window; }); const createMain = Effect.gen(function* () { const window = yield* createWindow(); yield* electronWindow.setMain(window); yield* logWindowInfo("main window created"); return window; }).pipe(Effect.withSpan("desktop.window.createMain")); const ensureMain = Effect.gen(function* () { const existingWindow = yield* currentMainWindow; if (Option.isSome(existingWindow)) {