1export interface WindowsForegroundApi {
2 readonly getCurrentThreadId: () => number;
3 readonly getForegroundWindow: () => bigint;
4 readonly getWindowClassName: (windowHandle: bigint) => string;
5 readonly getWindowThreadId: (windowHandle: bigint) => number;
6 readonly getWindowThreadAndProcessId: (windowHandle: bigint) => {
7 readonly threadId: number;
8 readonly processId: number;
9 };
10 readonly getWindowText: (windowHandle: bigint) => string;
11 readonly getWindowRect: (
12 windowHandle: bigint,
13 ) =>
14 | { readonly x: number; readonly y: number; readonly width: number; readonly height: number }
15 | undefined;
16 readonly getProcessImagePath: (processId: number) => string;
17 /** High bit of `GetAsyncKeyState`: whether the virtual key is currently down. */
18 readonly isKeyDown: (virtualKey: number) => boolean;
19 readonly attachThreadInput: (
20 sourceThreadId: number,
21 targetThreadId: number,
22 attach: boolean,
23 ) => boolean;
24 readonly setForegroundWindow: (windowHandle: bigint) => boolean;
25}
26
27const WINDOWS_SHELL_HOSTED_WINDOW_CLASSES = new Set(["ApplicationFrameWindow"]);
28
29function nativeWindowHandle(buffer: Buffer): bigint {
30 if (buffer.length === 8) return buffer.readBigUInt64LE();
31 if (buffer.length === 4) return BigInt(buffer.readUInt32LE());
32 throw new Error(`Unsupported Windows window handle size: ${String(buffer.length)} bytes.`);
33}
34
35export function activateWindowsForegroundWithApi(
36 handleBuffer: Buffer,
37 api: WindowsForegroundApi,
38): boolean {
39 const targetWindow = nativeWindowHandle(handleBuffer);
40 const foregroundWindow = api.getForegroundWindow();
41 if (targetWindow === foregroundWindow) return true;
42
43 const currentThreadId = api.getCurrentThreadId();
44 const foregroundThreadId = foregroundWindow === 0n ? 0 : api.getWindowThreadId(foregroundWindow);
45 const shouldAttach =
46 currentThreadId !== 0 && foregroundThreadId !== 0 && currentThreadId !== foregroundThreadId;
47 const attached = shouldAttach && api.attachThreadInput(currentThreadId, foregroundThreadId, true);
48
49 try {
50 return api.setForegroundWindow(targetWindow);
51 } finally {
52 if (attached) {
53 api.attachThreadInput(currentThreadId, foregroundThreadId, false);
54 }
55 }
56}
57
58export function isWindowsShellHostedForegroundWithApi(api: WindowsForegroundApi): boolean {
59 const foregroundWindow = api.getForegroundWindow();
60 return (
61 foregroundWindow !== 0n &&
62 WINDOWS_SHELL_HOSTED_WINDOW_CLASSES.has(api.getWindowClassName(foregroundWindow))
63 );
64}
65
66let windowsForegroundApiPromise: Promise<WindowsForegroundApi> | undefined;
67
68const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
69
70export function loadWindowsForegroundApi(): Promise<WindowsForegroundApi> {
71 windowsForegroundApiPromise ??= import("ffi-rs").then(({ DataType, load, open }) => {
72 const kernel32 = "t3-kernel32";
73 const user32 = "t3-user32";
74 open({ library: kernel32, path: "kernel32.dll" });
75 open({ library: user32, path: "user32.dll" });
76
77 return {
78 getCurrentThreadId: () =>
79 load({
80 library: kernel32,
81 funcName: "GetCurrentThreadId",
82 retType: DataType.U32,
83 paramsType: [],
84 paramsValue: [],
85 }),
86 getForegroundWindow: () =>
87 load({
88 library: user32,
89 funcName: "GetForegroundWindow",
90 retType: DataType.BigInt,
91 paramsType: [],
92 paramsValue: [],
93 }) as bigint,
94 getWindowClassName: (windowHandle) => {
95 const buffer = Buffer.alloc(512);
96 const length = load({
97 library: user32,
98 funcName: "GetClassNameW",
99 retType: DataType.I32,
100 paramsType: [DataType.BigInt, DataType.U8Array, DataType.I32],
101 paramsValue: [windowHandle, buffer, buffer.byteLength / 2],
102 });
103 return length > 0 ? buffer.subarray(0, length * 2).toString("utf16le") : "";
104 },
105 getWindowThreadId: (windowHandle) =>
106 load({
107 library: user32,
108 funcName: "GetWindowThreadProcessId",
109 retType: DataType.U32,
110 paramsType: [DataType.BigInt, DataType.BigInt],
111 paramsValue: [windowHandle, 0n],
112 }),
113 getWindowThreadAndProcessId: (windowHandle) => {
114 const out = Buffer.alloc(4);
115 const threadId = load({
116 library: user32,
117 funcName: "GetWindowThreadProcessId",
118 retType: DataType.U32,
119 paramsType: [DataType.BigInt, DataType.U8Array],
120 paramsValue: [windowHandle, out],
121 });
122 return { threadId, processId: out.readUInt32LE(0) };
123 },
124 getWindowText: (windowHandle) => {
125 const buffer = Buffer.alloc(2 * 1024);
126 const length = load({
127 library: user32,
128 funcName: "GetWindowTextW",
129 retType: DataType.I32,
130 paramsType: [DataType.BigInt, DataType.U8Array, DataType.I32],
131 paramsValue: [windowHandle, buffer, buffer.byteLength / 2],
132 });
133 return length > 0 ? buffer.subarray(0, length * 2).toString("utf16le") : "";
134 },
135 getWindowRect: (windowHandle) => {
136 const rect = Buffer.alloc(16);
137 const ok = load({
138 library: user32,
139 funcName: "GetWindowRect",
140 retType: DataType.Boolean,
141 paramsType: [DataType.BigInt, DataType.U8Array],
142 paramsValue: [windowHandle, rect],
143 });
144 if (!ok) return undefined;
145 const left = rect.readInt32LE(0);
146 const top = rect.readInt32LE(4);
147 return {
148 x: left,
149 y: top,
150 width: Math.max(0, rect.readInt32LE(8) - left),
151 height: Math.max(0, rect.readInt32LE(12) - top),
152 };
153 },
154 getProcessImagePath: (processId) => {
155 const handle = load({
156 library: kernel32,
157 funcName: "OpenProcess",
158 retType: DataType.BigInt,
159 paramsType: [DataType.U32, DataType.Boolean, DataType.U32],
160 paramsValue: [PROCESS_QUERY_LIMITED_INFORMATION, false, processId],
161 }) as bigint;
162 if (handle === 0n) return "";
163 try {
164 const buffer = Buffer.alloc(2 * 32_768);
165 const size = Buffer.alloc(4);
166 size.writeUInt32LE(buffer.byteLength / 2, 0);
167 const ok = load({
168 library: kernel32,
169 funcName: "QueryFullProcessImageNameW",
170 retType: DataType.Boolean,
171 paramsType: [DataType.BigInt, DataType.U32, DataType.U8Array, DataType.U8Array],
172 paramsValue: [handle, 0, buffer, size],
173 });
174 return ok ? buffer.subarray(0, size.readUInt32LE(0) * 2).toString("utf16le") : "";
175 } finally {
176 load({
177 library: kernel32,
178 funcName: "CloseHandle",
179 retType: DataType.Boolean,
180 paramsType: [DataType.BigInt],
181 paramsValue: [handle],
182 });
183 }
184 },
185 isKeyDown: (virtualKey) =>
186 (load({
187 library: user32,
188 funcName: "GetAsyncKeyState",
189 retType: DataType.I16,
190 paramsType: [DataType.I32],
191 paramsValue: [virtualKey],
192 }) &
193 0x8000) !==
194 0,
195 attachThreadInput: (sourceThreadId, targetThreadId, attach) =>
196 load({
197 library: user32,
198 funcName: "AttachThreadInput",
199 retType: DataType.Boolean,
200 paramsType: [DataType.U32, DataType.U32, DataType.Boolean],
201 paramsValue: [sourceThreadId, targetThreadId, attach],
202 }),
203 setForegroundWindow: (windowHandle) =>
204 load({
205 library: user32,
206 funcName: "SetForegroundWindow",
207 retType: DataType.Boolean,
208 paramsType: [DataType.BigInt],
209 paramsValue: [windowHandle],
210 }),
211 } satisfies WindowsForegroundApi;
212 });
213 return windowsForegroundApiPromise;
214}
215
216export async function activateWindowsForeground(handleBuffer: Buffer): Promise<void> {
217 const api = await loadWindowsForegroundApi();
218 if (activateWindowsForegroundWithApi(handleBuffer, api)) return;
219 throw new Error("Windows refused to activate The Knife.");
220}
221
222export async function isWindowsShellHostedForeground(): Promise<boolean> {
223 return isWindowsShellHostedForegroundWithApi(await loadWindowsForegroundApi());
224}