// @effect-diagnostics nodeBuiltinImport:off -- This desktop-only service resolves its bundled helper beside the Electron entrypoint. import * as NodePath from "node:path"; import * as HostProcess from "@t3tools/shared/HostProcess"; import type * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Electron from "electron"; import { activateWindowsForeground, isWindowsShellHostedForeground, loadWindowsForegroundApi, } from "./WindowsForeground.ts"; import { startWindowsForegroundFocusThread } from "./WindowsForegroundFocusThread.ts"; function windowsForegroundFocusTarget(window: Electron.BrowserWindow) { return { windowId: window.id, processId: process.pid, title: window.getTitle(), // xa11y reports physical desktop pixels on Windows; Electron bounds are DIPs. bounds: Electron.screen.dipToScreenRect(window, window.getBounds()), contentBounds: Electron.screen.dipToScreenRect(window, window.getContentBounds()), }; } async function isWindowsBrowserWindowForeground(window: Electron.BrowserWindow): Promise { const foreground = await loadWindowsForegroundApi() .then((api) => api.getForegroundWindow()) .catch(() => undefined); if (window.isDestroyed() || foreground === undefined || foreground === 0n) return false; const handle = window.getNativeWindowHandle(); const hwnd = handle.length === 8 ? handle.readBigUInt64LE() : BigInt(handle.readUInt32LE()); return foreground === hwnd; } const ElectronWindowCreateOptions = Schema.Struct({ title: Schema.NullOr(Schema.String), width: Schema.NullOr(Schema.Number), height: Schema.NullOr(Schema.Number), minWidth: Schema.NullOr(Schema.Number), minHeight: Schema.NullOr(Schema.Number), show: Schema.NullOr(Schema.Boolean), modal: Schema.NullOr(Schema.Boolean), frame: Schema.NullOr(Schema.Boolean), transparent: Schema.NullOr(Schema.Boolean), backgroundColor: Schema.NullOr(Schema.String), webPreferences: Schema.Struct({ preload: Schema.NullOr(Schema.String), partition: Schema.NullOr(Schema.String), backgroundThrottling: Schema.NullOr(Schema.Boolean), sandbox: Schema.NullOr(Schema.Boolean), contextIsolation: Schema.NullOr(Schema.Boolean), nodeIntegration: Schema.NullOr(Schema.Boolean), webviewTag: Schema.NullOr(Schema.Boolean), }), }); const ElectronWindowOperation = Schema.Literals([ "list-windows", "get-focused-window", "inspect-window", "reveal-window", "send-window-message", "destroy-window", ]); export class ElectronWindowCreateError extends Schema.TaggedError()( "ElectronWindowCreateError", { options: ElectronWindowCreateOptions, cause: Schema.Defect(), }, ) { override get message(): string { const title = this.options.title === null ? "" : ` "${this.options.title}"`; const dimensions = this.options.width === null || this.options.height === null ? "" : ` (${this.options.width}x${this.options.height})`; return `Failed to create Electron BrowserWindow${title}${dimensions}.`; } } export class ElectronWindowOperationError extends Schema.TaggedError()( "ElectronWindowOperationError", { operation: ElectronWindowOperation, platform: Schema.String, windowId: Schema.NullOr(Schema.Number), channel: Schema.NullOr(Schema.String), cause: Schema.Defect(), }, ) { override get message(): string { const window = this.windowId === null ? "" : ` for window ${this.windowId}`; const channel = this.channel === null ? "" : ` on channel ${JSON.stringify(this.channel)}`; return `Electron window operation ${JSON.stringify(this.operation)} failed${window}${channel} on ${this.platform}.`; } } export class ElectronWindow extends Context.Service< ElectronWindow, { /** Auxiliary windows are never returned as the main-or-first fallback. */ readonly create: ( options: Electron.BrowserWindowConstructorOptions, behavior?: { readonly auxiliary?: boolean }, ) => Effect.Effect; readonly main: Effect.Effect>; readonly currentMainOrFirst: Effect.Effect>; readonly focusedMainOrFirst: Effect.Effect>; readonly setMain: (window: Electron.BrowserWindow) => Effect.Effect; readonly clearMain: (window: Option.Option) => Effect.Effect; readonly prepareReveal: (window: Electron.BrowserWindow) => Effect.Effect; readonly reveal: (window: Electron.BrowserWindow) => Effect.Effect; readonly sendAll: (channel: string, ...args: readonly unknown[]) => Effect.Effect; readonly destroyAll: Effect.Effect; readonly syncAllAppearance: ( sync: (window: Electron.BrowserWindow) => Effect.Effect, ) => Effect.Effect; } >()("@t3tools/desktop/electron/ElectronWindow") {} /** @public */ export const make = Effect.gen(function* () { const platform = yield* HostProcess.Platform; // Lazy-load native accessibility on capture reveal to avoid startup cost for other users. let windowsForegroundFocus: ReturnType | undefined; const ensureWindowsForegroundFocus = () => { windowsForegroundFocus ??= startWindowsForegroundFocusThread( NodePath.join(__dirname, "electron", "WindowsForegroundFocusWorker.cjs"), ); return windowsForegroundFocus; }; const captureRevealWindows = new Set(); yield* Effect.addFinalizer(() => Effect.sync(() => windowsForegroundFocus?.close())); const mainWindowRef = yield* Ref.make>(Option.none()); const auxiliaryWindows = new WeakSet(); const listWindows = Effect.try({ try: () => Electron.BrowserWindow.getAllWindows(), catch: (cause) => new ElectronWindowOperationError({ operation: "list-windows", platform, windowId: null, channel: null, cause, }), }).pipe(Effect.orDie); const isWindowDestroyed = (window: Electron.BrowserWindow) => Effect.try({ try: () => window.isDestroyed(), catch: (cause) => new ElectronWindowOperationError({ operation: "inspect-window", platform, windowId: window.id, channel: null, cause, }), }).pipe(Effect.orDie); const liveMain = Effect.gen(function* () { const main = yield* Ref.get(mainWindowRef); if (Option.isNone(main) || (yield* isWindowDestroyed(main.value))) { return Option.none(); } return main; }); const currentMainOrFirst = Effect.gen(function* () { const main = yield* liveMain; if (Option.isSome(main)) { return main; } const first = Option.fromNullishOr( (yield* listWindows).find((window) => !auxiliaryWindows.has(window)) ?? null, ); if (Option.isNone(first) || (yield* isWindowDestroyed(first.value))) { return Option.none(); } return first; }); const focusedMainOrFirst = Effect.gen(function* () { const focused = yield* Effect.try({ try: () => Option.fromNullishOr(Electron.BrowserWindow.getFocusedWindow() ?? null), catch: (cause) => new ElectronWindowOperationError({ operation: "get-focused-window", platform, windowId: null, channel: null, cause, }), }).pipe(Effect.orDie); if (Option.isSome(focused) && !(yield* isWindowDestroyed(focused.value))) { return focused; } return yield* currentMainOrFirst; }); return ElectronWindow.of({ create: Effect.fn("ElectronWindow.create")(function (options, behavior) { const webPreferences = options.webPreferences; const diagnosticOptions = { title: options.title ?? null, width: options.width ?? null, height: options.height ?? null, minWidth: options.minWidth ?? null, minHeight: options.minHeight ?? null, show: options.show ?? null, modal: options.modal ?? null, frame: options.frame ?? null, transparent: options.transparent ?? null, backgroundColor: options.backgroundColor ?? null, webPreferences: { preload: webPreferences?.preload ?? null, partition: webPreferences?.partition ?? null, backgroundThrottling: webPreferences?.backgroundThrottling ?? null, sandbox: webPreferences?.sandbox ?? null, contextIsolation: webPreferences?.contextIsolation ?? null, nodeIntegration: webPreferences?.nodeIntegration ?? null, webviewTag: webPreferences?.webviewTag ?? null, }, } satisfies typeof ElectronWindowCreateOptions.Type; return Effect.try({ try: () => { const window = new Electron.BrowserWindow(options); if (behavior?.auxiliary === true) auxiliaryWindows.add(window); return window; }, catch: (cause) => new ElectronWindowCreateError({ options: diagnosticOptions, cause }), }); }), main: liveMain, currentMainOrFirst, focusedMainOrFirst, setMain: Effect.fn("ElectronWindow.setMain")(function (window) { return Ref.set(mainWindowRef, Option.some(window)); }), clearMain: Effect.fn("ElectronWindow.clearMain")(function (window) { return Ref.update(mainWindowRef, (current) => { if (Option.isNone(current)) { return current; } if (Option.isSome(window) && current.value !== window.value) { return current; } return Option.none(); }); }), prepareReveal: Effect.fn("ElectronWindow.prepareReveal")(function (window) { return Effect.promise(async () => { if (platform !== "win32" || window.isDestroyed()) { return false; } captureRevealWindows.add(window.id); return ensureWindowsForegroundFocus() .prepare(windowsForegroundFocusTarget(window)) .catch(() => false); }); }), reveal: Effect.fn("ElectronWindow.reveal")(function (window) { return Effect.tryPromise({ try: async () => { if (window.isDestroyed()) { return; } const captureReveal = platform === "win32" && captureRevealWindows.delete(window.id); const shellHostedForeground = captureReveal && (await isWindowsShellHostedForeground().catch(() => false)); if (window.isMinimized()) { window.restore(); } if (captureReveal) { Electron.app.focus(); } if (captureReveal || !window.isVisible()) { window.show(); } if (captureReveal) { window.moveTop(); } if (platform === "darwin") { Electron.app.focus({ steal: true }); } window.focus(); if (captureReveal) { if (shellHostedForeground) { await windowsForegroundFocus ?.focus(windowsForegroundFocusTarget(window)) .catch(() => false); } try { await activateWindowsForeground(window.getNativeWindowHandle()); } catch { const needsFocus = !(await isWindowsBrowserWindowForeground(window)); if (needsFocus && !window.isDestroyed()) { await windowsForegroundFocus ?.focus(windowsForegroundFocusTarget(window)) .catch(() => false); } if (!window.isDestroyed()) { await activateWindowsForeground(window.getNativeWindowHandle()); } } } }, catch: (cause) => new ElectronWindowOperationError({ operation: "reveal-window", platform, windowId: window.id, channel: null, cause, }), }).pipe(Effect.orDie); }), sendAll: /* Window broadcasts run for streamed updates; avoid a span per broadcast. */ Effect.fnUntraced(function* (channel, ...args) { for (const window of yield* listWindows) { if (yield* isWindowDestroyed(window)) { continue; } yield* Effect.try({ try: () => window.webContents.send(channel, ...args), catch: (cause) => new ElectronWindowOperationError({ operation: "send-window-message", platform, windowId: window.id, channel, cause, }), }).pipe(Effect.orDie); } }), destroyAll: Effect.gen(function* () { let firstFailure: Cause.Cause | undefined; for (const window of yield* listWindows) { const exit = yield* Effect.exit( Effect.try({ try: () => window.destroy(), catch: (cause) => new ElectronWindowOperationError({ operation: "destroy-window", platform, windowId: window.id, channel: null, cause, }), }).pipe(Effect.orDie), ); if (Exit.isFailure(exit)) { firstFailure ??= exit.cause; } } if (firstFailure !== undefined) { return yield* Effect.failCause(firstFailure); } }), syncAllAppearance: Effect.fn("desktop.electron.window.syncAllAppearance")(function* ( sync: (window: Electron.BrowserWindow) => Effect.Effect, ) { const windows = yield* listWindows; for (const window of windows) { if (yield* isWindowDestroyed(window)) { continue; } yield* sync(window); } }), }); }); export const layer = Layer.effect(ElectronWindow, make);