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,648 @@
1
+ # Desktop integration
2
+
3
+ What an app has to tell the desktop about itself, beyond drawing. Today that
4
+ is startup notification, which is on by default and has nothing to call; the
5
+ launcher's view of the app — a badge on its icon, a bounce for attention, the
6
+ Dock's menu; what a password field has to do to be reachable by the desktop's
7
+ password managers; and the one switch that turns off everything here that
8
+ talks over D-Bus.
9
+
10
+ The menu bar is the other half of this and has a page of its own —
11
+ [globalmenu.md](globalmenu.md) — because on a desktop that shows application
12
+ menus in its panel, `MenuBar` hands the menu over and stops drawing. Same
13
+ default shape as startup notification: on with no configuration, and one
14
+ switch to turn it off.
15
+
16
+ Everything on this page is the app talking _outwards_. The one thing that
17
+ comes the other way — the desktop handing the app a `myapp://…` link to open,
18
+ and the single-instance question that goes with it — is
19
+ [uri-schemes.md](uri-schemes.md). It shares the launch timestamp described
20
+ below: `ctx.timestamp` there is the same `_TIME` suffix `launchTimestamp()`
21
+ parses here, and for the same reason.
22
+
23
+ ## Startup notification
24
+
25
+ When a launcher spawns an app it opens a **startup sequence** — that is what
26
+ the busy cursor is — and closes it when the app says it is up. An app that
27
+ never says so leaves the sequence running until the desktop gives up on it:
28
+ mutter's `STARTUP_TIMEOUT_MS` is 15 seconds, long after the window is on
29
+ screen and being clicked. The mutter source is blunt about what that looks
30
+ like, in a comment on the constant itself: people assume the launch failed
31
+ and start it again.
32
+
33
+ Two other things ride on the same handshake:
34
+
35
+ - **Focus.** `_NET_WM_USER_TIME` is the evidence focus-stealing prevention
36
+ weighs when deciding whether a new window may come to the front. With
37
+ none, a strict desktop opens the window behind whatever the user was
38
+ doing — correctly, from its point of view, since nothing said a user asked
39
+ for this.
40
+ - **Placement.** The sequence records which workspace the launch happened
41
+ on. Without one the window goes wherever new windows go.
42
+
43
+ react-x11 does all of it with no configuration:
44
+
45
+ ```jsx
46
+ const root = await createRoot(); // that is the whole of it
47
+ ```
48
+
49
+ `DESKTOP_STARTUP_ID` is read from the environment and **removed from it**.
50
+ That is deliberate and it matters: the variable names one launch, so a child
51
+ process that inherits it would claim a sequence that is not its own and end
52
+ it early — the parent's cursor stops when the child starts. Every toolkit
53
+ that gets this wrong produces that same bug.
54
+
55
+ With no id in the environment — a terminal, CI, XQuartz — nothing is set and
56
+ nothing is sent.
57
+
58
+ ### When is an app "started"?
59
+
60
+ The default is **the first frame that actually painted**.
61
+
62
+ GTK ends the sequence when the toplevel maps, and copying that here would be
63
+ subtly wrong. This renderer does not paint on map: `invalidate()` schedules
64
+ through the frame clock and the drawing lands in `flush()` a frame later. So
65
+ a mapped window is an empty rectangle, and stopping the busy cursor there is
66
+ compliant and dishonest — it says "ready" over a blank window.
67
+
68
+ A suspense fallback counts as painted, and should. If the first frame is a
69
+ spinner because the tree is waiting on data, that is exactly the right
70
+ moment to stop the _system's_ spinner: the app is up and is telling the user
71
+ what it is doing. There is no signal for "finished loading", and guessing at
72
+ one is how this ends back at fifteen seconds.
73
+
74
+ `completeOn` is there for apps the default does not suit:
75
+
76
+ ```jsx
77
+ await createRoot({ startupNotification: { completeOn: 'map' } });
78
+ ```
79
+
80
+ | `completeOn` | |
81
+ | ------------ | ------------------------------------------------------- |
82
+ | `'paint'` | the first frame that drew (default) |
83
+ | `'map'` | the first toplevel mapping — earlier, and what GTK does |
84
+ | `'manual'` | nothing automatic; call `notifyStartupComplete()` |
85
+
86
+ `'map'` suits an app whose first frame is expensive enough that it would
87
+ rather the cursor stopped before it. `'manual'` suits one that is not up
88
+ until it says so — restoring a session behind a splash, say:
89
+
90
+ ```jsx
91
+ import { notifyStartupComplete } from 'react-x11';
92
+
93
+ await createRoot({ startupNotification: { completeOn: 'manual' } });
94
+ await restoreSession();
95
+ notifyStartupComplete(); // idempotent, and a no-op if there is no sequence
96
+ ```
97
+
98
+ **A backstop ends the sequence regardless**, ten seconds after the window
99
+ maps, whichever mode is in force. An app that never paints, or that forgets
100
+ to call, cannot leave the cursor spinning — which would be this feature
101
+ reproducing the bug it exists to fix. Ten is chosen to beat mutter's fifteen
102
+ by a margin while being far longer than any honest first frame.
103
+
104
+ ### Turning it off, and supplying an id
105
+
106
+ ```jsx
107
+ await createRoot({ startupNotification: false });
108
+ await createRoot({ startupNotification: 'launcher/app/1-0_TIME9876' });
109
+ ```
110
+
111
+ `false` is for an app that runs its own sequence, or an embedder that owns
112
+ the toplevel. Note that opting out leaves `DESKTOP_STARTUP_ID` in the
113
+ environment — if you are managing the sequence yourself, the id is yours to
114
+ read and yours to clear.
115
+
116
+ A string supplies the id for a launch where it did not arrive in the
117
+ environment. A D-Bus-activated app gets it in `platform_data` instead.
118
+
119
+ ### `launchTimestamp()`
120
+
121
+ ```jsx
122
+ import { launchTimestamp } from 'react-x11';
123
+
124
+ const when = launchTimestamp(); // number | null
125
+ ```
126
+
127
+ The X server timestamp of the user action that launched the app, parsed from
128
+ the id's `_TIME` suffix. **`null` is a real answer**, not a failure: an app
129
+ started from a shell has no launch timestamp and never will.
130
+
131
+ It is the "when" that any later request to come forward is weighed against.
132
+ Do not substitute `0` for a missing one — EWMH gives zero its own meaning,
133
+ "do not focus this window when it maps".
134
+
135
+ ### On macOS
136
+
137
+ quartz-wm implements none of this, so on XQuartz the messages go to a root
138
+ window nobody is listening at and `_NET_WM_USER_TIME` is ignored. Harmless,
139
+ and worth knowing before concluding from a Mac that the feature is broken.
140
+
141
+ ### What is deliberately not here
142
+
143
+ - **The launcher half.** An app that spawns _another_ app should generate an
144
+ id, send `new:`, put `DESKTOP_STARTUP_ID` in the child's environment and
145
+ close the sequence itself. Same encoder, different persona.
146
+ - **Ongoing `_NET_WM_USER_TIME` maintenance.** Setting it once at launch is
147
+ this. Keeping it current on every keypress is a separate design with a
148
+ real cost — EWMH is explicit that storing a frequently-changing property
149
+ on the toplevel wakes every client watching that window, which is what
150
+ `_NET_WM_USER_TIME_WINDOW` exists to avoid.
151
+
152
+ ### Security
153
+
154
+ The id is broadcast to the root window, so every client on the display sees
155
+ it. On X11 that is the pre-existing no-isolation story rather than a new
156
+ exposure — see [security.md](security.md) — but it is the reason the
157
+ messages carry the id the launcher gave us and nothing invented, and the
158
+ reason the variable's value is never logged.
159
+
160
+ ## The launcher: a badge, attention, and the Dock menu
161
+
162
+ The one thing an app says to the desktop that the user reads without opening
163
+ it is the mark on its icon — an unread count on the Dock tile, a dot on the
164
+ taskbar entry. What lives here is on a backend only where that backend has a
165
+ mechanism for it:
166
+
167
+ | | Linux (`launcherentry`) | macOS (`cocoa`) | Windows (`taskbar`) |
168
+ | --------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
169
+ | a **badge** | `com.canonical.Unity.LauncherEntry` — KDE, elementary, Cairo-Dock listen; GNOME needs an extension | `NSDockTile.badgeLabel` | an overlay icon with the label drawn into it |
170
+ | badge **text** | — the protocol carries a count and nothing else | any label | up to three glyphs; longer clamps to `99+`, and it is ~16px on screen |
171
+ | **progress** | `useProgress(0…1)` — a bar across the tile | — `NSDockTile` has none | `ITaskbarList3::SetProgressValue`, across the taskbar button |
172
+ | **attention** | `states={['demands_attention']}`, or `setUrgent()` for the launcher entry itself | the same prop: the Dock icon bounces until activated | `FlashWindowEx` |
173
+ | the **launcher menu** | `useLauncherMenu(items)` — the launcher protocol's quicklist | `useLauncherMenu(items)` | — the taskbar button's menu is the jump list, whose entries start a _new_ process, so it is `features.tasks` and `useJumpList` rather than this ([windows-integrations.md](windows-integrations.md)) |
174
+ | **tasks** | — `Actions=` in the `.desktop` file, not driven from here | — | `useJumpList(tasks)` — the jump list's Tasks category |
175
+ | a **hover toolbar** | — | — | `useThumbnailToolbar(buttons)` — up to seven, under the preview |
176
+ | **recent documents** | — | — the platform has one; this backend does not use it | `useRecentDocument(path)` — the shell's Recent lists |
177
+
178
+ ### The badge
179
+
180
+ ```jsx
181
+ import { setBadge, useBadge } from 'react-x11';
182
+
183
+ useBadge(unread); // a count: 3 shows "3", 0 clears
184
+ await setBadge(null); // the imperative twin, for code with no component
185
+ ```
186
+
187
+ A badge is a **count**, because that is the one shape both desktops agree
188
+ on: a number shows on both. A string (`'•'`, `'!'`) shows on macOS, where the
189
+ tile takes any label, and is a visible count of nothing on Linux, where the
190
+ protocol has no text field — pass one only where the Mac is the audience.
191
+ `0`, `null` and `''` all clear it. `useBadge` clears on unmount; `setBadge`
192
+ resolves to whether a launcher was told and never rejects for anything about
193
+ the machine.
194
+
195
+ On Linux the count travels as one D-Bus signal, `LauncherEntry.Update`,
196
+ attributed to the app by `application://<appId>.desktop` — so it needs the
197
+ identity [`registerApplication({ appId })`](uri-schemes.md) establishes and
198
+ a `.desktop` file of that name, or the launcher has nothing to pin the
199
+ count to and `setBadge` resolves `false`. The entry stays on the bus while
200
+ a badge is shown and is released when it is cleared. A launcher that starts
201
+ after the badge was set will not see it until the next `setBadge`; the
202
+ protocol has no query.
203
+
204
+ ### Progress
205
+
206
+ ```jsx
207
+ import { useProgress, setProgress } from 'react-x11';
208
+
209
+ useProgress(downloaded / total); // 0…1; null or undefined clears it
210
+ await setProgress(null); // the imperative twin
211
+ ```
212
+
213
+ A bar drawn across the app's icon, for a download, an export, a long build.
214
+ Out-of-range values are clamped rather than refused — a caller dividing by a
215
+ total that briefly went to zero should not get an exception.
216
+
217
+ **Linux launchers only.** `NSDockTile` has no progress bar; an app that wants
218
+ one on a Mac draws it into a custom tile view, which is app-side art rather
219
+ than a call. `useDesktopCapability('launcher').features.progress` is the
220
+ honest answer to branch on.
221
+
222
+ ### Attention
223
+
224
+ Nothing new to call: `<window states={['demands_attention']}>` is the
225
+ existing request, and on the cocoa backend it is
226
+ `NSApp.requestUserAttention` — the Dock icon bounces until the user
227
+ activates the app, and stops when the prop drops the state or the window
228
+ goes away. AppKit ignores the request while the app is already active, which
229
+ is the same outcome a window manager gives a focused window's urgency hint.
230
+ `fullscreen`, `maximized` and `hidden` are AppKit's full-screen Space, zoom
231
+ and miniaturize there (@windowkit/appkit 0.30.0), and the other `states`
232
+ names are inert on that backend.
233
+
234
+ ### The Dock menu
235
+
236
+ ```jsx
237
+ useLauncherMenu([
238
+ { label: 'New Window', onSelect: openWindow },
239
+ { type: 'separator' },
240
+ {
241
+ label: 'Recent',
242
+ items: recent.map((r) => ({ label: r.name, onSelect: () => open(r) })),
243
+ },
244
+ ]);
245
+ ```
246
+
247
+ The same `items` vocabulary `MenuBar` and `ContextMenu` take ([menuitem
248
+ names](globalmenu.md)), so one authoring model covers the window's menu, the
249
+ panel's, the tray's and the Dock's. Installed while the component is mounted,
250
+ replaced when `items` changes, taken down on unmount.
251
+
252
+ `NSDockTile`'s menu on the cocoa backend; on Linux it is the launcher
253
+ protocol's **quicklist** — an object path to a `com.canonical.dbusmenu` tree,
254
+ which is the same menu protocol the tray and the global menu already speak.
255
+ ubuntu-dock/Dash-to-Dock, Plank and the Unity-heritage launchers render it.
256
+
257
+ > An earlier version of this page said the freedesktop counterpart was
258
+ > `Actions=` in the `.desktop` file, an install step rather than runtime code.
259
+ > That is a different feature wearing a similar hat: desktop actions are the
260
+ > right place for entries that must work **while the app is not running**, and
261
+ > the quicklist is the right place for a menu that follows the app's state.
262
+
263
+ Like the badge, it needs the identity
264
+ [`registerApplication({ appId })`](uri-schemes.md) establishes and a
265
+ `.desktop` file of that name for a launcher to attach it to.
266
+
267
+ ### The app's presence
268
+
269
+ Two things the Dock and the ⌘-Tab switcher show that a bare `node` process
270
+ gets wrong, as root options on the cocoa backend:
271
+
272
+ ```js
273
+ await createRoot({
274
+ cocoa: {
275
+ appName: 'Notes', // what the Dock, ⌘-Tab and the app menu print (was "node")
276
+ activationPolicy: 'regular', // 'accessory' for a menu-bar app: no tile, no ⌘-Tab entry
277
+ },
278
+ });
279
+ ```
280
+
281
+ `activationPolicy` is fixed before the app finishes launching — a Regular
282
+ launch registers a Dock tile, and an agent app that switched afterwards would
283
+ already have flashed its icon — so it is a root option rather than a hook.
284
+ `appName` renames LaunchServices' record of an unbundled process; a bundle's
285
+ `Info.plist` wins, as it should.
286
+
287
+ ## The tray
288
+
289
+ ```jsx
290
+ import { useTray } from 'react-x11';
291
+
292
+ const { available, backend, features, error } = useTray({
293
+ icon: 'mail-unread', // a themed icon name (an SF Symbol on macOS), or PNG bytes
294
+ tooltip: 'Notifications',
295
+ menu: [
296
+ { label: 'Open', onSelect: open },
297
+ { type: 'separator' },
298
+ { label: 'Quit', onSelect: quit },
299
+ ],
300
+ });
301
+ ```
302
+
303
+ An icon in the system tray for as long as the component is mounted. With
304
+ `menu` a click opens it — the same `items` vocabulary as `MenuBar` and the
305
+ Dock menu, so the three menus an app puts on the desktop are one authoring
306
+ model. Without one, `onClick` gets the button, where the click was and, where
307
+ the backend knows it, the item's screen rect — all in logical screen pixels,
308
+ the unit a `<popup>`'s `x`/`y` are in, and what a popover under the item
309
+ anchors to ([elements.md](elements.md#a-popover-under-a-tray-item)). `title` shows text beside the icon or alone;
310
+ `visible`, `tooltip`, `attention` and the rest follow their values while
311
+ mounted, and the item is removed on unmount. `null` means no item.
312
+
313
+ Several trays coexist: each is its own item.
314
+
315
+ | | Linux (`statusnotifier`) | macOS (`cocoa`) | Windows (`shellnotifyicon`) |
316
+ | --------------- | --------------------------------------- | ---------------------------- | --------------------------- |
317
+ | mechanism | `org.kde.StatusNotifierItem` over D-Bus | `NSStatusItem` | `Shell_NotifyIcon` |
318
+ | menu | `com.canonical.dbusmenu` | `NSMenu` | a tracked popup menu |
319
+ | icon by name | the desktop's icon theme | SF Symbols | — no icon theme to name |
320
+ | icon by bytes | ARGB32 pixmap | a template image | an `HICON` from RGBA |
321
+ | title | text beside the icon | text beside the icon | — an icon and a tooltip |
322
+ | attention | `Status = NeedsAttention` | — | — |
323
+ | click rect | — | the item's frame | — |
324
+ | click modifiers | — | shift/control/option/command | — |
325
+ | scroll | `Scroll(delta, orientation)` | — | — |
326
+
327
+ **On Linux** this is StatusNotifierItem, which is D-Bus only — so it works
328
+ identically under X11, XWayland and Wayland. It needs something _hosting_ a
329
+ tray: KDE Plasma and most panels do out of the box, and GNOME needs an
330
+ AppIndicator extension. A stock GNOME session with no extension has no tray,
331
+ `available` is `false`, and nothing is logged, because that is a
332
+ configuration rather than a fault.
333
+
334
+ **A click's position on Linux** is a point whose unit the protocol never
335
+ names, and hosts differ. Plasma sends device pixels, and so does GNOME in its
336
+ physical layout, the default up to GNOME 49. GNOME's logical layout — the
337
+ default from 50, and on Fedora and Debian 13 before it — sends logical
338
+ pixels, as xfce4-panel, LXQt and Cinnamon do. So the point is read both
339
+ ways: a reading that lands on no monitor is dropped, then the one the pointer
340
+ is on is kept, and otherwise the numbers are taken as sent. What that cannot
341
+ tell apart is a device-pixel host under XWayland whose tray, read as logical
342
+ pixels, still lands on a monitor — in the top-left quarter of the screen, or
343
+ with another monitor beyond it — and there the point is `scale` times as far
344
+ from the screen's origin as the click was.
345
+
346
+ **On the cocoa backend** this is `NSStatusItem`, the menu-bar extra. A
347
+ menu-bar app that wants no Dock tile pairs it with
348
+ `createRoot({ cocoa: { activationPolicy: 'accessory' } })` above. A PNG icon
349
+ is drawn as a **template** — its shape in the bar's ink, so it follows light
350
+ and dark the way a symbol does; `template: false` keeps its colours. AppKit
351
+ is handed a new `NSMenu` only when the menu's items change, so a title that
352
+ ticks every second leaves the menu alone, whether `menu` is memoized or
353
+ written inline.
354
+
355
+ ### What the hook hands back
356
+
357
+ `available` **settles**. Finding a freedesktop tray takes a bus round trip, so
358
+ it is `false` on the first frame and `true` a tick later if a host is there —
359
+ render the fallback first and upgrade, never the other way round. It follows
360
+ the host for the life of the item, so a panel that exits flips it back.
361
+
362
+ **`settled` says whether `available` is an answer yet.** For a feature inside
363
+ a window the order above is all an app needs. For an app whose whole UI is its
364
+ tray item, the fallback _is_ a window, and rendering it first flashes a window
365
+ up and away on every start on a desktop that has a tray. So wait for the
366
+ answer instead:
367
+
368
+ ```jsx
369
+ const tray = useTray({ icon: 'waveform', onClick: open });
370
+ if (!tray.settled) return null; // a bus round trip, not a timeout
371
+ if (!tray.available) return <window title="Hush">{controls}</window>;
372
+ return popover;
373
+ ```
374
+
375
+ It is `true` on the first frame on macOS, where the status item is made there
376
+ and then, and for `useTray(null)`; on the freedesktop tray it turns `true` once
377
+ the host has taken or refused the item, and stays so.
378
+
379
+ `features` is the same vocabulary [feature discovery](#feature-discovery)
380
+ uses, for the rung that answered. Read it instead of testing the platform:
381
+
382
+ ```jsx
383
+ // Right: ask about the thing you need.
384
+ {
385
+ features.clickModifiers && <ShiftClickHint />;
386
+ }
387
+ ```
388
+
389
+ `error` is the fourth field because **"this desktop has no tray" and "there is
390
+ a tray and it refused me" are different facts**, and only one of them has a
391
+ fix. A missing tray leaves `error` null; a host that rejected the registration
392
+ puts the reason there.
393
+
394
+ ## Feature discovery
395
+
396
+ Everything on this page is a _ladder_, and which rung a machine lands on
397
+ changes what the app may do. `available` alone is not enough to branch on,
398
+ because it is true on machines that mean very different things by it — a
399
+ notification daemon with `actions` gives you a banner with buttons that call
400
+ back, and `notify-send` gives you one-way text with an urgency and an icon.
401
+ An app that posts reply buttons on the second one gets a banner with no
402
+ buttons and no callback, silently.
403
+
404
+ So the unit of discovery is a **feature set**, not a flag:
405
+
406
+ ```jsx
407
+ import { useDesktopCapability } from 'react-x11';
408
+
409
+ const notifications = useDesktopCapability('notifications');
410
+
411
+ if (notifications.features.actions) return <ReplyFromBanner />;
412
+ if (notifications.available) return <BannerThenOpenApp />;
413
+ return <InAppToastOnly />;
414
+ ```
415
+
416
+ `{ available, backend, features, settled }`. `backend` names the **mechanism** —
417
+ `'dbus'`, `'cocoa'`, `'statusnotifier'`, `'launcherentry'`, `'notify-send'`,
418
+ `'osascript'`, `'win32'`, `'shellnotifyicon'`, `'taskbar'` — and never the
419
+ platform, so that a second Linux mechanism does not need a second name for
420
+ Linux.
421
+
422
+ A backend **declares** its mechanism rather than having one inferred from it,
423
+ because backends install the same method names for the same rung on purpose —
424
+ that sharing is what makes `useTray` one hook — so there is nothing in the
425
+ methods to tell two of them apart. Branch on `features`; `backend` is for logs
426
+ and bug reports.
427
+
428
+ `settled` is `false` until the first probe answers and `true` from then on, so
429
+ an app can hold its fallback back rather than flash it; it is `true` on the
430
+ first frame for the answers that need nothing asked, the tray and the Dock tile
431
+ on macOS.
432
+
433
+ Capability names are `'notifications'`, `'tray'` and `'launcher'`.
434
+ `desktopCapability(name)` is the imperative twin for code with no component.
435
+
436
+ ### Which one to ask
437
+
438
+ A hook that _does_ the thing already knows whether it worked, and that
439
+ measurement beats any prediction:
440
+
441
+ - **`useTray(options)`** — "did my icon get taken?" It tried; it knows.
442
+ - **`useDesktopCapability('tray')`** — "would one be taken if I asked?" This
443
+ is what a settings screen needs: a "Show tray icon" checkbox has to render
444
+ correctly _without_ putting an icon in the tray to find out.
445
+
446
+ Prefer the feature hook wherever the feature is actually mounted. Both read
447
+ the same probe, so they cannot disagree.
448
+
449
+ ### Degrading well
450
+
451
+ Three rules, in the order they matter:
452
+
453
+ 1. **Render the fallback first.** Every desktop answer takes a round trip, so
454
+ the first frame has none. Upgrading a fallback is invisible; downgrading a
455
+ feature the user already reached for is not.
456
+ 2. **Never disable a feature permanently on a `false`.** A panel restarts, an
457
+ extension is enabled, a daemon is installed. The hooks re-probe on
458
+ `NameOwnerChanged` and nothing is cached — an app that latched the first
459
+ answer would outlive every one of those fixes.
460
+ 3. **A missing desktop service is not an error.** No bus, no tray host, no
461
+ notification daemon and no launcher are all ordinary states of a healthy
462
+ machine. Nothing is logged and nothing throws; the feature is simply not
463
+ there. Reserve `error` for a service that answered and then refused.
464
+
465
+ ### All of it, in one app
466
+
467
+ `examples/desktop.jsx` (`npm run examples:desktop`) is a backup agent that
468
+ uses every hook on this page at once — the tray icon with its attention state
469
+ and its overlay, the badge, the progress bar, the quicklist, the urgency flag,
470
+ and a banner whose buttons are offered only where the daemon has `actions` —
471
+ and whose second pane renders `useDesktopCapability()` for each of
472
+ `CAPABILITIES` beside the live answers, so the prediction and the measurement
473
+ are on screen together. It is the one to read when the question is what an app
474
+ will get on a machine you do not have. The three feature-sized examples are
475
+ `badge.jsx` (the launcher), `tray.jsx` (the tray) and `notify.jsx` (the
476
+ notification).
477
+
478
+ ## Turning the desktop off
479
+
480
+ Three things react-x11 turns on for you reach the **session bus**, and none
481
+ of them was asked for:
482
+
483
+ | | | |
484
+ | ------------ | ----------------------------------------------------------------------- | ------------------------------------ |
485
+ | `appearance` | following the desktop's light/dark, accent, contrast and reduced motion | [appearance.md](appearance.md) |
486
+ | `a11y` | the AT-SPI bridge — whether a screen reader can see the app | [accessibility.md](accessibility.md) |
487
+ | `globalMenu` | a `MenuBar` handing its menu to the panel instead of drawing it | [globalmenu.md](globalmenu.md) |
488
+
489
+ Each is the right default: an app that follows the desktop looks like it
490
+ belongs there, one a screen reader can read is one more person can use, and a
491
+ menu in the panel is what the panel is for. But sooner or later an embedder
492
+ owns the thing we assumed was ours, and until
493
+ [#417](https://github.com/sidorares/react-x11/issues/417) the only way out was
494
+ an **environment variable** — `NO_AT_BRIDGE=1`,
495
+ `REACT_X11_NO_GLOBAL_MENU=1`, or unsetting `DBUS_SESSION_BUS_ADDRESS`. That is
496
+ a seam an app cannot reach for itself: the environment is inherited, so
497
+ setting one before `createRoot()` sets it for every child process too, and the
498
+ D-Bus one takes the portals and the app's own exported services down with the
499
+ follower.
500
+
501
+ ```jsx
502
+ await createRoot({ desktop: false }); // none of the three
503
+ await createRoot({ desktop: { appearance: false } }); // just that one
504
+ ```
505
+
506
+ With all three off, **nothing dials the session bus at startup** — no
507
+ connection, no subprocess, no portal probe. That is the shape an embedder
508
+ wants, and a kiosk, and a test that needs the same answer on every machine.
509
+
510
+ Turning `appearance` off means the app draws in its own palette rather than
511
+ the desktop's: `useSystemAppearance()` reports `'no-preference'` with
512
+ `source: null`, and the remembered answer on disk is not read either — an
513
+ opted-out app that started in whatever colours the machine was in last time
514
+ would be the opposite of what the switch is for.
515
+
516
+ **Off is process-wide, and it latches.** There is one desktop, one D-Bus
517
+ identity and one AT-SPI bridge per process, so the policy cannot honestly be
518
+ per-root: a second root turning back on what the first turned off would be a
519
+ bug rather than a feature. Nothing turns an integration back on, and a
520
+ feature already started stays started — pass this on the first root.
521
+
522
+ ## Not blocking the first frame on the bus
523
+
524
+ Worth knowing because it is invisible when it works, and was worth ~150 ms
525
+ when it did not.
526
+
527
+ On macOS the session bus address is not in the environment; D-Bus advertises
528
+ it through **launchd**, and finding it means running `launchctl getenv`.
529
+ `dbus-native`'s entry point is synchronous, so it has to do that with
530
+ `spawnSync` — 120–150 ms of _blocked event loop_ on a cold page cache, landing
531
+ in front of the X handshake, the layout engine's WebAssembly and the first
532
+ paint, all so the accessibility bridge could find out there was nothing to
533
+ connect to.
534
+
535
+ Every dial now goes through one asynchronous, once-per-process lookup instead,
536
+ and hands `dbus-native` a resolved `unix:path=…`. The same wall clock is
537
+ spent, none of it on the loop: the handshake and the layout engine run
538
+ straight through it. There is nothing to configure — it is the same address,
539
+ found the same way, off the critical path.
540
+
541
+ Linux is unaffected: `$DBUS_SESSION_BUS_ADDRESS` or `$XDG_RUNTIME_DIR/bus`
542
+ answers without a subprocess, and always did.
543
+
544
+ ## Password fields and password managers
545
+
546
+ There is **no password-field protocol on the Linux desktop.** No toolkit
547
+ publishes "this is a password field, fill it", and no manager asks. What
548
+ exists instead are four seams, none of which a widget can opt into by
549
+ declaring itself — they are things an application either supports or does
550
+ not. `PasswordInput` supports the two that reach a field, and this is what
551
+ they turn out to be.
552
+
553
+ ### 1. Typing — XTEST auto-type, and the keymap race
554
+
555
+ The mechanism nearly every desktop manager uses is **auto-type**: KeePassXC
556
+ matches the focused window's **title** against its entries, then synthesises
557
+ the keystrokes. Its X11 backend is the whole of the story — `SendKeyEvent()`
558
+ sends `XTestFakeKeyEvent()`, and for a character the current layout cannot
559
+ type, `RemapKeycode()` writes the keysym into a spare keycode with
560
+ `XkbSetMap()` first, `XSync`ing before it types.
561
+
562
+ For us that is good news and one hazard:
563
+
564
+ - **A faked key is an ordinary key.** XTEST events are delivered by the
565
+ server through the normal event path, so `PasswordInput` — and every other
566
+ focusable node — cannot tell auto-type from a person, and nothing has to be
567
+ done to support it. An auto-type sequence of `{USERNAME}{TAB}{PASSWORD}
568
+ {ENTER}` walks a react-x11 form because Tab moves focus and Enter reaches
569
+ `onSubmit`.
570
+ - **The window needs a title worth matching.** Auto-type's default matching is
571
+ on the window title, so `<window title="…">` is the integration surface.
572
+ A window titled after the document with nothing identifying the app is one
573
+ a user cannot write an auto-type rule for; `appId` is worth setting too,
574
+ since a manager that grew a smarter matcher would read that.
575
+ - **The keymap race is real.** ntk refetches the mapping when the server sends
576
+ `MappingNotify`, which is what makes a remapped keycode decode correctly —
577
+ but the refetch is a round trip, and a manager that remaps, syncs and types
578
+ immediately can land its key before the reply does. The window is small and
579
+ only affects characters outside the user's layout, but a password of ASCII
580
+ is not the case that fails. Nothing here can close it; the fix belongs where
581
+ the map lives.
582
+
583
+ ### 2. Pasting — the clipboard, and the hint that keeps it out of history
584
+
585
+ The other path every manager offers is copy-to-clipboard, usually with a
586
+ countdown before it clears. That is ordinary `CLIPBOARD` interop
587
+ ([clipboard.md](clipboard.md)); `PasswordInput` takes Ctrl+V and
588
+ Shift+Insert, and strips control characters so a manager's trailing newline
589
+ does not end up inside the secret.
590
+
591
+ If your app ever puts a secret **on** the clipboard — this widget never does
592
+ — offer `x-kde-passwordManagerHint` with the value `secret` beside the text.
593
+ Klipper drops such an offer from its history, KDE Connect refuses to forward
594
+ it, and `wl-clipboard` marks the state sensitive. It is a convention rather
595
+ than a specification, and it is the only thing standing between a copied
596
+ password and a clipboard manager's on-disk history:
597
+
598
+ ```js
599
+ clipboard.write({ UTF8_STRING: secret, 'x-kde-passwordManagerHint': 'secret' });
600
+ ```
601
+
602
+ `PasswordInput` also never takes the **PRIMARY** selection, which a
603
+ `<textinput>` does on every selection: PRIMARY is pasted by a middle click in
604
+ any window on the display, and a secret does not belong in a selection that
605
+ can be spent by accident.
606
+
607
+ ### 3. Fetching it yourself — the Secret Service
608
+
609
+ When the app has an account of its own to unlock, the seam is not a field at
610
+ all: the **Secret Service** D-Bus API, `org.freedesktop.secrets`, which
611
+ gnome-keyring, KWallet and KeePassXC all implement, and which `libsecret`
612
+ speaks for C applications. react-x11 has no wrapper for it and does not need
613
+ one — `useSessionBus()` reaches it directly ([dbus.md](dbus.md)) — and the
614
+ shape is: search by attributes, unlock the collection if it is locked, read
615
+ the secret back. An app that does this shows no password field on most
616
+ launches, which is a better outcome than any field can offer.
617
+
618
+ Inside a sandbox that name is not there. Flatpak's answer is
619
+ `org.freedesktop.portal.Secret`, whose `RetrieveSecret` hands the app a
620
+ per-application master secret down a pipe; libsecret switches to it
621
+ automatically and encrypts a local store with it.
622
+
623
+ ### 4. Reading the field — AT-SPI, which does not mark it yet
624
+
625
+ The accessibility bus is the only channel through which an outside program
626
+ can see a field's _contents and role_ rather than guess at a window title:
627
+ AT-SPI2 gives a password entry the role `password text`, which is how a
628
+ screen reader knows to announce "bullet" instead of the character. Some
629
+ automation leans on the same tree. react-x11 does publish one
630
+ ([accessibility.md](accessibility.md)), but `PasswordInput` is a plain
631
+ `role="textbox"` in it and nothing maps a node to `password text`
632
+ (`src/a11y.js` declares the role and no node takes it), so this seam is still
633
+ closed — worth opening as a role in the shared model rather than as a
634
+ password-shaped hole beside it.
635
+
636
+ ### What a field can still do wrong
637
+
638
+ None of the above stops the field itself from leaking. What `PasswordInput`
639
+ does about that, and what it cannot:
640
+
641
+ - the masked value is **never laid out or drawn** — no glyphs of the secret
642
+ reach ntk's shaping cache or the X server, and the mask's width is measured
643
+ from one reference character;
644
+ - there is **no copy, no selection, no undo history**;
645
+ - the value is still a JavaScript string, and strings are immutable. It
646
+ cannot be zeroed, it lives until the garbage collector takes it, and every
647
+ intermediate value typed on the way lives alongside it. A design that needs
648
+ more than that needs the secret to never enter this process.