import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process"; import { EventEmitter } from "node:events"; import { existsSync } from "node:fs"; import { createInterface } from "node:readline"; import type { GlimpseOpenOptions } from "glimpseui"; interface NativeHostInfo { path: string; extraArgs?: string[]; buildHint?: string; } interface GlimpseProtocolMessage { type?: string; data?: unknown; screen?: unknown; screens?: unknown; appearance?: unknown; cursor?: unknown; cursorTip?: unknown; } export interface QuietGlimpseWindow { on(event: "message", listener: (data: unknown) => void): this; on(event: "closed", listener: () => void): this; on(event: "error", listener: (error: Error) => void): this; removeListener(event: "message", listener: (data: unknown) => void): this; removeListener(event: "closed", listener: () => void): this; removeListener(event: "error", listener: (error: Error) => void): this; send(js: string): void; close(): void; } class QuietGlimpseWindowImpl extends EventEmitter implements QuietGlimpseWindow { #proc: ChildProcessWithoutNullStreams; #closed = false; #pendingHTML: string | null; #stderr = ""; constructor(proc: ChildProcessWithoutNullStreams, initialHTML: string) { super(); this.#proc = proc; this.#pendingHTML = initialHTML; proc.stdin.on("error", () => {}); proc.stderr.on("data", (chunk) => { this.#stderr += chunk.toString(); }); const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity }); rl.on("line", (line) => { let message: GlimpseProtocolMessage; try { message = JSON.parse(line) as GlimpseProtocolMessage; } catch { this.emit("error", new Error(`Malformed glimpse protocol line: ${line}`)); return; } switch (message.type) { case "ready": if (this.#pendingHTML != null) { this.setHTML(this.#pendingHTML); this.#pendingHTML = null; } break; case "message": this.emit("message", message.data); break; case "closed": this.#markClosed(); break; default: break; } }); proc.on("error", (error) => this.emit("error", error)); proc.on("exit", (code) => { const stderr = this.#stderr.trim(); if (!this.#closed && code && stderr) { this.emit("error", new Error(stderr)); } this.#markClosed(); }); } send(js: string): void { this.#write({ type: "eval", js }); } close(): void { this.#write({ type: "close" }); } #markClosed(): void { if (this.#closed) return; this.#closed = true; this.emit("closed"); } #write(obj: Record): void { if (this.#closed) return; this.#proc.stdin.write(`${JSON.stringify(obj)}\n`); } setHTML(html: string): void { this.#write({ type: "html", html: Buffer.from(html).toString("base64") }); } } async function getNativeHostInfo(): Promise { const glimpseModule = (await import("glimpseui")) as unknown as { getNativeHostInfo: () => NativeHostInfo }; return glimpseModule.getNativeHostInfo(); } export async function openQuietGlimpse(html: string, options: GlimpseOpenOptions = {}): Promise { const host = await getNativeHostInfo(); if (!existsSync(host.path)) { const hint = host.buildHint ? ` ${host.buildHint}` : ""; throw new Error(`Glimpse host not found at '${host.path}'.${hint}`); } const args: string[] = []; if (options.width != null) args.push("--width", String(options.width)); if (options.height != null) args.push("--height", String(options.height)); if (options.title != null) args.push("--title", options.title); if (options.frameless) args.push("--frameless"); if (options.floating) args.push("--floating"); if (options.transparent) args.push("--transparent"); if (options.clickThrough) args.push("--click-through"); if (options.hidden) args.push("--hidden"); if (options.autoClose) args.push("--auto-close"); if (options.x != null) args.push(`--x=${options.x}`); if (options.y != null) args.push(`--y=${options.y}`); if (options.cursorOffset?.x != null) args.push(`--cursor-offset-x=${options.cursorOffset.x}`); if (options.cursorOffset?.y != null) args.push(`--cursor-offset-y=${options.cursorOffset.y}`); if (options.cursorAnchor != null) args.push("--cursor-anchor", options.cursorAnchor); if (options.followMode != null) args.push("--follow-mode", options.followMode); if (options.followCursor) args.push("--follow-cursor"); const proc = spawn(host.path, [...(host.extraArgs ?? []), ...args], { stdio: ["pipe", "pipe", "pipe"], windowsHide: process.platform === "win32", env: { ...process.env, OS_ACTIVITY_MODE: process.env.OS_ACTIVITY_MODE ?? "disable", }, }); return new QuietGlimpseWindowImpl(proc, html); }