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.
Files changed (91) hide show
  1. package/README.md +87 -54
  2. package/docs/README.md +410 -0
  3. package/docs/accessibility.md +628 -0
  4. package/docs/appearance.md +416 -0
  5. package/docs/architecture/accessibility.md +470 -0
  6. package/docs/architecture/animation.md +707 -0
  7. package/docs/architecture/container-queries.md +525 -0
  8. package/docs/architecture/custom-layout.md +334 -0
  9. package/docs/architecture/drag-and-drop.md +941 -0
  10. package/docs/architecture/element-layer-contents.md +622 -0
  11. package/docs/architecture/frame-pacing.md +255 -0
  12. package/docs/architecture/grid-layout.md +358 -0
  13. package/docs/architecture/protocol-efficiency.md +357 -0
  14. package/docs/architecture/video.md +1149 -0
  15. package/docs/architecture/windowed-regions.md +854 -0
  16. package/docs/click-to-component.md +125 -0
  17. package/docs/clipboard.md +281 -0
  18. package/docs/components.md +1749 -0
  19. package/docs/dbus.md +277 -0
  20. package/docs/debugging.md +464 -0
  21. package/docs/desktop-calendar.md +284 -0
  22. package/docs/desktop.md +648 -0
  23. package/docs/devtools.md +250 -0
  24. package/docs/drag-and-drop.md +663 -0
  25. package/docs/ecosystem/animation.md +394 -0
  26. package/docs/ecosystem/data-fetching.md +201 -0
  27. package/docs/ecosystem/dev-tooling.md +436 -0
  28. package/docs/ecosystem/forms.md +313 -0
  29. package/docs/ecosystem/headless.md +219 -0
  30. package/docs/ecosystem/i18n.md +89 -0
  31. package/docs/ecosystem/icons.md +410 -0
  32. package/docs/ecosystem/layout.md +216 -0
  33. package/docs/ecosystem/routing.md +146 -0
  34. package/docs/ecosystem/state.md +361 -0
  35. package/docs/ecosystem/testing.md +500 -0
  36. package/docs/ecosystem/theming.md +540 -0
  37. package/docs/ecosystem.md +473 -0
  38. package/docs/elements.md +2762 -0
  39. package/docs/embedding.md +288 -0
  40. package/docs/events.md +1025 -0
  41. package/docs/extending.md +2242 -0
  42. package/docs/eyedropper.md +223 -0
  43. package/docs/filedialog.md +258 -0
  44. package/docs/frame.md +580 -0
  45. package/docs/gl.md +170 -0
  46. package/docs/globalmenu.md +278 -0
  47. package/docs/glx.md +137 -0
  48. package/docs/macos.md +3295 -0
  49. package/docs/notifications.md +164 -0
  50. package/docs/packaging.md +611 -0
  51. package/docs/permissions.md +148 -0
  52. package/docs/react-features.md +430 -0
  53. package/docs/remote.md +223 -0
  54. package/docs/scale.md +339 -0
  55. package/docs/security.md +132 -0
  56. package/docs/styling.md +1928 -0
  57. package/docs/system.md +599 -0
  58. package/docs/testing.md +514 -0
  59. package/docs/typescript.md +117 -0
  60. package/docs/uri-schemes.md +380 -0
  61. package/docs/wayland-backend.md +591 -0
  62. package/docs/wayland.md +545 -0
  63. package/docs/windows-embedding.md +271 -0
  64. package/docs/windows-gl.md +221 -0
  65. package/docs/windows-integrations.md +233 -0
  66. package/docs/windows.md +1404 -0
  67. package/package.json +10 -3
  68. package/src/bus.js +39 -26
  69. package/src/cli/create.d.ts +43 -0
  70. package/src/cli/create.js +305 -0
  71. package/src/cli/env.js +484 -0
  72. package/src/cli/index.js +29 -2
  73. package/src/cli/templates/AGENTS.template.md +195 -0
  74. package/src/cli/templates/DESIGN.template.md +269 -0
  75. package/src/desktopintegration.js +2 -1
  76. package/src/desktopsettings.js +193 -24
  77. package/src/filedialog.js +3 -3
  78. package/src/keyboardstate.js +28 -0
  79. package/src/node.d.ts +10 -10
  80. package/src/portal.js +31 -0
  81. package/src/screencolor.js +5 -10
  82. package/src/types/system.d.ts +8 -6
  83. package/src/wayland/app.js +59 -0
  84. package/src/wayland/clipboard.js +14 -7
  85. package/src/wayland/connection.js +3 -0
  86. package/src/wayland/context2d.js +129 -10
  87. package/src/wayland/protocols/index.json +6 -0
  88. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  89. package/src/wayland/seat.js +28 -1
  90. package/src/wayland/swapchain.js +23 -0
  91. package/src/wayland/xkb.js +137 -0
