1import * as NodeWorkerThreads from "node:worker_threads";
2
3import type { WindowsForegroundFocusTarget } from "./WindowsForegroundFocusThread.ts";
4import type { Element } from "@crowecawcaw/xa11y";
5
6type FocusRequest = {
7 readonly type: "prepare" | "focus";
8 readonly requestId: number;
9 readonly target: WindowsForegroundFocusTarget;
10};
11
12function matchesTarget(
13 element: {
14 readonly name?: string | null;
15 readonly bounds: WindowsForegroundFocusTarget["bounds"] | null;
16 },
17 target: WindowsForegroundFocusTarget,
18): boolean {
19 if ((element.name ?? "").trim() !== target.title.trim()) return false;
20 if (!element.bounds) return false;
21 return [target.bounds, target.contentBounds].some((bounds) =>
22 (["x", "y", "width", "height"] as const).every(
23 (key) => Math.abs(element.bounds![key] - bounds[key]) <= 2,
24 ),
25 );
26}
27
28async function findTarget(
29 App: (typeof import("@crowecawcaw/xa11y"))["App"],
30 target: WindowsForegroundFocusTarget,
31): Promise<Element | undefined> {
32 const children = await App.byPid(target.processId, { timeout: 0 })
33 .then((app) => app.children())
34 .catch(() => []);
35 return (
36 children.find((candidate) => matchesTarget(candidate, target)) ??
37 (await App.list())
38 .filter((candidate) => candidate.pid === target.processId)
39 .map((candidate) => candidate.asElement())
40 .find((candidate) => matchesTarget(candidate, target))
41 );
42}
43
44const cachedElements = new Map<number, Element>();
45
46async function prepareTarget(
47 App: (typeof import("@crowecawcaw/xa11y"))["App"],
48 target: WindowsForegroundFocusTarget,
49): Promise<boolean> {
50 const element = await findTarget(App, target);
51 if (!element) {
52 cachedElements.delete(target.windowId);
53 return false;
54 }
55 cachedElements.set(target.windowId, element);
56 return true;
57}
58
59async function focusTarget(
60 App: (typeof import("@crowecawcaw/xa11y"))["App"],
61 target: WindowsForegroundFocusTarget,
62): Promise<boolean> {
63 let element = cachedElements.get(target.windowId);
64 if (!element) {
65 element = await findTarget(App, target);
66 if (!element) return false;
67 cachedElements.set(target.windowId, element);
68 }
69 try {
70 await element.focus();
71 return true;
72 } catch {
73 cachedElements.delete(target.windowId);
74 return false;
75 }
76}
77
78async function start() {
79 const { App } = await import("@crowecawcaw/xa11y");
80 const parentPort = NodeWorkerThreads.parentPort;
81 if (!parentPort) return;
82 let work = Promise.resolve();
83 parentPort.on("message", (message: FocusRequest) => {
84 if (message.type !== "prepare" && message.type !== "focus") return;
85 work = work.then(async () => {
86 const focused = await (
87 message.type === "prepare"
88 ? prepareTarget(App, message.target)
89 : focusTarget(App, message.target)
90 ).catch(() => false);
91 // oxlint-disable-next-line unicorn/require-post-message-target-origin -- Node workers do not accept a target origin.
92 parentPort.postMessage({ type: "result", requestId: message.requestId, focused });
93 });
94 });
95 // oxlint-disable-next-line unicorn/require-post-message-target-origin -- Node workers do not accept a target origin.
96 parentPort.postMessage("ready");
97}
98
99void start();