react-x11 2.13.0 → 2.15.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 (70) hide show
  1. package/package.json +11 -8
  2. package/src/Reconciler.js +34 -21
  3. package/src/components/Select.js +8 -2
  4. package/src/events.js +8 -2
  5. package/src/glnodes.js +15 -3
  6. package/src/index.d.ts +5 -0
  7. package/src/nodes/boxpaint.js +9 -0
  8. package/src/nodes/preedit.js +64 -14
  9. package/src/ntk.js +10 -1
  10. package/src/scale.js +52 -22
  11. package/src/screencolor.js +104 -17
  12. package/src/types/system.d.ts +14 -1
  13. package/src/wayland/app.js +574 -0
  14. package/src/wayland/backendwindow.js +1199 -0
  15. package/src/wayland/clipboard.js +326 -0
  16. package/src/wayland/connection.js +485 -0
  17. package/src/wayland/context2d.js +2290 -0
  18. package/src/wayland/decorations.js +476 -0
  19. package/src/wayland/device.js +55 -0
  20. package/src/wayland/dmabuf.js +89 -0
  21. package/src/wayland/dnd.js +581 -0
  22. package/src/wayland/fdutil.js +108 -0
  23. package/src/wayland/framestyle.js +257 -0
  24. package/src/wayland/framewatch.js +190 -0
  25. package/src/wayland/glarea.js +372 -0
  26. package/src/wayland/glcontext.js +415 -0
  27. package/src/wayland/glyphatlas.js +237 -0
  28. package/src/wayland/input.js +417 -0
  29. package/src/wayland/keysymnames.js +35 -0
  30. package/src/wayland/layershell.js +363 -0
  31. package/src/wayland/outputs.js +601 -0
  32. package/src/wayland/protocols/cursor-shape-v1.json +1 -0
  33. package/src/wayland/protocols/ext-idle-notify-v1.json +1 -0
  34. package/src/wayland/protocols/ext-image-capture-source-v1.json +1 -0
  35. package/src/wayland/protocols/ext-image-copy-capture-v1.json +1 -0
  36. package/src/wayland/protocols/fractional-scale-v1.json +1 -0
  37. package/src/wayland/protocols/index.json +131 -0
  38. package/src/wayland/protocols/kde-server-decoration.json +1 -0
  39. package/src/wayland/protocols/keyboard-shortcuts-inhibit-unstable-v1.json +1 -0
  40. package/src/wayland/protocols/linux-dmabuf-v1.json +1 -0
  41. package/src/wayland/protocols/pointer-constraints-unstable-v1.json +1 -0
  42. package/src/wayland/protocols/presentation-time.json +1 -0
  43. package/src/wayland/protocols/primary-selection-unstable-v1.json +1 -0
  44. package/src/wayland/protocols/relative-pointer-unstable-v1.json +1 -0
  45. package/src/wayland/protocols/tablet-v2.json +1 -0
  46. package/src/wayland/protocols/text-input-unstable-v3.json +1 -0
  47. package/src/wayland/protocols/viewporter.json +1 -0
  48. package/src/wayland/protocols/wayland.json +1 -0
  49. package/src/wayland/protocols/wlr-layer-shell-unstable-v1.json +1 -0
  50. package/src/wayland/protocols/wlr-screencopy-unstable-v1.json +1 -0
  51. package/src/wayland/protocols/xdg-activation-v1.json +1 -0
  52. package/src/wayland/protocols/xdg-decoration-unstable-v1.json +1 -0
  53. package/src/wayland/protocols/xdg-output-unstable-v1.json +1 -0
  54. package/src/wayland/protocols/xdg-shell.json +1 -0
  55. package/src/wayland/protocols/xdg-toplevel-icon-v1.json +1 -0
  56. package/src/wayland/readback.js +99 -0
  57. package/src/wayland/screencopy.js +584 -0
  58. package/src/wayland/seat.js +584 -0
  59. package/src/wayland/shm.js +226 -0
  60. package/src/wayland/ssd.js +234 -0
  61. package/src/wayland/surface.js +141 -0
  62. package/src/wayland/swapchain.js +411 -0
  63. package/src/wayland/tablet.js +522 -0
  64. package/src/wayland/target.js +269 -0
  65. package/src/wayland/text.js +113 -0
  66. package/src/wayland/textinput.js +671 -0
  67. package/src/wayland/touch.js +284 -0
  68. package/src/wayland/window.js +854 -0
  69. package/src/wayland/xkb.js +425 -0
  70. package/src/windowstate.js +41 -1
