react-x11 2.52.0 → 2.54.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 (115) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1096 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/imagedecode-worker.js +32 -0
  76. package/src/imagedecode.js +140 -2
  77. package/src/layouts.js +1 -1
  78. package/src/nodes/animation.js +1 -1
  79. package/src/nodes/invalidate.js +1 -1
  80. package/src/nodes/layouthost.js +1 -1
  81. package/src/nodes/video.js +19 -8
  82. package/src/nodes/window/droptarget.js +1 -1
  83. package/src/ntk.d.ts +12 -0
  84. package/src/pacing.js +1 -1
  85. package/src/refresh/index.js +2 -1
  86. package/src/types/video.d.ts +5 -2
  87. package/src/videobackend.js +28 -0
  88. package/src/videoframes.js +29 -16
  89. package/src/wayland/app.js +27 -0
  90. package/src/wayland/video.js +376 -0
  91. package/src/win32/a11y.js +1 -1
  92. package/src/win32/app.js +2 -2
  93. package/src/win32/fonts.js +1 -1
  94. package/src/win32/glarea.js +4 -4
  95. package/src/win32/overlay.js +1 -1
  96. package/src/win32/panewindow.js +3 -3
  97. package/src/win32/shell.js +1 -1
  98. package/src/win32/window.js +2 -2
  99. package/src/windowid.js +1 -1
  100. package/src/xvideo.js +189 -0
  101. package/docs/architecture/accessibility.md +0 -470
  102. package/docs/architecture/animation.md +0 -707
  103. package/docs/architecture/container-queries.md +0 -525
  104. package/docs/architecture/custom-layout.md +0 -334
  105. package/docs/architecture/drag-and-drop.md +0 -941
  106. package/docs/architecture/element-layer-contents.md +0 -622
  107. package/docs/architecture/frame-pacing.md +0 -255
  108. package/docs/architecture/grid-layout.md +0 -358
  109. package/docs/architecture/protocol-efficiency.md +0 -357
  110. package/docs/architecture/video.md +0 -1149
  111. package/docs/architecture/windowed-regions.md +0 -854
  112. package/docs/glx.md +0 -137
  113. package/docs/wayland.md +0 -545
  114. package/docs/windows-embedding.md +0 -271
  115. package/docs/windows-gl.md +0 -221
package/docs/testing.md CHANGED
@@ -2,23 +2,20 @@
2
2
 
3
3
  `react-x11/test` renders your app through a **real X11 protocol connection
4
4
  to a real X server, in this process** — no `$DISPLAY`, no Xvfb, no native
5
- dependency, works on macOS. So a test can read pixels back with `GetImage`
6
- and inject input through the server's own grab and focus machinery, which
7
- means a passing test has exercised the path a user actually takes.
8
-
9
- That server is node-x11's `lib/xserver`, written in JavaScript. It arrives
10
- with ntk, so there is nothing new to install.
5
+ dependency, works on macOS. A test can read pixels back with `GetImage` and
6
+ inject input through the server's own grab and focus machinery, so a
7
+ passing test has exercised the path a user actually takes. The server is
8
+ node-x11's `lib/xserver`, written in JavaScript; it arrives with ntk, so
9
+ there is nothing new to install.
11
10
 
12
11
  **The harness drives the X11 backend**, on every platform including macOS —
13
12
  there is no `'cocoa'` backend for `renderX11()`, and `createRoot()`'s own
