react-x11 1.2.0 → 2.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.
- package/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
package/src/bus.js
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
// The D-Bus floor: one connection per bus, shared by everything in the
|
|
2
|
+
// process, acquired lazily and never able to crash an app that has no bus.
|
|
3
|
+
//
|
|
4
|
+
// Everything the desktop wants from an app — a file dialog, the colour
|
|
5
|
+
// scheme, notifications, a global menu, a tray icon — is a D-Bus call, and
|
|
6
|
+
// each of those is a separate feature that will want a connection. This
|
|
7
|
+
// module exists so that they all get the *same* one.
|
|
8
|
+
//
|
|
9
|
+
// ## Why not just call `dbus.sessionBus()`
|
|
10
|
+
//
|
|
11
|
+
// Because `dbus-native`'s `sessionBus()` is a constructor in disguise: every
|
|
12
|
+
// call opens a new socket and gets its own unique name. Menu + tray + the
|
|
13
|
+
// app's own exported service would be three connections and three identities,
|
|
14
|
+
// and the desktop would see three half-applications. A D-Bus connection is
|
|
15
|
+
// *process* identity, so sharing is the design, not an optimisation.
|
|
16
|
+
//
|
|
17
|
+
// And because it throws. `createStream` (dbus-native index.js:32-37) throws
|
|
18
|
+
// `unknown bus address` synchronously when `DBUS_SESSION_BUS_ADDRESS` is
|
|
19
|
+
// unset — which is every environment react-x11 was designed for: a bare
|
|
20
|
+
// `startx`, a WM keybinding, an ssh session, a container, CI. That throw must
|
|
21
|
+
// never reach an app.
|
|
22
|
+
//
|
|
23
|
+
// ## The rule
|
|
24
|
+
//
|
|
25
|
+
// **Nothing here throws at import time, and nothing here crashes an app
|
|
26
|
+
// running where there is no session bus.** `sessionBus()` answers `null`; the
|
|
27
|
+
// caller turns its feature off. `required: true` is the escape hatch for an
|
|
28
|
+
// app whose whole purpose is the bus, and it is the only shape that rejects.
|
|
29
|
+
//
|
|
30
|
+
// ## Lifecycle: connect on first use, stay connected
|
|
31
|
+
//
|
|
32
|
+
// The ref count drives the socket's *event-loop hold*, not its existence:
|
|
33
|
+
//
|
|
34
|
+
// refs > 0 connected, `ref()`d — the process stays alive for it
|
|
35
|
+
// refs == 0 connected, `unref()`d — warm, costs one idle FD, holds nothing
|
|
36
|
+
//
|
|
37
|
+
// Closing at zero refs would be wrong for the consumers this actually has. A
|
|
38
|
+
// file dialog that mounts on click and unmounts on dismiss would tear the
|
|
39
|
+
// connection down and rebuild it every time — and every rebuild is a *new
|
|
40
|
+
// unique name*, with the daemon forgetting match rules and well-known names
|
|
41
|
+
// along with the old one. `closeBus()` is the way out for an app that wants
|
|
42
|
+
// the connection gone; it is never the automatic consequence of an unmount.
|
|
43
|
+
|
|
44
|
+
/** @typedef {'session'|'system'} BusKind */
|
|
45
|
+
|
|
46
|
+
const KINDS = ['session', 'system'];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* There is no bus to be had, and this is why.
|
|
50
|
+
*
|
|
51
|
+
* `cause` is the real reason — no address, `ECONNREFUSED`, an auth rejection,
|
|
52
|
+
* `dbus-native` not installed. Deliberately not a second failure taxonomy: a
|
|
53
|
+
* coarse enum layered on top would have to stay true across platforms, npm
|
|
54
|
+
* layouts and transport versions, and in practice the reason gets logged far
|
|
55
|
+
* more often than it gets branched on.
|
|
56
|
+
*/
|
|
57
|
+
export class BusUnavailableError extends Error {
|
|
58
|
+
/**
|
|
59
|
+
* @param {BusKind} kind
|
|
60
|
+
* @param {unknown} [cause]
|
|
61
|
+
*/
|
|
62
|
+
constructor(kind, cause) {
|
|
63
|
+
super(
|
|
64
|
+
`react-x11: no ${kind} bus available` +
|
|
65
|
+
(cause instanceof Error ? ` (${cause.message})` : ''),
|
|
66
|
+
{ cause },
|
|
67
|
+
);
|
|
68
|
+
this.name = 'BusUnavailableError';
|
|
69
|
+
/** @type {BusKind} which bus was asked for */
|
|
70
|
+
this.kind = kind;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Per-kind state. One entry, replaced wholesale on every generation — a dead
|
|
76
|
+
* connection is never resurrected, because a new socket is a new unique name
|
|
77
|
+
* and callers holding the old one are talking to nobody.
|
|
78
|
+
*/
|
|
79
|
+
const state = {
|
|
80
|
+
session: newState('session', 0),
|
|
81
|
+
system: newState('system', 0),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function newState(kind, generation) {
|
|
85
|
+
return {
|
|
86
|
+
kind,
|
|
87
|
+
/** Bumped per connection attempt; a ref remembers which one it belongs to. */
|
|
88
|
+
generation,
|
|
89
|
+
/** The in-flight connect, shared by concurrent acquisitions. */
|
|
90
|
+
connecting: null,
|
|
91
|
+
bus: null,
|
|
92
|
+
uniqueName: null,
|
|
93
|
+
/** Live refs on this generation. */
|
|
94
|
+
refs: 0,
|
|
95
|
+
/** Set when the connection died under us; the generation is retired. */
|
|
96
|
+
dead: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --------------------------------------------------------------------------
|
|
101
|
+
// The transport
|
|
102
|
+
// --------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* `dbus-native`, imported on the first acquisition and never at module load.
|
|
106
|
+
*
|
|
107
|
+
* It is an `optionalDependency`, so on Node 20 — where its own
|
|
108
|
+
* `engines: ">=22.12.0"` makes npm skip it — this import fails, and "no
|
|
109
|
+
* transport installed" collapses into the "no bus here" path rather than
|
|
110
|
+
* adding a mode.
|
|
111
|
+
*/
|
|
112
|
+
let transport = null;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Not public. `portal.js` needs `Variant` from the same copy of the transport
|
|
116
|
+
* to build `a{sv}` option dicts, and there is only ever one copy — importing
|
|
117
|
+
* `dbus-native` a second time from another module would be a second resolve
|
|
118
|
+
* of a package that may not be installed.
|
|
119
|
+
*/
|
|
120
|
+
export async function loadTransport() {
|
|
121
|
+
if (transport) return transport;
|
|
122
|
+
const mod = await import('dbus-native');
|
|
123
|
+
// CJS with an `exports` map: named exports do not reliably survive
|
|
124
|
+
// cjs-module-lexer, so take the default and destructure from it.
|
|
125
|
+
transport = mod.default ?? mod;
|
|
126
|
+
return transport;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The address seam is D-Bus's own — `DBUS_SESSION_BUS_ADDRESS` — plus the
|
|
131
|
+
* `$XDG_RUNTIME_DIR/bus` fallback `dbus-native` does not have yet
|
|
132
|
+
* (sidorares/dbus-native#389). Delete the fallback when that lands.
|
|
133
|
+
*
|
|
134
|
+
* Deliberately no `busAddress` parameter on the public API: a per-call
|
|
135
|
+
* address is incoherent under sharing. Two callers, two addresses, one
|
|
136
|
+
* socket — which would win? Tests use this same seam.
|
|
137
|
+
*
|
|
138
|
+
* Not public, but exported for atspi.js, whose one-shot discovery probe
|
|
139
|
+
* dials its own short-lived connection rather than the shared one — see
|
|
140
|
+
* the note in `accessibilityBusAddress`.
|
|
141
|
+
*/
|
|
142
|
+
export function addressFor(kind) {
|
|
143
|
+
if (kind === 'system') {
|
|
144
|
+
return (
|
|
145
|
+
process.env.DBUS_SYSTEM_BUS_ADDRESS ||
|
|
146
|
+
'unix:path=/var/run/dbus/system_bus_socket'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (process.env.DBUS_SESSION_BUS_ADDRESS) {
|
|
150
|
+
return process.env.DBUS_SESSION_BUS_ADDRESS;
|
|
151
|
+
}
|
|
152
|
+
// macOS advertises the session bus through launchd, which dbus-native
|
|
153
|
+
// already falls back to on its own. Leave it undefined and let it.
|
|
154
|
+
if (process.platform === 'darwin') return undefined;
|
|
155
|
+
const runtimeDir = process.env.XDG_RUNTIME_DIR;
|
|
156
|
+
return runtimeDir ? `unix:path=${runtimeDir}/bus` : undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function noAddressError(kind) {
|
|
160
|
+
return new Error(
|
|
161
|
+
`react-x11: no ${kind} bus address. ` +
|
|
162
|
+
(kind === 'session'
|
|
163
|
+
? '$DBUS_SESSION_BUS_ADDRESS is unset and $XDG_RUNTIME_DIR is not ' +
|
|
164
|
+
'set either, which is normal over ssh, under a bare startx and in ' +
|
|
165
|
+
'most containers.'
|
|
166
|
+
: '$DBUS_SYSTEM_BUS_ADDRESS is unset and there is no default.'),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function noTransportError(cause) {
|
|
171
|
+
return new Error(
|
|
172
|
+
'react-x11: the D-Bus transport is not installed. dbus-native is an ' +
|
|
173
|
+
'optional dependency and npm skips it on Node < 22.12 (this process ' +
|
|
174
|
+
`is ${process.version}). Install it explicitly — npm i dbus-native — ` +
|
|
175
|
+
'or run on Node >= 22.12.',
|
|
176
|
+
{ cause },
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --------------------------------------------------------------------------
|
|
181
|
+
// Connecting
|
|
182
|
+
// --------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/** Swallow whatever a doomed connection has left to say. */
|
|
185
|
+
function silence(bus) {
|
|
186
|
+
bus.connection.removeAllListeners('error');
|
|
187
|
+
bus.connection.on('error', () => {});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function discard(bus) {
|
|
191
|
+
silence(bus);
|
|
192
|
+
try {
|
|
193
|
+
await bus.close();
|
|
194
|
+
} catch {
|
|
195
|
+
// Already gone. The cause we are on our way to reporting is the
|
|
196
|
+
// interesting one, not this.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* One connect attempt: dial, wait for the handshake, wait for the unique
|
|
202
|
+
* name, and take ownership of the connection's `'error'` channel.
|
|
203
|
+
*
|
|
204
|
+
* Resolves with `{ bus, uniqueName }` or rejects with a `BusUnavailableError`
|
|
205
|
+
* carrying the real reason. Never resolves before `bus.name` is set.
|
|
206
|
+
*/
|
|
207
|
+
async function connect(kind, generation) {
|
|
208
|
+
const fail = (cause) => {
|
|
209
|
+
throw new BusUnavailableError(kind, cause);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
let dbus;
|
|
213
|
+
try {
|
|
214
|
+
dbus = await loadTransport();
|
|
215
|
+
} catch (cause) {
|
|
216
|
+
return fail(noTransportError(cause));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const busAddress = addressFor(kind);
|
|
220
|
+
if (!busAddress && process.platform !== 'darwin') {
|
|
221
|
+
return fail(noAddressError(kind));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let bus;
|
|
225
|
+
try {
|
|
226
|
+
// Reconnection stays off, deliberately: a new socket is a new unique
|
|
227
|
+
// name, and the daemon forgets names and match rules along with the old
|
|
228
|
+
// one. The replacement is a new generation, which is explicit.
|
|
229
|
+
bus = dbus.createClient({ busAddress });
|
|
230
|
+
} catch (cause) {
|
|
231
|
+
// The synchronous throw this whole module exists to absorb.
|
|
232
|
+
return fail(cause);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const conn = bus.connection;
|
|
236
|
+
|
|
237
|
+
// **This layer owns the connection's one 'error' listener.** dbus-native
|
|
238
|
+
// deliberately attaches none — an unlistened 'error' crashes the process,
|
|
239
|
+
// which is the right contract for a caller-owned connection and the wrong
|
|
240
|
+
// one for a shared one. Consumers never attach their own.
|
|
241
|
+
let onConnect, onClose, onError;
|
|
242
|
+
try {
|
|
243
|
+
await new Promise((resolve, reject) => {
|
|
244
|
+
onConnect = () => resolve();
|
|
245
|
+
onClose = (cause) =>
|
|
246
|
+
reject(cause ?? new Error(`react-x11: the ${kind} bus closed`));
|
|
247
|
+
onError = (err) => reject(err);
|
|
248
|
+
conn.once('connect', onConnect);
|
|
249
|
+
conn.once('close', onClose);
|
|
250
|
+
conn.on('error', onError);
|
|
251
|
+
});
|
|
252
|
+
} catch (cause) {
|
|
253
|
+
await discard(bus);
|
|
254
|
+
return fail(cause);
|
|
255
|
+
} finally {
|
|
256
|
+
conn.removeListener('connect', onConnect);
|
|
257
|
+
conn.removeListener('close', onClose);
|
|
258
|
+
conn.removeListener('error', onError);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Name-ready. `bus.name` is assigned in the Hello reply callback
|
|
262
|
+
// (dbus-native lib/bus.js:1002-1009), so it is *not* set when the
|
|
263
|
+
// constructor returns. One org.freedesktop.DBus round trip is an ordering
|
|
264
|
+
// barrier: replies arrive behind the Hello dbus-native sent first. Without
|
|
265
|
+
// it, the first consumer to derive state from the app's own identity — a
|
|
266
|
+
// portal Request path, say — races Hello and builds a path with
|
|
267
|
+
// `undefined` in it.
|
|
268
|
+
try {
|
|
269
|
+
await bus.listNames();
|
|
270
|
+
} catch (cause) {
|
|
271
|
+
await discard(bus);
|
|
272
|
+
return fail(cause);
|
|
273
|
+
}
|
|
274
|
+
if (bus.name == null) {
|
|
275
|
+
await discard(bus);
|
|
276
|
+
return fail(
|
|
277
|
+
new Error(
|
|
278
|
+
`react-x11: the ${kind} bus answered a call but never assigned a ` +
|
|
279
|
+
'unique name — the peer is not a message bus.',
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Live. From here a failure is a *death* rather than a failed connect, so
|
|
285
|
+
// both channels retarget onto the generation.
|
|
286
|
+
conn.on('error', () => bury(kind, generation));
|
|
287
|
+
conn.once('close', () => bury(kind, generation));
|
|
288
|
+
|
|
289
|
+
return { bus, uniqueName: bus.name };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The connection died. Retire the generation rather than resurrecting it:
|
|
294
|
+
* refs on it fail their calls with the transport's `ConnectionClosedError`
|
|
295
|
+
* and `release()` harmlessly, mounted hooks move to `'closed'`, and the next
|
|
296
|
+
* acquisition dials a fresh socket with a fresh unique name.
|
|
297
|
+
*/
|
|
298
|
+
function bury(kind, generation) {
|
|
299
|
+
const s = state[kind];
|
|
300
|
+
if (s.generation !== generation || s.dead) return;
|
|
301
|
+
s.dead = true;
|
|
302
|
+
s.refs = 0;
|
|
303
|
+
s.connecting = null;
|
|
304
|
+
notify(kind);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// --------------------------------------------------------------------------
|
|
308
|
+
// The event-loop hold
|
|
309
|
+
// --------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* `ref()`/`unref()` the live socket, so the ref count decides whether the
|
|
313
|
+
* connection *holds the process open* without deciding whether it exists.
|
|
314
|
+
*
|
|
315
|
+
* `bus.connection.stream` is a `net.Socket` on every path this module dials
|
|
316
|
+
* (dbus-native index.js:169, :30-31). It is reaching into an internal, hence
|
|
317
|
+
* the guards: a caller-supplied stream need not be a socket. A missing `ref`
|
|
318
|
+
* costs nothing worse than a process that exits a beat earlier than it should
|
|
319
|
+
* have, which the exit test would catch.
|
|
320
|
+
*/
|
|
321
|
+
function hold(s, held) {
|
|
322
|
+
const stream = s.bus?.connection?.stream;
|
|
323
|
+
if (!stream) return;
|
|
324
|
+
if (held) stream.ref?.();
|
|
325
|
+
else stream.unref?.();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// --------------------------------------------------------------------------
|
|
329
|
+
// Acquisition
|
|
330
|
+
// --------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @param {BusKind} kind
|
|
334
|
+
* @param {{ required?: boolean }} [opts]
|
|
335
|
+
* @returns {Promise<BusRefShape | null>}
|
|
336
|
+
*/
|
|
337
|
+
async function acquire(kind, opts = {}) {
|
|
338
|
+
let s = state[kind];
|
|
339
|
+
|
|
340
|
+
// A dead generation is retired: a fresh acquisition starts a new one rather
|
|
341
|
+
// than handing out a corpse.
|
|
342
|
+
if (s.dead) {
|
|
343
|
+
s = state[kind] = newState(kind, s.generation + 1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!s.bus) {
|
|
347
|
+
// Concurrent acquisitions share one connect attempt.
|
|
348
|
+
if (!s.connecting) {
|
|
349
|
+
const generation = s.generation;
|
|
350
|
+
const attempt = connect(kind, generation).then((result) => {
|
|
351
|
+
// A `closeBus()` that landed mid-connect wins: drop what we dialled.
|
|
352
|
+
if (state[kind] !== s) {
|
|
353
|
+
discard(result.bus);
|
|
354
|
+
throw new BusUnavailableError(
|
|
355
|
+
kind,
|
|
356
|
+
new Error(`react-x11: the ${kind} bus was closed while connecting`),
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
s.bus = result.bus;
|
|
360
|
+
s.uniqueName = result.uniqueName;
|
|
361
|
+
// Nothing holds a ref yet, so the socket must not keep the process
|
|
362
|
+
// alive on its own.
|
|
363
|
+
hold(s, false);
|
|
364
|
+
notify(kind);
|
|
365
|
+
return result;
|
|
366
|
+
});
|
|
367
|
+
// **Failure is not cached.** A failed attempt answers its waiters and is
|
|
368
|
+
// forgotten: a session bus can genuinely appear later — the moment
|
|
369
|
+
// something creates $XDG_RUNTIME_DIR/bus — feature probes happen once
|
|
370
|
+
// per feature init so the retry costs nothing, and a transient failure
|
|
371
|
+
// must never become a process-lifetime false negative.
|
|
372
|
+
s.connecting = attempt;
|
|
373
|
+
attempt
|
|
374
|
+
.catch(() => {})
|
|
375
|
+
.then(() => {
|
|
376
|
+
if (s.connecting === attempt) s.connecting = null;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
await s.connecting;
|
|
381
|
+
} catch (cause) {
|
|
382
|
+
if (opts.required) throw cause;
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
// `closeBus()` can land between that resolving and this line.
|
|
386
|
+
if (state[kind] !== s || !s.bus) return acquire(kind, opts);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return makeRef(s);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** @typedef {{ bus: any, uniqueName: string, release(): Promise<void> }} BusRefShape */
|
|
393
|
+
|
|
394
|
+
function makeRef(s) {
|
|
395
|
+
const generation = s.generation;
|
|
396
|
+
// Captured, not read through the state: a ref's `bus` must stay the object
|
|
397
|
+
// it was handed even after the generation is retired, so that a call on it
|
|
398
|
+
// fails with ConnectionClosedError rather than on `null`.
|
|
399
|
+
const bus = s.bus;
|
|
400
|
+
const uniqueName = s.uniqueName;
|
|
401
|
+
let live = true;
|
|
402
|
+
|
|
403
|
+
// No `notify()` here or in `release()`: the ref count is not part of any
|
|
404
|
+
// status a watcher renders, and waking every mounted hook on every
|
|
405
|
+
// acquire/release would be render churn with nothing behind it.
|
|
406
|
+
if (s.refs++ === 0) hold(s, true);
|
|
407
|
+
|
|
408
|
+
const ref = {
|
|
409
|
+
bus,
|
|
410
|
+
uniqueName,
|
|
411
|
+
/**
|
|
412
|
+
* Drop this consumer's reference. Idempotent *per ref*, so `finally`
|
|
413
|
+
* blocks and `asyncDispose` compose without double-decrement bugs.
|
|
414
|
+
*
|
|
415
|
+
* It does not close anything. `release()` is the only lifecycle verb a
|
|
416
|
+
* ref holder has, deliberately: `ref.bus.close()` would tear the
|
|
417
|
+
* connection out from under the tray, the menu and the app's own service.
|
|
418
|
+
*/
|
|
419
|
+
async release() {
|
|
420
|
+
if (!live) return;
|
|
421
|
+
live = false;
|
|
422
|
+
// A ref on a retired generation decrements nothing — `bury` and
|
|
423
|
+
// `closeBus` already zeroed the count, and this ref's slot went with it.
|
|
424
|
+
if (state[s.kind] !== s || s.generation !== generation || s.dead) return;
|
|
425
|
+
if (--s.refs === 0) hold(s, false);
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
ref[Symbol.asyncDispose] = () => ref.release();
|
|
429
|
+
return ref;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* A shared session bus, or `null`.
|
|
434
|
+
*
|
|
435
|
+
* ```js
|
|
436
|
+
* const ref = await sessionBus();
|
|
437
|
+
* if (!ref) return; // no bus here; turn the feature off
|
|
438
|
+
* const player = await ref.bus.proxy('org.mpris.MediaPlayer2.vlc', '/org/mpris/MediaPlayer2');
|
|
439
|
+
* await ref.release();
|
|
440
|
+
* ```
|
|
441
|
+
*
|
|
442
|
+
* Never rejects — unless `required: true` asks it to, which converts
|
|
443
|
+
* unavailability into a `BusUnavailableError` carrying the reason. `null` is
|
|
444
|
+
* right for feature-probing plumbing, and hides *why* from an app whose whole
|
|
445
|
+
* purpose is the bus.
|
|
446
|
+
*
|
|
447
|
+
* @param {{ required?: boolean }} [opts]
|
|
448
|
+
*/
|
|
449
|
+
export function sessionBus(opts) {
|
|
450
|
+
return acquire('session', opts);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* A shared system bus, or `null`. Hardware, networking, logind — read-mostly
|
|
455
|
+
* for an application. Same contract as {@link sessionBus}.
|
|
456
|
+
*
|
|
457
|
+
* @param {{ required?: boolean }} [opts]
|
|
458
|
+
*/
|
|
459
|
+
export function systemBus(opts) {
|
|
460
|
+
return acquire('system', opts);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Close the shared connection outright. Rare, explicit, never automatic.
|
|
465
|
+
*
|
|
466
|
+
* This is the app-level decision a consumer must not make for its siblings.
|
|
467
|
+
* Outstanding refs are retired with the generation: their calls fail with
|
|
468
|
+
* `ConnectionClosedError` and `release()` still succeeds.
|
|
469
|
+
*
|
|
470
|
+
* @param {BusKind} kind
|
|
471
|
+
*/
|
|
472
|
+
export async function closeBus(kind) {
|
|
473
|
+
if (!KINDS.includes(kind)) {
|
|
474
|
+
throw new Error(
|
|
475
|
+
`react-x11: closeBus(${JSON.stringify(kind)}) — expected ` +
|
|
476
|
+
`${KINDS.map((k) => JSON.stringify(k)).join(' or ')}.`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
const s = state[kind];
|
|
480
|
+
const bus = s.bus;
|
|
481
|
+
// **The teardown is itself a use of the connection.** At zero refs the
|
|
482
|
+
// socket is `unref()`d, so `bus.close()` — which resolves on the socket's
|
|
483
|
+
// own 'close' event — has nothing holding the event loop open, and a
|
|
484
|
+
// process with no other work drains before the event arrives: the promise
|
|
485
|
+
// never settles. Re-ref for the duration, and let the close destroy it.
|
|
486
|
+
hold(s, true);
|
|
487
|
+
// Swap the state first, so anything awaiting the old generation sees it
|
|
488
|
+
// retired rather than racing the socket teardown.
|
|
489
|
+
state[kind] = newState(kind, s.generation + 1);
|
|
490
|
+
notify(kind);
|
|
491
|
+
if (!bus) return;
|
|
492
|
+
silence(bus);
|
|
493
|
+
await bus.close();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// --------------------------------------------------------------------------
|
|
497
|
+
// Subscription, for the hooks
|
|
498
|
+
// --------------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
const watchers = { session: new Set(), system: new Set() };
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Re-render when a bus's status changes. Not public: `useSessionBus()` is the
|
|
504
|
+
* public shape, and a consumer reaching for the imperative API does not want
|
|
505
|
+
* a store.
|
|
506
|
+
*/
|
|
507
|
+
export function watchBus(kind, onChange) {
|
|
508
|
+
watchers[kind].add(onChange);
|
|
509
|
+
return () => watchers[kind].delete(onChange);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function notify(kind) {
|
|
513
|
+
for (const fn of [...watchers[kind]]) fn();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Whether the generation a ref belongs to has been retired. Not public. */
|
|
517
|
+
export function busGeneration(kind) {
|
|
518
|
+
return state[kind].generation;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Whether the live generation for `kind` has been buried. Not public. */
|
|
522
|
+
export function busIsDead(kind) {
|
|
523
|
+
return state[kind].dead;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Live refs on the current generation. Not public — it exists so a test can
|
|
528
|
+
* assert that a *subscription* did not leave one behind, since a held ref is
|
|
529
|
+
* a `ref()`d socket and an app that never exits.
|
|
530
|
+
*/
|
|
531
|
+
export function busRefs(kind) {
|
|
532
|
+
return state[kind].refs;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Test seam, not public: forget every connection without closing anything.
|
|
537
|
+
* `withBus()` hard-closes first and then calls this, so that no match rule or
|
|
538
|
+
* exported object survives into the next test.
|
|
539
|
+
*/
|
|
540
|
+
export function _resetBusState() {
|
|
541
|
+
for (const kind of KINDS) {
|
|
542
|
+
state[kind] = newState(kind, state[kind].generation + 1);
|
|
543
|
+
notify(kind);
|
|
544
|
+
}
|
|
545
|
+
}
|
package/src/bushooks.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// `useSessionBus()` / `useSystemBus()` — the surface almost everyone uses.
|
|
2
|
+
//
|
|
3
|
+
// ## No provider, and why that is not the trick `useApp()` uses
|
|
4
|
+
//
|
|
5
|
+
// The requirement is the same: a component that talks to the desktop works in
|
|
6
|
+
// any tree with *nothing for the app author to wrap*. `useApp()` gets there
|
|
7
|
+
// with a provider `Reconciler.js` injects, because the X connection is
|
|
8
|
+
// genuinely per-tree — `createRoot()` opens one per root and a process can
|
|
9
|
+
// drive several.
|
|
10
|
+
//
|
|
11
|
+
// A D-Bus connection is **process** identity. Every consumer shares one socket
|
|
12
|
+
// and one unique name on purpose, so that an app's exported service, its menu
|
|
13
|
+
// and its tray are visibly one application. There is therefore nothing to
|
|
14
|
+
// provide: these hooks read the module-global in `bus.js` and manage their own
|
|
15
|
+
// acquire/release. Putting a process-global on a tree-scoped context would
|
|
16
|
+
// assert a scoping relationship that does not exist — two roots on two X
|
|
17
|
+
// displays share one session bus.
|
|
18
|
+
//
|
|
19
|
+
// ## The terse path has to be correct on its own
|
|
20
|
+
//
|
|
21
|
+
// const { bus } = useSessionBus();
|
|
22
|
+
// if (!bus) return null; // right, with no reference to status
|
|
23
|
+
//
|
|
24
|
+
// If reading `status` were *required* to avoid a bug, the shape would be
|
|
25
|
+
// wrong. `status` and `cause` are there for the cases that need more —
|
|
26
|
+
// diagnostics ("why is my tray icon missing") and the app-author persona —
|
|
27
|
+
// not as a step every consumer has to remember.
|
|
28
|
+
|
|
29
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
busGeneration,
|
|
33
|
+
busIsDead,
|
|
34
|
+
sessionBus,
|
|
35
|
+
systemBus,
|
|
36
|
+
watchBus,
|
|
37
|
+
} from './bus.js';
|
|
38
|
+
|
|
39
|
+
/** @typedef {'connecting'|'ready'|'unavailable'|'closed'} BusStatus */
|
|
40
|
+
|
|
41
|
+
const CONNECTING = { bus: null, uniqueName: null, status: 'connecting' };
|
|
42
|
+
|
|
43
|
+
function useBus(kind, acquire) {
|
|
44
|
+
const [attempt, setAttempt] = useState(0);
|
|
45
|
+
// The attempt each answer belongs to, so a `retry()` shows 'connecting'
|
|
46
|
+
// immediately instead of leaving the stale answer up until the new dial
|
|
47
|
+
// settles — and without an extra render to reset it.
|
|
48
|
+
const [answer, setAnswer] = useState({ attempt: -1, handle: CONNECTING });
|
|
49
|
+
|
|
50
|
+
// The way back from `'closed'`. Deliberately not automatic: silently
|
|
51
|
+
// re-acquiring would hand the consumer a fresh `bus` under the same
|
|
52
|
+
// variable and hide the unique-name change that disabling reconnect exists
|
|
53
|
+
// to expose. A feature-level hook that knows how to rebuild its bus-side
|
|
54
|
+
// state can call this; a generic one cannot.
|
|
55
|
+
const retry = useCallback(() => setAttempt((n) => n + 1), []);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
let live = true;
|
|
59
|
+
let acquired = null;
|
|
60
|
+
let acquiredGeneration = null;
|
|
61
|
+
let unwatch = null;
|
|
62
|
+
const settle = (handle) => setAnswer({ attempt, handle });
|
|
63
|
+
|
|
64
|
+
// `required: true` purely to obtain the reason. The imperative default
|
|
65
|
+
// stays never-rejecting; the hook is that option's first consumer,
|
|
66
|
+
// converting the rejection into rendering state.
|
|
67
|
+
acquire({ required: true }).then(
|
|
68
|
+
(ref) => {
|
|
69
|
+
if (!live) return void ref.release();
|
|
70
|
+
acquired = ref;
|
|
71
|
+
acquiredGeneration = busGeneration(kind);
|
|
72
|
+
settle({
|
|
73
|
+
bus: ref.bus,
|
|
74
|
+
uniqueName: ref.uniqueName,
|
|
75
|
+
status: 'ready',
|
|
76
|
+
});
|
|
77
|
+
// The connection can die under a live ref. When it does the
|
|
78
|
+
// generation is retired, which is what distinguishes "the daemon went
|
|
79
|
+
// away" from "there was never a bus here".
|
|
80
|
+
unwatch = watchBus(kind, () => {
|
|
81
|
+
if (!live) return;
|
|
82
|
+
if (!busIsDead(kind) && busGeneration(kind) === acquiredGeneration) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
settle({ bus: null, uniqueName: null, status: 'closed' });
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
(cause) => {
|
|
89
|
+
if (!live) return;
|
|
90
|
+
settle({ bus: null, uniqueName: null, status: 'unavailable', cause });
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
live = false;
|
|
96
|
+
unwatch?.();
|
|
97
|
+
acquired?.release();
|
|
98
|
+
};
|
|
99
|
+
}, [kind, acquire, attempt]);
|
|
100
|
+
|
|
101
|
+
const handle = answer.attempt === attempt ? answer.handle : CONNECTING;
|
|
102
|
+
// Memoized so the handle is stable between renders and safe in a dependency
|
|
103
|
+
// array — a fresh object every render would restart any effect keyed on it.
|
|
104
|
+
return useMemo(() => ({ ...handle, retry }), [handle, retry]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The session bus, as rendering state.
|
|
109
|
+
*
|
|
110
|
+
* ```jsx
|
|
111
|
+
* function ThemeWatcher() {
|
|
112
|
+
* const { bus } = useSessionBus();
|
|
113
|
+
* if (!bus) return null; // no bus here: no desktop theme to read
|
|
114
|
+
* ...
|
|
115
|
+
* }
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* | `status` | |
|
|
119
|
+
* | -------------- | ------------------------------------------------------ |
|
|
120
|
+
* | `'connecting'` | the handshake is in flight — also the first render |
|
|
121
|
+
* | `'ready'` | `bus` and `uniqueName` are set |
|
|
122
|
+
* | `'unavailable'` | no daemon, socket or permission here; `cause` says why |
|
|
123
|
+
* | `'closed'` | the connection died; call `retry()` to dial a new one |
|
|
124
|
+
*
|
|
125
|
+
* **`'unavailable'` is a snapshot, not a verdict.** Failure is not cached, so
|
|
126
|
+
* a session bus that appears later will be found — do not permanently disable
|
|
127
|
+
* a feature on seeing it.
|
|
128
|
+
*/
|
|
129
|
+
export function useSessionBus() {
|
|
130
|
+
return useBus('session', sessionBus);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The system bus, as rendering state. Same shape as {@link useSessionBus}. */
|
|
134
|
+
export function useSystemBus() {
|
|
135
|
+
return useBus('system', systemBus);
|
|
136
|
+
}
|