@@ -0,0 +1,485 @@
1
+ // The Wayland connection: a socket that can carry file descriptors, the
2
+ // protocol definitions loaded on top of it, and the globals the compositor
3
+ // advertises.
4
+ //
5
+ // Two things about this transport are worth stating up front, because they
6
+ // are the reason this backend exists as a separate thing rather than as a
7
+ // branch inside the X11 one.
8
+ //
9
+ // **Descriptors are not an optimisation here, they are the protocol.** The
10
+ // compositor sends the keymap as an fd; `wl_shm` pools go over as fds; a
11
+ // clipboard offer is a pipe fd. A transport that cannot receive descriptors
12
+ // is not a limited Wayland client, it is not a Wayland client. Node cannot on
13
+ // its own: an fd arriving on a libuv-read socket aborts the process, and
14
+ // nodejs/node#53391 — filed for exactly this — is closed "not planned". So
15
+ // there are two transports here, tried in order:
16
+ //
17
+ // 1. `x11-dri`'s `UnixSocket` — a native socket on the event loop
18
+ // (`uv_poll`), no thread, works on Node and Bun. The measured cost of
19
+ // the alternative below is what justified writing it.
20
+ // 2. node-x11's `fdpass-bun.js` — `bun:ffi` to `sendmsg`/`recvmsg`, with a
21
+ // reader thread blocked in `poll(2)` because Bun's own reader drops
22
+ // ancillary data it did not ask for. Bun only, and ~37µs of round-trip
23
+ // latency for the thread hop (measured: 0.178ms vs 0.141ms sync→done).
24
+ //
25
+ // **The wire matches descriptors to arguments by position in the stream, not
26
+ // by message.** The spec is blunt that any byte, even a message header, may
27
+ // carry the ancillary data. That is why the fd queue is drained in parse
28
+ // order and never searched: the nth `fd` argument parsed takes the nth
29
+ // descriptor received, and any cleverness beyond that is a bug waiting for a
30
+ // compositor that batches differently.
31
+ //
32
+ // The protocol definitions are vendored as JSON under `./protocols/`
33
+ // (converted from wayland-protocols with the library's own parser), so the
34
+ // backend does not depend on the distribution's XML being installed or on
35
+ // the optional `xml-js`.
36
+
37
+ import { createRequire } from 'node:module';
38
+ import { EventEmitter } from 'node:events';
39
+ import { readFileSync } from 'node:fs';
40
+ import path from 'node:path';
41
+ import { fileURLToPath } from 'node:url';
42
+
43
+ const require = createRequire(import.meta.url);
44
+ const WAYLAND_CLIENT = ['@windowkit', 'wayland'].join('/');
45
+ const here = path.dirname(fileURLToPath(import.meta.url));
46
+ const PROTOCOL_DIR = path.join(here, 'protocols');
47
+
48
+ /**
49
+ * The protocols this backend knows how to speak, over and above core
50
+ * `wayland.xml`. Each is optional: a compositor that does not advertise the
51
+ * global simply leaves the corresponding feature unavailable, which is the
52
+ * same shape as an X extension that is not there.
53
+ */
54
+ export const PROTOCOLS = [
55
+ // The core protocol first: the library ships its own copy of wayland.xml,
56
+ // and the vendored one is newer (wl_seat 9, with axis_value120 and
57
+ // friends). Loading it over the library's updates every interface except
58
+ // the two whose proxies already exist — see `loadCore`.
59
+ 'wayland',
60
+ 'xdg-shell',
61
+ 'linux-dmabuf-v1',
62
+ 'presentation-time',
63
+ 'viewporter',
64
+ 'fractional-scale-v1',
65
+ 'cursor-shape-v1',
66
+ 'tablet-v2',
67
+ 'xdg-activation-v1',
68
+ 'xdg-decoration-unstable-v1',
69
+ // the older KDE protocol that does the same job, for the compositors that
70
+ // have only that one (ssd.js picks between them)
71
+ 'kde-server-decoration',
72
+ 'primary-selection-unstable-v1',
73
+ 'text-input-unstable-v3',
74
+ 'pointer-constraints-unstable-v1',
75
+ 'relative-pointer-unstable-v1',
76
+ 'ext-idle-notify-v1',
77
+ 'xdg-toplevel-icon-v1',
78
+ 'xdg-output-unstable-v1',
79
+ 'wlr-layer-shell-unstable-v1',
80
+ 'wlr-screencopy-unstable-v1',
81
+ 'ext-image-capture-source-v1',
82
+ 'ext-image-copy-capture-v1',
83
+ ];
84
+
85
+ const definitions = new Map();
86
+ function protocolDefinitions(name) {
87
+ let defs = definitions.get(name);
88
+ if (!defs) {
89
+ defs = JSON.parse(
90
+ readFileSync(path.join(PROTOCOL_DIR, `${name}.json`), 'utf8'),
91
+ );
92
+ definitions.set(name, defs);
93
+ }
94
+ return defs;
95
+ }
96
+
97
+ /**
98
+ * Open the fd-capable socket, or explain why there is none.
99
+ *
100
+ * @returns {{socket: object, transport: string}}
101
+ */
102
+ function openSocket(socketPath, prefer) {
103
+ const attempts = [];
104
+ const order = prefer ? [prefer] : ['x11-dri', 'fdpass-bun'];
105
+
106
+ for (const kind of order) {
107
+ if (kind === 'x11-dri') {
108
+ if (typeof Bun !== 'undefined') {
109
+ // Bun exports libuv's symbols but aborts the process from several
110
+ // of them (uv_poll_init included); nothing can be probed safely.
111
+ attempts.push(
112
+ 'x11-dri: UnixSocket needs libuv polling, which Bun does not give addons',
113
+ );
114
+ continue;
115
+ }
116
+ try {
117
+ const dri = require('x11-dri');
118
+ if (typeof dri.UnixSocket === 'function') {
119
+ return {
120
+ socket: new dri.UnixSocket(socketPath),
121
+ transport: 'x11-dri',
122
+ };
123
+ }
124
+ attempts.push(
125
+ 'x11-dri: installed, but this version has no UnixSocket (needs >= 0.9)',
126
+ );
127
+ } catch (err) {
128
+ attempts.push(`x11-dri: ${err.message.split('\n')[0]}`);
129
+ }
130
+ } else if (kind === 'fdpass-bun') {
131
+ try {
132
+ const fdpass = require('x11/lib/fdpass-bun.js');
133
+ if (fdpass.available()) {
134
+ const socket = fdpass.connect(socketPath, { receiveFds: true });
135
+ if (socket) return { socket, transport: 'fdpass-bun' };
136
+ attempts.push('fdpass-bun: connect returned null');
137
+ } else {
138
+ attempts.push('fdpass-bun: needs Bun (bun:ffi)');
139
+ }
140
+ } catch (err) {
141
+ attempts.push(`fdpass-bun: ${err.message.split('\n')[0]}`);
142
+ }
143
+ }
144
+ }
145
+ throw new Error(
146
+ 'no transport can pass file descriptors over the Wayland socket, which the protocol requires.\n' +
147
+ attempts.map((a) => ` - ${a}`).join('\n') +
148
+ '\nInstall x11-dri >= 0.9 (native, Node and Bun), or run under Bun.',
149
+ );
150
+ }
151
+
152
+ /**
153
+ * The compositor connection.
154
+ *
155
+ * Wraps `@windowkit/wayland`'s `Display` rather than replacing it: the wire codec
156
+ * and the XML-driven proxy generation are exactly the parts worth not
157
+ * rewriting, and the socket is injected through its constructor, which is the
158
+ * seam this needs.
159
+ */
160
+ export class WaylandConnection extends EventEmitter {
161
+ constructor(display, socket, transport) {
162
+ super();
163
+ this.display = display;
164
+ this.socket = socket;
165
+ /** which fd transport carried this connection: 'x11-dri' or 'fdpass-bun' */
166
+ this.transport = transport;
167
+ /** interface name -> bound proxy, for the singletons everyone shares */
168
+ this._bound = new Map();
169
+ /** the {@link Registry} view, once something has asked for it */
170
+ this._registry = null;
171
+ this.destroyed = false;
172
+ }
173
+
174
+ /**
175
+ * Connect, bring up `wl_registry`, and load the protocol definitions.
176
+ *
177
+ * @param {object} [opts]
178
+ * @param {string} [opts.display] `WAYLAND_DISPLAY`, or an absolute socket path
179
+ * @param {string[]} [opts.protocols] which of {@link PROTOCOLS} to load
180
+ * @param {'x11-dri'|'fdpass-bun'} [opts.transport] force one transport
181
+ */
182
+ static async open({
183
+ display: name,
184
+ protocols = PROTOCOLS,
185
+ transport,
186
+ socket: injected,
187
+ } = {}) {
188
+ // An open connection keeps the process alive, as a net.Socket would. The
189
+ // fd transports do not: Bun's reads happen on a worker the runtime does
190
+ // not count, so an app with nothing else pending — no timer, no server —
191
+ // exited the moment its module finished evaluating, before `connect`
192
+ // had even fired (`bun examples/simple.jsx` ran for 0.36 s and returned
193
+ // 0). A ref'd interval is the one portable handle that says "still
194
+ // running" to both runtimes; it starts before the first await and is
195
+ // cleared with the connection.
196
+ const keepAlive = setInterval(() => {}, 0x7fffffff);
197
+ try {
198
+ return await WaylandConnection._open(
199
+ { display: name, protocols, transport, socket: injected },
200
+ keepAlive,
201
+ );
202
+ } catch (err) {
203
+ clearInterval(keepAlive);
204
+ throw err;
205
+ }
206
+ }
207
+
208
+ static async _open(
209
+ { display: name, protocols, transport, socket: injected },
210
+ keepAlive,
211
+ ) {
212
+ // A computed specifier on purpose: a bundler (the docs site's esbuild)
213
+ // resolves a literal `import('@windowkit/wayland')` at build time and
214
+ // fails where the package is not installed — and it is an optional
215
+ // dependency, like the backend it serves. Loaded before the socket is
216
+ // opened, so its absence leaves nothing to close.
217
+ let Display;
218
+ try {
219
+ ({ Display } = await import(WAYLAND_CLIENT));
220
+ } catch (err) {
221
+ throw new Error(
222
+ 'the Wayland backend needs the optional @windowkit/wayland package, which is not installed',
223
+ { cause: err },
224
+ );
225
+ }
226
+ let socket;
227
+ let used;
228
+ let socketPath = '(injected socket)';
229
+ if (injected) {
230
+ // A connected socket the caller made — a socketpair end talking to an
231
+ // in-process compositor in the tests. It only has to be
232
+ // net.Socket-shaped; whether descriptors pass then depends on what it is.
233
+ socket = injected;
234
+ used = 'injected';
235
+ } else {
236
+ const wanted = name ?? process.env.WAYLAND_DISPLAY;
237
+ if (!wanted)
238
+ throw new Error('WAYLAND_DISPLAY is not set and no display was named');
239
+ socketPath = wanted.startsWith('/')
240
+ ? wanted
241
+ : path.join(process.env.XDG_RUNTIME_DIR ?? '/run/user/1000', wanted);
242
+ ({ socket, transport: used } = openSocket(socketPath, transport));
243
+ }
244
+ // The library's callback requests add a listener per in-flight call;
245
+ // a frame of `damage` calls followed by `sync` is well over ten.
246
+ socket.setMaxListeners?.(0);
247
+ if (!(injected && injected.connecting === false)) {
248
+ await new Promise((resolve, reject) => {
249
+ const ok = () => {
250
+ socket.off('error', fail);
251
+ resolve();
252
+ };
253
+ const fail = (err) => {
254
+ socket.off('connect', ok);
255
+ reject(
256
+ new Error(
257
+ `could not connect to the compositor at ${socketPath}: ${err.message}`,
258
+ ),
259
+ );
260
+ };
261
+ socket.once('connect', ok);
262
+ socket.once('error', fail);
263
+ });
264
+ }
265
+
266
+ const display = new Display(socket);
267
+ display.setMaxListeners(0);
268
+ const conn = new WaylandConnection(display, socket, used);
269
+ conn._keepAlive = keepAlive;
270
+
271
+ display.on('error', (err) => {
272
+ // Nothing after destroy() is news, and Bun's reader thread reports its
273
+ // own shutdown as two errors when the process exits under it — a
274
+ // connection that is going away is a close, not a failure.
275
+ if (conn.destroyed) return;
276
+ if (
277
+ /reader thread stopped|waiting on the connection failed/.test(
278
+ err?.message ?? '',
279
+ )
280
+ ) {
281
+ conn._closed();
282
+ return;
283
+ }
284
+ if (err?.name === 'WaylandProtocolError') {
285
+ // Fatal by definition: the compositor has closed its end already.
286
+ // Nothing may write to it from here — the next frame's commit would
287
+ // die of EPIPE, which was the error a user saw second, after the one
288
+ // that mattered — so the connection is dead before anyone hears
289
+ // why, and the keep-alive goes with it.
290
+ conn.destroyed = true;
291
+ conn._release();
292
+ }
293
+ conn.emit('error', err);
294
+ });
295
+ display.on('warning', (w) => conn.emit('warning', w));
296
+ display.on('close', () => {
297
+ if (conn.destroyed) return;
298
+ conn._closed();
299
+ });
300
+
301
+ await display.init();
302
+ for (const key of protocols) {
303
+ const defs = protocolDefinitions(key);
304
+ // `init()` created wl_display and wl_registry from the library's own
305
+ // definitions and patched wl_registry.bind's argument list in place;
306
+ // replacing those two would lose the patch and orphan the proxies.
307
+ await display.load(
308
+ key === 'wayland'
309
+ ? defs.filter(
310
+ (d) => d.name !== 'wl_display' && d.name !== 'wl_registry',
311
+ )
312
+ : defs,
313
+ );
314
+ }
315
+ return conn;
316
+ }
317
+
318
+ /** Every global the compositor advertised, by interface name. */
319
+ get globals() {
320
+ return new Set(this.display.listGlobals());
321
+ }
322
+
323
+ has(iface) {
324
+ return this.globals.has(iface);
325
+ }
326
+
327
+ /**
328
+ * Bind a singleton global once and hand the same proxy out afterwards.
329
+ *
330
+ * Returns null for a global the compositor does not advertise, so a caller
331
+ * can treat an absent protocol as a missing capability rather than an
332
+ * error — which is what most of them are.
333
+ */
334
+ async bind(iface, version) {
335
+ if (this._bound.has(iface)) return this._bound.get(iface);
336
+ if (!this.has(iface)) return null;
337
+ const proxy = await this.display.bind(iface, version);
338
+ proxy.setMaxListeners?.(0);
339
+ this._bound.set(iface, proxy);
340
+ return proxy;
341
+ }
342
+
343
+ /** Bind, or throw naming what the compositor would need to support. */
344
+ async require(iface, version) {
345
+ const proxy = await this.bind(iface, version);
346
+ if (!proxy) {
347
+ throw new Error(
348
+ `this compositor does not advertise ${iface}. ` +
349
+ `Available globals: ${[...this.globals].sort().join(', ')}`,
350
+ );
351
+ }
352
+ return proxy;
353
+ }
354
+
355
+ /**
356
+ * The registry as a list rather than a dictionary: every global with the
357
+ * numeric name the compositor gave it, and the arrivals and departures
358
+ * after startup.
359
+ *
360
+ * `bind()` above is enough for the singletons, but the library files
361
+ * globals by interface name, so of three `wl_output`s it remembers the
362
+ * last — and its own registry consumed the initial announcement inside
363
+ * `init()`, before anything here could listen. A second `wl_registry` is
364
+ * the protocol's own answer: the compositor replays its globals to each
365
+ * registry it hands out and sends `global`/`global_remove` to all of them
366
+ * afterwards, which is also what makes hot-plug visible. One round trip,
367
+ * paid by the first caller, then shared.
368
+ */
369
+ async registry() {
370
+ if (!this._registry) {
371
+ this._registry = (async () => {
372
+ const view = new Registry(this);
373
+ await view._open();
374
+ return view;
375
+ })();
376
+ }
377
+ return this._registry;
378
+ }
379
+
380
+ /**
381
+ * Wait for everything sent so far to have been processed.
382
+ *
383
+ * `wl_display.sync` is the only ordering primitive Wayland has — there are
384
+ * no replies to requests — so this stands in for every "did that work?"
385
+ * round trip an X client would write.
386
+ */
387
+ async roundtrip() {
388
+ await this.display.sync();
389
+ }
390
+
391
+ /** The compositor went away, or the socket did: one 'close', then quiet. */
392
+ _closed() {
393
+ if (this.destroyed) return;
394
+ this.destroyed = true;
395
+ this._release();
396
+ this.emit('close');
397
+ }
398
+
399
+ _release() {
400
+ if (this._keepAlive) {
401
+ clearInterval(this._keepAlive);
402
+ this._keepAlive = null;
403
+ }
404
+ }
405
+
406
+ destroy() {
407
+ if (this.destroyed) return;
408
+ this.destroyed = true;
409
+ this._release();
410
+ // The reader worker reports the descriptor closing under it as an
411
+ // error, on the socket, after this returns; with no listener left that
412
+ // is an uncaught exception at exit (examples/app.jsx died of it).
413
+ this.socket.on?.('error', () => {});
414
+ // `end(cb)` on the fd transports does not take a callback the way
415
+ // net.Socket does, and a reader thread keeps the process alive until the
416
+ // descriptor is actually gone — so tear down rather than half-close.
417
+ this.socket.destroy();
418
+ }
419
+ }
420
+
421
+ /**
422
+ * What {@link WaylandConnection.registry} hands out.
423
+ *
424
+ * Emits `global(name, interface, version)` and `global_remove(name)` for
425
+ * changes after the initial list — the initial list itself is in `globals`
426
+ * by the time the promise resolves, so a caller reads first and listens
427
+ * second without a gap between the two.
428
+ */
429
+ export class Registry extends EventEmitter {
430
+ constructor(conn) {
431
+ super();
432
+ this.setMaxListeners(0);
433
+ this.conn = conn;
434
+ /** numeric name -> { interface, version } */
435
+ this.globals = new Map();
436
+ this.proxy = null;
437
+ }
438
+
439
+ async _open() {
440
+ const display = this.conn.display;
441
+ // Synchronous on purpose: the listeners have to be on before the first
442
+ // announcement can be parsed, and the request's bytes go out now.
443
+ this.proxy = display.wl_display.$.get_registry();
444
+ this.proxy.setMaxListeners?.(0);
445
+ this.proxy.on('global', (name, iface, version) => {
446
+ this.globals.set(name, { interface: iface, version });
447
+ if (this._opened) this.emit('global', name, iface, version);
448
+ });
449
+ this.proxy.on('global_remove', (name) => {
450
+ this.globals.delete(name);
451
+ if (this._opened) this.emit('global_remove', name);
452
+ });
453
+ await this.conn.roundtrip();
454
+ this._opened = true;
455
+ }
456
+
457
+ /** Every global of one interface: `[{ name, interface, version }]`. */
458
+ of(iface) {
459
+ const out = [];
460
+ for (const [name, g] of this.globals)
461
+ if (g.interface === iface) out.push({ name, ...g });
462
+ return out;
463
+ }
464
+
465
+ /**
466
+ * Bind one global by its numeric name, at the highest version both sides
467
+ * speak (or `version`, when lower). A fresh proxy each time, the caller's
468
+ * to release; null once the global has gone.
469
+ */
470
+ bind(name, iface, version) {
471
+ const entry = this.globals.get(name);
472
+ if (!entry || entry.interface !== iface) return null;
473
+ const display = this.conn.display;
474
+ const def = display.getDefinition(iface);
475
+ const negotiated = Math.min(
476
+ def.version,
477
+ entry.version,
478
+ version ?? Infinity,
479
+ );
480
+ const proxy = display.createInterface(iface, negotiated);
481
+ proxy.setMaxListeners?.(0);
482
+ this.proxy.$.bind(name, iface, negotiated, proxy.id);
483
+ return proxy;
484
+ }
485
+ }