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
package/docs/system.md ADDED
@@ -0,0 +1,519 @@
1
+ # The machine around the app
2
+
3
+ Hooks for what an application reads off the system rather than working out
4
+ itself: the monitors, what the window manager did with the window, whether
5
+ anyone is at the desk, the state of the keyboard, how the desktop wants an app
6
+ to feel, and the language it was launched in — and a store for what the app
7
+ remembers itself, between launches.
8
+
9
+ ```jsx
10
+ import {
11
+ useScreens,
12
+ useWindowState,
13
+ useIdle,
14
+ useKeepAwake,
15
+ useKeyboardState,
16
+ useDesktopSettings,
17
+ createSettings,
18
+ useLocale,
19
+ } from 'react-x11';
20
+ ```
21
+
22
+ They share a shape:
23
+
24
+ - **Nothing is asked of the server until a component asks.** An app that
25
+ never calls them pays for none of it.
26
+ - **Every one has an answer with no extension present.** "This display cannot
27
+ say" is a real state rather than an error. Each hook below names what it
28
+ reports then, and which field tells that apart from a genuine "no".
29
+ - **The colours are somewhere else.** Light or dark, the accent and reduced
30
+ motion are [system appearance](appearance.md).
31
+
32
+ ---
33
+
34
+ ## `useScreens()` — the monitors
35
+
36
+ ```jsx
37
+ const { screens, primary, workArea, virtual, source } = useScreens();
38
+
39
+ <Select
40
+ value={monitor}
41
+ onChange={setMonitor}
42
+ options={screens.map((s) => ({
43
+ value: s.name,
44
+ label: `${s.name} — ${s.width}×${s.height}`,
45
+ }))}
46
+ />;
47
+ ```
48
+
49
+ Each entry in `screens`:
50
+
51
+ | | |
52
+ | ------------------------ | -------------------------------------------------------------- |
53
+ | `name` | `'HDMI-1'`, `'eDP-1'` — **null** where the server has no RandR |
54
+ | `x` `y` `width` `height` | the rect in virtual-screen coordinates |
55
+ | `available` | that rect minus the panels |
56
+ | `primary` | the desktop's main monitor |
57
+ | `widthMM` `heightMM` | physical size, or null |
58
+ | `refreshRate` | Hz to two decimals (`59.99`), or null |
59
+ | `rotation` | `0`, `90`, `180` or `270` |
60
+ | `outputs` | every output on this monitor — two names means it is mirrored |
61
+ | `scale` | this monitor's device pixels per logical pixel |
62
+
63
+ Re-renders when a monitor is plugged in or unplugged, when the arrangement
64
+ changes, and when a panel appears, moves or auto-hides.
65
+
66
+ The rects are in **logical pixels**, like every other length an app touches,
67
+ and `scale` is the per-monitor answer the display-scale ladder arrived at — a
68
+ retina lid and an office monitor genuinely differ. The root's own scale is the
69
+ primary monitor's; see [scale.md](scale.md).
70
+
71
+ ### The names arrive after the geometry
72
+
73
+ `source` says which tier answered. The geometry resolves during `createRoot()`
74
+ from **Xinerama**; the names, the primary flag and the physical sizes come
75
+ from **RandR** a moment later, without holding startup up. So a component can
76
+ render once with `source: 'xinerama'` and null names, and again with
77
+ `source: 'randr'` and real ones. The rects do not move between the two: what
78
+ arrives late is added, not corrected. Where a name is persisted — "reopen on
79
+ the monitor I was on" — treat null as _not known yet_ and keep the last one
80
+ rather than writing it.
81
+
82
+ `'screen'` is the tier under both: one entry covering the whole display, for a
83
+ server with neither extension. `null` is a headless mock with no display at
84
+ all.
85
+
86
+ ### `available` is an approximation on X11, and the only one here
87
+
88
+ `_NET_WORKAREA` — the desktop minus the panels that reserved space — is
89
+ published for the **whole virtual desktop**, not per monitor. So it is applied
90
+ as a per-axis bound on each monitor's rect: exact on one head; on several, a
91
+ top or bottom panel comes off the height without knowing which head it is on.
92
+ For a true per-monitor work area, read `_NET_WM_STRUT_PARTIAL` off every
93
+ window yourself through `useApp().X`.
94
+
95
+ ### On Wayland
96
+
97
+ The same shape, from `wl_output` ([wayland-backend.md](wayland-backend.md)):
98
+ `name` is the compositor's connector name (`DP-1`, `eDP-1`) and arrives with
99
+ the geometry rather than after it; the rects are xdg_output's logical ones.
100
+ There is no primary — the entry at the origin is flagged — and no work area:
101
+ `available` is each monitor's own rect less the bounds the compositor gave a
102
+ window on it, so it is exact per monitor once a window has appeared there.
103
+ `scale` is the monitor's own, a fraction under fractional scaling. `source`
104
+ reads `'test'` on both native backends.
105
+
106
+ **On macOS it is exact.** Each monitor's `available` is its `visibleFrame` —
107
+ that display's own menu bar and Dock taken off. `workArea` is one rect by
108
+ definition, the primary screen's; on a multi-display Mac prefer
109
+ `screens[n].available`. `primary` is the screen with the menu bar,
110
+ `refreshRate` the panel's own rate, and `name` is null.
111
+
112
+ ### On macOS the layout is asked for, not announced
113
+
114
+ macOS sends a client no event when the displays change, so react-x11 re-reads
115
+ the layout at the moments it matters:
116
+
117
+ - **before a placement** — a popup flipped and clamped into a monitor, an
118
+ auto-sized window capped by one;
119
+ - **when a window reports that it moved**, at most once a second, because a
120
+ display arriving or leaving moves the windows that were on it;
121
+ - **on a clock while a `useScreens()` subscriber is mounted**, every
122
+ `createRoot({ cocoa: { screenPoll } })` ms — 500 by default, `0` for
123
+ never. An app that never calls `useScreens()` never polls.
124
+
125
+ So a component watching the layout sees a monitor plugged in about half a
126
+ second later than an X11 one does, and everything else sees it at the moment
127
+ it asks. `useApp().refreshScreens()` publishes the layout right now.
128
+
129
+ ### Mirroring
130
+
131
+ Two outputs showing the same pixels are reported as **one** monitor, with both
132
+ names in `outputs`. A laptop mirroring to a projector is one screen to put a
133
+ window on.
134
+
135
+ ---
136
+
137
+ ## `useWindowState()` — what the window manager did
138
+
139
+ ```jsx
140
+ const { focused, visible, fullscreen } = useWindowState();
141
+
142
+ // a title bar that dims when the window is not the active one
143
+ <text style={{ color: focused ? theme.text : theme.textMuted }}>{title}</text>;
144
+
145
+ // and work that is pointless while nobody can see it
146
+ useEffect(() => {
147
+ if (!visible) return;
148
+ const id = setInterval(poll, 1000);
149
+ return () => clearInterval(id);
150
+ }, [visible]);
151
+ ```
152
+
153
+ | | |
154
+ | ------------ | ---------------------------------------------------------------------- |
155
+ | `focused` | this window has the keyboard |
156
+ | `visible` | **the one to branch on** — not minimized, not covered, still drawing |
157
+ | `minimized` | `_NET_WM_STATE_HIDDEN`: iconified, or shaded away |
158
+ | `maximized` | both axes; one axis alone shows up in `states` |
159
+ | `fullscreen` | what the WM actually did, not what `<window fullscreen>` asked for |
160
+ | `obscured` | fully covered — **always false under a compositor** |
161
+ | `presenting` | the display is still scheduling frames — Wayland; true everywhere else |
162
+ | `states` | the raw `_NET_WM_STATE` names |
163
+ | `desktop` | the workspace index, or null |
164
+
165
+ `<window states>` is what a window **asks for**; this is what it **got**. They
166
+ part company routinely — a user hits a maximize hotkey, a tiling WM declines a
167
+ fullscreen request — and an app that mirrors the state in its own UI has
168
+ nowhere else to read it.
169
+
170
+ On the **Cocoa** backend the same rows come from AppKit, in the same
171
+ `_NET_WM_STATE` names: `fullscreen` is the title bar's green button or
172
+ `<window fullscreen>`, `maximized` the zoom (`maximized_vert` and
173
+ `maximized_horz` together), `minimized` the Dock (`hidden`), and `states`
174
+ lists `focused` for the key window. `fullscreen` turns as AppKit's transition
175
+ **begins**, so a toolbar an app hides in full screen goes while the window
176
+ grows rather than after it:
177
+
178
+ ```jsx
179
+ const { fullscreen } = useWindowState();
180
+ return fullscreen ? <Page /> : [<Toolbar />, <Page />];
181
+ ```
182
+
183
+ `obscured` and `desktop` keep their initial values there — a Space is not a
184
+ numbered workspace.
185
+
186
+ With no argument it reads the window the component is in, inferred the way
187
+ [`useTopLevelWindow()`](filedialog.md) infers it: exact for a one-window app,
188
+ and a documented guess for a tree with several. Pass a ref to be certain.
189
+
190
+ ```jsx
191
+ const win = useRef(null);
192
+ const { fullscreen } = useWindowState(win);
193
+ return <window ref={win}>…</window>;
194
+ ```
195
+
196
+ The first render answers `focused: true, visible: true` rather than waiting
197
+ for a round trip, so a title bar does not render dimmed on frame one and
198
+ un-dim on frame two.
199
+
200
+ ### `obscured` and the compositor
201
+
202
+ **A composited window is never obscured.** The compositor redirects it to an
203
+ offscreen pixmap, so the server considers it entirely visible whatever is
204
+ stacked on top — and every stock GNOME and KDE session runs one. So `obscured`
205
+ is a bare-WM optimisation, and `visible`, which folds in `minimized` (that
206
+ _does_ survive compositing), is the field to branch on. An animation paused on
207
+ `visible` stops when the window is minimized everywhere, and additionally when
208
+ it is buried on a desktop with no compositor.
209
+
210
+ ### `presenting`, and the Wayland frame clock
211
+
212
+ A Wayland compositor stops sending **frame callbacks** to a surface it is not
213
+ showing, and on that backend frame callbacks pace the frame loop. So a window
214
+ nobody is showing draws once and then stops, while React above it goes on
215
+ re-rendering into a tree that reaches no screen.
216
+
217
+ `presenting` is that state, published. It goes false after a couple of
218
+ seconds of silence with a repaint waiting, and true again on the very next
219
+ frame callback, which the compositor sends the moment the surface is shown
220
+ again. It folds into `visible`, so an animation already paused on `visible`
221
+ pauses here too:
222
+
223
+ ```jsx
224
+ const { visible, presenting } = useWindowState();
225
+
226
+ // nothing this draws would reach the screen; stop simulating as well
227
+ useEffect(() => {
228
+ if (!visible) return;
229
+ const id = setInterval(step, 16);
230
+ return () => clearInterval(id);
231
+ }, [visible]);
232
+
233
+ // and the reason, where a status line would rather say it than go blank
234
+ const status = presenting ? 'live' : 'paused — this window is not on screen';
235
+ ```
236
+
237
+ It is a signal to **stop work** on, not a frame-accurate one: nothing that has
238
+ to be exact should read it. On X11, macOS and the headless mock it is `true`
239
+ and stays there; `obscured` is the question to ask there instead. The Wayland
240
+ backend also says it once on stderr in development; see
241
+ [wayland-backend.md](wayland-backend.md#a-surface-the-compositor-stops-showing-stops-drawing).
242
+
243
+ ---
244
+
245
+ ## `useIdle()` and `useKeepAwake()` — is anyone there
246
+
247
+ ```jsx
248
+ const away = useIdle(5 * 60_000);
249
+ <Avatar status={away ? 'away' : 'online'} />;
250
+ ```
251
+
252
+ Idleness is the **whole display's**, not this window's: it counts input on
253
+ every device, whichever application it went to. That is the right meaning for
254
+ a presence indicator, an auto-save, or a dashboard that stops refreshing when
255
+ the desk is empty — and the wrong one for "has the user ignored _my_ window",
256
+ which is `useWindowState().focused`.
257
+
258
+ - **Xorg**, with an `IDLETIME` counter: SYNC alarms fire when the counter
259
+ crosses `timeout` and when input pulls it back — **no timer at all**.
260
+ - **XQuartz**, with none: MIT-SCREEN-SAVER, polled on a schedule against the
261
+ remaining time rather than on a tick.
262
+ - **Windows**: `GetLastInputInfo`, the whole session's, on the same schedule.
263
+ - **macOS**: the session's idle time, on the same schedule — the bridge's
264
+ `lastInputMs()`, or else `ioreg`'s `HIDIdleTime`, a child process per check
265
+ (one per `timeout` while the user is there; one every quarter of it, at most
266
+ every half minute, while away).
267
+ - **An X display with neither extension**: always `false`.
268
+
269
+ Nothing is armed until a component subscribes, and the alarm or the poll
270
+ stops when the last subscriber to that `timeout` unmounts. `timeout` is a
271
+ dependency, so hold it in a constant rather than building it inline from state
272
+ that moves.
273
+
274
+ ### Keeping the screen on
275
+
276
+ ```jsx
277
+ useKeepAwake(playing, 'Playing a video');
278
+ ```
279
+
280
+ Held while the flag is true and the component is mounted, released on either —
281
+ including an unmount mid-playback, the case that leaves a desktop permanently
282
+ un-blanking when this is done by hand.
283
+
284
+ - **Windows**: `SetThreadExecutionState` with `ES_CONTINUOUS`
285
+ ([windows-integrations.md](windows-integrations.md)) — the display **and**
286
+ the system; counted, so one caller finishing does not release another's.
287
+ - **Elsewhere**: the first that answers of the desktop portal's `Inhibit`,
288
+ `org.freedesktop.ScreenSaver`, and `ScreenSaverSuspend` (a plain X request,
289
+ no session bus). These inhibit **screen blanking only** — nothing stops a
290
+ suspend.
291
+
292
+ `reason` is shown by desktops that list what is holding the screen on, so
293
+ write it as a sentence about the app's state rather than the app's name, which
294
+ they already show. Windows takes no string — `powercfg /requests` names the
295
+ process — so there it is dropped.
296
+
297
+ Failure is silent by design: a machine with no portal, no screensaver service
298
+ and no MIT-SCREEN-SAVER cannot be asked. `keepAwake()` is the imperative twin,
299
+ and resolves to the release.
300
+
301
+ ---
302
+
303
+ ## `useKeyboardState()` — the locks and the layout
304
+
305
+ ```jsx
306
+ const { capsLock, layout } = useKeyboardState();
307
+
308
+ <PasswordInput value={password} onChange={setPassword} />;
309
+ {
310
+ capsLock && <text style={{ color: theme.warning }}>Caps Lock is on</text>;
311
+ }
312
+ ```
313
+
314
+ | | |
315
+ | -------------------- | ------------------------------------------ |
316
+ | `capsLock` `numLock` | on or off, **now** |
317
+ | `group` | the active XKB group, `0`–`3` |
318
+ | `layout` | that group's layout code, `'ru'` — or null |
319
+ | `layouts` | every configured layout, `['us', 'ru']` |
320
+
321
+ The locks are true of the **keyboard**, not of an event: a key event carries
322
+ the modifier state at the moment it was pressed, which is no help to a
323
+ password field that wants to warn while merely focused, before anything has
324
+ been typed.
325
+
326
+ The layout is for a status-bar indicator, and for a password typed in the
327
+ wrong script, which looks identical to a wrong password. `layout` is a code
328
+ (`'ru'`, not `'Russian'`) because that is what an indicator shows; uppercase it
329
+ for display.
330
+
331
+ Everything here needs **XKB**, which Xorg and XQuartz both have. Where it is
332
+ missing the locks read false and `layouts` is empty — so `layouts` is the
333
+ field to check when "off" and "not known" differ. Changes arrive with no
334
+ polling, even while another application has the keyboard.
335
+
336
+ The [native Wayland backend](wayland-backend.md) answers the same fields from
337
+ its seat. A compositor tells only the focused client, so a lock toggled while
338
+ another app has the keyboard is seen when focus returns — for a password
339
+ field, the window is focused already.
340
+
341
+ Caps Lock is `LockMask`; Num Lock is taken to be `Mod2`, where every Linux and
342
+ BSD desktop and XQuartz put it.
343
+
344
+ ---
345
+
346
+ ## `useDesktopSettings()` — how the desktop wants an app to feel
347
+
348
+ ```jsx
349
+ const { doubleClickMs, dragThreshold } = useDesktopSettings();
350
+ ```
351
+
352
+ | | |
353
+ | --------------------- | -------------------------------------------------------------------- |
354
+ | `caretBlink` | **false means do not blink at all** |
355
+ | `caretBlinkMs` | how long a caret stays in each state (530) |
356
+ | `doubleClickMs` | how long a second click has to arrive in (400) |
357
+ | `doubleClickDistance` | and how far it may land from the first (4px) |
358
+ | `dragThreshold` | how far a press moves before it is a drag (4px) |
359
+ | `animations` | **false means reduce motion** — see below |
360
+ | `source` | `'xsettings'`, `'portal'`, `'macos'`, or null where nothing answered |
361
+
362
+ **The built-in controls already follow these.** `<textinput>`'s caret, the
363
+ click counting behind `ev.detail`, and the drag threshold all read the same
364
+ values. The hook is for an app drawing its own: a `<canvas>` with a text
365
+ cursor in it, a custom gesture, a diagram editor with its own drag.
366
+
367
+ `animations` is the desktop's "reduce motion" — GTK's
368
+ `gtk-enable-animations` on X11. **Looping animations already
369
+ honour it** — a [`style.animation`](styling.md#loops) never starts while it is
370
+ false, so `<ProgressBar indeterminate>` and anything else built on the loop
371
+ primitive comes to rest. Transitions are left alone: a 120ms fade that ends is
372
+ not the unbounded motion the setting exists for. Read the field where the
373
+ _resting_ look is your decision — a spinner that does not spin should still
374
+ say it is working, which `<ProgressBar indeterminate>` does by filling its
375
+ track instead.
376
+
377
+ `caretBlink` is not cosmetic: a moving thing on screen is what some people
378
+ cannot read past. **Draw the caret solid rather than not at all** — that is
379
+ what the built-in controls do.
380
+
381
+ Where the values come from:
382
+
383
+ - **X11** (`'xsettings'`): [XSETTINGS](appearance.md#what-each-rung-can-actually-answer),
384
+ the only source; there is no standard portal interface for any of this.
385
+ - **[Native Wayland](wayland-backend.md)** (`'portal'`): the settings portal's
386
+ GNOME keys — `org.gnome.desktop.interface` (`cursor-blink`,
387
+ `cursor-blink-time`, `enable-animations`) and
388
+ `org.gnome.desktop.peripherals.mouse` (`double-click`, `drag-threshold`) —
389
+ live, while `createRoot({ desktop })` leaves `appearance` on. A portal that
390
+ does not pass them through (KDE's, a bare compositor's) leaves the defaults.
391
+ - **macOS** (`'macos'`): **Reduce motion** only (System Settings ›
392
+ Accessibility › Display), as `animations: false`, live — flipping it stops
393
+ a spinner already going round. The other fields keep the defaults.
394
+ - **No settings daemon** — a bare `startx`, most window managers, XQuartz
395
+ (`null`): this renderer's own defaults above, deliberately not GTK's 1200ms
396
+ blink and 8px drag.
397
+
398
+ `createRoot()` starts XSETTINGS but does not wait for it, so a field focused
399
+ on the very first frame gets the built-in default, and the desktop's cadence
400
+ from its next focus onward.
401
+
402
+ ---
403
+
404
+ ## `createSettings()` — what the app remembers
405
+
406
+ The desktop's settings are read above; an app's own — the volume it was left
407
+ at, the theme it was switched to — are kept here, in one JSON file per app in
408
+ the per-user directory for it:
409
+
410
+ ```jsx
411
+ import { createSettings } from 'react-x11';
412
+
413
+ const settings = createSettings({
414
+ appId: 'com.example.hush',
415
+ defaults: { noiseType: 'brown', volume: 0.5, dark: false },
416
+ });
417
+
418
+ function Volume() {
419
+ const [volume, setVolume] = settings.use('volume');
420
+ return <Slider value={volume} onChange={setVolume} />;
421
+ }
422
+ ```
423
+
424
+ `settings.use(key)` is a `useState` every component shares: every component
425
+ using the key, in any window, sees one value, and setting it from one moves
426
+ them all. It takes an updater function too, and `settings.use(key, fallback)`
427
+ covers a key with no default.
428
+
429
+ | platform | file |
430
+ | --------- | -------------------------------------------------------- |
431
+ | macOS | `~/Library/Application Support/<appId>/settings.json` |
432
+ | elsewhere | `$XDG_CONFIG_HOME/<appId>/settings.json` (`~/.config/…`) |
433
+
434
+ The `appId` is a reverse-DNS name no other app uses, the shape
435
+ [`registerApplication`](uri-schemes.md) takes; the store registers nothing
436
+ with it. Creating the store twice with the same id — in a module a hot reload
437
+ evaluates again, or in two call sites — is one store with one set of values.
438
+
439
+ **It is read synchronously, the first time a value is asked for**, so the
440
+ first render already has what was saved rather than the defaults and then a
441
+ jump. It is written asynchronously:
442
+
443
+ - **coalesced** — a quarter second after the last change, and at least once a
444
+ second while changes keep coming, so a drag on a slider is a few writes and
445
+ not one per pointer move;
446
+ - **atomically** — into a temporary file renamed over the old one, so a crash
447
+ mid-write leaves the file as it was;
448
+ - **on exit** — whatever is still unsaved when the process exits is written
449
+ on the way out, including a change whose write was in flight.
450
+ `await settings.flush()` writes it now.
451
+
452
+ Outside React, `settings.get(key)`, `settings.set(key, value)`,
453
+ `settings.reset(key)` — the default again — and `settings.subscribe(listener)`
454
+ are the same store. A value is whatever JSON can keep; one it cannot, a
455
+ function or a `BigInt`, is a `TypeError` at `set`. A file that is not JSON is
456
+ reported once and the defaults stand; the next change replaces it.
457
+
458
+ Two copies of the app running at once share the file, and the last write
459
+ wins: nothing watches for another process changing it.
460
+
461
+ ---
462
+
463
+ ## `useLocale()` — the language and its conventions
464
+
465
+ ```jsx
466
+ const { locale, weekStartsOn } = useLocale();
467
+
468
+ // a date grid — `@react-x11/components`' <Calendar> takes both
469
+ <Calendar locale={locale} weekStartsOn={weekStartsOn} />;
470
+ <text>{new Intl.NumberFormat(locale).format(total)}</text>;
471
+ ```
472
+
473
+ | | |
474
+ | -------------- | ----------------------------------------------------------- |
475
+ | `locale` | a BCP-47 tag: `'en-GB'`, `'ru-RU'` |
476
+ | `direction` | `'ltr'` or `'rtl'` |
477
+ | `weekStartsOn` | `0` Sunday … `6` Saturday, from CLDR |
478
+ | `timeZone` | `'Europe/London'`, or null |
479
+ | `source` | `'env'` when `LANG` and friends said, `'intl'` when ICU did |
480
+
481
+ **`LC_ALL` / `LC_MESSAGES` / `LANG` win over `Intl`'s own answer**, which is
482
+ whatever ICU has compiled in — a small-ICU build of node reports `en-US`
483
+ everywhere. `C` and `POSIX` are the _absence_ of a locale, so they fall
484
+ through to ICU rather than becoming a tag.
485
+
486
+ **This does not change while the app runs**, and there is no subscription
487
+ behind it: the environment is fixed at exec, so a desktop that switches
488
+ language does so for the _next_ login.
489
+
490
+ For text direction inside a component prefer `useDirection()` over
491
+ `useLocale().direction` — it also honours `<ThemeProvider direction>` and the
492
+ `direction` style property, which the locale knows nothing about. See
493
+ [styling](styling.md). The default theme is already seeded from the locale, so
494
+ an app started under `LANG=ar_EG.UTF-8` mirrors without being told to.
495
+
496
+ ---
497
+
498
+ ## What is not here
499
+
500
+ - **Network status** — not built: there is no X-level answer, and a failed
501
+ request is a better truth than a flag that says the link is up.
502
+ - **Battery, brightness, volume, media keys** — D-Bus services a third party
503
+ can wrap without anything from here ([dbus.md](dbus.md)).
504
+ - **Elsewhere:** notifications ([notifications.md](notifications.md)); the
505
+ tray, [`useTray()`](desktop.md#the-tray); a launcher badge,
506
+ [`useBadge()`](desktop.md#the-badge); attention — the taskbar urgency or
507
+ Dock bounce — `states: ['demands_attention']`
508
+ ([desktop.md](desktop.md#attention)); the user's calendar,
509
+ `useDesktopCalendarEvents()` ([desktop-calendar.md](desktop-calendar.md));
510
+ camera/mic/location authorization, `usePermission()`
511
+ ([permissions.md](permissions.md)) — macOS TCC today, not yet the
512
+ freedesktop device portals.
513
+ - **Session lifecycle** — "we are suspending, save now", "I have unsaved work,
514
+ do not log out yet" — not built. On the cocoa backend a quit request from
515
+ the Dock, ⌘Q or a logout already reaches the primary window's
516
+ `onCloseRequest` ([uri-schemes.md](uri-schemes.md), "On macOS").
517
+
518
+ Why, and what was considered:
519
+ [design/system.md](../design/system.md#what-is-not-here).