@@ -0,0 +1,380 @@
1
+ # Custom URI schemes and single instances
2
+
3
+ Being the app that `com.example.myapp://…` opens: a link clicked in a browser,
4
+ a chat message or a launcher reaches the **already-running** app, and its
5
+ window comes to the front.
6
+
7
+ The flow this exists for is logging in through the browser. The app opens the
8
+ system browser, the provider redirects to
9
+ `com.example.myapp://auth?code=…`, the desktop routes that URI back, and the
10
+ app finishes the login without asking anyone to paste anything.
11
+
12
+ ## Read this first: for a login, you probably want a loopback port
13
+
14
+ RFC 8252 §7.3 has a second answer that needs none of the machinery below —
15
+ redirect to `http://127.0.0.1:<port>/cb` and listen on it:
16
+
17
+ ```js
18
+ import { createServer } from 'node:http';
19
+
20
+ const server = createServer((req, res) => {
21
+ const url = new URL(req.url, 'http://127.0.0.1');
22
+ res.end('You can close this tab.');
23
+ finishLogin(url.searchParams.get('code'));
24
+ });
25
+ server.listen(0); // "Authorization servers MUST allow any port" — RFC 8252 §7.3
26
+ const port = server.address().port;
27
+ ```
28
+
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
32
+ cannot.**
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.
39
+
40
+ ## Four things have to be true
41
+
42
+ | | | where |
43
+ | ---------------- | ---------------------------------------------------------- | ----------------------- |
44
+ | **Registration** | a `.desktop` entry claiming `x-scheme-handler/…` | an install step |
45
+ | **Dispatch** | the URI reaching the running process | `registerApplication()` |
46
+ | **Delivery** | `org.freedesktop.Application.Open`, answered before the UI | `useAppOpen()` |
47
+ | **The raise** | `_NET_ACTIVE_WINDOW`, with the right timestamp | `activateWindow()` |
48
+
49
+ The last one is the one that decides whether a user calls the feature working;
50
+ see [the raise is the part that fails](#the-raise-is-the-part-that-fails).
51
+
52
+ ## The code
53
+
54
+ ```js
55
+ import { createRoot, registerApplication } from 'react-x11';
56
+
57
+ const app = await registerApplication({
58
+ appId: 'com.example.myapp',
59
+ schemes: ['com.example.myapp'],
60
+ });
61
+ if (app?.role === 'secondary') process.exit(0); // we do not exit for you
62
+
63
+ const root = await createRoot();
64
+ root.render(<App />);
65
+ ```
66
+
67
+ ```jsx
68
+ import { activateWindow, useAppOpen } from 'react-x11';
69
+
70
+ function App() {
71
+ const win = useRef(null);
72
+ useAppOpen((uris, ctx) => {
73
+ activateWindow(win, { timestamp: ctx.timestamp });
74
+ const code = new URL(uris[0]).searchParams.get('code');
75
+ finishLogin(code);
76
+ });
77
+ return <window ref={win}>…</window>;
78
+ }
79
+ ```
80
+
81
+ That is the whole runtime half. What follows is why each line is shaped the
82
+ way it is.
83
+
84
+ ### `registerApplication()`
85
+
86
+ ```ts
87
+ function registerApplication(options: {
88
+ appId: string;
89
+ schemes?: string[];
90
+ onOpen?: (uris: string[], ctx: LaunchContext) => void;
91
+ onActivate?: (ctx: LaunchContext) => void;
92
+ onAction?: (name: string, params: unknown[], ctx: LaunchContext) => void;
93
+ argv?: string[];
94
+ }): Promise<AppRegistration | null>;
95
+
96
+ interface AppRegistration {
97
+ readonly role: 'primary' | 'secondary';
98
+ readonly appId: string;
99
+ readonly objectPath: string;
100
+ release(): Promise<void>;
101
+ }
102
+ ```
103
+
104
+ **Call it before `createRoot()`.** On the D-Bus path the bus _started this
105
+ process_ because someone called `Open`, and that call is outstanding while the
106
+ app boots — registering from inside a component answers it with an
107
+ unknown-method error, and the launch is lost.
108
+
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.
116
+
117
+ **`null` means there is no session bus** — ssh, a bare `startx`, a container,
118
+ CI, Node 20 without the transport. The app runs as an ordinary single-window
119
+ program. A URI that arrived in `argv` is still delivered to `useAppOpen`: a
120
+ cold-start deep link needs no bus, only a second instance does.
121
+
122
+ **A malformed `appId` or an unusable scheme throws**, and that is not a hole in
123
+ [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.
126
+
127
+ ### Two dispatch paths, and why `role` exists
128
+
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:
145
+
146
+ | `role` | |
147
+ | ------------- | ------------------------------------------------------------------ |
148
+ | `'primary'` | this process owns the name and will receive `Open` |
149
+ | `'secondary'` | another instance owns it; this launch was forwarded — **exit now** |
150
+
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.
153
+
154
+ 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.
158
+
159
+ ### Delivery: buffered, and replayed
160
+
161
+ `Open` is answered on the turn it arrives — never when the UI is ready. The
162
+ URIs are buffered and replayed to the **first handler that attaches**, so an
163
+ app whose React tree mounts a second later loses nothing, and an app whose
164
+ first frame is slow does not creep towards the caller's 25-second timeout.
165
+
166
+ Replay happens once. A second subscriber does not get the same login again.
167
+
168
+ ```ts
169
+ function useAppOpen(
170
+ handler: (uris: string[], ctx: LaunchContext) => void,
171
+ ): void;
172
+ function useAppActivate(handler: (ctx: LaunchContext) => void): void;
173
+
174
+ // the same two, for code that has no component to hang off
175
+ function onAppOpen(handler): () => void;
176
+ function onAppActivate(handler): () => void;
177
+
178
+ interface LaunchContext {
179
+ readonly platformData: Readonly<Record<string, unknown>>;
180
+ readonly timestamp: number | null;
181
+ readonly startupId: string | null;
182
+ readonly activationToken: string | null;
183
+ }
184
+ ```
185
+
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.
189
+
190
+ `schemes` filters what arrives: a URI whose scheme you did not register is
191
+ 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.
194
+ Declaring no schemes at all filters nothing.
195
+
196
+ ### The raise is the part that fails
197
+
198
+ ```ts
199
+ function activateWindow(
200
+ target?: NtkWindow | DrawnNode | RefObject<…> | number | null,
201
+ options?: { timestamp?: number | null; source?: 1 | 2 },
202
+ ): boolean;
203
+ ```
204
+
205
+ Sends `_NET_ACTIVE_WINDOW` to the root window: source indication, timestamp,
206
+ and this app's currently active toplevel. `target` is anything
207
+ [`windowIdOf()`](elements.md) accepts, or nothing — in which case this app's
208
+ only top-level window is used, its focused one when it has several.
209
+
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.
217
+
218
+ | `timestamp` | |
219
+ | --------------- | ------------------------------------------------------------------------------------------------------------- |
220
+ | omitted | the last input this app saw — EWMH's own definition of the field |
221
+ | `ctx.timestamp` | **what a deep link should pass**: the click in the browser is the user action, not whatever this app last saw |
222
+ | `null` | `CurrentTime`; legal, and most window managers will decline it |
223
+
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.
228
+
229
+ ## Registration: the half that is not runtime code
230
+
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.
236
+
237
+ `~/.local/share/applications/com.example.myapp.desktop` — the basename is tied
238
+ to the bus name by the spec:
239
+
240
+ ```ini
241
+ [Desktop Entry]
242
+ Type=Application
243
+ Name=My App
244
+ Exec=my-app %u
245
+ Icon=com.example.myapp
246
+ DBusActivatable=true
247
+ StartupWMClass=com.example.myapp
248
+ MimeType=x-scheme-handler/com.example.myapp;
249
+ ```
250
+
251
+ `StartupWMClass` must match the `appId` prop on your `<window>`, or the
252
+ window manager cannot match the window that appears to the launch that asked
253
+ for it.
254
+
255
+ Then tell the MIME database it changed:
256
+
257
+ ```bash
258
+ update-desktop-database ~/.local/share/applications
259
+ ```
260
+
261
+ And, so that the bus can _start_ the app when it is not running,
262
+ `~/.local/share/dbus-1/services/com.example.myapp.service`:
263
+
264
+ ```ini
265
+ [D-BUS Service]
266
+ Name=com.example.myapp
267
+ Exec=/usr/bin/my-app
268
+ ```
269
+
270
+ ### Checking it by hand
271
+
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
+ ```bash
277
+ gio open com.example.myapp://test # path A: D-Bus activation
278
+ ```
279
+
280
+ ```bash
281
+ env -u XDG_CURRENT_DESKTOP xdg-open com.example.myapp://test # path B: Exec %u
282
+ ```
283
+
284
+ Run each twice: once with the app closed (it should start and handle the link)
285
+ and once with it running (it should come to the front, and no second window
286
+ should appear). `REACT_X11_DEBUG_URI=1` narrates what this module decided —
287
+ with every URI's query and fragment stripped, so the output is safe to paste
288
+ into an issue.
289
+
290
+ ## Security
291
+
292
+ The URI is attacker-controlled input from an unauthenticated local caller.
293
+
294
+ - **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.
300
+ - **`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.
304
+ - **`platform_data` is spoofable.** A hostile local process can fabricate a
305
+ `desktop-startup-id` and make your app raise itself. Same uid, so it is focus
306
+ theft rather than data loss — but treat `ctx.timestamp` as a hint, not a
307
+ credential.
308
+ - **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.
312
+ - **Never log the URI.** react-x11 does not: everything on
313
+ `REACT_X11_DEBUG_URI` goes through a redaction that drops the query, the
314
+ fragment and any userinfo. Hold your own logging to the same rule — the code
315
+ is _in_ the URI.
316
+
317
+ ## On macOS
318
+
319
+ 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`:
323
+
324
+ | | |
325
+ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
326
+ | `kAEGetURL` — a URL for a scheme the bundle claims | `useAppOpen(uris, ctx)`, filtered by the registration's `schemes` like an `Open` |
327
+ | `kAEOpenDocuments` — a document from the Finder | `useAppOpen`, as `file://` URLs (`file:` is always allowed) |
328
+ | `kAEReopenApplication` — a Dock click, `open -a` | `useAppActivate(ctx)`, with `ctx.platformData['has-visible-windows']` |
329
+ | `kAEQuitApplication` — Dock › Quit, ⌘Q, a logout | the **primary window's close request**: `onCloseRequest` where there is one, else the default, which closes the app |
330
+
331
+ Three things differ from the bus, and none of them changes app code:
332
+
333
+ - **Single instance is the platform default.** Launch Services activates the
334
+ running app rather than starting a second one, so there is no `secondary`
335
+ role to exit from; `registerApplication()` resolves `null` on a Mac with no
336
+ D-Bus (the normal Mac) and its `schemes` still filter what arrives.
337
+ - **`ctx.timestamp` is `null`.** Apple Events carry no startup id, and the
338
+ raise does not need one: `activateWindow()` there is `NSApp.activate`, which
339
+ no focus-stealing rule weighs.
340
+ - **Registration is `Info.plist`, not a `.desktop` file** — `CFBundleURLTypes`
341
+ for the scheme and `CFBundleDocumentTypes` for documents, in an app bundle
342
+ (see [packaging.md](packaging.md)). A bare `node` process receives nothing
343
+ from Launch Services, because there is no bundle for it to look up.
344
+
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
351
+ (`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.
355
+
356
+ ## Over ssh
357
+
358
+ Both mechanisms only work when the browser runs on the **same host as the
359
+ 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.
362
+
363
+ ## Deliberately not here
364
+
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.