ispyx 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +183 -2
  3. package/config.example.json +15 -0
  4. package/package.json +47 -4
  5. package/src/core/RESIDENT.md +45 -0
  6. package/src/core/auth.js +129 -0
  7. package/src/core/guard.js +287 -0
  8. package/src/core/image.js +117 -0
  9. package/src/core/ipc.js +139 -0
  10. package/src/core/launcher.js +70 -0
  11. package/src/core/mode.js +17 -0
  12. package/src/core/motion.js +57 -0
  13. package/src/core/panel/README.md +70 -0
  14. package/src/core/panel/index.js +144 -0
  15. package/src/core/panel/page.html +434 -0
  16. package/src/core/resident.js +271 -0
  17. package/src/core/server.js +832 -0
  18. package/src/effects/appicon.js +46 -0
  19. package/src/effects/scan.js +850 -0
  20. package/src/macos-entry.js +16 -0
  21. package/src/platform/CONTRACT.md +131 -0
  22. package/src/platform/darwin/PROTOCOL.md +82 -0
  23. package/src/platform/darwin/hotkey.js +41 -0
  24. package/src/platform/darwin/index.js +118 -0
  25. package/src/platform/darwin/keys.js +49 -0
  26. package/src/platform/darwin/layer.js +87 -0
  27. package/src/platform/darwin/overlay.js +83 -0
  28. package/src/platform/darwin/panel.js +25 -0
  29. package/src/platform/darwin/rpc-worker.js +132 -0
  30. package/src/platform/darwin/rpc.js +146 -0
  31. package/src/platform/fake/index.js +162 -0
  32. package/src/platform/index.js +13 -0
  33. package/src/platform/win32/auth-dialog.ps1 +140 -0
  34. package/src/platform/win32/autostart.js +54 -0
  35. package/src/platform/win32/dialog.js +41 -0
  36. package/src/platform/win32/hotkey.js +77 -0
  37. package/src/platform/win32/index.js +31 -0
  38. package/src/platform/win32/keys.js +31 -0
  39. package/src/platform/win32/layer.js +120 -0
  40. package/src/platform/win32/overlay.js +265 -0
  41. package/src/platform/win32/pump.js +37 -0
  42. package/src/platform/win32/tray-worker.js +172 -0
  43. package/src/platform/win32/tray.js +103 -0
  44. package/src/platform/win32/win32.js +577 -0
  45. package/src/server.js +14 -0
