143 lines · 4.6 KB · TypeScript
RawDownload
// @effect-diagnostics nodeBuiltinImport:off -- This platform boundary asks the OS for its foreground window with Node. import * as NodeChildProcess from "node:child_process"; import * as Schema from "effect/Schema"; import { loadWindowsForegroundApi } from "../electron/WindowsForeground.ts"; /** * The foreground window as the snapshot service needs it. `id` is the * CGWindowNumber on macOS and the HWND on Windows, which is what the capture * backends key on. */export type ActiveWindow = {  readonly platform: "macos" | "windows";  readonly id: number;  readonly title: string;  readonly bounds: {    readonly x: number;    readonly y: number;    readonly width: number;    readonly height: number;  };  readonly owner: {    readonly name: string;    readonly processId: number;    readonly path: string;    readonly bundleId?: string;  };}; const MAC_LOOKUP_TIMEOUT_MS = 5_000; const MacActiveWindow = Schema.Struct({  id: Schema.Number,  title: Schema.String,  bounds: Schema.Struct({    x: Schema.Number,    y: Schema.Number,    width: Schema.Number,    height: Schema.Number,  }),  owner: Schema.Struct({    name: Schema.String,    processId: Schema.Number,    path: Schema.String,    bundleId: Schema.String,  }),});const decodeMacActiveWindow = Schema.decodeUnknownSync(Schema.fromJsonString(MacActiveWindow)); // The frontmost app's first on-screen, layer-0 window in front-to-back order is// the active window. Window titles need Screen Recording, which the snapshot// service has already requested by the time this runs.const MAC_LOOKUP_SCRIPT = `ObjC.import("CoreGraphics");ObjC.import("AppKit");function run() {  const app = $.NSWorkspace.sharedWorkspace.frontmostApplication;  if (app.isNil()) return "";  const pid = app.processIdentifier;  const list = $.CGWindowListCopyWindowInfo(    $.kCGWindowListOptionOnScreenOnly | $.kCGWindowListExcludeDesktopElements,    $.kCGNullWindowID,  );  $.CFMakeCollectable(list);  const count = $.CFArrayGetCount(list);  for (let i = 0; i < count; i++) {    const w = ObjC.castRefToObject($.CFArrayGetValueAtIndex(list, i));    if (w.objectForKey("kCGWindowOwnerPID").js !== pid) continue;    if (w.objectForKey("kCGWindowLayer").js !== 0) continue;    const b = ObjC.deepUnwrap(w.objectForKey("kCGWindowBounds"));    return JSON.stringify({      id: w.objectForKey("kCGWindowNumber").js,      title: String(ObjC.unwrap(w.objectForKey("kCGWindowName")) || ""),      bounds: { x: b.X, y: b.Y, width: b.Width, height: b.Height },      owner: {        name: String(app.localizedName.js || ObjC.unwrap(w.objectForKey("kCGWindowOwnerName")) || ""),        processId: pid,        path: String(app.bundleURL.path.js || ""),        bundleId: String(app.bundleIdentifier.js || ""),      },    });  }  return "";}`; function runMacLookup(): Promise<string> {  return new Promise((resolve, reject) => {    NodeChildProcess.execFile(      "/usr/bin/osascript",      ["-l", "JavaScript", "-e", MAC_LOOKUP_SCRIPT],      { timeout: MAC_LOOKUP_TIMEOUT_MS, maxBuffer: 64 * 1024 },      (error, stdout) => {        if (error) reject(error);        else resolve(stdout);      },    );  });} async function macActiveWindow(): Promise<ActiveWindow | undefined> {  const output = (await runMacLookup()).trim();  if (!output) return undefined;  const window = decodeMacActiveWindow(output);  return {    platform: "macos",    id: window.id,    title: window.title,    bounds: window.bounds,    owner: {      name: window.owner.name,      processId: window.owner.processId,      path: window.owner.path,      ...(window.owner.bundleId ? { bundleId: window.owner.bundleId } : {}),    },  };} async function windowsActiveWindow(): Promise<ActiveWindow | undefined> {  const api = await loadWindowsForegroundApi();  const handle = api.getForegroundWindow();  if (handle === 0n) return undefined;  const bounds = api.getWindowRect(handle);  if (!bounds) return undefined;  const { processId } = api.getWindowThreadAndProcessId(handle);  const path = processId === 0 ? "" : api.getProcessImagePath(processId);  const name = path.split(/[\\/]/).pop() ?? "";  return {    platform: "windows",    id: Number(handle),    title: api.getWindowText(handle),    bounds,    owner: { name, processId, path },  };} /** Resolve the OS foreground window, or `undefined` when there is none. */export function activeWindow(platform: NodeJS.Platform): Promise<ActiveWindow | undefined> {  if (platform === "darwin") return macActiveWindow();  if (platform === "win32") return windowsActiveWindow();  return Promise.resolve(undefined);}