1// @effect-diagnostics globalTimers:off -- Bound session-bus registration at a native callback boundary.
2import { Message, MessageType, NameFlag, RequestNameReply, sessionBus } from "dbus-next";
3
4import {
5 NIRI_CAPTURE_INTERFACE as INTERFACE,
6 NIRI_CAPTURE_PATH as PATH,
7} from "./linuxCaptureSession.ts";
8
9/** Niri owns the keybinding; this endpoint triggers capture without first focusing T3. */
10export async function startNiriCaptureShortcut(
11 appId: string,
12 onCapture: () => void,
13 onFailure: () => void,
14 bus = sessionBus(),
15): Promise<() => void> {
16 let closed = false;
17 const close = () => {
18 if (closed) return;
19 closed = true;
20 bus.disconnect();
21 };
22 const failure = new Promise<never>((_, reject) => {
23 bus.on("error", (error: Error) => {
24 reject(error);
25 if (!closed) onFailure();
26 close();
27 });
28 });
29 void failure.catch(() => undefined);
30 bus.addMethodHandler((message: Message) => {
31 if (message.path !== PATH || message.interface !== INTERFACE || message.member !== "Capture")
32 return false;
33 if (message.signature || message.body.length) {
34 // Preserve dbus-next's numeric reply serial; its newError factory has incorrect types.
35 const reply = Message.newMethodReturn(message, "s", ["Capture takes no arguments."]);
36 reply.type = MessageType.ERROR;
37 reply.errorName = "org.freedesktop.DBus.Error.InvalidArgs";
38 bus.send(reply);
39 return true;
40 }
41 if (!closed) onCapture();
42 bus.send(Message.newMethodReturn(message));
43 return true;
44 });
45 let timer: ReturnType<typeof setTimeout> | undefined;
46 try {
47 const result = await Promise.race([
48 bus.requestName(`${appId}.SnapShot`, NameFlag.DO_NOT_QUEUE),
49 failure,
50 new Promise<never>((_, reject) => {
51 timer = setTimeout(
52 () => reject(new Error("Niri capture shortcut registration timed out.")),
53 5_000,
54 );
55 }),
56 ]);
57 if (result !== RequestNameReply.PRIMARY_OWNER)
58 throw new Error("Another The Knife instance already owns the capture shortcut.");
59 return close;
60 } catch (error) {
61 close();
62 throw error;
63 } finally {
64 clearTimeout(timer);
65 }
66}