@@ -0,0 +1,577 @@
1
+ // Thin koffi bindings over the Win32 calls the server needs. Windows only.
2
+ import koffi from 'koffi';
3
+
4
+ const user32 = koffi.load('user32.dll');
5
+ const kernel32 = koffi.load('kernel32.dll');
6
+ const gdi32 = koffi.load('gdi32.dll');
7
+ const advapi32 = koffi.load('advapi32.dll');
8
+ const dwmapi = koffi.load('dwmapi.dll');
9
+ const ole32 = koffi.load('ole32.dll');
10
+ const winmm = koffi.load('winmm.dll');
11
+ const timeBeginPeriod = winmm.func('uint32 timeBeginPeriod(uint32 ms)');
12
+
13
+ export const RECT = koffi.struct('RECT', { left: 'int32', top: 'int32', right: 'int32', bottom: 'int32' });
14
+ export const POINT = koffi.struct('POINT', { x: 'int32', y: 'int32' });
15
+ const MONITORINFO = koffi.struct('MONITORINFO', {
16
+ cbSize: 'uint32', rcMonitor: RECT, rcWork: RECT, dwFlags: 'uint32',
17
+ });
18
+ const GUITHREADINFO = koffi.struct('GUITHREADINFO', {
19
+ cbSize: 'uint32', flags: 'uint32', hwndActive: 'intptr_t', hwndFocus: 'intptr_t',
20
+ hwndCapture: 'intptr_t', hwndMenuOwner: 'intptr_t', hwndMoveSize: 'intptr_t',
21
+ hwndCaret: 'intptr_t', rcCaret: RECT,
22
+ });
23
+ const BITMAPINFOHEADER = koffi.struct('BITMAPINFOHEADER', {
24
+ biSize: 'uint32', biWidth: 'int32', biHeight: 'int32', biPlanes: 'uint16', biBitCount: 'uint16',
25
+ biCompression: 'uint32', biSizeImage: 'uint32', biXPelsPerMeter: 'int32', biYPelsPerMeter: 'int32',
26
+ biClrUsed: 'uint32', biClrImportant: 'uint32',
27
+ });
28
+ const MOUSEINPUT = koffi.struct('MOUSEINPUT', {
29
+ dx: 'int32', dy: 'int32', mouseData: 'uint32', dwFlags: 'uint32', time: 'uint32', dwExtraInfo: 'uintptr_t',
30
+ });
31
+ const KEYBDINPUT = koffi.struct('KEYBDINPUT', {
32
+ wVk: 'uint16', wScan: 'uint16', dwFlags: 'uint32', time: 'uint32', dwExtraInfo: 'uintptr_t',
33
+ });
34
+ const INPUT_U = koffi.union('INPUT_U', { mi: MOUSEINPUT, ki: KEYBDINPUT });
35
+ const INPUT = koffi.struct('INPUT', { type: 'uint32', u: INPUT_U });
36
+
37
+ const EnumWindowsProc = koffi.proto('bool EnumWindowsProc(intptr_t hwnd, intptr_t lparam)');
38
+ const MonitorEnumProc = koffi.proto('bool MonitorEnumProc(intptr_t hmon, intptr_t hdc, RECT *rc, intptr_t lparam)');
39
+
40
+ const f = {
41
+ SetProcessDpiAwarenessContext: user32.func('bool SetProcessDpiAwarenessContext(intptr_t ctx)'),
42
+ EnumWindows: user32.func('bool EnumWindows(EnumWindowsProc *cb, intptr_t lparam)'),
43
+ IsWindow: user32.func('bool IsWindow(intptr_t h)'),
44
+ IsWindowVisible: user32.func('bool IsWindowVisible(intptr_t h)'),
45
+ IsIconic: user32.func('bool IsIconic(intptr_t h)'),
46
+ GetWindowTextW: user32.func('int GetWindowTextW(intptr_t h, _Out_ uint16_t *buf, int n)'),
47
+ GetClassNameW: user32.func('int GetClassNameW(intptr_t h, _Out_ uint16_t *buf, int n)'),
48
+ GetWindowRect: user32.func('bool GetWindowRect(intptr_t h, _Out_ RECT *r)'),
49
+ FindWindowW: user32.func('intptr_t FindWindowW(str16 cls, str16 title)'),
50
+ GetWindowThreadProcessId: user32.func('uint32 GetWindowThreadProcessId(intptr_t h, _Out_ uint32_t *pid)'),
51
+ GetKeyboardLayout: user32.func('intptr_t GetKeyboardLayout(uint32 tid)'),
52
+ GetWindowLongPtrW: user32.func('intptr_t GetWindowLongPtrW(intptr_t h, int idx)'),
53
+ GetAncestor: user32.func('intptr_t GetAncestor(intptr_t h, uint32 flags)'),
54
+ WindowFromPoint: user32.func('intptr_t WindowFromPoint(POINT p)'),
55
+ GetForegroundWindow: user32.func('intptr_t GetForegroundWindow()'),
56
+ SetForegroundWindow: user32.func('bool SetForegroundWindow(intptr_t h)'),
57
+ BringWindowToTop: user32.func('bool BringWindowToTop(intptr_t h)'),
58
+ ShowWindow: user32.func('bool ShowWindow(intptr_t h, int cmd)'),
59
+ AttachThreadInput: user32.func('bool AttachThreadInput(uint32 from, uint32 to, bool attach)'),
60
+ GetGUIThreadInfo: user32.func('bool GetGUIThreadInfo(uint32 tid, _Inout_ GUITHREADINFO *i)'),
61
+ PostMessageW: user32.func('bool PostMessageW(intptr_t h, uint32 msg, uintptr_t w, intptr_t l)'),
62
+ GetCursorPos: user32.func('bool GetCursorPos(_Out_ POINT *p)'),
63
+ SetCursorPos: user32.func('bool SetCursorPos(int x, int y)'),
64
+ GetSystemMetrics: user32.func('int GetSystemMetrics(int i)'),
65
+ GetDpiForWindow: user32.func('uint32 GetDpiForWindow(intptr_t h)'),
66
+ SendInput: user32.func('uint32 SendInput(uint32 n, INPUT *inputs, int cb)'),
67
+ keybd_event: user32.func('void keybd_event(uint8_t vk, uint8_t scan, uint32 flags, uintptr_t extra)'),
68
+ MapVirtualKeyW: user32.func('uint32 MapVirtualKeyW(uint32 code, uint32 type)'),
69
+ GetDC: user32.func('intptr_t GetDC(intptr_t h)'),
70
+ GetWindowDC: user32.func('intptr_t GetWindowDC(intptr_t h)'),
71
+ ReleaseDC: user32.func('int ReleaseDC(intptr_t h, intptr_t dc)'),
72
+ PrintWindow: user32.func('bool PrintWindow(intptr_t h, intptr_t dc, uint32 flags)'),
73
+ OpenInputDesktop: user32.func('intptr_t OpenInputDesktop(uint32 flags, bool inherit, uint32 access)'),
74
+ CloseDesktop: user32.func('bool CloseDesktop(intptr_t d)'),
75
+ EnumDisplayMonitors: user32.func('bool EnumDisplayMonitors(intptr_t dc, RECT *clip, MonitorEnumProc *cb, intptr_t lparam)'),
76
+ GetMonitorInfoW: user32.func('bool GetMonitorInfoW(intptr_t m, _Inout_ MONITORINFO *i)'),
77
+ GetClipboardData: user32.func('intptr_t GetClipboardData(uint32 fmt)'),
78
+ IsClipboardFormatAvailable: user32.func('bool IsClipboardFormatAvailable(uint32 fmt)'),
79
+ OpenClipboard: user32.func('bool OpenClipboard(intptr_t owner)'),
80
+ CloseClipboard: user32.func('bool CloseClipboard()'),
81
+ EmptyClipboard: user32.func('bool EmptyClipboard()'),
82
+ SetClipboardData: user32.func('intptr_t SetClipboardData(uint32 fmt, intptr_t mem)'),
83
+ // gdi32
84
+ CreateCompatibleDC: gdi32.func('intptr_t CreateCompatibleDC(intptr_t dc)'),
85
+ CreateCompatibleBitmap: gdi32.func('intptr_t CreateCompatibleBitmap(intptr_t dc, int w, int h)'),
86
+ SelectObject: gdi32.func('intptr_t SelectObject(intptr_t dc, intptr_t obj)'),
87
+ DeleteObject: gdi32.func('bool DeleteObject(intptr_t obj)'),
88
+ DeleteDC: gdi32.func('bool DeleteDC(intptr_t dc)'),
89
+ BitBlt: gdi32.func('bool BitBlt(intptr_t dst, int x, int y, int w, int h, intptr_t src, int sx, int sy, uint32 rop)'),
90
+ GetDIBits: gdi32.func('int GetDIBits(intptr_t dc, intptr_t bmp, uint32 start, uint32 lines, _Out_ uint8_t *bits, _Inout_ BITMAPINFOHEADER *bi, uint32 usage)'),
91
+ // kernel32
92
+ OpenProcess: kernel32.func('intptr_t OpenProcess(uint32 access, bool inherit, uint32 pid)'),
93
+ CloseHandle: kernel32.func('bool CloseHandle(intptr_t h)'),
94
+ QueryFullProcessImageNameW: kernel32.func('bool QueryFullProcessImageNameW(intptr_t h, uint32 flags, _Out_ uint16_t *buf, _Inout_ uint32_t *size)'),
95
+ GetCurrentProcess: kernel32.func('intptr_t GetCurrentProcess()'),
96
+ GetCurrentThreadId: kernel32.func('uint32 GetCurrentThreadId()'),
97
+ GlobalAlloc: kernel32.func('intptr_t GlobalAlloc(uint32 flags, size_t n)'),
98
+ GlobalLock: kernel32.func('intptr_t GlobalLock(intptr_t h)'),
99
+ GlobalSize: kernel32.func('size_t GlobalSize(intptr_t h)'),
100
+ GlobalUnlock: kernel32.func('bool GlobalUnlock(intptr_t h)'),
101
+ GlobalFree: kernel32.func('intptr_t GlobalFree(intptr_t h)'),
102
+ CopyToMem: kernel32.func('RtlMoveMemory', 'void', ['intptr_t', 'uint8_t *', 'size_t']),
103
+ CopyFromMem: kernel32.func('RtlMoveMemory', 'void', ['_Out_ uint8_t *', 'intptr_t', 'size_t']),
104
+ // advapi32
105
+ OpenProcessToken: advapi32.func('bool OpenProcessToken(intptr_t p, uint32 access, _Out_ intptr_t *tok)'),
106
+ GetTokenInformation: advapi32.func('bool GetTokenInformation(intptr_t tok, int cls, _Out_ uint32_t *info, uint32 len, _Out_ uint32_t *ret)'),
107
+ // dwmapi
108
+ DwmGetWindowAttribute: dwmapi.func('int32 DwmGetWindowAttribute(intptr_t h, uint32 attr, _Out_ uint32_t *v, uint32 n)'),
109
+ // ole32
110
+ CoInitializeEx: ole32.func('int32 CoInitializeEx(intptr_t reserved, uint32 flags)'),
111
+ CoCreateInstance: ole32.func('int32 CoCreateInstance(uint8_t *clsid, intptr_t outer, uint32 ctx, uint8_t *iid, _Out_ intptr_t *out)'),
112
+ };
113
+
114
+ const MOUSE = { MOVE: 0x1, LEFTDOWN: 0x2, LEFTUP: 0x4, RIGHTDOWN: 0x8, RIGHTUP: 0x10, MIDDLEDOWN: 0x20, MIDDLEUP: 0x40, WHEEL: 0x800, VIRTUALDESK: 0x4000, ABSOLUTE: 0x8000 };
115
+ const BUTTONS = {
116
+ left: [MOUSE.LEFTDOWN, MOUSE.LEFTUP],
117
+ right: [MOUSE.RIGHTDOWN, MOUSE.RIGHTUP],
118
+ middle: [MOUSE.MIDDLEDOWN, MOUSE.MIDDLEUP],
119
+ };
120
+
121
+ export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
122
+
123
+ let initialised = false;
124
+ export function init() {
125
+ if (initialised) return;
126
+ initialised = true;
127
+ // PER_MONITOR_AWARE_V2 = -4. Every coordinate in this process is a physical pixel.
128
+ if (!f.SetProcessDpiAwarenessContext(-4)) {
129
+ throw new Error('Could not switch the process to per-monitor DPI awareness (v2); coordinates would be wrong.');
130
+ }
131
+ timeBeginPeriod(1); // 1 ms timer ticks, so pointer glides update ~100+ times a second instead of ~64
132
+ f.CoInitializeEx(0, 2); // apartment threaded, needed for UI Automation
133
+ }
134
+
135
+ // ---------- strings / windows ----------
136
+
137
+ function readW(fn, hwnd) {
138
+ const buf = new Uint16Array(512);
139
+ const n = fn(hwnd, buf, 512);
140
+ return n > 0 ? Buffer.from(buf.buffer, 0, n * 2).toString('utf16le') : '';
141
+ }
142
+ export const getTitle = (h) => readW(f.GetWindowTextW, h);
143
+ export const getClass = (h) => readW(f.GetClassNameW, h);
144
+
145
+ export function getRect(hwnd) {
146
+ const r = {};
147
+ if (!f.GetWindowRect(hwnd, r)) return null;
148
+ return { left: r.left, top: r.top, width: r.right - r.left, height: r.bottom - r.top };
149
+ }
150
+
151
+ export function getPid(hwnd) {
152
+ const out = [0];
153
+ f.GetWindowThreadProcessId(hwnd, out);
154
+ return out[0];
155
+ }
156
+
157
+ const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
158
+ const processCache = new Map();
159
+ export function processInfo(pid) {
160
+ if (!pid) return { name: '', path: '', elevated: false, unknown: true };
161
+ const h = f.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
162
+ if (!h) return { name: '', path: '', elevated: true, unknown: true };
163
+ try {
164
+ const buf = new Uint16Array(1024);
165
+ const size = [1024];
166
+ let path = '';
167
+ if (f.QueryFullProcessImageNameW(h, 0, buf, size)) path = Buffer.from(buf.buffer, 0, size[0] * 2).toString('utf16le');
168
+ const name = path.split('\\').pop().toLowerCase();
169
+ return { name, path, elevated: isElevated(h) };
170
+ } finally {
171
+ f.CloseHandle(h);
172
+ }
173
+ }
174
+
175
+ let selfElevated;
176
+ function isElevated(hProcess) {
177
+ const tok = [0];
178
+ if (!f.OpenProcessToken(hProcess, 0x8, tok)) return true; // denied to a non-elevated caller means a higher integrity level
179
+ try {
180
+ const info = [0];
181
+ const ret = [0];
182
+ if (!f.GetTokenInformation(tok[0], 20, info, 4, ret)) return false;
183
+ return info[0] !== 0;
184
+ } finally {
185
+ f.CloseHandle(tok[0]);
186
+ }
187
+ }
188
+ export function selfIsElevated() {
189
+ if (selfElevated === undefined) selfElevated = isElevated(f.GetCurrentProcess());
190
+ return selfElevated;
191
+ }
192
+
193
+ export function describeWindow(hwnd) {
194
+ const pid = getPid(hwnd);
195
+ const p = processInfo(pid);
196
+ return {
197
+ hwnd,
198
+ title: getTitle(hwnd),
199
+ className: getClass(hwnd),
200
+ pid,
201
+ process: p.name,
202
+ elevated: p.elevated,
203
+ minimized: f.IsIconic(hwnd),
204
+ rect: getRect(hwnd),
205
+ };
206
+ }
207
+
208
+ function isCloaked(hwnd) {
209
+ const v = [0];
210
+ return f.DwmGetWindowAttribute(hwnd, 14, v, 4) === 0 && v[0] !== 0;
211
+ }
212
+
213
+ export function listWindows({ includeUntitled = false } = {}) {
214
+ const out = [];
215
+ const cb = koffi.register((hwnd) => {
216
+ try {
217
+ if (!f.IsWindowVisible(hwnd) || isCloaked(hwnd)) return true;
218
+ const title = getTitle(hwnd);
219
+ if (!title && !includeUntitled && !/^Shell_(Secondary)?TrayWnd$/.test(getClass(hwnd))) return true;
220
+ out.push(describeWindow(hwnd));
221
+ } catch { /* skip a window that vanished mid-enumeration */ }
222
+ return true;
223
+ }, koffi.pointer(EnumWindowsProc));
224
+ try {
225
+ f.EnumWindows(cb, 0);
226
+ } finally {
227
+ koffi.unregister(cb);
228
+ }
229
+ return out;
230
+ }
231
+
232
+ export const windowExists = (h) => !!h && f.IsWindow(h);
233
+ export const rootWindowAt = (x, y) => {
234
+ const h = f.WindowFromPoint({ x, y });
235
+ return h ? f.GetAncestor(h, 2) : 0;
236
+ };
237
+ export const foregroundWindow = () => f.GetForegroundWindow();
238
+
239
+ export function isScreenLocked() {
240
+ // The input desktop cannot be opened while the secure (lock / UAC) desktop is active.
241
+ const d = f.OpenInputDesktop(0, false, 0x100);
242
+ if (!d) return true;
243
+ f.CloseDesktop(d);
244
+ return false;
245
+ }
246
+
247
+ export function windowDpi(hwnd) {
248
+ return f.GetDpiForWindow(hwnd);
249
+ }
250
+
251
+ // ---------- focus ----------
252
+
253
+ export async function bringToForeground(hwnd) {
254
+ if (f.IsIconic(hwnd)) {
255
+ f.ShowWindow(hwnd, 9); // SW_RESTORE
256
+ await sleep(150);
257
+ }
258
+ const ok = () => f.GetForegroundWindow() === hwnd;
259
+ if (ok()) return true;
260
+ f.SetForegroundWindow(hwnd);
261
+ await sleep(80);
262
+ if (ok()) return true;
263
+
264
+ // Workaround 1: join the input queue of the current foreground thread.
265
+ const fg = f.GetForegroundWindow();
266
+ const fgThread = fg ? f.GetWindowThreadProcessId(fg, [0]) : 0;
267
+ const me = f.GetCurrentThreadId();
268
+ if (fgThread && fgThread !== me && f.AttachThreadInput(me, fgThread, true)) {
269
+ try {
270
+ f.BringWindowToTop(hwnd);
271
+ f.SetForegroundWindow(hwnd);
272
+ } finally {
273
+ f.AttachThreadInput(me, fgThread, false);
274
+ }
275
+ await sleep(80);
276
+ if (ok()) return true;
277
+ }
278
+ // Workaround 2: a synthetic Alt press lifts the foreground lock for this process.
279
+ f.keybd_event(0x12, 0, 0, 0);
280
+ f.keybd_event(0x12, 0, 2, 0);
281
+ f.SetForegroundWindow(hwnd);
282
+ await sleep(120);
283
+ return ok();
284
+ }
285
+
286
+ // ---------- screen geometry ----------
287
+
288
+ export function virtualScreen() {
289
+ return {
290
+ x: f.GetSystemMetrics(76), y: f.GetSystemMetrics(77),
291
+ width: f.GetSystemMetrics(78), height: f.GetSystemMetrics(79),
292
+ };
293
+ }
294
+
295
+ export function listMonitors() {
296
+ const out = [];
297
+ const cb = koffi.register((hmon) => {
298
+ const mi = { cbSize: 40, rcMonitor: {}, rcWork: {}, dwFlags: 0 };
299
+ f.GetMonitorInfoW(hmon, mi);
300
+ const r = mi.rcMonitor;
301
+ out.push({ left: r.left, top: r.top, width: r.right - r.left, height: r.bottom - r.top, primary: (mi.dwFlags & 1) === 1 });
302
+ return true;
303
+ }, koffi.pointer(MonitorEnumProc));
304
+ try {
305
+ f.EnumDisplayMonitors(0, null, cb, 0);
306
+ } finally {
307
+ koffi.unregister(cb);
308
+ }
309
+ return out;
310
+ }
311
+
312
+ export function cursorPos() {
313
+ const p = {};
314
+ f.GetCursorPos(p);
315
+ return { x: p.x, y: p.y };
316
+ }
317
+
318
+ // ---------- capture ----------
319
+
320
+ function grab(w, h, draw) {
321
+ const screenDc = f.GetDC(0);
322
+ const memDc = f.CreateCompatibleDC(screenDc);
323
+ const bmp = f.CreateCompatibleBitmap(screenDc, w, h);
324
+ const old = f.SelectObject(memDc, bmp);
325
+ try {
326
+ draw(memDc, screenDc);
327
+ const bi = {
328
+ biSize: 40, biWidth: w, biHeight: -h, biPlanes: 1, biBitCount: 32, biCompression: 0,
329
+ biSizeImage: 0, biXPelsPerMeter: 0, biYPelsPerMeter: 0, biClrUsed: 0, biClrImportant: 0,
330
+ };
331
+ const bits = Buffer.alloc(w * h * 4);
332
+ if (!f.GetDIBits(memDc, bmp, 0, h, bits, bi, 0)) throw new Error('Could not read the captured pixels.');
333
+ return bits; // BGRA, top-down
334
+ } finally {
335
+ f.SelectObject(memDc, old);
336
+ f.DeleteObject(bmp);
337
+ f.DeleteDC(memDc);
338
+ f.ReleaseDC(0, screenDc);
339
+ }
340
+ }
341
+
342
+ const isBlank = (bits) => {
343
+ for (let i = 0; i < bits.length; i += 4 * 61) if (bits[i] | bits[i + 1] | bits[i + 2]) return false;
344
+ return true;
345
+ };
346
+
347
+ /** Window-only capture through PrintWindow(PW_RENDERFULLCONTENT). Falls back to a screen copy if that returns black. */
348
+ export function captureWindow(hwnd) {
349
+ const r = getRect(hwnd);
350
+ if (!r || r.width <= 0 || r.height <= 0) throw new Error('The window has no visible area, so it cannot be captured.');
351
+ let bits = grab(r.width, r.height, (memDc) => {
352
+ f.PrintWindow(hwnd, memDc, 2);
353
+ });
354
+ let method = 'PrintWindow';
355
+ if (isBlank(bits)) {
356
+ if (foregroundWindow() !== hwnd) throw new Error('The window rendered nothing through PrintWindow and is not in front, so it cannot be captured. Focus it first.');
357
+ bits = grab(r.width, r.height, (memDc, screenDc) => {
358
+ f.BitBlt(memDc, 0, 0, r.width, r.height, screenDc, r.left, r.top, 0x00CC0020 | 0x40000000);
359
+ });
360
+ method = 'BitBlt';
361
+ }
362
+ return { bits, width: r.width, height: r.height, rect: r, method };
363
+ }
364
+
365
+ export function captureScreenRect(rect) {
366
+ const bits = grab(rect.width, rect.height, (memDc, screenDc) => {
367
+ f.BitBlt(memDc, 0, 0, rect.width, rect.height, screenDc, rect.left, rect.top, 0x00CC0020 | 0x40000000);
368
+ });
369
+ return { bits, width: rect.width, height: rect.height, rect };
370
+ }
371
+
372
+ // ---------- input ----------
373
+
374
+ function send(inputs) {
375
+ const sent = f.SendInput(inputs.length, inputs, 40);
376
+ if (sent !== inputs.length) {
377
+ throw new Error('Windows refused the input (blocked by another process, a higher-privilege window, or a locked screen).');
378
+ }
379
+ }
380
+
381
+ const mouseInput = (flags, data = 0, dx = 0, dy = 0) => ({
382
+ type: 0, u: { mi: { dx, dy, mouseData: data >>> 0, dwFlags: flags, time: 0, dwExtraInfo: 0 } },
383
+ });
384
+ const keyInput = (vk, scan, flags) => ({
385
+ type: 1, u: { ki: { wVk: vk, wScan: scan, dwFlags: flags, time: 0, dwExtraInfo: 0 } },
386
+ });
387
+
388
+ export function mouseMoveTo(x, y) {
389
+ const v = virtualScreen();
390
+ const nx = Math.round(((x - v.x) * 65535) / (v.width - 1));
391
+ const ny = Math.round(((y - v.y) * 65535) / (v.height - 1));
392
+ send([mouseInput(MOUSE.MOVE | MOUSE.ABSOLUTE | MOUSE.VIRTUALDESK, 0, nx, ny)]);
393
+ }
394
+ export const mouseDown = (button = 'left') => send([mouseInput(BUTTONS[button][0])]);
395
+ export const mouseUp = (button = 'left') => send([mouseInput(BUTTONS[button][1])]);
396
+ export const mouseWheel = (notches) => send([mouseInput(MOUSE.WHEEL, Math.round(notches * 120))]);
397
+ export const mouseHWheel = (notches) => send([mouseInput(0x1000, Math.round(notches * 120))]); // positive = right
398
+
399
+ /**
400
+ * One character at a time with a pause after each (delays[i] ms, never under 8 ms), like a person typing.
401
+ * Text is never sent as one batch: apps drop and repeat characters then (Notepad also mangles a batch that
402
+ * mixes ASCII and non-ASCII). Unicode packets do not depend on the keyboard layout.
403
+ */
404
+ export async function typeUnicode(text, check, delays) {
405
+ let i = 0;
406
+ for (const ch of text) {
407
+ if (ch === String.fromCharCode(13)) { i++; continue; }
408
+ check?.();
409
+ const inputs = [];
410
+ if (ch === String.fromCharCode(10)) {
411
+ inputs.push(keyInput(0x0d, 0, 0), keyInput(0x0d, 0, 2));
412
+ } else {
413
+ for (let k = 0; k < ch.length; k++) {
414
+ const unit = ch.charCodeAt(k);
415
+ inputs.push(keyInput(0, unit, 0x4), keyInput(0, unit, 0x4 | 0x2));
416
+ }
417
+ }
418
+ send(inputs);
419
+ await sleep(Math.max(8, delays[i++] ?? 8));
420
+ }
421
+ }
422
+
423
+ const EXTENDED = new Set([0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x2d, 0x2e, 0x6f, 0x5b, 0x5c, 0x5d]);
424
+ const kflag = (vk) => (EXTENDED.has(vk) ? 1 : 0);
425
+ export function keysDown(vks) {
426
+ send(vks.map((vk) => keyInput(vk, f.MapVirtualKeyW(vk, 0), kflag(vk))));
427
+ }
428
+ export function keysUp(vks) {
429
+ send([...vks].reverse().map((vk) => keyInput(vk, f.MapVirtualKeyW(vk, 0), kflag(vk) | 2)));
430
+ }
431
+ export function pressVirtualKeys(vks) {
432
+ const flag = (vk) => (EXTENDED.has(vk) ? 1 : 0);
433
+ const scan = (vk) => f.MapVirtualKeyW(vk, 0);
434
+ const down = vks.map((vk) => keyInput(vk, scan(vk), flag(vk)));
435
+ const up = [...vks].reverse().map((vk) => keyInput(vk, scan(vk), flag(vk) | 2));
436
+ send([...down, ...up]);
437
+ }
438
+
439
+ // ---------- clipboard ----------
440
+
441
+ function withClipboard(fn) {
442
+ let opened = false;
443
+ for (let i = 0; i < 10 && !opened; i++) {
444
+ opened = f.OpenClipboard(0);
445
+ if (!opened) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 30);
446
+ }
447
+ if (!opened) throw new Error('The clipboard is locked by another program. Try again in a moment.');
448
+ try {
449
+ f.EmptyClipboard();
450
+ fn();
451
+ } finally {
452
+ f.CloseClipboard();
453
+ }
454
+ }
455
+
456
+ function putGlobal(fmt, bytes) {
457
+ const h = f.GlobalAlloc(0x2, bytes.length); // GMEM_MOVEABLE
458
+ if (!h) throw new Error('Out of memory while preparing the clipboard.');
459
+ const p = f.GlobalLock(h);
460
+ f.CopyToMem(p, bytes, bytes.length);
461
+ f.GlobalUnlock(h);
462
+ if (!f.SetClipboardData(fmt, h)) {
463
+ f.GlobalFree(h);
464
+ throw new Error('Windows refused the clipboard data.');
465
+ }
466
+ }
467
+
468
+ export function getClipboardText() {
469
+ let opened = false;
470
+ for (let i = 0; i < 10 && !opened; i++) {
471
+ opened = f.OpenClipboard(0);
472
+ if (!opened) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 30);
473
+ }
474
+ if (!opened) throw new Error('The clipboard is locked by another program. Try again in a moment.');
475
+ try {
476
+ const formats = { text: f.IsClipboardFormatAvailable(13), image: f.IsClipboardFormatAvailable(2) || f.IsClipboardFormatAvailable(8), files: f.IsClipboardFormatAvailable(15) };
477
+ let text = null;
478
+ if (formats.text) {
479
+ const h = f.GetClipboardData(13);
480
+ if (h) {
481
+ const size = Number(f.GlobalSize(h));
482
+ const p = f.GlobalLock(h);
483
+ const buf = Buffer.alloc(size);
484
+ f.CopyFromMem(buf, p, size);
485
+ f.GlobalUnlock(h);
486
+ text = buf.toString('utf16le').replace(/.*$/s, '');
487
+ }
488
+ }
489
+ return { text, formats };
490
+ } finally {
491
+ f.CloseClipboard();
492
+ }
493
+ }
494
+
495
+ export function setClipboardText(text) {
496
+ const bytes = Buffer.from(text + '\0', 'utf16le');
497
+ withClipboard(() => putGlobal(13, bytes)); // CF_UNICODETEXT
498
+ }
499
+
500
+ export function setClipboardFile(path) {
501
+ const head = Buffer.alloc(20);
502
+ head.writeUInt32LE(20, 0); // pFiles
503
+ head.writeUInt32LE(1, 16); // fWide
504
+ const bytes = Buffer.concat([head, Buffer.from(path + '\0\0', 'utf16le')]);
505
+ withClipboard(() => putGlobal(15, bytes)); // CF_HDROP
506
+ }
507
+
508
+ // ---------- password-field detection ----------
509
+
510
+ const guid = (s) => {
511
+ const h = s.replace(/[{}-]/g, '');
512
+ const b = Buffer.alloc(16);
513
+ b.writeUInt32LE(parseInt(h.slice(0, 8), 16), 0);
514
+ b.writeUInt16LE(parseInt(h.slice(8, 12), 16), 4);
515
+ b.writeUInt16LE(parseInt(h.slice(12, 16), 16), 6);
516
+ Buffer.from(h.slice(16), 'hex').copy(b, 8);
517
+ return b;
518
+ };
519
+
520
+ const ComFn = {
521
+ getFocused: koffi.proto('int32 UiaGetFocused(intptr_t self, _Out_ intptr_t *el)'),
522
+ getBool: koffi.proto('int32 UiaGetBool(intptr_t self, _Out_ int32_t *v)'),
523
+ release: koffi.proto('uint32 UiaRelease(intptr_t self)'),
524
+ };
525
+ function vtableFn(obj, index, proto) {
526
+ const head = Buffer.alloc(8);
527
+ f.CopyFromMem(head, obj, 8);
528
+ const slot = Buffer.alloc(8);
529
+ f.CopyFromMem(slot, head.readBigUInt64LE(0) + BigInt(index * 8), 8);
530
+ const fn = koffi.decode(slot, 'void *');
531
+ return (...args) => koffi.call(fn, proto, ...args);
532
+ }
533
+
534
+ let uia = 0;
535
+ function uiaFocusedIsPassword() {
536
+ if (!uia) {
537
+ const out = [0];
538
+ // CLSID_CUIAutomation, IID_IUIAutomation; CLSCTX_INPROC_SERVER
539
+ const hr = f.CoCreateInstance(guid('ff48dba4-60ef-4201-aa87-54103eef594e'), 0, 1, guid('30cbe57d-d9d0-452a-ab13-7ac5ac4825ee'), out);
540
+ if (hr !== 0) throw new Error('UI Automation is unavailable.');
541
+ uia = out[0];
542
+ }
543
+ const el = [0];
544
+ const hr = vtableFn(uia, 8, ComFn.getFocused)(uia, el); // IUIAutomation::GetFocusedElement
545
+ if (hr !== 0 || !el[0]) return false;
546
+ try {
547
+ const v = [0];
548
+ const r2 = vtableFn(el[0], 35, ComFn.getBool)(el[0], v); // IUIAutomationElement::get_CurrentIsPassword
549
+ if (r2 !== 0) throw new Error('UI Automation could not read the focused field.');
550
+ return v[0] !== 0;
551
+ } finally {
552
+ vtableFn(el[0], 2, ComFn.release)(el[0]);
553
+ }
554
+ }
555
+
556
+ /** True when the keyboard focus sits in a password field. Throws when that cannot be determined. */
557
+ export function focusIsPassword() {
558
+ const fg = f.GetForegroundWindow();
559
+ if (fg) {
560
+ const tid = f.GetWindowThreadProcessId(fg, [0]);
561
+ const gti = { cbSize: 72, flags: 0, hwndActive: 0, hwndFocus: 0, hwndCapture: 0, hwndMenuOwner: 0, hwndMoveSize: 0, hwndCaret: 0, rcCaret: {} };
562
+ if (f.GetGUIThreadInfo(tid, gti) && gti.hwndFocus) {
563
+ const style = Number(f.GetWindowLongPtrW(gti.hwndFocus, -16)) >>> 0;
564
+ if (/edit/i.test(getClass(gti.hwndFocus)) && (style & 0x20) !== 0) return true; // ES_PASSWORD
565
+ }
566
+ }
567
+ return uiaFocusedIsPassword();
568
+ }
569
+
570
+ /** Language id (e.g. 0x41f = Turkish) of the keyboard layout active in the foreground window. */
571
+ export function foregroundLayout() {
572
+ const fg = f.GetForegroundWindow();
573
+ const tid = fg ? f.GetWindowThreadProcessId(fg, [0]) : 0;
574
+ return Number(BigInt.asUintN(16, BigInt(f.GetKeyboardLayout(tid))));
575
+ }
576
+
577
+ export const _raw = f;
package/src/server.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ // Entry point (bin `ispyx`; existing MCP registrations point here too).
3
+ // ispyx Windows with a console: start the resident app if needed, then exit
4
+ // ispyx --resident the resident app (tray, panel, tool queue)
5
+ // ispyx mcp stdio MCP server forwarding to the resident (started on demand)
6
+ // ispyx quit ask the resident to exit
7
+ // Windows without a console on stdin (an old MCP registration) behaves as `mcp`. Other platforms run the
8
+ // classic server (core/server.js), unchanged. See src/core/RESIDENT.md.
9
+ import { resolveMode } from "./core/mode.js";
10
+
11
+ const mode = resolveMode();
12
+ if (mode === "launcher") await (await import("./core/launcher.js")).runLauncher();
13
+ else if (mode === "quit") await (await import("./core/launcher.js")).runQuit();
14
+ else await import("./core/server.js");