react-x11 2.52.0 → 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 (112) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1091 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/layouts.js +1 -1
  76. package/src/nodes/animation.js +1 -1
  77. package/src/nodes/invalidate.js +1 -1
  78. package/src/nodes/layouthost.js +1 -1
  79. package/src/nodes/video.js +19 -8
  80. package/src/nodes/window/droptarget.js +1 -1
  81. package/src/pacing.js +1 -1
  82. package/src/refresh/index.js +2 -1
  83. package/src/types/video.d.ts +5 -2
  84. package/src/videobackend.js +28 -0
  85. package/src/videoframes.js +29 -16
  86. package/src/wayland/app.js +27 -0
  87. package/src/wayland/video.js +376 -0
  88. package/src/win32/a11y.js +1 -1
  89. package/src/win32/app.js +2 -2
  90. package/src/win32/fonts.js +1 -1
  91. package/src/win32/glarea.js +4 -4
  92. package/src/win32/overlay.js +1 -1
  93. package/src/win32/panewindow.js +3 -3
  94. package/src/win32/shell.js +1 -1
  95. package/src/win32/window.js +2 -2
  96. package/src/windowid.js +1 -1
  97. package/src/xvideo.js +189 -0
  98. package/docs/architecture/accessibility.md +0 -470
  99. package/docs/architecture/animation.md +0 -707
  100. package/docs/architecture/container-queries.md +0 -525
  101. package/docs/architecture/custom-layout.md +0 -334
  102. package/docs/architecture/drag-and-drop.md +0 -941
  103. package/docs/architecture/element-layer-contents.md +0 -622
  104. package/docs/architecture/frame-pacing.md +0 -255
  105. package/docs/architecture/grid-layout.md +0 -358
  106. package/docs/architecture/protocol-efficiency.md +0 -357
  107. package/docs/architecture/video.md +0 -1149
  108. package/docs/architecture/windowed-regions.md +0 -854
  109. package/docs/glx.md +0 -137
  110. package/docs/wayland.md +0 -545
  111. package/docs/windows-embedding.md +0 -271
  112. package/docs/windows-gl.md +0 -221
package/docs/desktop.md CHANGED
@@ -1,75 +1,43 @@
1
1
  # Desktop integration
2
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.
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.
22
8
 
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:
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.
34
13
 
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.
14
+ ## Startup notification
42
15
 
43
- react-x11 does all of it with no configuration:
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:
44
24
 
45
25
  ```jsx
46
26
  const root = await createRoot(); // that is the whole of it
47
27
  ```
48
28
 
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.
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.
57
33
 
58
34
  ### When is an app "started"?
59
35
 
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.
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".
73
41
 
74
42
  `completeOn` is there for apps the default does not suit:
75
43
 
@@ -96,10 +64,8 @@ notifyStartupComplete(); // idempotent, and a no-op if there is no sequence
96
64
  ```
97
65
 
98
66
  **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.
67
+ maps, whichever mode is in force, so an app that never paints or forgets to
68
+ call cannot leave the cursor spinning.
103
69
 
104
70
  ### Turning it off, and supplying an id
105
71
 
@@ -109,9 +75,8 @@ await createRoot({ startupNotification: 'launcher/app/1-0_TIME9876' });
109
75
  ```
110
76
 
111
77
  `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.
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.
115
80
 
116
81
  A string supplies the id for a launch where it did not arrive in the
117
82
  environment. A D-Bus-activated app gets it in `platform_data` instead.
@@ -125,55 +90,50 @@ const when = launchTimestamp(); // number | null
125
90
  ```
126
91
 
127
92
  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".
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".
134
97
 
135
98
  ### On macOS
136
99
 
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.
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.
140
103
 
141
104
  ### What is deliberately not here
142
105
 
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.
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.
151
110
 
152
111
  ### Security
153
112
 
154
113
  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.
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.
159
116
 
160
117
  ## The launcher: a badge, attention, and the Dock menu
161
118
 
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 |
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)).
177
137
 
178
138
  ### The badge
179
139
 
@@ -184,22 +144,18 @@ useBadge(unread); // a count: 3 shows "3", 0 clears
184
144
  await setBadge(null); // the imperative twin, for code with no component
185
145
  ```
186
146
 
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.
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`.
203
159
 
204
160
  ### Progress
205
161
 
@@ -211,25 +167,22 @@ await setProgress(null); // the imperative twin
211
167
  ```
212
168
 
213
169
  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.
170
+ Out-of-range values are clamped rather than refused, so a total that briefly
171
+ went to zero does not throw.
216
172
 
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.
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`.
221
176
 
222
177
  ### Attention
223
178
 
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.
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.
233
186
 
234
187
  ### The Dock menu
235
188
 
@@ -244,25 +197,18 @@ useLauncherMenu([
244
197
  ]);
245
198
  ```
246
199
 
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.
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.
262
204
 
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.
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.
266
212
 
267
213
  ### The app's presence
268
214
 
@@ -278,11 +224,10 @@ await createRoot({
278
224
  });
279
225
  ```
280
226
 
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.
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.
286
231
 
287
232
  ## The tray
288
233
 
