react-x11 2.51.1 → 2.53.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 (121) hide show
  1. package/README.md +94 -61
  2. package/docs/README.md +256 -0
  3. package/docs/accessibility.md +575 -0
  4. package/docs/appearance.md +317 -0
  5. package/docs/click-to-component.md +92 -0
  6. package/docs/clipboard.md +262 -0
  7. package/docs/components.md +1231 -0
  8. package/docs/dbus.md +205 -0
  9. package/docs/debugging.md +371 -0
  10. package/docs/desktop-calendar.md +247 -0
  11. package/docs/desktop.md +523 -0
  12. package/docs/devtools.md +133 -0
  13. package/docs/drag-and-drop.md +585 -0
  14. package/docs/ecosystem/animation.md +360 -0
  15. package/docs/ecosystem/data-fetching.md +186 -0
  16. package/docs/ecosystem/dev-tooling.md +393 -0
  17. package/docs/ecosystem/forms.md +277 -0
  18. package/docs/ecosystem/headless.md +194 -0
  19. package/docs/ecosystem/i18n.md +86 -0
  20. package/docs/ecosystem/icons.md +377 -0
  21. package/docs/ecosystem/layout.md +206 -0
  22. package/docs/ecosystem/routing.md +135 -0
  23. package/docs/ecosystem/state.md +344 -0
  24. package/docs/ecosystem/testing.md +474 -0
  25. package/docs/ecosystem/theming.md +494 -0
  26. package/docs/ecosystem.md +439 -0
  27. package/docs/elements.md +2079 -0
  28. package/docs/embedding.md +239 -0
  29. package/docs/events.md +742 -0
  30. package/docs/extending.md +1700 -0
  31. package/docs/eyedropper.md +164 -0
  32. package/docs/filedialog.md +222 -0
  33. package/docs/frame.md +436 -0
  34. package/docs/gl.md +173 -0
  35. package/docs/globalmenu.md +195 -0
  36. package/docs/macos.md +592 -0
  37. package/docs/notifications.md +150 -0
  38. package/docs/packaging.md +468 -0
  39. package/docs/permissions.md +143 -0
  40. package/docs/react-features.md +405 -0
  41. package/docs/remote.md +179 -0
  42. package/docs/scale.md +225 -0
  43. package/docs/security.md +132 -0
  44. package/docs/styling.md +1710 -0
  45. package/docs/system.md +519 -0
  46. package/docs/testing.md +494 -0
  47. package/docs/typescript.md +75 -0
  48. package/docs/uri-schemes.md +340 -0
  49. package/docs/wayland-backend.md +332 -0
  50. package/docs/windows-integrations.md +177 -0
  51. package/docs/windows.md +380 -0
  52. package/package.json +11 -3
  53. package/src/Reconciler.js +2 -2
  54. package/src/a11y.js +1 -1
  55. package/src/backend/context2d.js +3 -3
  56. package/src/bootstrap.js +1 -1
  57. package/src/bus.js +39 -26
  58. package/src/cli/create.d.ts +43 -0
  59. package/src/cli/create.js +305 -0
  60. package/src/cli/env.js +484 -0
  61. package/src/cli/index.js +29 -2
  62. package/src/cli/templates/AGENTS.template.md +195 -0
  63. package/src/cli/templates/DESIGN.template.md +269 -0
  64. package/src/cocoa/app.js +6 -6
  65. package/src/cocoa/bezels.js +1 -1
  66. package/src/cocoa/fonts.js +1 -1
  67. package/src/cocoa/globalmenu.js +1 -1
  68. package/src/cocoa/keymap.js +1 -1
  69. package/src/cocoa/panewindow.js +1 -1
  70. package/src/cocoa/player.js +1 -1
  71. package/src/cocoa/presenter.js +2 -2
  72. package/src/cocoa/promotion.js +1 -1
  73. package/src/cocoa/relaunch.js +1 -1
  74. package/src/cocoa/threaded.js +1 -1
  75. package/src/cocoa/video.js +1 -1
  76. package/src/cocoa/window.js +4 -4
  77. package/src/components/Menu.js +7 -2
  78. package/src/components/Switch.js +1 -1
  79. package/src/components/native.js +1 -1
  80. package/src/desktopintegration.js +2 -1
  81. package/src/desktopsettings.js +193 -24
  82. package/src/dnd.js +2 -2
  83. package/src/filedialog.js +3 -3
  84. package/src/glnodes.js +1 -1
  85. package/src/grid.js +1 -1
  86. package/src/keyboardstate.js +28 -0
  87. package/src/layouts.js +1 -1
  88. package/src/node.d.ts +10 -10
  89. package/src/nodes/animation.js +1 -1
  90. package/src/nodes/invalidate.js +1 -1
  91. package/src/nodes/layouthost.js +1 -1
  92. package/src/nodes/video.js +19 -8
  93. package/src/nodes/window/droptarget.js +1 -1
  94. package/src/pacing.js +1 -1
  95. package/src/portal.js +31 -0
  96. package/src/refresh/index.js +2 -1
  97. package/src/screencolor.js +5 -10
  98. package/src/types/system.d.ts +8 -6
  99. package/src/types/video.d.ts +5 -2
  100. package/src/videobackend.js +28 -0
  101. package/src/videoframes.js +29 -16
  102. package/src/wayland/app.js +86 -0
  103. package/src/wayland/clipboard.js +14 -7
  104. package/src/wayland/connection.js +3 -0
  105. package/src/wayland/context2d.js +129 -10
  106. package/src/wayland/protocols/index.json +6 -0
  107. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  108. package/src/wayland/seat.js +28 -1
  109. package/src/wayland/swapchain.js +23 -0
  110. package/src/wayland/video.js +376 -0
  111. package/src/wayland/xkb.js +137 -0
  112. package/src/win32/a11y.js +1 -1
  113. package/src/win32/app.js +2 -2
  114. package/src/win32/fonts.js +1 -1
  115. package/src/win32/glarea.js +4 -4
  116. package/src/win32/overlay.js +1 -1
  117. package/src/win32/panewindow.js +3 -3
  118. package/src/win32/shell.js +1 -1
  119. package/src/win32/window.js +2 -2
  120. package/src/windowid.js +1 -1
  121. package/src/xvideo.js +189 -0
