react-x11 2.51.0 → 2.52.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 +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +10 -3
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/xkb.js +137 -0
package/docs/dbus.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# D-Bus
|
|
2
|
+
|
|
3
|
+
Everything the desktop wants from an app beyond drawing — a native file
|
|
4
|
+
dialog, the light/dark preference, notifications, a
|
|
5
|
+
[global menu](globalmenu.md), a tray icon
|
|
6
|
+
— is a D-Bus call. This page is the connection those features sit on, and
|
|
7
|
+
nothing else: no service is spoken to here, and the only traffic this layer
|
|
8
|
+
generates is its own handshake.
|
|
9
|
+
|
|
10
|
+
```jsx
|
|
11
|
+
import { useSessionBus } from 'react-x11';
|
|
12
|
+
|
|
13
|
+
function ColourScheme() {
|
|
14
|
+
const { bus } = useSessionBus();
|
|
15
|
+
if (!bus) return null; // no bus here — nothing to read a preference from
|
|
16
|
+
// ...call org.freedesktop.portal.Settings through `bus`
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
There is **nothing to wrap**. No provider, no prop drilling, no setup step
|
|
21
|
+
for the app author — a component library can call this, which is the whole
|
|
22
|
+
reason it is shaped this way.
|
|
23
|
+
|
|
24
|
+
## The rule everything else is subordinate to
|
|
25
|
+
|
|
26
|
+
**Nothing throws at import time, and nothing crashes an app running where
|
|
27
|
+
there is no session bus.**
|
|
28
|
+
|
|
29
|
+
Concretely, that has to hold on XQuartz, under a bare `startx`, in CI, in an
|
|
30
|
+
ssh session on a headless server, in a container, and on Node 20 where the
|
|
31
|
+
transport is not installed at all. The ssh case is not an edge case — it is
|
|
32
|
+
react-x11's flagship persona. **"No bus" is a first-class configuration, not
|
|
33
|
+
a degraded one.**
|
|
34
|
+
|
|
35
|
+
So `sessionBus()` answers `null` rather than rejecting, and `useSessionBus()`
|
|
36
|
+
reports `'unavailable'` rather than throwing, when:
|
|
37
|
+
|
|
38
|
+
- `dbus-native` is not installed,
|
|
39
|
+
- `$DBUS_SESSION_BUS_ADDRESS` is unset and there is no `$XDG_RUNTIME_DIR/bus`,
|
|
40
|
+
- the connect fails, authentication fails, or the platform has no bus at all.
|
|
41
|
+
|
|
42
|
+
## The hooks
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
function useSessionBus(): BusHandle;
|
|
46
|
+
function useSystemBus(): BusHandle;
|
|
47
|
+
|
|
48
|
+
interface BusHandle {
|
|
49
|
+
readonly bus: MessageBus | null; // null unless status === 'ready'
|
|
50
|
+
readonly uniqueName: string | null;
|
|
51
|
+
readonly status: 'connecting' | 'ready' | 'unavailable' | 'closed';
|
|
52
|
+
readonly cause?: BusUnavailableError; // set when status === 'unavailable'
|
|
53
|
+
retry(): void;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| `status` | |
|
|
58
|
+
| --------------- | -------------------------------------------------------- |
|
|
59
|
+
| `'connecting'` | the handshake is in flight — and also the first render |
|
|
60
|
+
| `'ready'` | `bus` and `uniqueName` are set |
|
|
61
|
+
| `'unavailable'` | no daemon, socket or permission here; `cause` says which |
|
|
62
|
+
| `'closed'` | the connection died under you; `retry()` dials a new one |
|
|
63
|
+
|
|
64
|
+
**The terse path is correct on its own.** `const { bus } = useSessionBus()`
|
|
65
|
+
followed by `if (!bus) return fallback` is right, with no reference to
|
|
66
|
+
`status`; if reading `status` were _required_ to avoid a bug the shape would
|
|
67
|
+
be wrong. For a colour-scheme subscriber, `'connecting'` and `'unavailable'`
|
|
68
|
+
render identically anyway. The states earn their keep in diagnostics ("why is
|
|
69
|
+
my tray icon missing") and for an app that is itself a service.
|
|
70
|
+
|
|
71
|
+
**"Idle" is not a fourth state.** The first render happens before the effect
|
|
72
|
+
runs, so nothing has been attempted yet — calling that `'connecting'` is a
|
|
73
|
+
small inaccuracy that becomes true within a tick, and an honest fourth state
|
|
74
|
+
would give every consumer a branch that never needs different handling.
|
|
75
|
+
|
|
76
|
+
**`'unavailable'` is a snapshot, not a verdict.** Failure is not cached: a
|
|
77
|
+
session bus can appear later — `$XDG_RUNTIME_DIR/bus` exists the moment
|
|
78
|
+
something starts one — and the next acquisition retries. Do not permanently
|
|
79
|
+
disable a feature on seeing it. The name invites the opposite reading, which
|
|
80
|
+
is why this paragraph exists.
|
|
81
|
+
|
|
82
|
+
**`'closed'` does not auto-retry, deliberately.** Silently re-acquiring would
|
|
83
|
+
hand you a fresh `bus` under the same variable and hide the unique-name change
|
|
84
|
+
(see [the sharing contract](#the-sharing-contract)). A feature-level hook that
|
|
85
|
+
knows how to rebuild its bus-side state can call `retry()`; a generic one
|
|
86
|
+
cannot, so the decision is yours.
|
|
87
|
+
|
|
88
|
+
## The imperative pair
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
function sessionBus(opts?: { required?: boolean }): Promise<BusRef | null>;
|
|
92
|
+
function systemBus(opts?: { required?: boolean }): Promise<BusRef | null>;
|
|
93
|
+
function closeBus(kind: 'session' | 'system'): Promise<void>;
|
|
94
|
+
|
|
95
|
+
interface BusRef {
|
|
96
|
+
readonly bus: MessageBus; // the full dbus-native surface, shared
|
|
97
|
+
readonly uniqueName: string; // this connection's unique name, e.g. ':1.42'
|
|
98
|
+
release(): Promise<void>; // drop this consumer's reference; idempotent
|
|
99
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This is what the hooks are built on, and it is public for two reasons that are
|
|
104
|
+
not "completeness":
|
|
105
|
+
|
|
106
|
+
- **Host-side code has no component to hang off.** The plumbing that later
|
|
107
|
+
features add — a settings subscriber, a notification sender — is module code
|
|
108
|
+
and event-loop glue, some of which must run with no root mounted at all.
|
|
109
|
+
- **A click handler wants to resolve at click time**, not at render time:
|
|
110
|
+
|
|
111
|
+
```jsx
|
|
112
|
+
<Button
|
|
113
|
+
onClick={async () => {
|
|
114
|
+
await using ref = await sessionBus({ required: true });
|
|
115
|
+
await ref.bus.proxy(/* … */);
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`required: true` is the service-author escape hatch. `null` is right for
|
|
121
|
+
feature-probing plumbing and hides _why_ from an app whose purpose is the bus,
|
|
122
|
+
so `required` converts unavailability into a rejection carrying the cause. The
|
|
123
|
+
default stays never-rejecting; the hooks use `required` internally purely to
|
|
124
|
+
obtain the reason for `cause`.
|
|
125
|
+
|
|
126
|
+
There is **no `busAddress` parameter**, deliberately: a per-call address is
|
|
127
|
+
incoherent under sharing — two callers, two addresses, one socket, which
|
|
128
|
+
would win? The address seam is D-Bus's own, `$DBUS_SESSION_BUS_ADDRESS`, plus
|
|
129
|
+
an `$XDG_RUNTIME_DIR/bus` fallback until
|
|
130
|
+
[dbus-native#389](https://github.com/sidorares/dbus-native/pull/389) lands.
|
|
131
|
+
|
|
132
|
+
On **macOS** there is a third source and it is not a variable: D-Bus
|
|
133
|
+
advertises the socket through launchd, and finding it means running
|
|
134
|
+
`launchctl getenv`. That lookup is asynchronous and happens once per process,
|
|
135
|
+
which is the whole of
|
|
136
|
+
[#417](https://github.com/sidorares/react-x11/issues/417) — `dbus-native`
|
|
137
|
+
would otherwise do it from a synchronous constructor with `spawnSync`, and
|
|
138
|
+
120–150 ms of blocked event loop in front of the first frame is not a price
|
|
139
|
+
for finding out whether there is a bus. See
|
|
140
|
+
[desktop.md](desktop.md#not-blocking-the-first-frame-on-the-bus).
|
|
141
|
+
|
|
142
|
+
## One connection, on purpose
|
|
143
|
+
|
|
144
|
+
`dbus-native`'s own `sessionBus()` is a constructor in disguise: every call
|
|
145
|
+
opens a new socket with its own unique name. A menu, a tray and an app's own
|
|
146
|
+
exported service would be three connections and three identities, and the
|
|
147
|
+
desktop would see three half-applications.
|
|
148
|
+
|
|
149
|
+
A D-Bus connection is **process** identity, so every consumer in the process
|
|
150
|
+
shares one socket and one unique name. That is also why these hooks need no
|
|
151
|
+
provider, where `useApp()` does: the X connection really is per-tree —
|
|
152
|
+
`createRoot()` opens one per root and a process can drive several — but two
|
|
153
|
+
roots on two X displays still share one session bus. Putting a process-global
|
|
154
|
+
on a tree-scoped context would assert a relationship that does not exist.
|
|
155
|
+
|
|
156
|
+
## Lifecycle: connect on first use, stay connected
|
|
157
|
+
|
|
158
|
+
The ref count drives the socket's **event-loop hold**, not its existence:
|
|
159
|
+
|
|
160
|
+
| refs | socket | process exit |
|
|
161
|
+
| ---- | --------------------- | --------------------------------------------------------- |
|
|
162
|
+
| `>0` | connected, `ref()`d | held open — someone is using the bus |
|
|
163
|
+
| `0` | connected, `unref()`d | not held; the connection stays warm and costs one idle FD |
|
|
164
|
+
|
|
165
|
+
Releasing the last ref does **not** close the connection. A file dialog that
|
|
166
|
+
mounts on click and unmounts on dismiss would otherwise tear the connection
|
|
167
|
+
down and rebuild it every time — and the real cost is not the handshake, it is
|
|
168
|
+
that **every reconnect is a new unique name**, with the daemon forgetting
|
|
169
|
+
match rules and well-known names along with the old connection. StrictMode's
|
|
170
|
+
double-mount becomes two counter flips rather than a full connect/disconnect
|
|
171
|
+
cycle.
|
|
172
|
+
|
|
173
|
+
`closeBus(kind)` is the way out for an app that genuinely wants the connection
|
|
174
|
+
gone. Rare, explicit, and never the automatic consequence of an unmount.
|
|
175
|
+
|
|
176
|
+
## The sharing contract
|
|
177
|
+
|
|
178
|
+
One connection with many consumers only works if the things one consumer can
|
|
179
|
+
break for another are named:
|
|
180
|
+
|
|
181
|
+
- **Never close a shared bus.** `release()` is the only lifecycle verb a ref
|
|
182
|
+
holder has. `ref.bus.close()` or `bus.connection.end()` tears the connection
|
|
183
|
+
out from under every other holder — libdbus makes closing a shared
|
|
184
|
+
connection a hard application error, and here it is a rule.
|
|
185
|
+
- **react-x11 owns the connection's one `'error'` listener.** `dbus-native`
|
|
186
|
+
deliberately attaches none, because an unlistened `'error'` crashing the
|
|
187
|
+
process is the right contract for a caller-owned connection and the wrong
|
|
188
|
+
one for a shared one. Do not attach your own.
|
|
189
|
+
- **A dead connection is not resurrected.** `dbus-native`'s reconnect stays
|
|
190
|
+
off: a new socket is a new unique name, and the daemon forgets names and
|
|
191
|
+
match rules with the old one. Refs on the dead connection fail their calls
|
|
192
|
+
with `ConnectionClosedError` and `release()` harmlessly; the next
|
|
193
|
+
`sessionBus()` dials fresh, and mounted hooks move to `'closed'`.
|
|
194
|
+
- **App-global stays app-global.** Well-known names (`RequestName` has no
|
|
195
|
+
per-consumer refcount), exported object paths (re-exporting the same path
|
|
196
|
+
and interface replaces silently) and `setValueShapes()` (which flips reply
|
|
197
|
+
shapes for _every_ holder) are process-level resources. Owning them is app
|
|
198
|
+
policy; a `BusRef` does not scope them, and react-x11 will never call
|
|
199
|
+
`setValueShapes()` on the shared bus.
|
|
200
|
+
|
|
201
|
+
## Being a service, not just a client
|
|
202
|
+
|
|
203
|
+
An app that wants to _be_ on the bus gets the full `dbus-native` surface
|
|
204
|
+
through `ref.bus`, on the **same connection and unique name** as react-x11's
|
|
205
|
+
own plumbing — one socket, one identity on the desktop:
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
const ref = await sessionBus({ required: true });
|
|
209
|
+
await ref.bus.requestName('org.example.MyApp', 0);
|
|
210
|
+
ref.bus.exportInterface(impl, '/org/example/MyApp', description);
|
|
211
|
+
// hold the ref for as long as you serve: it is what keeps the process alive
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Holding the ref is not bookkeeping — it is the event-loop hold above. Release
|
|
215
|
+
it and nothing keeps the process running.
|
|
216
|
+
|
|
217
|
+
The one service react-x11 exports for you is `org.freedesktop.Application`, so
|
|
218
|
+
that a `myapp://…` link opens the app that is already running —
|
|
219
|
+
[uri-schemes.md](uri-schemes.md). It is on this connection for exactly the
|
|
220
|
+
reason above: a separately-dialled one would own the app's name under a
|
|
221
|
+
different unique name than the one the global menu registers under and the one
|
|
222
|
+
portals build Request paths from, and the desktop's model of "this
|
|
223
|
+
application" would come apart.
|
|
224
|
+
|
|
225
|
+
## Installing, and Node 20
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
"optionalDependencies": { "dbus-native": "^0.16.0" }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`dbus-native` declares `engines: ">=22.12.0"` where react-x11 declares
|
|
232
|
+
`">=20.19"`, so **npm skips it on Node 20** and the install succeeds exactly
|
|
233
|
+
as before. There is no compiler and no install script either way — `npm i
|
|
234
|
+
react-x11` still needs no toolchain.
|
|
235
|
+
|
|
236
|
+
On Node 20, `useSessionBus()` reports `'unavailable'` with a `cause` that says
|
|
237
|
+
the transport is missing. That is the same code path an ssh session or a Mac
|
|
238
|
+
without a bus already takes: **"no transport installed" collapses into "no bus
|
|
239
|
+
here" rather than adding a mode.** To have it on Node 20 anyway, install it
|
|
240
|
+
explicitly:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
npm i dbus-native
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The declarations never name `dbus-native`, so they type-check on an install
|
|
247
|
+
where it is absent. `MessageBus` is a structural type covering the members
|
|
248
|
+
documented here, with an index signature over the rest of the package's
|
|
249
|
+
surface — code that wants `dbus-native`'s own checked types can import them
|
|
250
|
+
directly and cast; it is the same object.
|
|
251
|
+
|
|
252
|
+
## Known sharp edges
|
|
253
|
+
|
|
254
|
+
- **The `Hello` window is the one crash this layer cannot absorb.** A daemon
|
|
255
|
+
that authenticates and then never answers `Hello` throws from inside
|
|
256
|
+
`dbus-native` where nothing can catch it, 25 s in, from a timer tick —
|
|
257
|
+
[dbus-native#393](https://github.com/sidorares/dbus-native/issues/393). Small
|
|
258
|
+
window, real bug, tracked upstream.
|
|
259
|
+
- **Identical match rules are refcounted by the daemon, not by the client** —
|
|
260
|
+
[dbus-native#394](https://github.com/sidorares/dbus-native/issues/394).
|
|
261
|
+
Dormant here, since reconnect is off; recorded so that nobody enables
|
|
262
|
+
reconnect without reading it.
|
|
263
|
+
|
|
264
|
+
## Seeing it
|
|
265
|
+
|
|
266
|
+
The bus explorer that used to live in `examples/` — session and system side
|
|
267
|
+
by side, a tree of names → object paths → interfaces built from live
|
|
268
|
+
introspection, a pane of an object's methods, signals and property values —
|
|
269
|
+
has moved to
|
|
270
|
+
[`@react-x11/components`](https://github.com/sidorares/react-x11-components).
|
|
271
|
+
It was built on `Tree`, which now lives there too, and a browser of that shape
|
|
272
|
+
is a component rather than a demonstration of this API.
|
|
273
|
+
|
|
274
|
+
What it demonstrated of _this_ page still holds and is worth knowing: point
|
|
275
|
+
`DBUS_SESSION_BUS_ADDRESS` at nothing and a well-built consumer renders the
|
|
276
|
+
`'unavailable'` state rather than failing to start. That is the degradation
|
|
277
|
+
rule above, and it is the thing to check in your own app.
|