@lardexx/arenacode 2.7.0 → 2.9.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.
@@ -0,0 +1,809 @@
1
+ 'use strict';
2
+
3
+ // Linux backend: Xlib + XTest through koffi. No xdotool, no wmctrl, no scrot, no
4
+ // ImageMagick: the client talks to the X server itself, which is also what makes it
5
+ // testable (Xvfb gives a real display to move the pointer and read pixels from).
6
+
7
+ const fs = require('node:fs');
8
+ const os = require('node:os');
9
+ const path = require('node:path');
10
+ const zlib = require('node:zlib');
11
+ const { spawn } = require('node:child_process');
12
+ const { lib, fn } = require('./ffi');
13
+
14
+ const DISPLAY = () => process.env.DISPLAY || ':0';
15
+
16
+ let dpy = null;
17
+ let screen = 0;
18
+ let rootWindow = 0;
19
+
20
+ function openDisplay() {
21
+ if (dpy) return dpy;
22
+ const display = DISPLAY();
23
+ if (!/^:?\d|^.*:\d/.test(display)) throw new Error(`no X11 display (DISPLAY=${display})`);
24
+ const XOpenDisplay = fn('libX11.so.6', 'XOpenDisplay', 'void* XOpenDisplay(const char* name)');
25
+ if (!XOpenDisplay) throw new Error('libX11 not found');
26
+ const handle = XOpenDisplay(display);
27
+ if (!handle) throw new Error(`XOpenDisplay(${display}) failed`);
28
+ const XDefaultScreen = fn('libX11.so.6', 'XDefaultScreen', 'int XDefaultScreen(void* dpy)');
29
+ const XRootWindow = fn('libX11.so.6', 'XRootWindow', 'unsigned long XRootWindow(void* dpy, int screen)');
30
+ dpy = handle;
31
+ screen = XDefaultScreen(dpy);
32
+ rootWindow = XRootWindow(dpy, screen);
33
+ return dpy;
34
+ }
35
+
36
+ function closeDisplay() {
37
+ if (!dpy) return;
38
+ const XCloseDisplay = fn('libX11.so.6', 'XCloseDisplay', 'int XCloseDisplay(void* dpy)');
39
+ try {
40
+ if (XCloseDisplay) XCloseDisplay(dpy);
41
+ } catch {
42
+ /* ignore */
43
+ }
44
+ dpy = null;
45
+ }
46
+
47
+ const flush = () => {
48
+ const XFlush = fn('libX11.so.6', 'XFlush', 'int XFlush(void* dpy)');
49
+ if (XFlush) XFlush(dpy);
50
+ };
51
+ const sync = () => {
52
+ const XSync = fn('libX11.so.6', 'XSync', 'int XSync(void* dpy, int discard)');
53
+ if (XSync) XSync(dpy, 0);
54
+ };
55
+
56
+ function requireX() {
57
+ openDisplay();
58
+ const Xtst = lib('libXtst.so.6');
59
+ if (!Xtst) throw new Error('libXtst not found (install libxtst6): input injection needs XTest');
60
+ }
61
+
62
+ // --------------------------------------------------------------------- input
63
+
64
+ const BUTTONS = { left: 1, middle: 2, right: 3 };
65
+
66
+ function fakeButton(button, down) {
67
+ const XTestFakeButtonEvent = fn(
68
+ 'libXtst.so.6',
69
+ 'XTestFakeButtonEvent',
70
+ 'int XTestFakeButtonEvent(void* dpy, unsigned int button, bool is_press, unsigned long delay)'
71
+ );
72
+ XTestFakeButtonEvent(dpy, button, down, 0);
73
+ flush();
74
+ }
75
+
76
+ function moveMouse(x, y) {
77
+ requireX();
78
+ const XTestFakeMotionEvent = fn(
79
+ 'libXtst.so.6',
80
+ 'XTestFakeMotionEvent',
81
+ 'int XTestFakeMotionEvent(void* dpy, int screen, int x, int y, unsigned long delay)'
82
+ );
83
+ XTestFakeMotionEvent(dpy, screen, Math.round(x), Math.round(y), 0);
84
+ flush();
85
+ return { ok: true, x, y };
86
+ }
87
+
88
+ function click(x, y, { button = 'left', count = 1 } = {}) {
89
+ requireX();
90
+ moveMouse(x, y);
91
+ const b = BUTTONS[button] || 1;
92
+ for (let i = 0; i < Math.max(1, count); i += 1) {
93
+ fakeButton(b, true);
94
+ fakeButton(b, false);
95
+ if (i + 1 < count) sync();
96
+ }
97
+ return { ok: true, x, y, button, count };
98
+ }
99
+
100
+ function mouseButton(x, y, button, down) {
101
+ requireX();
102
+ if (Number.isFinite(x) && Number.isFinite(y)) moveMouse(x, y);
103
+ fakeButton(BUTTONS[button] || 1, down);
104
+ return { ok: true, x, y, button };
105
+ }
106
+
107
+ function drag(x, y, x2, y2) {
108
+ requireX();
109
+ moveMouse(x, y);
110
+ fakeButton(1, true);
111
+ const steps = 12;
112
+ for (let i = 1; i <= steps; i += 1) {
113
+ const nx = Math.round(x + ((x2 - x) * i) / steps);
114
+ const ny = Math.round(y + ((y2 - y) * i) / steps);
115
+ const XTestFakeMotionEvent = fn(
116
+ 'libXtst.so.6',
117
+ 'XTestFakeMotionEvent',
118
+ 'int XTestFakeMotionEvent(void* dpy, int screen, int x, int y, unsigned long delay)'
119
+ );
120
+ XTestFakeMotionEvent(dpy, screen, nx, ny, 0);
121
+ flush();
122
+ }
123
+ fakeButton(1, false);
124
+ return { ok: true, from: [x, y], to: [x2, y2] };
125
+ }
126
+
127
+ function scroll(amount, x = null, y = null) {
128
+ requireX();
129
+ if (Number.isFinite(x) && Number.isFinite(y)) moveMouse(x, y);
130
+ const button = amount > 0 ? 4 : 5;
131
+ for (let i = 0; i < Math.abs(Math.trunc(amount) || 1); i += 1) {
132
+ fakeButton(button, true);
133
+ fakeButton(button, false);
134
+ }
135
+ return { ok: true, amount };
136
+ }
137
+
138
+ // ------------------------------------------------------------------ keyboard
139
+
140
+ const MOD_KEYSYMS = {
141
+ ctrl: 'Control_L',
142
+ control: 'Control_L',
143
+ alt: 'Alt_L',
144
+ option: 'Alt_L',
145
+ shift: 'Shift_L',
146
+ win: 'Super_L',
147
+ super: 'Super_L',
148
+ meta: 'Super_L',
149
+ cmd: 'Super_L',
150
+ command: 'Super_L',
151
+ };
152
+
153
+ const KEY_NAMES = {
154
+ enter: 'Return',
155
+ return: 'Return',
156
+ tab: 'Tab',
157
+ esc: 'Escape',
158
+ escape: 'Escape',
159
+ space: 'space',
160
+ spacebar: 'space',
161
+ backspace: 'BackSpace',
162
+ bs: 'BackSpace',
163
+ delete: 'Delete',
164
+ del: 'Delete',
165
+ insert: 'Insert',
166
+ ins: 'Insert',
167
+ home: 'Home',
168
+ end: 'End',
169
+ pageup: 'Page_Up',
170
+ pagedown: 'Page_Down',
171
+ pgup: 'Page_Up',
172
+ pgdn: 'Page_Down',
173
+ up: 'Up',
174
+ down: 'Down',
175
+ left: 'Left',
176
+ right: 'Right',
177
+ arrowup: 'Up',
178
+ arrowdown: 'Down',
179
+ arrowleft: 'Left',
180
+ arrowright: 'Right',
181
+ capslock: 'Caps_Lock',
182
+ numlock: 'Num_Lock',
183
+ scrolllock: 'Scroll_Lock',
184
+ printscreen: 'Print',
185
+ prtsc: 'Print',
186
+ pause: 'Pause',
187
+ menu: 'Menu',
188
+ contextmenu: 'Menu',
189
+ apps: 'Menu',
190
+ plus: 'plus',
191
+ minus: 'minus',
192
+ comma: 'comma',
193
+ period: 'period',
194
+ slash: 'slash',
195
+ backslash: 'backslash',
196
+ semicolon: 'semicolon',
197
+ quote: 'apostrophe',
198
+ bracketleft: 'bracketleft',
199
+ bracketright: 'bracketright',
200
+ backtick: 'grave',
201
+ equal: 'equal',
202
+ numpad0: 'KP_0',
203
+ numpad1: 'KP_1',
204
+ numpad2: 'KP_2',
205
+ numpad3: 'KP_3',
206
+ numpad4: 'KP_4',
207
+ numpad5: 'KP_5',
208
+ numpad6: 'KP_6',
209
+ numpad7: 'KP_7',
210
+ numpad8: 'KP_8',
211
+ numpad9: 'KP_9',
212
+ };
213
+ for (let i = 1; i <= 24; i += 1) KEY_NAMES[`f${i}`] = `F${i}`;
214
+
215
+ const keysymOf = (name) => {
216
+ const XStringToKeysym = fn('libX11.so.6', 'XStringToKeysym', 'unsigned long XStringToKeysym(const char* name)');
217
+ return Number(XStringToKeysym(name)) || 0;
218
+ };
219
+
220
+ const keycodeOf = (keysym) => {
221
+ const XKeysymToKeycode = fn('libX11.so.6', 'XKeysymToKeycode', 'unsigned char XKeysymToKeycode(void* dpy, unsigned long keysym)');
222
+ return Number(XKeysymToKeycode(dpy, keysym)) || 0;
223
+ };
224
+
225
+ function fakeKey(keycode, down) {
226
+ const XTestFakeKeyEvent = fn(
227
+ 'libXtst.so.6',
228
+ 'XTestFakeKeyEvent',
229
+ 'int XTestFakeKeyEvent(void* dpy, unsigned int keycode, bool is_press, unsigned long delay)'
230
+ );
231
+ XTestFakeKeyEvent(dpy, keycode, down, 0);
232
+ flush();
233
+ }
234
+
235
+ /** Does this keysym sit in the shifted column of its keycode? */
236
+ function needsShift(keysym, keycode) {
237
+ const XGetKeyboardMapping = fn(
238
+ 'libX11.so.6',
239
+ 'XGetKeyboardMapping',
240
+ 'unsigned long* XGetKeyboardMapping(void* dpy, unsigned char first_keycode, int keycode_count, int* keysyms_per_keycode)'
241
+ );
242
+ const XFree = fn('libX11.so.6', 'XFree', 'int XFree(void* data)');
243
+ const per = [0];
244
+ const ptr = XGetKeyboardMapping(dpy, keycode, 1, per);
245
+ if (!ptr) return false;
246
+ let shifted = false;
247
+ try {
248
+ const count = per[0] || 1;
249
+ const koffi = require('koffi');
250
+ const arr = koffi.decode(ptr, koffi.array('unsigned long', count));
251
+ if (Number(arr[0]) !== keysym && Number(arr[1]) === keysym) shifted = true;
252
+ } catch {
253
+ shifted = false;
254
+ }
255
+ if (XFree) XFree(ptr);
256
+ return shifted;
257
+ }
258
+
259
+ let spareKeycode = 0;
260
+ let restoreTimer = null;
261
+
262
+ /** Gives the borrowed keycode back to the keymap (NoSymbol). */
263
+ function releaseKeycode() {
264
+ restoreTimer = null;
265
+ if (!spareKeycode || !dpy) return;
266
+ const koffi = require('koffi');
267
+ const XChangeKeyboardMapping = fn(
268
+ 'libX11.so.6',
269
+ 'XChangeKeyboardMapping',
270
+ 'int XChangeKeyboardMapping(void* dpy, int first_keycode, int keysyms_per_keycode, unsigned long* keysyms, int num_codes)'
271
+ );
272
+ const empty = koffi.alloc('unsigned long', 2);
273
+ koffi.encode(empty, koffi.array('unsigned long', 2), [0, 0]);
274
+ XChangeKeyboardMapping(dpy, spareKeycode, 2, empty, 1);
275
+ sync();
276
+ }
277
+
278
+ function unicodeKeycode(codepoint) {
279
+ // Borrow an unused keycode, map it to the character, press it, give it back.
280
+ const koffi = require('koffi');
281
+ const XDisplayKeycodes = fn('libX11.so.6', 'XDisplayKeycodes', 'int XDisplayKeycodes(void* dpy, _Out_ int* min, _Out_ int* max)');
282
+ const XChangeKeyboardMapping = fn(
283
+ 'libX11.so.6',
284
+ 'XChangeKeyboardMapping',
285
+ 'int XChangeKeyboardMapping(void* dpy, int first_keycode, int keysyms_per_keycode, unsigned long* keysyms, int num_codes)'
286
+ );
287
+ if (!spareKeycode) {
288
+ const min = [0];
289
+ const max = [0];
290
+ XDisplayKeycodes(dpy, min, max);
291
+ // the last keycodes are normally free
292
+ spareKeycode = Math.max(min[0] || 8, (max[0] || min[0] + 1) - 1);
293
+ if (process.env.ARENACODE_DEBUG_X) console.error('keycodes', min[0], max[0], 'spare', spareKeycode);
294
+ }
295
+ const syms = koffi.alloc('unsigned long', 2);
296
+ koffi.encode(syms, koffi.array('unsigned long', 2), [codepoint, codepoint]);
297
+ if (restoreTimer) {
298
+ clearTimeout(restoreTimer);
299
+ restoreTimer = null;
300
+ releaseKeycode();
301
+ }
302
+ XChangeKeyboardMapping(dpy, spareKeycode, 2, syms, 1);
303
+ sync();
304
+ fakeKey(spareKeycode, true);
305
+ fakeKey(spareKeycode, false);
306
+ sync();
307
+ // The target translates the event when it receives it, slightly after this request
308
+ // was queued: keep the character mapped for a moment, then give the keycode back.
309
+ if (process.env.ARENACODE_KEEP_KEYMAP !== '1') {
310
+ restoreTimer = setTimeout(releaseKeycode, 60);
311
+ if (restoreTimer.unref) restoreTimer.unref();
312
+ }
313
+ return spareKeycode;
314
+ }
315
+
316
+ function typeText(text, x = null, y = null) {
317
+ requireX();
318
+ if (x !== null && y !== null) {
319
+ moveMouse(x, y);
320
+ click(x, y);
321
+ }
322
+ const value = String(text || '');
323
+ let typed = 0;
324
+ for (const ch of value) {
325
+ if (ch === '\n' || ch === '\r') {
326
+ pressKeys('enter');
327
+ typed += 1;
328
+ continue;
329
+ }
330
+ const code = ch.codePointAt(0);
331
+ if (code >= 0x100) {
332
+ unicodeKeycode(code);
333
+ typed += 1;
334
+ continue;
335
+ }
336
+ const keysym = code;
337
+ const keycode = keycodeOf(keysym);
338
+ if (!keycode) {
339
+ unicodeKeycode(code);
340
+ typed += 1;
341
+ continue;
342
+ }
343
+ const shift = needsShift(keysym, keycode);
344
+ const shiftKey = shift ? keycodeOf(keysymOf('Shift_L')) : 0;
345
+ if (shiftKey) fakeKey(shiftKey, true);
346
+ fakeKey(keycode, true);
347
+ fakeKey(keycode, false);
348
+ if (shiftKey) fakeKey(shiftKey, false);
349
+ typed += 1;
350
+ }
351
+ sync();
352
+ return { ok: true, chars: value.length, typed };
353
+ }
354
+
355
+ function pressKeys(combo) {
356
+ requireX();
357
+ const parts = String(combo || '')
358
+ .toLowerCase()
359
+ .split('+')
360
+ .map((p) => p.trim())
361
+ .filter(Boolean);
362
+ if (!parts.length) return { ok: false, error: `unreadable key combination: ${combo}` };
363
+ let repeat = 1;
364
+ const mods = [];
365
+ const keys = [];
366
+ for (const part of parts) {
367
+ if (part === 'repeat') {
368
+ repeat = 2;
369
+ continue;
370
+ }
371
+ if (/^x\d+$/.test(part)) {
372
+ repeat = Number(part.slice(1));
373
+ continue;
374
+ }
375
+ const modSym = MOD_KEYSYMS[part];
376
+ if (modSym) {
377
+ const kc = keycodeOf(keysymOf(modSym));
378
+ if (kc) mods.push(kc);
379
+ continue;
380
+ }
381
+ const name = KEY_NAMES[part] || (part.length === 1 ? null : part);
382
+ const keysym = name ? keysymOf(name) : part.toUpperCase().charCodeAt(0);
383
+ const kc = keycodeOf(keysym || part.toUpperCase().charCodeAt(0));
384
+ if (kc) keys.push({ kc, shift: name ? needsShift(keysym, kc) : false });
385
+ }
386
+ if (!mods.length && !keys.length) return { ok: false, error: `unreadable key combination: ${combo}` };
387
+ const shiftKc = keycodeOf(keysymOf('Shift_L'));
388
+ for (let i = 0; i < Math.min(50, Math.max(1, repeat)); i += 1) {
389
+ if (!keys.length && mods.length) {
390
+ const only = mods[mods.length - 1];
391
+ fakeKey(only, true);
392
+ fakeKey(only, false);
393
+ continue;
394
+ }
395
+ for (const kc of mods) fakeKey(kc, true);
396
+ for (const key of keys) {
397
+ if (key.shift && shiftKc) fakeKey(shiftKc, true);
398
+ fakeKey(key.kc, true);
399
+ fakeKey(key.kc, false);
400
+ if (key.shift && shiftKc) fakeKey(shiftKc, false);
401
+ }
402
+ for (const kc of [...mods].reverse()) fakeKey(kc, false);
403
+ }
404
+ sync();
405
+ return { ok: true, keys: combo };
406
+ }
407
+
408
+ // ------------------------------------------------------------------ windows
409
+
410
+ function screenSize() {
411
+ openDisplay();
412
+ const XDisplayWidth = fn('libX11.so.6', 'XDisplayWidth', 'int XDisplayWidth(void* dpy, int screen)');
413
+ const XDisplayHeight = fn('libX11.so.6', 'XDisplayHeight', 'int XDisplayHeight(void* dpy, int screen)');
414
+ return { ok: true, width: XDisplayWidth(dpy, screen), height: XDisplayHeight(dpy, screen) };
415
+ }
416
+
417
+ function decodeCString(ptr, max = 512, encoding = 'latin1') {
418
+ const koffi = require('koffi');
419
+ try {
420
+ const bytes = koffi.decode(ptr, 'uint8_t', max);
421
+ return Buffer.from(bytes).toString(encoding).split('\0')[0];
422
+ } catch {
423
+ return '';
424
+ }
425
+ }
426
+
427
+ function windowName(win) {
428
+ const XFetchName = fn('libX11.so.6', 'XFetchName', 'int XFetchName(void* dpy, unsigned long win, _Out_ void** name)');
429
+ const XFree = fn('libX11.so.6', 'XFree', 'int XFree(void* data)');
430
+ const out = [null];
431
+ let name = '';
432
+ if (XFetchName && XFetchName(dpy, win, out) && out[0]) {
433
+ name = decodeCString(out[0], 512);
434
+ if (XFree) XFree(out[0]);
435
+ }
436
+ if (!name) name = netWmName(win);
437
+ return name;
438
+ }
439
+
440
+ function netWmName(win) {
441
+ const XInternAtom = fn('libX11.so.6', 'XInternAtom', 'unsigned long XInternAtom(void* dpy, const char* name, bool only_if_exists)');
442
+ const XGetWindowProperty = fn(
443
+ 'libX11.so.6',
444
+ 'XGetWindowProperty',
445
+ 'int XGetWindowProperty(void* dpy, unsigned long win, unsigned long property, long long_offset, long long_length, bool delete, unsigned long req_type, _Out_ unsigned long* actual_type, _Out_ int* actual_format, _Out_ unsigned long* nitems, _Out_ unsigned long* bytes_after, _Out_ void** prop)'
446
+ );
447
+ const XFree = fn('libX11.so.6', 'XFree', 'int XFree(void* data)');
448
+ if (!XGetWindowProperty) return '';
449
+ const atom = XInternAtom(dpy, '_NET_WM_NAME', false);
450
+ const utf8 = XInternAtom(dpy, 'UTF8_STRING', false);
451
+ const type = [0];
452
+ const format = [0];
453
+ const nitems = [0];
454
+ const after = [0];
455
+ const prop = [null];
456
+ if (XGetWindowProperty(dpy, win, atom, 0, 1024, false, utf8, type, format, nitems, after, prop) !== 0) return '';
457
+ if (!prop[0]) return '';
458
+ const text = decodeCString(prop[0], Math.max(16, Number(nitems[0]) + 1), 'utf8');
459
+ if (XFree) XFree(prop[0]);
460
+ return text;
461
+ }
462
+
463
+ function windows() {
464
+ openDisplay();
465
+ const koffi = require('koffi');
466
+ const XQueryTree = fn(
467
+ 'libX11.so.6',
468
+ 'XQueryTree',
469
+ 'int XQueryTree(void* dpy, unsigned long win, _Out_ unsigned long* root, _Out_ unsigned long* parent, _Out_ void** children, _Out_ unsigned int* nchildren)'
470
+ );
471
+ const XGetGeometry = fn(
472
+ 'libX11.so.6',
473
+ 'XGetGeometry',
474
+ 'int XGetGeometry(void* dpy, unsigned long drawable, _Out_ unsigned long* root, _Out_ int* x, _Out_ int* y, _Out_ unsigned int* width, _Out_ unsigned int* height, _Out_ unsigned int* border, _Out_ unsigned int* depth)'
475
+ );
476
+ const XTranslateCoordinates = fn(
477
+ 'libX11.so.6',
478
+ 'XTranslateCoordinates',
479
+ 'bool XTranslateCoordinates(void* dpy, unsigned long src, unsigned long dest, int sx, int sy, _Out_ int* dx, _Out_ int* dy, _Out_ unsigned long* child)'
480
+ );
481
+ const XFree = fn('libX11.so.6', 'XFree', 'int XFree(void* data)');
482
+ const wroot = [0];
483
+ const parent = [0];
484
+ const children = [null];
485
+ const count = [0];
486
+ if (!XQueryTree(dpy, rootWindow, wroot, parent, children, count)) return { ok: true, windows: [] };
487
+ const list = [];
488
+ try {
489
+ const handles = count[0] ? koffi.decode(children[0], koffi.array('unsigned long', count[0])) : [];
490
+ for (const h of handles) {
491
+ const win = Number(h);
492
+ const rr = [0];
493
+ const gx = [0];
494
+ const gy = [0];
495
+ const gw = [0];
496
+ const gh = [0];
497
+ const border = [0];
498
+ const depth = [0];
499
+ if (!XGetGeometry(dpy, win, rr, gx, gy, gw, gh, border, depth)) continue;
500
+ if (!gw[0] || !gh[0]) continue;
501
+ const name = windowName(win);
502
+ if (!name) continue;
503
+ const dx = [0];
504
+ const dy = [0];
505
+ const child = [0];
506
+ let x = gx[0];
507
+ let y = gy[0];
508
+ if (XTranslateCoordinates(dpy, win, rootWindow, 0, 0, dx, dy, child)) {
509
+ // XTranslateCoordinates returns the client area: drop the border so the
510
+ // coordinates match the ones XMoveResizeWindow accepts.
511
+ x = dx[0] - border[0];
512
+ y = dy[0] - border[0];
513
+ }
514
+ list.push({
515
+ name,
516
+ handle: win,
517
+ hwnd: win,
518
+ x,
519
+ y,
520
+ width: gw[0],
521
+ height: gh[0],
522
+ process_id: 0,
523
+ status: 'normal',
524
+ });
525
+ }
526
+ } finally {
527
+ if (children[0] && XFree) XFree(children[0]);
528
+ }
529
+ return { ok: true, windows: list };
530
+ }
531
+
532
+ function focusWindow(handle) {
533
+ openDisplay();
534
+ const XRaiseWindow = fn('libX11.so.6', 'XRaiseWindow', 'int XRaiseWindow(void* dpy, unsigned long win)');
535
+ const XSetInputFocus = fn('libX11.so.6', 'XSetInputFocus', 'int XSetInputFocus(void* dpy, unsigned long win, int revert, unsigned long time)');
536
+ XRaiseWindow(dpy, handle);
537
+ XSetInputFocus(dpy, handle, 2 /* RevertToParent */, 0 /* CurrentTime */);
538
+ sync();
539
+ return { ok: true, name: windowName(handle), handle };
540
+ }
541
+
542
+ function resizeWindow(handle, x, y, width, height) {
543
+ openDisplay();
544
+ const XMoveResizeWindow = fn(
545
+ 'libX11.so.6',
546
+ 'XMoveResizeWindow',
547
+ 'int XMoveResizeWindow(void* dpy, unsigned long win, int x, int y, unsigned int w, unsigned int h)'
548
+ );
549
+ XMoveResizeWindow(dpy, handle, x, y, width, height);
550
+ sync();
551
+ return { ok: true, name: windowName(handle), x, y, width, height };
552
+ }
553
+
554
+ // ---------------------------------------------------------------- screenshot
555
+
556
+ function crc32(buf) {
557
+ let c = 0xffffffff;
558
+ for (let i = 0; i < buf.length; i += 1) {
559
+ c ^= buf[i];
560
+ for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
561
+ }
562
+ return (c ^ 0xffffffff) >>> 0;
563
+ }
564
+
565
+ function pngChunk(type, data) {
566
+ const len = Buffer.alloc(4);
567
+ len.writeUInt32BE(data.length);
568
+ const body = Buffer.concat([Buffer.from(type, 'ascii'), data]);
569
+ const crc = Buffer.alloc(4);
570
+ crc.writeUInt32BE(crc32(body));
571
+ return Buffer.concat([len, body, crc]);
572
+ }
573
+
574
+ function encodePng(rgb, width, height) {
575
+ const raw = Buffer.alloc((width * 3 + 1) * height);
576
+ for (let y = 0; y < height; y += 1) {
577
+ raw[y * (width * 3 + 1)] = 0;
578
+ rgb.copy(raw, y * (width * 3 + 1) + 1, y * width * 3, (y + 1) * width * 3);
579
+ }
580
+ const ihdr = Buffer.alloc(13);
581
+ ihdr.writeUInt32BE(width, 0);
582
+ ihdr.writeUInt32BE(height, 4);
583
+ ihdr[8] = 8;
584
+ ihdr[9] = 2;
585
+ return Buffer.concat([
586
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
587
+ pngChunk('IHDR', ihdr),
588
+ pngChunk('IDAT', zlib.deflateSync(raw, { level: 6 })),
589
+ pngChunk('IEND', Buffer.alloc(0)),
590
+ ]);
591
+ }
592
+
593
+ function screenshot(dest) {
594
+ openDisplay();
595
+ const XGetImage = fn(
596
+ 'libX11.so.6',
597
+ 'XGetImage',
598
+ 'void* XGetImage(void* dpy, unsigned long drawable, int x, int y, unsigned int width, unsigned int height, unsigned long plane_mask, int format)'
599
+ );
600
+ const XGetPixel = fn('libX11.so.6', 'XGetPixel', 'unsigned long XGetPixel(void* image, int x, int y)');
601
+ const XDestroyImage = fn('libX11.so.6', 'XDestroyImage', 'int XDestroyImage(void* image)');
602
+ const size = screenSize();
603
+ const image = XGetImage(dpy, rootWindow, 0, 0, size.width, size.height, 0xffffffff, 2 /* ZPixmap */);
604
+ if (!image) return { ok: false, error: 'XGetImage failed' };
605
+ const rgb = Buffer.alloc(size.width * size.height * 3);
606
+ try {
607
+ for (let y = 0; y < size.height; y += 1) {
608
+ for (let x = 0; x < size.width; x += 1) {
609
+ const pixel = Number(XGetPixel(image, x, y));
610
+ const o = (y * size.width + x) * 3;
611
+ rgb[o] = (pixel >> 16) & 0xff;
612
+ rgb[o + 1] = (pixel >> 8) & 0xff;
613
+ rgb[o + 2] = pixel & 0xff;
614
+ }
615
+ }
616
+ } finally {
617
+ if (XDestroyImage) XDestroyImage(image);
618
+ }
619
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
620
+ fs.writeFileSync(dest, encodePng(rgb, size.width, size.height));
621
+ return { ok: true, file: dest, width: size.width, height: size.height, mime: 'image/png' };
622
+ }
623
+
624
+ // -------------------------------------------------------------------- misc
625
+
626
+ function pointer() {
627
+ openDisplay();
628
+ const koffi = require('koffi');
629
+ const XQueryPointer = fn(
630
+ 'libX11.so.6',
631
+ 'XQueryPointer',
632
+ 'bool XQueryPointer(void* dpy, unsigned long win, _Out_ unsigned long* root, _Out_ unsigned long* child, _Out_ int* rx, _Out_ int* ry, _Out_ int* wx, _Out_ int* wy, _Out_ unsigned int* mask)'
633
+ );
634
+ const r = [0];
635
+ const c = [0];
636
+ const rx = [0];
637
+ const ry = [0];
638
+ const wx = [0];
639
+ const wy = [0];
640
+ const mask = [0];
641
+ XQueryPointer(dpy, rootWindow, r, c, rx, ry, wx, wy, mask);
642
+ return { x: rx[0], y: ry[0], mask: mask[0] };
643
+ }
644
+
645
+ function launchApp(name) {
646
+ const command = String(name || '');
647
+ if (!command) return { ok: false, error: 'app name required' };
648
+ const child = spawn(command, [], { detached: true, stdio: 'ignore', shell: false });
649
+ child.on('error', () => {});
650
+ child.unref();
651
+ return { ok: true, app: command };
652
+ }
653
+
654
+ function listApps() {
655
+ const dirs = [
656
+ '/usr/share/applications',
657
+ '/usr/local/share/applications',
658
+ path.join(os.homedir(), '.local/share/applications'),
659
+ ];
660
+ const out = [];
661
+ for (const dir of dirs) {
662
+ let files = [];
663
+ try {
664
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.desktop'));
665
+ } catch {
666
+ continue;
667
+ }
668
+ for (const file of files) {
669
+ try {
670
+ const text = fs.readFileSync(path.join(dir, file), 'utf8');
671
+ if (/^NoDisplay=true/m.test(text)) continue;
672
+ const name = /^Name=(.+)$/m.exec(text);
673
+ if (name) out.push({ name: name[1].trim(), app_id: file.replace(/\.desktop$/, '') });
674
+ } catch {
675
+ /* ignore */
676
+ }
677
+ }
678
+ }
679
+ return { ok: true, apps: out };
680
+ }
681
+
682
+ function activeWindow() {
683
+ openDisplay();
684
+ const XGetInputFocus = fn(
685
+ 'libX11.so.6',
686
+ 'XGetInputFocus',
687
+ 'int XGetInputFocus(void* dpy, _Out_ unsigned long* focus, _Out_ int* revert)'
688
+ );
689
+ const focus = [0];
690
+ const revert = [0];
691
+ if (!XGetInputFocus(dpy, focus, revert)) return { ok: true, name: '', handle: null };
692
+ const handle = Number(focus[0]);
693
+ if (!handle || handle === 1) return { ok: true, name: '', handle: null }; // None / PointerRoot
694
+ return { ok: true, name: windowName(handle), handle };
695
+ }
696
+
697
+ /** The closest thing to windows-use's desktop state that X11 can answer natively. */
698
+ function state(handle = null) {
699
+ const wins = windows();
700
+ const size = screenSize();
701
+ const active = activeWindow();
702
+ const list = wins.ok ? wins.windows : [];
703
+ const focused = handle || (active.ok ? active.handle : null);
704
+ const elements = list
705
+ .filter((w) => !focused || w.handle === focused)
706
+ .map((w) => ({
707
+ name: w.name,
708
+ control_type: 'Window',
709
+ kind: 'Window',
710
+ bounding_box: { left: w.x, top: w.y, right: w.x + w.width, bottom: w.y + w.height },
711
+ center: { x: w.x + Math.floor(w.width / 2), y: w.y + Math.floor(w.height / 2) },
712
+ value: '',
713
+ enabled: true,
714
+ automation_id: '',
715
+ }));
716
+ return {
717
+ ok: true,
718
+ windows: list,
719
+ active_window: active.ok && active.handle ? list.find((w) => w.handle === active.handle) || { name: active.name, handle: active.handle } : null,
720
+ interactive_elements: elements,
721
+ scrollable_elements: [],
722
+ interactive_elements_text: elements
723
+ .map((e, i) => `${i + 1}. ${e.control_type} '${e.name}' at (${e.center.x}, ${e.center.y})`)
724
+ .join('\n'),
725
+ scrollable_elements_text: '',
726
+ tree_status: 'x11',
727
+ system: {
728
+ windows_version: `${process.platform} ${os.release()}`,
729
+ os_version: os.release(),
730
+ screen_size: { width: size.width, height: size.height },
731
+ user_account_type: 'Local Account',
732
+ default_language: process.env.LANG || 'en',
733
+ dpi_scaling: 1,
734
+ },
735
+ metadata: { depth: 'top-level', kind: 'windows-list', all_desktops: false },
736
+ };
737
+ }
738
+
739
+ function available() {
740
+ try {
741
+ const size = screenSize();
742
+ return { ok: true, backend: 'linux', detail: 'X11 via FFI (Xlib + XTest)', screen: size };
743
+ } catch (err) {
744
+ return { ok: false, backend: 'linux', error: err.message };
745
+ }
746
+ }
747
+
748
+ function sysinfo() {
749
+ const size = screenSize();
750
+ return {
751
+ ok: true,
752
+ system: {
753
+ windows_version: `${process.platform} ${os.release()}`,
754
+ os_version: os.release(),
755
+ screen_size: { width: size.width, height: size.height },
756
+ user_account_type: 'Local Account',
757
+ default_language: process.env.LANG || 'en',
758
+ dpi_scaling: 1,
759
+ },
760
+ };
761
+ }
762
+
763
+ const backend = {
764
+ name: 'x11-ffi',
765
+ available,
766
+ screenSize,
767
+ windows,
768
+ activeWindow,
769
+ state,
770
+ moveMouse,
771
+ click,
772
+ mouseButton,
773
+ drag,
774
+ scroll,
775
+ typeText,
776
+ pressKeys,
777
+ focusWindow,
778
+ resizeWindow,
779
+ screenshot,
780
+ launchApp,
781
+ listApps,
782
+ sysinfo,
783
+ };
784
+
785
+ module.exports = {
786
+ backend,
787
+ activeWindow,
788
+ state,
789
+ available,
790
+ sysinfo,
791
+ openDisplay,
792
+ closeDisplay,
793
+ screenSize,
794
+ moveMouse,
795
+ click,
796
+ mouseButton,
797
+ drag,
798
+ scroll,
799
+ typeText,
800
+ pressKeys,
801
+ windows,
802
+ focusWindow,
803
+ resizeWindow,
804
+ screenshot,
805
+ pointer,
806
+ launchApp,
807
+ listApps,
808
+ encodePng,
809
+ };