14
- choice does not apply here because the harness always passes it an `app`.
15
- That is deliberate rather than pending: the point of this harness is a real
16
- protocol connection to a real server in-process, and macOS has no protocol
17
- to connect to. What it means in practice is that an app targeting both
18
- backends tests its **tree, its queries and its behaviour** here — all of
19
- which are shared — while anything that is genuinely per-backend (a native
20
- bezel's pixels, an `NSMenu`, a pasteboard flavour) is covered by the
21
- renderer's own Cocoa tests against a fake bridge, and by looking at a Mac.
13
+ choice does not apply because the harness always passes it an `app`. This
14
+ is by design: macOS has no protocol to connect to in-process. An app
15
+ targeting several backends tests its **tree, its queries and its
16
+ behaviour** here — all shared — while anything genuinely per-backend (a
17
+ native bezel's pixels, an `NSMenu`, a pasteboard flavour) is checked by
18
+ running on that platform.
22
19
 
23
20
  ```js
24
21
  import { test, afterEach } from 'node:test';
@@ -45,10 +42,9 @@ test('adding a task', async () => {
45
42
  });
46
43
  ```
47
44
 
48
- The vocabulary is Testing Library's, because that is the one React
49
- developers already have. It is not Testing Library — there is no document
50
- for it to query — but `getByRole` / `userEvent.click` / `waitFor` mean here
51
- what they mean there.
45
+ The vocabulary is Testing Library's. It is not Testing Library — there is
46
+ no document for it to query — but `getByRole` / `userEvent.click` /
47
+ `waitFor` mean here what they mean there.
52
48
 
53
49
  ## `renderX11(element, options)`
54
50
 
@@ -130,10 +126,9 @@ point `AT_SPI_BUS_ADDRESS` at an in-process bus instead — see
130
126
 
131
127
  ## Components
132
128
 
133
- The queries above see what a user sees. Sometimes a test needs the React
134
- side instead — _which component_ made this node, with what props and state —
135
- and this is the layer for it. Prefer the user-visible queries when either
136
- would do; reach here when the question is genuinely about a component.
129
+ The queries above see what a user sees. When a test needs the React side
130
+ instead — _which component_ made this node, with what props and state — this
131
+ is the layer for it. Prefer the user-visible queries when either would do.
137
132
 
138
133
  ```js
139
134
  const row = screen.getByComponent('TaskRow'); // also RegExp or predicate
@@ -147,10 +142,10 @@ sourceOf(node); // { file, line, column } of the JSX that made it
147
142
  its output — the container `within()` wants next. Ownership is React's JSX
148
143
  owner (_who wrote the element_), so content passed as children belongs to
149
144
  the component whose JSX it appears in, and a miss lists the component names
150
- that are in the tree. All of this reads debug info development React records
151
- on every fiber (`_debugOwner`, `_debugStack` — the same data
152
- [click-to-component](click-to-component.md) navigates by), so it costs no
153
- setup and no dependency, and returns nothing in production builds.
145
+ that are in the tree. All of this reads the debug info development React
146
+ records on every fiber (the data [click-to-component](click-to-component.md)
147
+ navigates by), so it needs no setup and no dependency, and returns nothing
148
+ in production builds.
154
149
 
155
150
  ### `inspect(node)` and `setHook`
156
151
 
@@ -164,19 +159,17 @@ await c.setHook(0, 41); // set a useState/useReducer value…
164
159
  await c.setHook(1, ['filter', 'text'], 'milk'); // …or a path inside one
165
160
  ```
166
161
 
167
- This drives the **React DevTools backend in-process**: the same
168
- react-debug-tools inspection and the same override path the DevTools app
169
- uses, minus its WebSocket — nothing listens on anything, and `ws` is never
170
- loaded ([devtools.md](devtools.md) has the wiring). It needs the
171
- `react-devtools-core` package in your devDependencies; the queries above
172
- work without it.
162
+ This drives the **React DevTools backend in-process** — the same inspection
163
+ and override path the DevTools app uses, minus its WebSocket: nothing
164
+ listens on anything, and `ws` is never loaded ([devtools.md](devtools.md)).
165
+ It needs the `react-devtools-core` package in your devDependencies; the
166
+ queries above work without it.
173
167
 
174
168
  What to know before leaning on it:
175
169
 
176
- - **Reading hooks re-runs the component.** That is how react-debug-tools
177
- recovers hook names: the render function is invoked once under a stub
178
- dispatcher, output discarded, `console` muted. A render-counting test
179
- will see it.
170
+ - **Reading hooks re-runs the component** once, output discarded and
171
+ `console` muted — that is how hook names are recovered. A render-counting
172
+ test will see it.
180
173
  - **Values:** `props` and editable hook values are the live objects;
181
174
  everything else is DevTools' copy — primitives exact, deep objects as
182
175
  the same previews the DevTools panel shows.
@@ -211,7 +204,7 @@ await userEvent.wheel(list, { deltaY: 0.25, smooth: true }); // a touchpad
211
204
  await userEvent.clickOutside(); // dismisses a menu — see below
212
205
  ```
213
206
 
214
- Two things this buys that emitting on the ntk window never could:
207
+ Input goes through the server, so:
215
208
 
216
209
  **Grabs are real.** A `<popup grab>` is dismissed by a real press outside
217
210
  it. But note _where_ outside: react-x11's popups grab with **owner events**,
@@ -222,17 +215,15 @@ how a test says that; clicking another widget and expecting the menu to close
222
215
  is testing the wrong thing.
223
216
 
224
217
  **Focus is real**, so `trapFocus`, `autoFocus` and Tab traversal are
225
- exercised rather than simulated. Deciding which window gets the keyboard is a
226
- window manager's job and there is no window manager, so `renderX11` plays the
227
- part with one `SetInputFocus` at mount — otherwise keys would go to whatever
228
- the pointer happened to be over, which is a rule nobody should have to learn.
229
-
230
- **A smooth scroll is the one exception**, and says so. Whole notches go
231
- through the server as the button presses X really uses, but a touchpad's
232
- fraction of a notch is XI2, and the in-process server has no XInput — there
233
- are no valuators to inject. `{ smooth: true }` therefore delivers the `wheel`
234
- event ntk would have derived from them, straight to the window. Everything
235
- above it is still the real path: the dispatch, the chain, the default action.
218
+ exercised rather than simulated. There is no window manager, so `renderX11`
219
+ gives the window input focus once at mount — otherwise keys would go to
220
+ whatever the pointer happened to be over.
221
+
222
+ **A smooth scroll is the one exception.** Whole notches go through the
223
+ server as the button presses X really uses, but a touchpad's fraction of a
224
+ notch is XI2, which the in-process server lacks, so `{ smooth: true }`
225
+ delivers the `wheel` event straight to the window. Everything above it is
226
+ still the real path: the dispatch, the chain, the default action.
236
227
 
237
228
  **Typing works for text the layout does not have.** `userEvent.type(input,
238
229
  'héllo')` binds a spare keycode for `é` on both the server's keymap and the
@@ -349,12 +340,9 @@ event, a re-render of React state and a frame: `act` runs all three, so the
349
340
  repaint is on screen when `userEvent.hover` resolves.
350
341
 
351
342
  It `act`s once more after the attempt that passes, so what it resolves with
352
- is on screen. That matters for a node. What an attempt finds can have arrived
353
- in the pause before it — an fs read, a timer — and that commits outside
354
- `act`, on React's own scheduler, while the frame that lays it out runs on the
355
- third clock, which need not have ticked since. Without that last `act`, a
356
- row found that way could have no rect yet, and the click after it would throw
357
- `has no laid-out rect yet`.
343
+ is on screen and laid out: a row that arrived from an fs read or a timer
344
+ committed outside `act`, and without that last frame the click after it
345
+ would throw `has no laid-out rect yet`.
358
346
 
359
347
  ```js
360
348
  const row = await waitFor(() => screen.getByText('notes.txt'));
@@ -397,14 +385,10 @@ await toPNG(ctx, '/tmp/shot.png', { width: 320, height: 240 });
397
385
  ```
398
386
 
399
387
  A pixel proves the tree, the layout, the clip, the paint order, the colour
400
- parsing and the wire encoding at once — and it is the only assertion that
401
- catches the class of bug where everything commits correctly and nothing
402
- reaches the screen. Two details it demands: the readback is straight
403
- **RGBA**, as on a canvas, and antialiasing means every comparison takes a
404
- **tolerance**.
405
-
406
- `toPNG` is also how a pull request gets a screenshot rendered by its own
407
- code, which this repo asks for ([AGENTS.md](../AGENTS.md), "Pull requests").
388
+ parsing and the wire encoding at once — the only assertion that catches
389
+ everything committing correctly and nothing reaching the screen. The
390
+ readback is straight **RGBA**, as on a canvas, and antialiasing means every
391
+ comparison takes a **tolerance**.
408
392
 
409
393
  ## Animations
410
394
 
@@ -420,17 +404,16 @@ clock.restore();
420
404
 
421
405
  Install it **before** the render. Every timestamp in the tree has to come
422
406
  from the same clock: mixing a real start time with a frozen `now` gives a
423
- transition that never progresses, which is a confusing way to spend an hour.
407
+ transition that never progresses.
424
408
 
425
409
  ## The desktop's appearance
426
410
 
427
- The built-in palette follows the desktop, so `renderX11` **pins** it: an
428
- unpinned suite renders in whatever colours the machine running it is in, and
429
- a pixel assertion passes on a light desktop and fails on a dark one with
430
- nothing in the test to say why. The pin is light unless the render says
431
- otherwise, and it covers everything `useSystemAppearance()` reports — so a
432
- component that follows reduced motion, contrast or the accent is tested the
433
- way one that follows the scheme is:
411
+ The built-in palette follows the desktop, so `renderX11` **pins** it —
412
+ otherwise a pixel assertion would pass on a light desktop and fail on a
413
+ dark one. The pin is light unless the render says otherwise, and it covers
414
+ everything `useSystemAppearance()` reports, so a component that follows
415
+ reduced motion, contrast or the accent is tested the way one that follows
416
+ the scheme is:
434
417
 
435
418
  ```jsx
436
419
  await renderX11(<App />); // light
@@ -480,13 +463,12 @@ Three mistakes throw rather than pass quietly:
480
463
  - **`setAppearance` before `renderX11`.** The render pins as it mounts and
481
464
  would replace what was set, so it throws and says to pass `appearance`.
482
465
 
483
- `cleanup()` releases the pin, and only the pin: what a real desktop answered
484
- is not the harness's to throw away. A pinned value is never written to the
485
- cache a real app starts its first frame from ([appearance.md](appearance.md)),
486
- so a suite run on a developer's desktop leaves that desktop's remembered
487
- answer as it found it — and an answer the desktop sends while a pin is held,
488
- from a probe a `'system'` test started, is dropped rather than painted into
489
- the next test.
466
+ `cleanup()` releases the pin, and only the pin. A pinned value is never
467
+ written to the cache a real app starts its first frame from
468
+ ([appearance.md](appearance.md)), so a suite run on a developer's desktop
469
+ leaves that desktop's remembered answer as it found it, and an answer the
470
+ desktop sends while a pin is held is dropped rather than painted into the
471
+ next test.
490
472
 
491
473
  ## The mock backend
492
474
 
@@ -502,13 +484,11 @@ rather than failing the mount. A component that asks before rendering one
502
484
  mounts its fallback here; one that does not still mounts.
503
485
 
504
486
  A batch records as the fills it stands for: `ctx.fillRects([[x, y, w, h], …])`
505
- leaves one `fillRect` operation per rectangle, because the only difference
506
- between them is a request count and there is no server here to send it to. So
507
- an assertion on what a drawing painted holds whether or not it batched.
487
+ leaves one `fillRect` operation per rectangle, so an assertion on what a
488
+ drawing painted holds whether or not it batched.
508
489
 
509
490
  ## Testing Library itself
510
491
 
511
492
  It does not work here and no shim will fix it: its queries are built on a
512
- document. The five failure modes are recorded in the
513
- [negative-results register](ecosystem.md#render-time). This page is the
514
- replacement, not a workaround for it.
493
+ document ([ecosystem.md](ecosystem.md#render-time) lists the failure
494
+ modes). This page is the replacement.
@@ -22,43 +22,16 @@ against React's namespace, which describes the DOM.
22
22
 
23
23
  ## Why a JSX runtime and not an augmentation
24
24
 
25
- The usual way a library adds elements to JSX is to merge into React's
26
- namespace:
27
-
28
- ```ts
29
- declare module 'react' {
30
- namespace JSX {
31
- interface IntrinsicElements {
32
- window: WindowProps;
33
- }
34
- }
35
- }
36
- ```
37
-
38
- That cannot work here. Five element names — `text`, `image`, `canvas`,
39
- `html`, `svg` — are also HTML or SVG element names, already declared by
40
- `@types/react` with completely different props, and declaration merging
41
- cannot replace an existing member:
42
-
43
- ```
44
- Interface 'IntrinsicElements' incorrectly extends interface 'ReactX11Elements'.
45
- The types of 'text.ref' are incompatible between these types.
46
- Type 'Ref<SVGTextElement>' is not assignable to type 'Ref<DrawnNode>'.
47
- ```
48
-
49
- So react-x11 provides its own JSX source instead — `react-x11/jsx-runtime`,
50
- which re-exports React's actual runtime and carries a `JSX` namespace of its
51
- own. That turns out to be the better answer regardless: the namespace holds
52
- the X11 elements _and nothing else_, so
25
+ react-x11 cannot merge its elements into React's `JSX` namespace: `text`,
26
+ `image`, `canvas`, `html` and `svg` are already declared there by
27
+ `@types/react` with DOM props, and declaration merging cannot replace them.
28
+ So `react-x11/jsx-runtime` re-exports React's runtime with a `JSX` namespace
29
+ of its own that holds the react-x11 elements _and nothing else_:
53
30
 
54
31
  ```tsx
55
32
  <div /> // error: Property 'div' does not exist on type 'JSX.IntrinsicElements'
56
33
  ```
57
34
 
58
- is caught at compile time rather than throwing at runtime. An augmentation
59
- could never have done that, because `@types/react` declares every HTML and
60
- SVG tag unconditionally.
61
-
62
35
  If you need extra elements of your own, `IntrinsicElements` is an interface,
63
36
  so merging still works:
64
37
 
@@ -99,19 +72,4 @@ the four yoga values, a state block only takes paint properties (the same
99
72
  rule `createStyles` enforces at runtime), and `width` takes
100
73
  `number | '50%' | 'auto'`.
101
74
 
102
- ## Keeping them honest
103
-
104
- Hand-written declarations drift. `test/types/api.tsx` is compiled by
105
- `npm run typecheck` — in CI alongside lint — and exercises the API the way
106
- the examples do, including `@ts-expect-error` lines for things that must
107
- _not_ compile:
108
-
109
- ```tsx
110
- // @ts-expect-error — layout properties may not go in a state block
111
- createStyles({ bad: { ':hover': { padding: 4 } } });
112
- ```
113
-
114
- Those fail the build if the rejection stops happening, which is the half of
115
- a type test that usually goes missing.
116
-
117
- When you change a prop, change the declaration and add a line to that file.
75
+ Design notes: [design/typescript.md](../design/typescript.md).
@@ -27,15 +27,13 @@ const port = server.address().port;
27
27
  ```
28
28
 
29
29
  No desktop registration, no D-Bus, no install step, and it works on XQuartz,
30
- in a container and over ssh in ways a scheme handler does not. **For an OAuth
31
- flow, make this the default and reach for the rest of this page only when you
30
+ in a container and over ssh where a scheme handler does not. **For an OAuth
31
+ flow, make this the default and use the rest of this page only when you
32
32
  cannot.**
33
33
 
34
- Custom schemes still earn their place three ways: providers that only accept a
35
- registered private-use scheme; deep links that are not logins at all
36
- (`myapp://project/42` out of a chat message, a document opened from a
37
- launcher); and the fact that owning a name on the bus is the same machinery
38
- the [global menu](globalmenu.md) and a tray icon need anyway.
34
+ Custom schemes are still needed for providers that only accept a registered
35
+ private-use scheme, and for deep links that are not logins
36
+ (`myapp://project/42` from a chat message, a document opened from a launcher).
39
37
 
40
38
  ## Four things have to be true
41
39
 
@@ -46,7 +44,7 @@ the [global menu](globalmenu.md) and a tray icon need anyway.
46
44
  | **Delivery** | `org.freedesktop.Application.Open`, answered before the UI | `useAppOpen()` |
47
45
  | **The raise** | `_NET_ACTIVE_WINDOW`, with the right timestamp | `activateWindow()` |
48
46
 
49
- The last one is the one that decides whether a user calls the feature working;
47
+ The raise is the one that decides whether a user calls the feature working;
50
48
  see [the raise is the part that fails](#the-raise-is-the-part-that-fails).
51
49
 
52
50
  ## The code
@@ -78,8 +76,7 @@ function App() {
78
76
  }
79
77
  ```
80
78
 
81
- That is the whole runtime half. What follows is why each line is shaped the
82
- way it is.
79
+ That is the whole runtime half.
83
80
 
84
81
  ### `registerApplication()`
85
82
 
@@ -106,55 +103,40 @@ process_ because someone called `Open`, and that call is outstanding while the
106
103
  app boots — registering from inside a component answers it with an
107
104
  unknown-method error, and the launch is lost.
108
105
 
109
- **`appId` is not a free parameter.** The desktop entry spec ties three things
110
- to one string: the well-known bus name, the `.desktop` file's basename, and —
111
- by RFC 8252 §7.1 — the URI scheme. The object path follows from it too, and is
112
- derived rather than passed: dots become slashes, a slash goes on the front,
113
- and **a dash becomes an underscore**. `com.example.my-app` is served at
114
- `/com/example/my_app`; get that wrong and the desktop calls a path you do not
115
- serve, which looks exactly like nothing happening.
106
+ **`appId` is not a free parameter.** The desktop entry spec ties the
107
+ well-known bus name, the `.desktop` file's basename and (RFC 8252 §7.1) the URI
108
+ scheme to one string. The object path is derived from it: dots become slashes,
109
+ a slash goes on the front, and **a dash becomes an underscore**
110
+ (`com.example.my-app` is served at `/com/example/my_app`).
116
111
 
117
112
  **`null` means there is no session bus** — ssh, a bare `startx`, a container,
118
113
  CI, Node 20 without the transport. The app runs as an ordinary single-window
119
114
  program. A URI that arrived in `argv` is still delivered to `useAppOpen`: a
120
115
  cold-start deep link needs no bus, only a second instance does.
121
116
 
122
- **A malformed `appId` or an unusable scheme throws**, and that is not a hole in
117
+ **A malformed `appId` or an unusable scheme throws**, which is not a hole in
123
118
  [the never-throws rule](dbus.md#the-rule-everything-else-is-subordinate-to):
124
- `null` means "this machine has no bus", and using it for a typo would hide the
125
- typo on every box without one.
119
+ `null` means "this machine has no bus", and a typo must not hide behind it.
126
120
 
127
121
  ### Two dispatch paths, and why `role` exists
128
122
 
129
- The desktop entry spec makes D-Bus activation sound like the whole story.
130
- `DBusActivatable=true`, and implementations _should_ ignore `Exec` and send a
131
- D-Bus message instead. _Should_ — and the most common opener does not.
132
-
133
- - **GIO honours it.** `g_app_info_launch_default_for_uri` calls `Open` on the
134
- app's well-known name. `gio open`, and most of GNOME, take this path.
135
- - **`xdg-open`'s generic fallback does not.** It resolves the handler with
136
- `xdg-mime query default`, reads the `Exec` key and runs it;
137
- `DBusActivatable` is never read on that branch. Chromium shells out to
138
- `xdg-open`, and `xdg-open` takes the generic branch on every desktop it
139
- cannot identify — which is every WM-only session, react-x11's own persona.
140
-
141
- So on half of all desktops **a second copy of your app is spawned with the URI
142
- in `argv`**, and it has to hand that URI to the first copy and exit. An app
143
- that only exports the interface opens a second window every time.
144
- `registerApplication` does both halves, which is why it answers a role:
123
+ Openers that honour `DBusActivatable=true` (GIO: `gio open`, most of GNOME)
124
+ call `Open` on the running app. `xdg-open`'s generic fallback — which Chromium
125
+ uses, and which every WM-only session gets — ignores it and runs the `Exec`
126
+ line, so **a second copy of your app is spawned with the URI in `argv`**.
127
+ `registerApplication` handles both: the second copy forwards the URI to the
128
+ first and reports its role.
145
129
 
146
130
  | `role` | |
147
131
  | ------------- | ------------------------------------------------------------------ |
148
132
  | `'primary'` | this process owns the name and will receive `Open` |
149
133
  | `'secondary'` | another instance owns it; this launch was forwarded — **exit now** |
150
134
 
151
- It does not call `process.exit()` for you. Exiting is an application decision
152
- and a library that takes it is a library that surprises somebody.
135
+ It does not call `process.exit()` for you; exiting is the app's decision.
153
136
 
154
137
  A second launch with **no** URI forwards `Activate` rather than an empty
155
- `Open`: "the user started the app again" and "the user clicked a link" are
156
- different events, and an app that cannot tell them apart cannot answer the
157
- first one properly.
138
+ `Open`, so "the user started the app again" and "the user clicked a link"
139
+ arrive as different events.
158
140
 
159
141
  ### Delivery: buffered, and replayed
160
142
 
@@ -183,14 +165,12 @@ interface LaunchContext {
183
165
  }
184
166
  ```
185
167
 
186
- There is **nothing to wrap** — no provider, no prop drilling. An application
187
- has one identity on the desktop, so the registration is process-global and a
188
- component library can call these hooks.
168
+ There is **nothing to wrap** — no provider, no prop drilling. The registration
169
+ is process-global, so a component library can call these hooks.
189
170
 
190
171
  `schemes` filters what arrives: a URI whose scheme you did not register is
191
172
  dropped before your handler sees it. `file:` is always allowed alongside them,
192
- because `Open` is also how a file manager says "open this document with you"
193
- and declaring a custom scheme must not stop your app opening its own files.
173
+ because `Open` is also how a file manager says "open this document with you".
194
174
  Declaring no schemes at all filters nothing.
195
175
 
196
176
  ### The raise is the part that fails
@@ -207,13 +187,11 @@ and this app's currently active toplevel. `target` is anything
207
187
  [`windowIdOf()`](elements.md) accepts, or nothing — in which case this app's
208
188
  only top-level window is used, its focused one when it has several.
209
189
 
210
- **The timestamp is the whole feature.** Per the startup-notification protocol
211
- the digits after `_TIME` in a startup id are the X timestamp of the user action
212
- that triggered the launch, and that timestamp is what a window manager's
213
- focus-stealing prevention weighs before letting a window come forward. Get it
214
- wrong and nothing reports an error: the user finishes logging in, the browser
215
- redirects, your app receives the code correctly, the WM declines, and the
216
- taskbar entry blinks. Every layer says it worked.
190
+ **The timestamp is the whole feature.** A window manager's focus-stealing
191
+ prevention weighs the X timestamp of the user action that triggered the launch
192
+ (the digits after `_TIME` in the startup id). Get it wrong and nothing reports
193
+ an error: the app receives the code, the WM declines to raise it, and the
194
+ taskbar entry blinks.
217
195
 
218
196
  | `timestamp` | |
219
197
  | --------------- | ------------------------------------------------------------------------------------------------------------- |
@@ -221,18 +199,15 @@ taskbar entry blinks. Every layer says it worked.
221
199
  | `ctx.timestamp` | **what a deep link should pass**: the click in the browser is the user action, not whatever this app last saw |
222
200
  | `null` | `CurrentTime`; legal, and most window managers will decline it |
223
201
 
224
- The return value is whether the request was **issued**, not whether it worked.
225
- EWMH is explicit that the window manager may refuse and set
226
- `_NET_WM_STATE_DEMANDS_ATTENTION` instead — that is the blinking taskbar entry,
227
- it is the WM's call, and no client can tell the difference from here.
202
+ The return value is whether the request was **issued**, not whether it worked:
203
+ the window manager may refuse and set `_NET_WM_STATE_DEMANDS_ATTENTION`
204
+ instead (the blinking taskbar entry), and no client can tell.
228
205
 
229
206
  ## Registration: the half that is not runtime code
230
207
 
231
- react-x11 does **not** write into `~/.local/share` for you. A library that
232
- edits a user's data directory on import is the kind of thing that surprises
233
- people, and it cannot work for a system-installed or Flatpak app anyway. Ship
234
- these from your installer, your package, or a one-line setup command your users
235
- run.
208
+ react-x11 does **not** write into `~/.local/share` for you (that would not
209
+ work for a system-installed or Flatpak app anyway). Ship these from your
210
+ installer, your package, or a setup command your users run.
236
211
 
237
212
  `~/.local/share/applications/com.example.myapp.desktop` — the basename is tied
238
213
  to the bus name by the spec:
@@ -269,10 +244,6 @@ Exec=/usr/bin/my-app
269
244
 
270
245
  ### Checking it by hand
271
246
 
272
- Neither branch below is reachable from `npm test` — the in-process broker has
273
- no service activation and no security policy — so this is the pass that covers
274
- them:
275
-
276
247
  ```bash
277
248
  gio open com.example.myapp://test # path A: D-Bus activation
278
249
  ```
@@ -292,23 +263,21 @@ into an issue.
292
263
  The URI is attacker-controlled input from an unauthenticated local caller.
293
264
 
294
265
  - **Scheme squatting is unfixable and expected.** Nothing in the MIME database
295
- enforces uniqueness; the last handler to register wins. RFC 8252 §7.1 says as
296
- much, which is why §6 requires PKCE for public native clients — **use it**,
297
- and do not treat receiving a code as proof of anything. The reverse-domain
298
- naming rule is the other half of the mitigation, and it is why `appId` is
299
- validated the way it is.
266
+ enforces uniqueness; the last handler to register wins. RFC 8252 §6 requires
267
+ PKCE for public native clients — **use it**, and do not treat receiving a
268
+ code as proof of anything. The reverse-domain naming rule (why `appId` is
269
+ validated) is the other half of the mitigation.
300
270
  - **`Open` is callable by any peer on the session bus.** It is not an
301
- authenticated entry point. The bus is a per-user socket, not an authorization
302
- check. Validate what you receive, and never turn a URI's path into a
303
- filesystem path.
271
+ authenticated entry point: the bus is a per-user socket, not an
272
+ authorization check. Validate what you receive, and never turn a URI's path
273
+ into a filesystem path.
304
274
  - **`platform_data` is spoofable.** A hostile local process can fabricate a
305
275
  `desktop-startup-id` and make your app raise itself. Same uid, so it is focus
306
276
  theft rather than data loss — but treat `ctx.timestamp` as a hint, not a
307
277
  credential.
308
278
  - **On the `xdg-open` path the URI lands in `argv`**, so a code is briefly
309
- visible in `/proc/<pid>/cmdline` to processes of the same user. One more
310
- reason to point login flows at loopback first; the D-Bus path does not have
311
- this property.
279
+ visible in `/proc/<pid>/cmdline` to processes of the same user. The D-Bus
280
+ path does not have this property; loopback avoids it too.
312
281
  - **Never log the URI.** react-x11 does not: everything on
313
282
  `REACT_X11_DEBUG_URI` goes through a redaction that drops the query, the
314
283
  fragment and any userinfo. Hold your own logging to the same rule — the code
@@ -317,9 +286,8 @@ The URI is attacker-controlled input from an unauthenticated local caller.
317
286
  ## On macOS
318
287
 
319
288
  The same hooks, a different transport. On the cocoa backend the OS talks to
320
- the application as a whole through **Apple Events**, and `@windowkit/appkit`
321
- (≥ 0.5) forwards them as backend events the renderer routes into the very
322
- same delivery as a D-Bus `Open`:
289
+ the application through **Apple Events**, which `@windowkit/appkit` (≥ 0.5)
290
+ forwards into the same delivery as a D-Bus `Open`:
323
291
 
324
292
  | | |
325
293
  | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
@@ -342,39 +310,31 @@ Three things differ from the bus, and none of them changes app code:
342
310
  (see [packaging.md](packaging.md)). A bare `node` process receives nothing
343
311
  from Launch Services, because there is no bundle for it to look up.
344
312
 
345
- Quit is worth a sentence, because macOS asks the app before ending it. The
346
- bridge answers `applicationShouldTerminate:` with "cancel" and hands the
347
- question over, so a quit becomes the same close request the red light sends
348
- to the app's primary window: an `onCloseRequest` that opens a "save your
349
- work?" dialog has vetoed it until it decides, and one that unmounts has
350
- accepted it. Once the app has closed on a quit, the process ends
313
+ A quit becomes the same close request the red light sends to the app's primary
314
+ window: an `onCloseRequest` that opens a "save your work?" dialog has vetoed it
315
+ until it decides, and one that unmounts has accepted it. Once the app has
316
+ closed on a quit, the process ends
351
317
  (`createRoot({ cocoa: { exitOnQuit: false } })` turns that off for an embedder
352
- that owns the process). One limit to know: because the bridge's answer is
353
- "cancel" rather than "later", a **logout** is cancelled by macOS the moment it
354
- asks, and the app then quits on its own — the logout has to be started again.
318
+ that owns the process). One limit: a **logout** is cancelled by macOS the
319
+ moment it asks, and the app then quits on its own — the logout has to be
320
+ started again.
355
321
 
356
322
  ## Over ssh
357
323
 
358
324
  Both mechanisms only work when the browser runs on the **same host as the
359
325
  app**. A URL pasted into your laptop's browser reaches neither a loopback port
360
- nor a scheme handler on the server you are `ssh -X`'d into. That is not fixable
361
- here; it is the shape of the problem.
326
+ nor a scheme handler on the server you are `ssh -X`'d into.
362
327
 
363
328
  ## Deliberately not here
364
329
 
365
- - **`ActivateAction` beyond a correct reply.** Desktop actions (jump-list
366
- entries) want a design alongside the global menu. The method answers properly
367
- today, and `onAction` is there if you want it — a stub that _errors_ would
368
- make a shell conclude the app is broken rather than that the action is
369
- unhandled.
370
- - **Writing `.desktop` files.** See above. A generator belongs with the
371
- packaging work ([packaging.md](packaging.md)), not on an import.
372
- - **A full OAuth client.** This is the transport for a redirect, not a PKCE
373
- implementation or a token store.
374
- - **`http`/`https` handling.** Being the default browser is not this.
375
- - **Wayland's `xdg-activation` token.** `platform_data.activation-token` is
376
- read and exposed on `ctx`, and ignored. On the
377
- [Wayland backend](wayland-backend.md) `activateWindow()` does go through
378
- xdg-activation, but with a token it requests itself against the seat's
379
- last input serial (`src/wayland/app.js`, `activate`) — the launcher's token
380
- is not handed to the compositor.
330
+ - **Desktop actions** (jump-list entries): `ActivateAction` replies correctly
331
+ and `onAction` receives it; nothing more.
332
+ - **Writing `.desktop` files** — your installer's job (see above).
333
+ - **A full OAuth client** — no PKCE implementation or token store.
334
+ - **`http`/`https` handling** — being the default browser is not this.
335
+ - **The launcher's Wayland `xdg-activation` token** is exposed on
336
+ `ctx.activationToken` and otherwise ignored; on the
337
+ [Wayland backend](wayland-backend.md) `activateWindow()` requests a token of
338
+ its own.
339
+
340
+ Design notes: [design/uri-schemes.md](../design/uri-schemes.md).