@@ -0,0 +1,523 @@
1
+ # Desktop integration
2
+
3
+ What an app tells the desktop about itself, beyond drawing: startup
4
+ notification (on by default, nothing to call); the launcher's view of the app
5
+ — a badge, a progress bar, a bounce for attention, the Dock's menu; the tray;
6
+ what a password field needs to be reachable by password managers; and the one
7
+ switch that turns off everything here that talks over D-Bus.
8
+
9
+ The menu bar has its own page, [globalmenu.md](globalmenu.md). The other
10
+ direction — the desktop handing the app a `myapp://…` link, and single
11
+ instance — is [uri-schemes.md](uri-schemes.md); its `ctx.timestamp` is the
12
+ same `_TIME` suffix [`launchTimestamp()`](#launchtimestamp) parses here.
13
+
14
+ ## Startup notification
15
+
16
+ A launcher that spawns an app opens a **startup sequence** — the busy cursor —
17
+ and closes it when the app says it is up. An app that never says so leaves the
18
+ cursor spinning until the desktop gives up (15 seconds on mutter), long after
19
+ its window is on screen, and users take that for a failed launch. The same
20
+ handshake carries **focus** — `_NET_WM_USER_TIME`, without which a strict
21
+ desktop opens the new window behind whatever the user was doing — and
22
+ **placement**, the workspace the launch happened on. react-x11 does all of it
23
+ with no configuration:
24
+
25
+ ```jsx
26
+ const root = await createRoot(); // that is the whole of it
27
+ ```
28
+
29
+ `DESKTOP_STARTUP_ID` is read from the environment and **removed from it**: the
30
+ variable names one launch, and a child process that inherited it would claim
31
+ the parent's sequence and end it early. With no id in the environment — a
32
+ terminal, CI, XQuartz — nothing is set and nothing is sent.
33
+
34
+ ### When is an app "started"?
35
+
36
+ The default is **the first frame that actually painted**. A window maps a
37
+ frame before it draws, so ending the sequence on map (what GTK does) would say
38
+ "ready" over a blank window. A suspense fallback counts as painted: the app is
39
+ up and telling the user what it is doing, and there is no signal for "finished
40
+ loading".
41
+
42
+ `completeOn` is there for apps the default does not suit:
43
+
44
+ ```jsx
45
+ await createRoot({ startupNotification: { completeOn: 'map' } });
46
+ ```
47
+
48
+ | `completeOn` | |
49
+ | ------------ | ------------------------------------------------------- |
50
+ | `'paint'` | the first frame that drew (default) |
51
+ | `'map'` | the first toplevel mapping — earlier, and what GTK does |
52
+ | `'manual'` | nothing automatic; call `notifyStartupComplete()` |
53
+
54
+ `'map'` suits an app whose first frame is expensive enough that it would
55
+ rather the cursor stopped before it. `'manual'` suits one that is not up
56
+ until it says so — restoring a session behind a splash, say:
57
+
58
+ ```jsx
59
+ import { notifyStartupComplete } from 'react-x11';
60
+
61
+ await createRoot({ startupNotification: { completeOn: 'manual' } });
62
+ await restoreSession();
63
+ notifyStartupComplete(); // idempotent, and a no-op if there is no sequence
64
+ ```
65
+
66
+ **A backstop ends the sequence regardless**, ten seconds after the window
67
+ maps, whichever mode is in force, so an app that never paints or forgets to
68
+ call cannot leave the cursor spinning.
69
+
70
+ ### Turning it off, and supplying an id
71
+
72
+ ```jsx
73
+ await createRoot({ startupNotification: false });
74
+ await createRoot({ startupNotification: 'launcher/app/1-0_TIME9876' });
75
+ ```
76
+
77
+ `false` is for an app that runs its own sequence, or an embedder that owns
78
+ the toplevel. Opting out leaves `DESKTOP_STARTUP_ID` in the environment — if
79
+ you manage the sequence yourself, the id is yours to read and to clear.
80
+
81
+ A string supplies the id for a launch where it did not arrive in the
82
+ environment. A D-Bus-activated app gets it in `platform_data` instead.
83
+
84
+ ### `launchTimestamp()`
85
+
86
+ ```jsx
87
+ import { launchTimestamp } from 'react-x11';
88
+
89
+ const when = launchTimestamp(); // number | null
90
+ ```
91
+
92
+ The X server timestamp of the user action that launched the app, parsed from
93
+ the id's `_TIME` suffix — the "when" any later request to come forward is
94
+ weighed against. **`null` is a real answer**, not a failure: an app started
95
+ from a shell has no launch timestamp. Do not substitute `0` for a missing one;
96
+ EWMH reads zero as "do not focus this window when it maps".
97
+
98
+ ### On macOS
99
+
100
+ XQuartz's quartz-wm implements none of this: the messages go to a root window
101
+ nobody listens at and `_NET_WM_USER_TIME` is ignored. Harmless, and not a sign
102
+ the feature is broken.
103
+
104
+ ### What is deliberately not here
105
+
106
+ - **The launcher half** — an app that spawns _another_ app manages that
107
+ child's sequence itself.
108
+ - **Ongoing `_NET_WM_USER_TIME` maintenance** — it is set once, at launch, not
109
+ kept current on every keypress.
110
+
111
+ ### Security
112
+
113
+ The id is broadcast to the root window, so every client on the display sees
114
+ it — X11's usual lack of isolation ([security.md](security.md)). The messages
115
+ carry only the id the launcher gave, and its value is never logged.
116
+
117
+ ## The launcher: a badge, attention, and the Dock menu
118
+
119
+ The marks the user reads on the app's icon without opening it. Each exists on
120
+ a backend only where that backend has a mechanism for it:
121
+
122
+ | | Linux (`launcherentry`) | macOS (`cocoa`) | Windows (`taskbar`) |
123
+ | --------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------- |
124
+ | a **badge** | `com.canonical.Unity.LauncherEntry` | `NSDockTile.badgeLabel` | an overlay icon with the label drawn into it |
125
+ | badge **text** | — the protocol carries a count and nothing else | any label | up to three glyphs; longer clamps to `99+`, ~16px on screen |
126
+ | **progress** | `useProgress(0…1)` — a bar across the tile | — `NSDockTile` has none | `ITaskbarList3::SetProgressValue`, across the taskbar button |
127
+ | **attention** | `states={['demands_attention']}`, or `setUrgent()` for the launcher entry itself | the same prop: the Dock icon bounces until activated | `FlashWindowEx` |
128
+ | the **launcher menu** | `useLauncherMenu(items)` — the launcher protocol's quicklist | `useLauncherMenu(items)` | — the jump list instead (below) |
129
+ | **tasks** | — `Actions=` in the `.desktop` file, not driven from here | — | `useJumpList(tasks)` — the jump list's Tasks category |
130
+ | a **hover toolbar** | — | — | `useThumbnailToolbar(buttons)` — up to seven, under the preview |
131
+ | **recent documents** | — | — the platform has one; this backend does not use it | `useRecentDocument(path)` — the shell's Recent lists |
132
+
133
+ On Linux, KDE, elementary and Cairo-Dock show the badge; GNOME needs an
134
+ extension. On Windows the taskbar button's menu is the jump list, whose entries
135
+ start a _new_ process, so it is `features.tasks` and `useJumpList` rather than
136
+ `useLauncherMenu` ([windows-integrations.md](windows-integrations.md)).
137
+
138
+ ### The badge
139
+
140
+ ```jsx
141
+ import { setBadge, useBadge } from 'react-x11';
142
+
143
+ useBadge(unread); // a count: 3 shows "3", 0 clears
144
+ await setBadge(null); // the imperative twin, for code with no component
145
+ ```
146
+
147
+ A badge is a **count**, the one shape both desktops show. A string (`'•'`,
148
+ `'!'`) shows on macOS, where the tile takes any label, and as a count of
149
+ nothing on Linux, whose protocol has no text — pass one only where the Mac is
150
+ the audience. `0`, `null` and `''` all clear it. `useBadge` clears on unmount;
151
+ `setBadge` resolves to whether a launcher was told and never rejects for
152
+ anything about the machine.
153
+
154
+ On Linux the launcher pins the count to the app's `.desktop` file, so it needs
155
+ the identity [`registerApplication({ appId })`](uri-schemes.md) establishes
156
+ and a `.desktop` file of that name; without them `setBadge` resolves `false`.
157
+ A launcher that starts after the badge was set does not see it until the next
158
+ `setBadge`.
159
+
160
+ ### Progress
161
+
162
+ ```jsx
163
+ import { useProgress, setProgress } from 'react-x11';
164
+
165
+ useProgress(downloaded / total); // 0…1; null or undefined clears it
166
+ await setProgress(null); // the imperative twin
167
+ ```
168
+
169
+ A bar drawn across the app's icon, for a download, an export, a long build.
170
+ Out-of-range values are clamped rather than refused, so a total that briefly
171
+ went to zero does not throw.
172
+
173
+ **Not on macOS:** `NSDockTile` has no progress bar; an app that wants one
174
+ there draws it into a custom tile view itself. Branch on
175
+ `useDesktopCapability('launcher').features.progress`.
176
+
177
+ ### Attention
178
+
179
+ Nothing new to call: `<window states={['demands_attention']}>`. On the cocoa
180
+ backend it is `NSApp.requestUserAttention` — the Dock icon bounces until the
181
+ user activates the app, and stops when the prop drops the state or the window
182
+ goes away. AppKit ignores the request while the app is already active, as a
183
+ window manager ignores a focused window's urgency hint. There, `fullscreen`,
184
+ `maximized` and `hidden` are AppKit's full-screen Space, zoom and miniaturize,
185
+ and the other `states` names are inert.
186
+
187
+ ### The Dock menu
188
+
189
+ ```jsx
190
+ useLauncherMenu([
191
+ { label: 'New Window', onSelect: openWindow },
192
+ { type: 'separator' },
193
+ {
194
+ label: 'Recent',
195
+ items: recent.map((r) => ({ label: r.name, onSelect: () => open(r) })),
196
+ },
197
+ ]);
198
+ ```
199
+
200
+ The same `items` vocabulary as `MenuBar` and `ContextMenu` ([menuitem
201
+ names](globalmenu.md)), so the window's, the panel's, the tray's and the
202
+ Dock's menus are one authoring model. Installed while the component is
203
+ mounted, replaced when `items` changes, taken down on unmount.
204
+
205
+ On the cocoa backend it is `NSDockTile`'s menu; on Linux, the launcher
206
+ protocol's **quicklist**, which ubuntu-dock/Dash-to-Dock, Plank and the
207
+ Unity-heritage launchers render. Entries that must work **while the app is not
208
+ running** belong in `Actions=` in the `.desktop` file instead; the quicklist
209
+ is for a menu that follows the app's state. Like the badge, it needs
210
+ [`registerApplication({ appId })`](uri-schemes.md) and a `.desktop` file of
211
+ that name.
212
+
213
+ ### The app's presence
214
+
215
+ Two things the Dock and the ⌘-Tab switcher show that a bare `node` process
216
+ gets wrong, as root options on the cocoa backend:
217
+
218
+ ```js
219
+ await createRoot({
220
+ cocoa: {
221
+ appName: 'Notes', // what the Dock, ⌘-Tab and the app menu print (was "node")
222
+ activationPolicy: 'regular', // 'accessory' for a menu-bar app: no tile, no ⌘-Tab entry
223
+ },
224
+ });
225
+ ```
226
+
227
+ `activationPolicy` is a root option rather than a hook because it is fixed
228
+ before the app finishes launching — an agent app that switched afterwards
229
+ would already have flashed its Dock icon. `appName` renames LaunchServices'
230
+ record of an unbundled process; a bundle's `Info.plist` wins.
231
+
232
+ ## The tray
233
+
234
+ ```jsx
235
+ import { useTray } from 'react-x11';
236
+
237
+ const { available, backend, features, error } = useTray({
238
+ icon: 'mail-unread', // a themed icon name (an SF Symbol on macOS), or PNG bytes
239
+ tooltip: 'Notifications',
240
+ menu: [
241
+ { label: 'Open', onSelect: open },
242
+ { type: 'separator' },
243
+ { label: 'Quit', onSelect: quit },
244
+ ],
245
+ });
246
+ ```
247
+
248
+ An icon in the system tray for as long as the component is mounted. With
249
+ `menu` a click opens it — the same `items` vocabulary as `MenuBar` and the
250
+ Dock menu. Without one, `onClick` gets the button, where the click was and,
251
+ where the backend knows it, the item's screen rect — all in logical screen
252
+ pixels, the unit a `<popup>`'s `x`/`y` are in, and what a popover under the
253
+ item anchors to ([elements.md](elements.md#a-popover-under-a-tray-item)).
254
+ `title` shows text beside the icon or alone; `visible`, `tooltip`, `attention`
255
+ and the rest follow their values while mounted, and the item is removed on
256
+ unmount. `null` means no item. Several trays coexist: each is its own item.
257
+
258
+ | | Linux (`statusnotifier`) | macOS (`cocoa`) | Windows (`shellnotifyicon`) |
259
+ | --------------- | --------------------------------------- | ---------------------------- | --------------------------- |
260
+ | mechanism | `org.kde.StatusNotifierItem` over D-Bus | `NSStatusItem` | `Shell_NotifyIcon` |
261
+ | menu | `com.canonical.dbusmenu` | `NSMenu` | a tracked popup menu |
262
+ | icon by name | the desktop's icon theme | SF Symbols | — no icon theme to name |
263
+ | icon by bytes | ARGB32 pixmap | a template image | an `HICON` from RGBA |
264
+ | title | text beside the icon | text beside the icon | — an icon and a tooltip |
265
+ | attention | `Status = NeedsAttention` | — | — |
266
+ | click rect | — | the item's frame | — |
267
+ | click modifiers | — | shift/control/option/command | — |
268
+ | scroll | `Scroll(delta, orientation)` | — | — |
269
+
270
+ **On Linux** it is D-Bus only, so it works identically under X11, XWayland and
271
+ Wayland. It needs something _hosting_ a tray: KDE Plasma and most panels do
272
+ out of the box; GNOME needs an AppIndicator extension. A stock GNOME session
273
+ with no extension has no tray, `available` is `false`, and nothing is logged —
274
+ that is a configuration, not a fault.
275
+
276
+ **A click's position on Linux** is in a unit the protocol never names: Plasma
277
+ and GNOME's physical layout send device pixels; GNOME's logical layout (the
278
+ default from GNOME 50), xfce4-panel, LXQt and Cinnamon send logical ones. The
279
+ point is read both ways: a reading on no monitor is dropped, then the one the
280
+ pointer is on wins, else the numbers are taken as sent. One case is misread —
281
+ a device-pixel host under XWayland whose tray, read as logical pixels, still
282
+ lands on a monitor (the top-left quarter of the screen, or with another
283
+ monitor beyond it): there the point is `scale` times as far from the screen's
284
+ origin as the click was.
285
+
286
+ **On the cocoa backend** it is `NSStatusItem`, the menu-bar extra. A menu-bar
287
+ app that wants no Dock tile pairs it with
288
+ `createRoot({ cocoa: { activationPolicy: 'accessory' } })` above. A PNG icon
289
+ is drawn as a **template** — its shape in the bar's ink, following light and
290
+ dark like a symbol; `template: false` keeps its colours. The menu is rebuilt
291
+ only when its items change, so a title that ticks every second leaves it
292
+ alone, whether `menu` is memoized or written inline.
293
+
294
+ ### What the hook hands back
295
+
296
+ `available` **settles**: finding a freedesktop tray takes a bus round trip, so
297
+ it is `false` on the first frame and `true` a tick later if a host is there —
298
+ render the fallback first and upgrade. It follows the host for the life of the
299
+ item, so a panel that exits flips it back.
300
+
301
+ **`settled` says whether `available` is an answer yet.** For an app whose
302
+ whole UI is its tray item, the fallback _is_ a window, and rendering it first
303
+ flashes a window up and away on every start on a desktop that has a tray. So
304
+ wait for the answer instead:
305
+
306
+ ```jsx
307
+ const tray = useTray({ icon: 'waveform', onClick: open });
308
+ if (!tray.settled) return null; // a bus round trip, not a timeout
309
+ if (!tray.available) return <window title="Hush">{controls}</window>;
310
+ return popover;
311
+ ```
312
+
313
+ It is `true` on the first frame on macOS, where the status item is made there
314
+ and then, and for `useTray(null)`; on the freedesktop tray it turns `true` once
315
+ the host has taken or refused the item, and stays so.
316
+
317
+ `features` is the same vocabulary [feature discovery](#feature-discovery)
318
+ uses, for the rung that answered. Read it instead of testing the platform:
319
+
320
+ ```jsx
321
+ // Right: ask about the thing you need.
322
+ {
323
+ features.clickModifiers && <ShiftClickHint />;
324
+ }
325
+ ```
326
+
327
+ `error` separates **"this desktop has no tray"** (`error` null) from **"there
328
+ is a tray and it refused me"** (the reason is there) — only the second has a
329
+ fix.
330
+
331
+ ## Feature discovery
332
+
333
+ Everything on this page is a _ladder_, and which rung a machine lands on
334
+ changes what the app may do. `available` alone is not enough: a notification
335
+ daemon with `actions` gives a banner with buttons that call back, and
336
+ `notify-send` gives one-way text — an app that posts reply buttons on the
337
+ second gets a banner with no buttons, silently. So the unit of discovery is a
338
+ **feature set**, not a flag:
339
+
340
+ ```jsx
341
+ import { useDesktopCapability } from 'react-x11';
342
+
343
+ const notifications = useDesktopCapability('notifications');
344
+
345
+ if (notifications.features.actions) return <ReplyFromBanner />;
346
+ if (notifications.available) return <BannerThenOpenApp />;
347
+ return <InAppToastOnly />;
348
+ ```
349
+
350
+ `{ available, backend, features, settled }`. `backend` names the
351
+ **mechanism** — `'dbus'`, `'cocoa'`, `'statusnotifier'`, `'launcherentry'`,
352
+ `'notify-send'`, `'osascript'`, `'win32'`, `'shellnotifyicon'`, `'taskbar'` —
353
+ never the platform. Branch on `features`; `backend` is for logs and bug
354
+ reports.
355
+
356
+ `settled` is `false` until the first probe answers and `true` from then on, so
357
+ an app can hold its fallback back rather than flash it; it is `true` on the
358
+ first frame for the answers that need nothing asked, the tray and the Dock tile
359
+ on macOS.
360
+
361
+ Capability names are `'notifications'`, `'tray'` and `'launcher'`.
362
+ `desktopCapability(name)` is the imperative twin for code with no component.
363
+
364
+ ### Which one to ask
365
+
366
+ A hook that _does_ the thing already knows whether it worked, and that
367
+ measurement beats any prediction:
368
+
369
+ - **`useTray(options)`** — "did my icon get taken?" It tried; it knows.
370
+ - **`useDesktopCapability('tray')`** — "would one be taken if I asked?" This
371
+ is what a settings screen needs: a "Show tray icon" checkbox has to render
372
+ correctly _without_ putting an icon in the tray to find out.
373
+
374
+ Prefer the feature hook wherever the feature is actually mounted. Both read
375
+ the same probe, so they cannot disagree.
376
+
377
+ ### Degrading well
378
+
379
+ 1. **Render the fallback first.** Every desktop answer takes a round trip, so
380
+ the first frame has none. Upgrading a fallback is invisible; downgrading a
381
+ feature the user already reached for is not.
382
+ 2. **Never disable a feature permanently on a `false`.** A panel restarts, an
383
+ extension is enabled, a daemon is installed. The hooks re-probe when a
384
+ service appears on the bus and nothing is cached — an app that latched the
385
+ first answer would outlive every one of those fixes.
386
+ 3. **A missing desktop service is not an error.** No bus, no tray host, no
387
+ notification daemon and no launcher are all ordinary states of a healthy
388
+ machine. Nothing is logged and nothing throws. `error` is for a service
389
+ that answered and then refused.
390
+
391
+ ### All of it, in one app
392
+
393
+ `examples/desktop.jsx` (`npm run examples:desktop`) is a backup agent that
394
+ uses every hook on this page at once — the tray icon with its attention state
395
+ and overlay, the badge, the progress bar, the quicklist, the urgency flag, and
396
+ a banner whose buttons are offered only where the daemon has `actions` — and
397
+ whose second pane renders `useDesktopCapability()` for each of `CAPABILITIES`
398
+ beside the live answers. Read it to see what an app will get on a machine you
399
+ do not have. The feature-sized examples are `badge.jsx` (the launcher),
400
+ `tray.jsx` (the tray) and `notify.jsx` (the notification).
401
+
402
+ ## Turning the desktop off
403
+
404
+ Three things react-x11 turns on for you reach the **session bus**:
405
+
406
+ | | | |
407
+ | ------------ | ----------------------------------------------------------------------- | ------------------------------------ |
408
+ | `appearance` | following the desktop's light/dark, accent, contrast and reduced motion | [appearance.md](appearance.md) |
409
+ | `a11y` | the AT-SPI bridge — whether a screen reader can see the app | [accessibility.md](accessibility.md) |
410
+ | `globalMenu` | a `MenuBar` handing its menu to the panel instead of drawing it | [globalmenu.md](globalmenu.md) |
411
+
412
+ Each is the right default, but an embedder, a kiosk or a test may own the
413
+ thing they assume is theirs:
414
+
415
+ ```jsx
416
+ await createRoot({ desktop: false }); // none of the three
417
+ await createRoot({ desktop: { appearance: false } }); // just that one
418
+ ```
419
+
420
+ With all three off, **nothing dials the session bus at startup** — no
421
+ connection, no subprocess, no portal probe.
422
+
423
+ Prefer this to the environment variables that do the same — `NO_AT_BRIDGE=1`,
424
+ `REACT_X11_NO_GLOBAL_MENU=1`, or unsetting `DBUS_SESSION_BUS_ADDRESS`: the
425
+ environment is inherited, so they reach every child process too, and the
426
+ D-Bus one also takes down the portals and the app's own exported services.
427
+
428
+ Turning `appearance` off means the app draws in its own palette:
429
+ `useSystemAppearance()` reports `'no-preference'` with `source: null`, and the
430
+ answer remembered on disk is not read either.
431
+
432
+ **Off is process-wide, and it latches.** There is one D-Bus identity and one
433
+ AT-SPI bridge per process, so the policy is not per-root: nothing turns an
434
+ integration back on, and a feature already started stays started — pass this
435
+ on the first root.
436
+
437
+ ## Not blocking the first frame on the bus
438
+
439
+ On macOS the session bus address comes from launchd rather than the
440
+ environment; react-x11 looks it up once, asynchronously, so it costs the event
441
+ loop and the first frame nothing. There is nothing to configure. On Linux
442
+ `$DBUS_SESSION_BUS_ADDRESS` or `$XDG_RUNTIME_DIR/bus` answers directly.
443
+ Design notes: [design/desktop.md](../design/desktop.md#not-blocking-the-first-frame-on-the-bus).
444
+
445
+ ## Password fields and password managers
446
+
447
+ There is **no password-field protocol on the Linux desktop**: no toolkit
448
+ publishes "this is a password field, fill it", and no manager asks. There are
449
+ four seams instead, which an application either supports or does not.
450
+ `PasswordInput` supports the two that reach a field.
451
+ How the managers do it: [design/desktop.md](../design/desktop.md#password-fields-and-password-managers).
452
+
453
+ ### 1. Typing — XTEST auto-type, and the keymap race
454
+
455
+ Most desktop managers **auto-type**: KeePassXC matches the focused window's
456
+ **title** against its entries and synthesises the keystrokes over XTEST.
457
+
458
+ - **A faked key is an ordinary key.** `PasswordInput` — and every other
459
+ focusable node — cannot tell auto-type from a person, and nothing has to be
460
+ done to support it. A sequence of `{USERNAME}{TAB}{PASSWORD}{ENTER}` walks a
461
+ react-x11 form because Tab moves focus and Enter reaches `onSubmit`.
462
+ - **The window needs a title worth matching.** `<window title="…">` is the
463
+ integration surface: a window titled after the document with nothing
464
+ identifying the app is one a user cannot write an auto-type rule for. Set
465
+ `appId` too, for managers that match on it.
466
+ - **Characters outside the user's layout can race.** A manager types them by
467
+ remapping a spare keycode first; one that types immediately after remapping
468
+ can land the key before react-x11 has refetched the keymap. ASCII passwords
469
+ on an ASCII layout are not affected, and nothing on this side can close it.
470
+
471
+ ### 2. Pasting — the clipboard, and the hint that keeps it out of history
472
+
473
+ Copy-to-clipboard is ordinary `CLIPBOARD` interop ([clipboard.md](clipboard.md)):
474
+ `PasswordInput` takes Ctrl+V and Shift+Insert, and strips control characters
475
+ so a manager's trailing newline does not end up inside the secret.
476
+
477
+ If your app ever puts a secret **on** the clipboard — this widget never does
478
+ — offer `x-kde-passwordManagerHint` with the value `secret` beside the text.
479
+ Klipper drops such an offer from its history, KDE Connect refuses to forward
480
+ it, and `wl-clipboard` marks it sensitive. It is a convention rather than a
481
+ specification, and the only thing between a copied password and a clipboard
482
+ manager's on-disk history:
483
+
484
+ ```js
485
+ clipboard.write({ UTF8_STRING: secret, 'x-kde-passwordManagerHint': 'secret' });
486
+ ```
487
+
488
+ `PasswordInput` never takes the **PRIMARY** selection, which a `<textinput>`
489
+ does on every selection: PRIMARY is pasted by a middle click in any window on
490
+ the display.
491
+
492
+ ### 3. Fetching it yourself — the Secret Service
493
+
494
+ An app with an account of its own to unlock needs no field at all: the
495
+ **Secret Service** D-Bus API, `org.freedesktop.secrets` (gnome-keyring,
496
+ KWallet, KeePassXC), is reachable through `useSessionBus()`
497
+ ([dbus.md](dbus.md)) — search by attributes, unlock the collection if locked,
498
+ read the secret back. react-x11 has no wrapper for it. Inside a Flatpak
499
+ sandbox that name is absent; `org.freedesktop.portal.Secret`'s
500
+ `RetrieveSecret` hands the app a per-application master secret instead, which
501
+ libsecret uses automatically to encrypt a local store.
502
+
503
+ ### 4. Reading the field — AT-SPI, which does not mark it yet
504
+
505
+ AT-SPI2 gives a password entry the role `password text`, which is how a screen
506
+ reader knows to announce "bullet" and how some automation finds the field.
507
+ react-x11 publishes an accessibility tree ([accessibility.md](accessibility.md)),
508
+ but `PasswordInput` is a plain `role="textbox"` in it, so this seam is still
509
+ closed.
510
+
511
+ ### What a field can still do wrong
512
+
513
+ None of the above stops the field itself from leaking. What `PasswordInput`
514
+ does about that, and what it cannot:
515
+
516
+ - the masked value is **never laid out or drawn** — no glyphs of the secret
517
+ reach ntk's shaping cache or the X server, and the mask's width is measured
518
+ from one reference character;
519
+ - there is **no copy, no selection, no undo history**;
520
+ - the value is still a JavaScript string, and strings are immutable. It
521
+ cannot be zeroed, it lives until the garbage collector takes it, and every
522
+ intermediate value typed on the way lives alongside it. A design that needs
523
+ more than that needs the secret to never enter this process.
@@ -0,0 +1,133 @@
1
+ # React DevTools
2
+
3
+ react-x11 speaks to the standalone React DevTools app over its WebSocket
4
+ bridge.
5
+
6
+ ## Setup
7
+
8
+ ```sh
9
+ npm i -D react-devtools-core # already a dev dep of this repo
10
+
11
+ # terminal 1 — the standalone UI (listens on :8097)
12
+ npx react-devtools
13
+
14
+ # terminal 2 — any app/example with the bridge enabled
15
+ REACT_X11_DEVTOOLS=1 npm run examples:dashboard
16
+ ```
17
+
18
+ Either order works: the backend retries every two seconds, so an app
19
+ started first attaches a moment after the UI comes up. Under
20
+ `npx react-x11 dev` the bridge is on by default wherever
21
+ `react-devtools-core` is installed — run `npx react-devtools` and the app
22
+ shows up, `REACT_X11_DEVTOOLS=0` turns it off — and where it is not
23
+ installed while a UI is listening, `dev` says what to install.
24
+
25
+ What you get:
26
+
27
+ - the **component tree**, live as the app updates;
28
+ - **props, hooks and state** when selecting a component, and editing any of
29
+ them — the app really re-renders;
30
+ - **highlight-on-hover** — hovering an element in the tree tints its rect
31
+ in the window;
32
+ - **"highlight updates when components render"** — the rects that just
33
+ re-rendered are outlined in DevTools' own colour ramp (teal for a first
34
+ render through amber for one that keeps re-rendering), fading out;
35
+ - the **element picker** — the crosshair in the DevTools toolbar. While it
36
+ is on, the pointer belongs to DevTools: motion tints whatever is under
37
+ it, a click selects that element in the tree, `Escape` gives up. The app
38
+ sees none of those events;
39
+ - the **style editor** — the box model and the flattened `style` of a
40
+ selected element, editable live. Host elements are hidden by the default
41
+ component filter, so turn that filter off (⚙ → Components → uncheck
42
+ "host components") to have something with a style to select;
43
+ - the Profiler's commit list and flamegraph, including **"reload and start
44
+ profiling"**, which re-execs the app with profiling already on so the
45
+ mount itself is recorded. **Not** its Timeline view, which this reconciler
46
+ build does not support;
47
+ - **DevTools' own settings** — component filters, console patching — kept
48
+ between runs, in `$XDG_CACHE_HOME/react-x11/devtools.json`
49
+ (`~/Library/Caches/react-x11/devtools.json` on macOS).
50
+
51
+ With `REACT_X11_CLICK_TO_COMPONENT=1` as well, an Alt+Click also selects
52
+ the clicked element in the DevTools tree on its way to opening the source
53
+ ([click-to-component.md](click-to-component.md)).
54
+
55
+ `REACT_X11_DEVTOOLS_HOST` / `REACT_X11_DEVTOOLS_PORT` point the backend at
56
+ a non-default standalone (e.g. devtools on another machine — handy when the
57
+ X app runs on a headless box). `REACT_X11_DEVTOOLS_STATE` moves the
58
+ settings file.
59
+
60
+ DevTools has no network panel — nothing in its protocol carries network
61
+ data. For that, `node --inspect` and `chrome://inspect` still work alongside
62
+ all of this.
63
+
64
+ ## Opening the source from the DevTools panel
65
+
66
+ The inspected-element panel already shows where a component was written —
67
+ `file:///…/tasks.jsx:42:7`, absolute and source-mapped. Making that line
68
+ _clickable_ is a standalone-side setting, with two independent routes:
69
+
70
+ - **Spawn an editor.** The standalone launches one itself, choosing it from
71
+ `REACT_EDITOR` (a full command line, shell-quoted), else — on macOS — an
72
+ editor already running, else `VISUAL`, else `EDITOR`. It knows each
73
+ editor's argument style (vim's `+42`, sublime's and JetBrains' `file:42`),
74
+ so this is the route to reach for:
75
+
76
+ ```sh
77
+ REACT_EDITOR=cursor npx react-devtools
78
+ ```
79
+
80
+ - **Open a URL.** ⚙ → General → "Open local files directly in your code
81
+ editor", with the VS Code preset or a custom template using `{path}`,
82
+ `{line}` and `{column}`. The setting lives in the standalone's own
83
+ storage, not the app's.
84
+
85
+ Both are configured where the standalone runs, which may be a different
86
+ machine from the app; the app cannot inject either.
87
+ [Click-to-component](click-to-component.md) is the app-side answer to the same
88
+ question, and the one that works when DevTools runs somewhere without your
89
+ editor on it.
90
+
91
+ The same backend, minus the socket, powers `react-x11/test`'s component
92
+ inspection: `inspect()`/`setHook` in
93
+ [testing.md](testing.md#inspectnode-and-sethook) run entirely in-process — no
94
+ standalone app, no `ws`, no env variable. A test run that also sets
95
+ `REACT_X11_DEVTOOLS=1` shares one renderer registration with them.
96
+
97
+ ## How it works (and its sharp edges)
98
+
99
+ The bridge is installed by `createRoot()` when `REACT_X11_DEVTOOLS` is set (by
100
+ `react-x11 dev`, earlier, before React Refresh loads). Its socket is `ws` from
101
+ `react-devtools-core`'s own dependencies, so `ws` is not a separate install.
102
+ What an app can notice while it is on:
103
+
104
+ - **It does not keep the process alive.** The backend's retry timer and socket
105
+ are unref'd, so the process still exits when the app's last window closes;
106
+ the app's own `setTimeout` and `WebSocket` are untouched.
107
+ - **Browser-ish globals appear.** `self` and `window` are set to the global
108
+ where absent, `requestAnimationFrame`/`cancelAnimationFrame` are shimmed
109
+ where absent, and `localStorage`/`sessionStorage` are defined (the store
110
+ behind DevTools' settings file).
111
+ - **`Error.stackTraceLimit` is raised to 50** (never lowered), because React
112
+ reads hook names and sources off a stack captured at each hook call and a
113
+ loader such as `tsx` eats the default 10 frames. Without it, hooks show with
114
+ no names and no source lines.
115
+ - **Selecting a component with hooks can blank the DevTools window.** That is
116
+ the standalone app's own renderer process aborting, upstream of anything the
117
+ app sends. Turning ⚙ → Components → "Always parse hook names for the
118
+ selected element" off makes it intermittent rather than reliable;
119
+ restarting `npx react-devtools` reconnects to the app, which is unaffected.
120
+
121
+ Design notes and maintainer notes:
122
+ [design/devtools.md](../design/devtools.md#how-it-works-and-its-sharp-edges).
123
+
124
+ ## Other debugging aids
125
+
126
+ - `REACT_X11_DEBUG_LAYOUT=1` — outline every laid-out node, color-coded by
127
+ tree depth.
128
+ - Refs: drawn elements expose the retained node (`abs` rect, `scrollTo`,
129
+ …); `<window>`/`<popup>` expose the live ntk window (`getContext('2d')`,
130
+ `queryTree`, `setCursor`, …).
131
+ - The in-process X server (node-x11's `lib/xserver`) renders headlessly and
132
+ reads pixels back; `react-x11/test` is built on it
133
+ ([testing.md](testing.md#pixels)).