Skip to content
Source
Sign inSign up

MacModifierPairShortcutProcess.ts

92 lines2.6 KB

1// @effect-diagnostics nodeBuiltinImport:off -- This macOS platform boundary spawns the native modifier-key poller with Node.
2​
3import * as NodeChildProcess from "node:child_process";
4​
5import type { SnapShotModifier } from "@t3tools/contracts";
6​
7const MAC_MODIFIER_PAIR_DEVICE_MASKS: Record<SnapShotModifier, readonly [number, number]> = {
8 shift: [0x2, 0x4],
9 control: [0x1, 0x2000],
10 alt: [0x20, 0x40],
11 meta: [0x8, 0x10],
12};
13​
14// CoreGraphics registers osascript as a foreground app; go background-only first to avoid a Dock tile.
15const POLLER_SCRIPT = `
16ObjC.import("AppKit");
17$.NSApplication.sharedApplication.setActivationPolicy($.NSApplicationActivationPolicyProhibited);
18ObjC.import("CoreGraphics");
19ObjC.import("unistd");
20function run(argv) {
21 const left = Number(argv[0]);
22 const right = Number(argv[1]);
23 const both = left | right;
24 let active = false;
25 console.log("ready");
26 while ($.getppid() !== 1) {
27 const pressed = ($.CGEventSourceFlagsState(0) & both) === both;
28 if (pressed && !active) console.log("trigger");
29 active = pressed;
30 delay(0.05);
31 }
32 return "orphaned";
33}`;
34​
35export function startMacModifierPairShortcutProcess(
36 modifier: SnapShotModifier,
37 onTrigger: () => void,
38 onFailure: (error: Error) => void,
39): Promise<() => void> {
40 const [left, right] = MAC_MODIFIER_PAIR_DEVICE_MASKS[modifier];
41 const poller = NodeChildProcess.spawn(
42 "/usr/bin/osascript",
43 ["-l", "JavaScript", "-e", POLLER_SCRIPT, String(left), String(right)],
44 { stdio: ["ignore", "ignore", "pipe"] },
45 );
46​
47 return new Promise((resolve, reject) => {
48 let settled = false;
49 let stopped = false;
50 let buffered = "";
51 const stop = () => {
52 if (stopped) return;
53 stopped = true;
54 poller.kill();
55 };
56 const fail = (error: Error) => {
57 if (stopped) return;
58 if (settled) {
59 stop();
60 onFailure(error);
61 return;
62 }
63 settled = true;
64 stop();
65 reject(error);
66 };
67​
68 poller.stderr.on("data", (chunk: Buffer) => {
69 buffered += chunk.toString();
70 const lines = buffered.split("\n");
71 buffered = lines.pop() ?? "";
72 for (const line of lines) {
73 const message = line.trim();
74 if (message === "ready" && !settled) {
75 settled = true;
76 resolve(stop);
77 continue;
78 }
79 if (message !== "trigger" || !settled || stopped) continue;
80 try {
81 onTrigger();
82 } catch {}
83 }
84 });
85 poller.once("error", (error) => {
86 fail(error);
87 });
88 poller.once("exit", (code) => {
89 fail(new Error(`Snapshot shortcut helper exited with code ${code}`));
90 });
91 });