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