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.
- package/README.md +10 -10
- package/docs/README.md +160 -314
- package/docs/accessibility.md +176 -229
- package/docs/appearance.md +77 -176
- package/docs/click-to-component.md +18 -51
- package/docs/clipboard.md +40 -59
- package/docs/components.md +518 -1036
- package/docs/dbus.md +61 -133
- package/docs/debugging.md +152 -245
- package/docs/desktop-calendar.md +51 -88
- package/docs/desktop.md +222 -347
- package/docs/devtools.md +50 -167
- package/docs/drag-and-drop.md +126 -204
- package/docs/ecosystem/animation.md +91 -125
- package/docs/ecosystem/data-fetching.md +38 -53
- package/docs/ecosystem/dev-tooling.md +159 -202
- package/docs/ecosystem/forms.md +61 -97
- package/docs/ecosystem/headless.md +64 -89
- package/docs/ecosystem/i18n.md +18 -21
- package/docs/ecosystem/icons.md +93 -126
- package/docs/ecosystem/layout.md +42 -52
- package/docs/ecosystem/routing.md +18 -29
- package/docs/ecosystem/state.md +71 -88
- package/docs/ecosystem/testing.md +131 -157
- package/docs/ecosystem/theming.md +119 -165
- package/docs/ecosystem.md +145 -179
- package/docs/elements.md +1096 -1774
- package/docs/embedding.md +83 -132
- package/docs/events.md +412 -695
- package/docs/extending.md +784 -1326
- package/docs/eyedropper.md +63 -122
- package/docs/filedialog.md +71 -107
- package/docs/frame.md +195 -339
- package/docs/gl.md +65 -62
- package/docs/globalmenu.md +63 -146
- package/docs/macos.md +531 -3234
- package/docs/notifications.md +33 -47
- package/docs/packaging.md +166 -309
- package/docs/permissions.md +9 -14
- package/docs/react-features.md +81 -106
- package/docs/remote.md +32 -76
- package/docs/scale.md +72 -186
- package/docs/styling.md +329 -547
- package/docs/system.md +200 -280
- package/docs/testing.md +63 -83
- package/docs/typescript.md +6 -48
- package/docs/uri-schemes.md +67 -107
- package/docs/wayland-backend.md +200 -459
- package/docs/windows-integrations.md +75 -131
- package/docs/windows.md +343 -1367
- package/package.json +3 -2
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/dnd.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/imagedecode-worker.js +32 -0
- package/src/imagedecode.js +140 -2
- package/src/layouts.js +1 -1
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/ntk.d.ts +12 -0
- package/src/pacing.js +1 -1
- package/src/refresh/index.js +2 -1
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +27 -0
- package/src/wayland/video.js +376 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
- package/docs/architecture/accessibility.md +0 -470
- package/docs/architecture/animation.md +0 -707
- package/docs/architecture/container-queries.md +0 -525
- package/docs/architecture/custom-layout.md +0 -334
- package/docs/architecture/drag-and-drop.md +0 -941
- package/docs/architecture/element-layer-contents.md +0 -622
- package/docs/architecture/frame-pacing.md +0 -255
- package/docs/architecture/grid-layout.md +0 -358
- package/docs/architecture/protocol-efficiency.md +0 -357
- package/docs/architecture/video.md +0 -1149
- package/docs/architecture/windowed-regions.md +0 -854
- package/docs/glx.md +0 -137
- package/docs/wayland.md +0 -545
- package/docs/windows-embedding.md +0 -271
- 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.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
151
|
-
on every fiber (
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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.
|
|
226
|
-
window
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
353
|
-
|
|
354
|
-
|
|
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 —
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
|
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
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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,
|
|
506
|
-
|
|
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.
|
|
513
|
-
|
|
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.
|
package/docs/typescript.md
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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).
|
package/docs/uri-schemes.md
CHANGED
|
@@ -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
|
|
31
|
-
flow, make this the default and
|
|
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
|
|
35
|
-
|
|
36
|
-
(`myapp://project/42`
|
|
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
|
|
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.
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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**,
|
|
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
|
|
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
|
-
|
|
130
|
-
`
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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
|
|
156
|
-
different events
|
|
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.
|
|
187
|
-
|
|
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.**
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
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 §
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
302
|
-
check. Validate what you receive, and never turn a URI's path
|
|
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.
|
|
310
|
-
|
|
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
|
|
321
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
353
|
-
|
|
354
|
-
|
|
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.
|
|
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
|
-
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
- **
|
|
371
|
-
|
|
372
|
-
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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).
|