// @effect-diagnostics globalTimers:off -- Child-process deadlines run outside Effect fibers. // @effect-diagnostics nodeBuiltinImport:off -- This desktop-only helper owns a Node child process. import * as NodeChildProcess from "node:child_process"; import * as Electron from "electron"; import type { ActiveWindow } from "./ActiveWindow.ts"; const CAPTURE_TIMEOUT_MS = 15_000; export type RegionSnapShotRequest = { readonly region: Electron.Rectangle }; export type RegionSnapShotResult = | { readonly type: "result"; readonly width: number; readonly height: number; readonly png: string; } | { readonly type: "error"; readonly message: string }; export type RegionSnapShotSource = { readonly appIcon?: Electron.NativeImage; readonly name: string; }; export type RegionSnapShotChild = { readonly send: (request: RegionSnapShotRequest) => void; readonly onMessage: (listener: (message: unknown) => void) => void; readonly onExit: (listener: () => void) => void; readonly kill: () => void; }; function forkRegionSnapShotChild(workerPath: string): RegionSnapShotChild { const child = NodeChildProcess.fork(workerPath, [], { env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" }, execArgv: [], stdio: ["ignore", "ignore", "inherit", "ipc"], }); return { send: (request) => child.send(request), onMessage: (listener) => child.on("message", listener), onExit: (listener) => { child.once("exit", listener); child.once("error", listener); }, kill: () => child.kill(), }; } export type RegionSnapShotProcess = { readonly alive: boolean; readonly capture: ( region: Electron.Rectangle, ) => Promise<{ width: number; height: number; png: Buffer }>; readonly close: () => void; }; /** * One capture child. `warm` it ahead of time so the native module is loaded * before the shortcut fires; each child serves a single capture and exits. */ export function startRegionSnapShotProcess( workerPath: string, fork: (workerPath: string) => RegionSnapShotChild = forkRegionSnapShotChild, ): RegionSnapShotProcess { let child: RegionSnapShotChild; try { child = fork(workerPath); } catch { return { alive: false, capture: () => Promise.reject(new Error("Windows window capture is unavailable.")), close: () => undefined, }; } const ready = Promise.withResolvers(); const result = Promise.withResolvers<{ width: number; height: number; png: Buffer }>(); let settled = false; const finish = (error?: Error, value?: { width: number; height: number; png: Buffer }) => { if (settled) return; settled = true; ready.resolve(); if (value) result.resolve(value); else result.reject(error ?? new Error("Windows window capture failed.")); child.kill(); }; child.onMessage((message) => { if (message === "ready") { ready.resolve(); return; } const response = message as RegionSnapShotResult; if (response.type === "error") finish(new Error(response.message)); else if (response.type === "result") { finish(undefined, { width: response.width, height: response.height, png: Buffer.from(response.png, "base64"), }); } }); child.onExit(() => finish(new Error("Windows window capture helper exited."))); void result.promise.catch(() => undefined); return { get alive() { return !settled; }, capture: async (region) => { if (settled) throw new Error("Windows window capture helper already used."); const timeout = setTimeout( () => finish(new Error("Windows window capture timed out. Try again.")), CAPTURE_TIMEOUT_MS, ); timeout.unref(); try { await ready.promise; if (settled) return await result.promise; child.send({ region }); return await result.promise; } finally { clearTimeout(timeout); } }, close: () => finish(new Error("Windows window capture cancelled.")), }; } export type RegionSnapShotPool = { readonly warm: () => void; readonly cool: () => void; readonly capture: ( region: Electron.Rectangle, ) => Promise<{ width: number; height: number; png: Buffer }>; readonly close: () => void; }; export function makeRegionSnapShotPool( workerPath: string, start: (workerPath: string) => RegionSnapShotProcess = startRegionSnapShotProcess, ): RegionSnapShotPool { let standby: RegionSnapShotProcess | undefined; let inFlight: RegionSnapShotProcess | undefined; let closed = false; const warm = () => { if (closed || standby?.alive) return; standby = start(workerPath); }; const cool = () => { standby?.close(); standby = undefined; }; return { warm, cool, capture: async (region) => { if (closed) throw new Error("Windows window capture is unavailable."); if (inFlight?.alive) { throw new Error("Windows window capture is still in progress. Try again."); } const current = standby?.alive ? standby : start(workerPath); standby = undefined; inFlight = current; try { return await current.capture(region); } finally { if (inFlight === current) inFlight = undefined; warm(); } }, close: () => { closed = true; cool(); inFlight?.close(); }, }; } export async function captureRegionWindowSnapshot( pool: Pick, active: ActiveWindow, region: Electron.Rectangle, maxSize: Electron.Size, ): Promise<{ readonly source: RegionSnapShotSource; readonly png: Buffer }> { const shot = await pool.capture(region); const scale = Math.min(maxSize.width / shot.width, maxSize.height / shot.height, 1); const png = scale < 1 ? Electron.nativeImage .createFromBuffer(shot.png) .resize({ width: Math.max(1, Math.round(shot.width * scale)), height: Math.max(1, Math.round(shot.height * scale)), quality: "best", }) .toPNG() : shot.png; return { source: { name: active.title.trim() || active.owner.name.trim() || "Window", }, png, }; }