@@ -302,15 +247,13 @@ const { available, backend, features, error } = useTray({
302
247
 
303
248
  An icon in the system tray for as long as the component is mounted. With
304
249
  `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.
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.
314
257
 
315
258
  | | Linux (`statusnotifier`) | macOS (`cocoa`) | Windows (`shellnotifyicon`) |
316
259
  | --------------- | --------------------------------------- | ---------------------------- | --------------------------- |
@@ -324,46 +267,41 @@ Several trays coexist: each is its own item.
324
267
  | click modifiers | — | shift/control/option/command | — |
325
268
  | scroll | `Scroll(delta, orientation)` | — | — |
326
269
 
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
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
348
288
  `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.
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.
354
293
 
355
294
  ### What the hook hands back
356
295
 
357
- `available` **settles**. Finding a freedesktop tray takes a bus round trip, so
296
+ `available` **settles**: finding a freedesktop tray takes a bus round trip, so
358
297
  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.
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.
361
300
 
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:
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:
367
305
 
368
306
  ```jsx
369
307
  const tray = useTray({ icon: 'waveform', onClick: open });
@@ -386,22 +324,18 @@ uses, for the rung that answered. Read it instead of testing the platform:
386
324
  }
387
325
  ```
388
326
 
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.
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.
393
330
 
394
331
  ## Feature discovery
395
332
 
396
333
  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:
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:
405
339
 
406
340
  ```jsx
407
341
  import { useDesktopCapability } from 'react-x11';
@@ -413,17 +347,11 @@ if (notifications.available) return <BannerThenOpenApp />;
413
347
  return <InAppToastOnly />;
414
348
  ```
415
349
 
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.
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.
427
355
 
428
356
  `settled` is `false` until the first probe answers and `true` from then on, so
429
357
  an app can hold its fallback back rather than flash it; it is `true` on the
@@ -448,37 +376,32 @@ the same probe, so they cannot disagree.
448
376
 
449
377
  ### Degrading well
450
378
 
451
- Three rules, in the order they matter:
452
-
453
379
  1. **Render the fallback first.** Every desktop answer takes a round trip, so
454
380
  the first frame has none. Upgrading a fallback is invisible; downgrading a
455
381
  feature the user already reached for is not.
456
382
  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.
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.
460
386
  3. **A missing desktop service is not an error.** No bus, no tray host, no
461
387
  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.
388
+ machine. Nothing is logged and nothing throws. `error` is for a service
389
+ that answered and then refused.
464
390
 
465
391
  ### All of it, in one app
466
392
 
467
393
  `examples/desktop.jsx` (`npm run examples:desktop`) is a backup agent that
468
394
  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).
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).
477
401
 
478
402
  ## Turning the desktop off
479
403
 
480
- Three things react-x11 turns on for you reach the **session bus**, and none
481
- of them was asked for:
404
+ Three things react-x11 turns on for you reach the **session bus**:
482
405
 
483
406
  | | | |
484
407
  | ------------ | ----------------------------------------------------------------------- | ------------------------------------ |
@@ -486,17 +409,8 @@ of them was asked for:
486
409
  | `a11y` | the AT-SPI bridge — whether a screen reader can see the app | [accessibility.md](accessibility.md) |
487
410
  | `globalMenu` | a `MenuBar` handing its menu to the panel instead of drawing it | [globalmenu.md](globalmenu.md) |
488
411
 
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.
412
+ Each is the right default, but an embedder, a kiosk or a test may own the
413
+ thing they assume is theirs:
500
414
 
501
415
  ```jsx
502
416
  await createRoot({ desktop: false }); // none of the three
@@ -504,134 +418,95 @@ await createRoot({ desktop: { appearance: false } }); // just that one
504
418
  ```
505
419
 
506
420
  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.
421
+ connection, no subprocess, no portal probe.
515
422
 
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.
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.
521
427
 
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.
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.
526
431
 
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.
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.
534
436
 
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.
437
+ ## Not blocking the first frame on the bus
540
438
 
541
- Linux is unaffected: `$DBUS_SESSION_BUS_ADDRESS` or `$XDG_RUNTIME_DIR/bus`
542
- answers without a subprocess, and always did.
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).
543
444
 
544
445
  ## Password fields and password managers
545
446
 
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.
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).
552
452
 
553
453
  ### 1. Typing — XTEST auto-type, and the keymap race
554
454
 
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.
455
+ Most desktop managers **auto-type**: KeePassXC matches the focused window's
456
+ **title** against its entries and synthesises the keystrokes over XTEST.
561
457
 
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
458
+ - **A faked key is an ordinary key.** `PasswordInput` — and every other
566
459
  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.
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.
582
470
 
583
471
  ### 2. Pasting — the clipboard, and the hint that keeps it out of history
584
472
 
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.
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.
590
476
 
591
477
  If your app ever puts a secret **on** the clipboard — this widget never does
592
478
  — offer `x-kde-passwordManagerHint` with the value `secret` beside the text.
593
479
  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:
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:
597
483
 
598
484
  ```js
599
485
  clipboard.write({ UTF8_STRING: secret, 'x-kde-passwordManagerHint': 'secret' });
600
486
  ```
601
487
 
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.
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.
606
491
 
607
492
  ### 3. Fetching it yourself — the Secret Service
608
493
 
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.
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.
622
502
 
623
503
  ### 4. Reading the field — AT-SPI, which does not mark it yet
624
504
 
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.
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.
635
510
 
636
511
  ### What a field can still do wrong
637
512