1// @effect-diagnostics nodeBuiltinImport:off -- This platform boundary asks the OS for its foreground window with Node.
2
3import * as NodeChildProcess from "node:child_process";
4
5import * as Schema from "effect/Schema";
6
7import { loadWindowsForegroundApi } from "../electron/WindowsForeground.ts";
8
9/**
10 * The foreground window as the snapshot service needs it. `id` is the
11 * CGWindowNumber on macOS and the HWND on Windows, which is what the capture
12 * backends key on.
13 */
14export type ActiveWindow = {
15 readonly platform: "macos" | "windows";
16 readonly id: number;
17 readonly title: string;
18 readonly bounds: {
19 readonly x: number;
20 readonly y: number;
21 readonly width: number;
22 readonly height: number;
23 };
24 readonly owner: {
25 readonly name: string;
26 readonly processId: number;
27 readonly path: string;
28 readonly bundleId?: string;
29 };
30};
31
32const MAC_LOOKUP_TIMEOUT_MS = 5_000;
33
34const MacActiveWindow = Schema.Struct({
35 id: Schema.Number,
36 title: Schema.String,
37 bounds: Schema.Struct({
38 x: Schema.Number,
39 y: Schema.Number,
40 width: Schema.Number,
41 height: Schema.Number,
42 }),
43 owner: Schema.Struct({
44 name: Schema.String,
45 processId: Schema.Number,
46 path: Schema.String,
47 bundleId: Schema.String,
48 }),
49});
50const decodeMacActiveWindow = Schema.decodeUnknownSync(Schema.fromJsonString(MacActiveWindow));
51
52// The frontmost app's first on-screen, layer-0 window in front-to-back order is
53// the active window. Window titles need Screen Recording, which the snapshot
54// service has already requested by the time this runs.
55const MAC_LOOKUP_SCRIPT = `
56ObjC.import("CoreGraphics");
57ObjC.import("AppKit");
58function run() {
59 const app = $.NSWorkspace.sharedWorkspace.frontmostApplication;
60 if (app.isNil()) return "";
61 const pid = app.processIdentifier;
62 const list = $.CGWindowListCopyWindowInfo(
63 $.kCGWindowListOptionOnScreenOnly | $.kCGWindowListExcludeDesktopElements,
64 $.kCGNullWindowID,
65 );
66 $.CFMakeCollectable(list);
67 const count = $.CFArrayGetCount(list);
68 for (let i = 0; i < count; i++) {
69 const w = ObjC.castRefToObject($.CFArrayGetValueAtIndex(list, i));
70 if (w.objectForKey("kCGWindowOwnerPID").js !== pid) continue;
71 if (w.objectForKey("kCGWindowLayer").js !== 0) continue;
72 const b = ObjC.deepUnwrap(w.objectForKey("kCGWindowBounds"));
73 return JSON.stringify({
74 id: w.objectForKey("kCGWindowNumber").js,
75 title: String(ObjC.unwrap(w.objectForKey("kCGWindowName")) || ""),
76 bounds: { x: b.X, y: b.Y, width: b.Width, height: b.Height },
77 owner: {
78 name: String(app.localizedName.js || ObjC.unwrap(w.objectForKey("kCGWindowOwnerName")) || ""),
79 processId: pid,
80 path: String(app.bundleURL.path.js || ""),
81 bundleId: String(app.bundleIdentifier.js || ""),
82 },
83 });
84 }
85 return "";
86}`;
87
88function runMacLookup(): Promise<string> {
89 return new Promise((resolve, reject) => {
90 NodeChildProcess.execFile(
91 "/usr/bin/osascript",
92 ["-l", "JavaScript", "-e", MAC_LOOKUP_SCRIPT],
93 { timeout: MAC_LOOKUP_TIMEOUT_MS, maxBuffer: 64 * 1024 },
94 (error, stdout) => {
95 if (error) reject(error);
96 else resolve(stdout);
97 },
98 );
99 });
100}
101
102async function macActiveWindow(): Promise<ActiveWindow | undefined> {
103 const output = (await runMacLookup()).trim();
104 if (!output) return undefined;
105 const window = decodeMacActiveWindow(output);
106 return {
107 platform: "macos",
108 id: window.id,
109 title: window.title,
110 bounds: window.bounds,
111 owner: {
112 name: window.owner.name,
113 processId: window.owner.processId,
114 path: window.owner.path,
115 ...(window.owner.bundleId ? { bundleId: window.owner.bundleId } : {}),
116 },
117 };
118}
119
120async function windowsActiveWindow(): Promise<ActiveWindow | undefined> {
121 const api = await loadWindowsForegroundApi();
122 const handle = api.getForegroundWindow();
123 if (handle === 0n) return undefined;
124 const bounds = api.getWindowRect(handle);
125 if (!bounds) return undefined;
126 const { processId } = api.getWindowThreadAndProcessId(handle);
127 const path = processId === 0 ? "" : api.getProcessImagePath(processId);
128 const name = path.split(/[\\/]/).pop() ?? "";
129 return {
130 platform: "windows",
131 id: Number(handle),
132 title: api.getWindowText(handle),
133 bounds,
134 owner: { name, processId, path },
135 };
136}
137
138/** Resolve the OS foreground window, or `undefined` when there is none. */
139export function activeWindow(platform: NodeJS.Platform): Promise<ActiveWindow | undefined> {
140 if (platform === "darwin") return macActiveWindow();
141 if (platform === "win32") return windowsActiveWindow();
142 return Promise.resolve(undefined);
143}