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,164 @@
1
+ # The eyedropper
2
+
3
+ Sample one pixel from the screen — any window, any application, the
4
+ wallpaper — and get a colour an app can paint with.
5
+
6
+ ```jsx
7
+ import { useEyedropper } from 'react-x11';
8
+
9
+ function StrokePicker({ value, onChange }) {
10
+ const eyedropper = useEyedropper();
11
+
12
+ return (
13
+ <Button
14
+ label="Pick from screen"
15
+ disabled={eyedropper.picking}
16
+ onPress={async () => {
17
+ const hex = await eyedropper.pick();
18
+ if (hex) onChange(hex); // '#rrggbb'
19
+ }}
20
+ />
21
+ );
22
+ }
23
+ ```
24
+
25
+ Cancelling — Escape, the desktop dialog's own cancel, a dismissed macOS
26
+ sampler — resolves to `null`. It is an ordinary outcome, not an exception,
27
+ on every rung, so the whole error path is one `if`, as with the
28
+ [file dialogs](filedialog.md).
29
+
30
+ ## The ladder
31
+
32
+ | | | |
33
+ | --- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | 1 | **the system sampler** | `NSColorSampler` on the [cocoa backend](macos.md) (`@windowkit/appkit` >= 0.9). macOS draws the loupe out of process, so the app needs no Screen Recording grant and the user gets the usual Mac magnifier. |
35
+ | 2 | **the portal** | `org.freedesktop.portal.Screenshot.PickColor` over D-Bus. The desktop draws its own magnifier and hands the colour back. GNOME and KDE ship it; it is also GNOME's only route on Wayland. |
36
+ | 3 | **the Wayland picker** | On the [Wayland backend](wayland-backend.md) where the compositor offers screen capture and layer-shell but no portal (wlroots: sway, labwc, …): the output is frozen into a capture overlay and the click is taken on that. |
37
+ | 4 | **X11** | Grab the pointer with a crosshair, wait for the click, read the pixel under it. Works under a bare WM, over ssh, on XQuartz — everywhere there is a display and nothing else. |
38
+
39
+ `screenColorBackend()` reports which rung this machine lands on without
40
+ grabbing anything; `useEyedropper().supported` is the same answer as render
41
+ state.
42
+
43
+ There is no rung that react-x11 draws itself — the screen is exactly what an
44
+ app cannot draw — so the ladder can run out. Where it does, `supported` is
45
+ `false` and a pick rejects with a typed error, never a crash. A cocoa app on a
46
+ bridge **older than 0.9** is that place ([macos.md](macos.md)): hide the
47
+ button there.
48
+
49
+ ### The version gate
50
+
51
+ `PickColor` arrived in **version 2** of the Screenshot interface, and a portal
52
+ on the bus may not have it (XFCE's ships no Screenshot interface at all). The
53
+ probe reads the interface's own `version` with `portalVersion()`, which is
54
+ exported:
55
+
56
+ ```js
57
+ import { portalVersion } from 'react-x11';
58
+
59
+ const v = await portalVersion('org.freedesktop.portal.Screenshot');
60
+ // 0 — not there at all; 1 — there, but before PickColor; 2+ — usable
61
+ ```
62
+
63
+ Below 2, the ladder falls through to the X11 rung.
64
+
65
+ ## `useEyedropper()`
66
+
67
+ ```ts
68
+ const { pick, supported, picking } = useEyedropper(defaults?);
69
+
70
+ pick(options?): Promise<string | null>; // '#rrggbb', or null on cancel
71
+ supported: boolean; // screenColorBackend() !== null, resolved for you
72
+ picking: boolean; // a pick is in flight — the button's pressed state
73
+ ```
74
+
75
+ | option | |
76
+ | -------------- | --------------------------------------------------------- |
77
+ | `parentWindow` | override the owner window; inferred otherwise |
78
+ | `signal` | an `AbortSignal` that ends the pick and releases the grab |
79
+ | `backend` | force a rung; the seam for a kiosk, and for tests |
80
+
81
+ Options given to the hook are defaults for every pick; options given to a
82
+ call win. The portal's dialog is parented to the window the component is in,
83
+ resolved when the pick starts — the
84
+ [`useFileDialog()` inference](filedialog.md#the-dialog-parents-itself), with
85
+ `parentWindow` as the override for a tree with several top-level windows.
86
+
87
+ - **`picking` is the pressed state.** While it is true, a second `pick()`
88
+ returns the promise already in flight rather than starting a second grab,
89
+ so a double-clicked button cannot make the user click twice.
90
+ - **`supported` is almost always true.** An X11 tree's connection _is_ the
91
+ fallback rung, and a cocoa tree has the system sampler. It goes false when a
92
+ forced `backend: 'portal'` finds no portal, and on a cocoa app whose bridge
93
+ predates the sampler.
94
+
95
+ ## The bare function
96
+
97
+ ```js
98
+ import { pickScreenColor, screenColorBackend } from 'react-x11';
99
+
100
+ const hex = await pickScreenColor({ app });
101
+ ```
102
+
103
+ Same options, same results. The one addition is `app`, because a bare function
104
+ has no tree to take a connection from: pass the app `createRoot()` returned
105
+ (or `useApp()` in a component, or a `parentWindow` that points at a mounted
106
+ node). With no app named, the connections the renderer is currently drawing
107
+ through are asked. Where no rung answers it rejects with `NoScreenColorError`
108
+ — a **typed** rejection, so it reads as "hide the button", not as a crash.
109
+
110
+ One pick per connection at a time **on the X11 rung**: a second concurrent
111
+ `pickScreenColor()` on the same app is refused loudly (two grabs from one
112
+ client would steal each other's click). The hook's shared in-flight promise is
113
+ the friendly version of the same rule. On the system sampler a second pick
114
+ joins the loupe that is up, and both callers get the same colour.
115
+
116
+ ## What differs between the rungs
117
+
118
+ - **The system rungs draw a magnifier; the X11 rung does not.** On X11 it is
119
+ grab, crosshair, click — and Escape cancels.
120
+ - **Only the system rungs need no permission.** The portal and
121
+ `NSColorSampler` run the picker in another process, so nothing asks for a
122
+ screen-capture grant (on macOS, no TCC prompt for Screen Recording).
123
+ - **On the X11 rung the keyboard also picks.** Return, KP Enter or space
124
+ samples the pixel under the pointer without a click, and the keyboard is
125
+ grabbed for the duration so Escape works wherever the focus is.
126
+ - **What is sampled is what is on the screen.** The X11 rung reads the
127
+ rendered pixel, so a colour under a translucent overlay is the blend, not
128
+ the window's own value. The portal's pickers (GNOME's, KDE's) read the
129
+ composited screen too.
130
+ - **`signal` means less on the cocoa rung.** AppKit cannot dismiss a sampler
131
+ from code, so an abort there ends the wait and nothing else: the loupe stays
132
+ up until the user picks or presses Escape, its colour is dropped, and until
133
+ then the pending sample holds the process's event loop open. On the portal
134
+ and X11 rungs an abort closes the request or releases the grab.
135
+ - **Every rung answers `'#rrggbb'`**, converted from the portal's and
136
+ sampler's sRGB floats or X11's server-layout pixel.
137
+
138
+ ## The grab, and what it displaces
139
+
140
+ The X11 rung holds a **server pointer grab** while it waits, and core owns its
141
+ whole lifecycle:
142
+
143
+ - Every way out — click, Escape, abort, error — releases the grab before the
144
+ promise settles.
145
+ - A `<popup grab>` that was open when the pick started (an open `Select`, a
146
+ menu, the colour panel the button sits in) gets its grab back when the pick
147
+ ends, so dismiss-on-outside-click still works afterwards.
148
+ - If this client's own popup teardown releases the grab mid-pick, the pick
149
+ takes it back rather than hanging.
150
+
151
+ Design notes: [design/eyedropper.md](../design/eyedropper.md).
152
+
153
+ ## Running it anywhere
154
+
155
+ | where | rung |
156
+ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------- |
157
+ | macOS, the cocoa backend, `@windowkit/appkit` >= 0.9 | cocoa — `NSColorSampler` ([macos.md](macos.md)) |
158
+ | macOS, the cocoa backend, an older bridge | none — `supported` is false, `pickScreenColor()` rejects typed |
159
+ | GNOME / KDE, or any portal with Screenshot ≥ 2 | portal |
160
+ | XFCE (portal present, no Screenshot interface) | X11 |
161
+ | ssh, `startx`, XQuartz, a container with a display | X11 |
162
+ | Node 20, where npm skips `dbus-native` | X11 |
163
+ | the Wayland backend, a portal with Screenshot ≥ 2 | portal |
164
+ | the Wayland backend on wlroots (sway, labwc, …) | wayland — the backend's own picker; there is no root window to read, so no X11 rung under it |
@@ -0,0 +1,222 @@
1
+ # File dialogs
2
+
3
+ Open a file, save a file, pick a folder — on whatever the machine has.
4
+
5
+ ```jsx
6
+ import { useFileDialog } from 'react-x11';
7
+
8
+ function Toolbar() {
9
+ const { openFile } = useFileDialog();
10
+
11
+ return (
12
+ <Button
13
+ label="Open…"
14
+ onPress={async () => {
15
+ const files = await openFile({
16
+ filters: [{ name: 'Markdown', extensions: ['md'] }],
17
+ });
18
+ if (!files) return; // cancelled
19
+ load(files[0]);
20
+ }}
21
+ />
22
+ );
23
+ }
24
+ ```
25
+
26
+ Cancelling resolves to `null`. It is an ordinary outcome, not an exception, on
27
+ every backend — so the whole error path for a file dialog is one `if`.
28
+
29
+ ## The ladder
30
+
31
+ The rung is chosen for you, top to bottom:
32
+
33
+ | | | |
34
+ | --- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | 1 | **the native panel** | `NSOpenPanel` / `NSSavePanel` on the [cocoa backend](macos.md) (`@windowkit/appkit` ≥ 0.5): a **sheet** on the window that asked, with every filter the OS type database knows. On the [win32 backend](windows.md), Windows' own open/save dialog, owned by the window that asked, takes the same rung (`fileDialogBackend()` reports `'cocoa'` for both). |
36
+ | 2 | **the portal** | `org.freedesktop.portal.FileChooser` over D-Bus: the desktop's own GTK or KDE dialog, with the user's bookmarks and recent files. |
37
+ | 3 | **`osascript`** | macOS on the X11 backend with no portal (every ordinary XQuartz install). `choose file` _is_ `NSOpenPanel`, in a third process. |
38
+ | 4 | **the built-in dialog** | A browser react-x11 draws itself. ssh, a bare `startx`, a container: everywhere there is a display and nothing else. |
39
+
40
+ The top rung is chosen by the window the dialog belongs to, never by naming a
41
+ backend. `fileDialogBackend()` reports which rung this machine lands on,
42
+ without showing anything — for a diagnostics panel, or a menu that wants to
43
+ say so.
44
+
45
+ ## `useFileDialog()`
46
+
47
+ ```ts
48
+ const { openFile, saveFile, selectFolder } = useFileDialog(defaults?);
49
+
50
+ openFile(options?): Promise<string[] | null>; // absolute paths
51
+ saveFile(options?): Promise<string | null>; // one path, may not exist yet
52
+ selectFolder(options?): Promise<string[] | null>;
53
+ ```
54
+
55
+ The hook is the surface to reach for: it has a tree to draw the built-in dialog
56
+ in, so it never runs out of rungs and never rejects for lack of a dialog.
57
+
58
+ | option | |
59
+ | ----------------------------- | ------------------------------------------------- |
60
+ | `title` | the dialog's title |
61
+ | `multiple` | several files at once; ignored when saving |
62
+ | `filters` | `{ name, extensions?, mimeTypes? }[]` |
63
+ | `defaultFolder` | where it opens |
64
+ | `defaultName` / `defaultPath` | what a save dialog starts on |
65
+ | `acceptLabel` | the confirm button's text — 'Import', 'Attach', … |
66
+ | `parentWindow` | override the owner window; inferred otherwise |
67
+ | `signal` | an `AbortSignal` that closes the dialog |
68
+ | `backend` | force a rung; the seam for a kiosk, and for tests |
69
+
70
+ Options given to the hook are defaults for every call; options given to a call
71
+ win.
72
+
73
+ ### The dialog parents itself
74
+
75
+ The dialog is `transientFor` the window the component is in (`parent_window`
76
+ for the portal), so the window manager stacks it above your window and keeps
77
+ it out of the task switcher. **You do not pass anything for that**:
78
+ `useFileDialog()` resolves the owner window through `useTopLevelWindow()` when
79
+ the dialog opens.
80
+
81
+ On the [native Wayland backend](wayland-backend.md) the portal is handed an
82
+ **xdg-foreign** export of the window (`wayland:<handle>`), which GNOME and KDE
83
+ both parent to. Where the compositor has no xdg-foreign the dialog opens
84
+ unparented.
85
+
86
+ The owner is inferred from the top-level windows this connection is rendering:
87
+
88
+ - **one top-level window** — that one; nearly every app.
89
+ - several, one focused — the focused one.
90
+ - several with nothing to separate them — the most recently opened, plus a
91
+ development warning naming `parentWindow`.
92
+ - **none at all** — the root-level `<popup>` that took the keyboard (a
93
+ menu-bar app's popover,
94
+ [elements.md](elements.md#a-popover-under-a-tray-item)).
95
+
96
+ `parentWindow` is the **override for multi-window apps**, and takes a
97
+ `<window>` ref, an XID, or anything `windowIdOf()` accepts:
98
+
99
+ ```jsx
100
+ const win = useRef(null);
101
+ const { openFile } = useFileDialog({ parentWindow: win });
102
+ return <window ref={win}>…</window>;
103
+ ```
104
+
105
+ `useTopLevelWindow()` is exported for anything else that needs the owner
106
+ window. It returns a **ref-like object** — the window is not realized on the
107
+ first render, so a value read then would be `null` on the render that matters —
108
+ which drops into `parentWindow`, `transientFor`, or `windowIdOf()`.
109
+
110
+ **The portal never embeds the dialog.** It is a top-level window in another
111
+ process; the file list never appears inside your window.
112
+
113
+ Design notes: [design/filedialog.md](../design/filedialog.md).
114
+
115
+ ## The imperative functions
116
+
117
+ ```ts
118
+ import { openFile, saveFile, selectFolder } from 'react-x11';
119
+ ```
120
+
121
+ Same options, same results — but they can only reach the native panel, the
122
+ portal and `osascript`, because a function has nowhere to draw. Where there is
123
+ none of those they reject with `NoFileDialogError`, which is a **typed**
124
+ rejection: the signal to show your own UI, not a crash.
125
+
126
+ Reach for these from host-side code and event-loop glue that has no component
127
+ to hang off. In a component, use the hook.
128
+
129
+ ## What differs between the rungs
130
+
131
+ - **MIME-type filters do not reach `osascript`.** `extensions` translate
132
+ exactly to every rung; `mimeTypes` reach the portal and the native panel
133
+ (the cocoa backend maps `image/png` to `public.png` through the OS type
134
+ database) and are dropped by `osascript`, where a wrong UTI guess would hide
135
+ the user's file. Give `extensions` where you can.
136
+ - **`osascript` ignores `parentWindow`; the native panel is a sheet on it.**
137
+ The `osascript` panel appears over the app but is not attached to it;
138
+ application modality holds only because your code is awaiting the promise.
139
+ On the cocoa backend the panel is a **sheet**, and the app keeps running
140
+ while it is up. With no window at all — a bare `openFile()` with nothing in
141
+ `parentWindow` and no tree to infer one from — the native panel is
142
+ app-modal and **blocks the process**, timers included, until it is
143
+ dismissed. `useFileDialog()` always names the window, so it never lands
144
+ there.
145
+ - **The built-in dialog has never seen the user's bookmarks.** It offers the
146
+ filesystem, a filter, hidden files, and a path you can type into — not a
147
+ re-creation of GTK's chooser.
148
+ - **`multiple` in the built-in dialog is a tick column**, not ctrl-click.
149
+
150
+ In the built-in dialog a double click opens a directory or picks a file, Enter
151
+ does the same to the selected row, `Up` goes to the parent, and Escape cancels.
152
+
153
+ ## Running it where there is no bus
154
+
155
+ | where | rung |
156
+ | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
157
+ | macOS, the **cocoa backend** | the native panel |
158
+ | GNOME / KDE / any desktop with xdg-desktop-portal | portal |
159
+ | macOS + XQuartz, app running **on the Mac** | `osascript` |
160
+ | macOS + XQuartz, app running on a **remote Linux box** | built-in — `osascript` would run on the wrong machine, and the paths it returned would not exist where the app is |
161
+ | ssh, `startx`, a container, CI | built-in |
162
+ | Node 20, where npm skips `dbus-native` | built-in on Linux, `osascript` on a Mac |
163
+
164
+ The `osascript` rung is reached by _not_ finding a portal, so it also covers a
165
+ Mac that has a session bus for other reasons. A portal shim on a Mac wins on
166
+ the X11 backend; on the cocoa backend the native panel is above it either way.
167
+
168
+ ## The portal machinery, if you need it directly
169
+
170
+ `portalRequest()` is exported for building other portals on. It calls a portal
171
+ method that shows UI and resolves with its `Request.Response`:
172
+
173
+ ```js
174
+ const { response, results } = await portalRequest(busRef, {
175
+ iface: 'org.freedesktop.portal.FileChooser',
176
+ member: 'OpenFile',
177
+ parentWindow: 'x11:1a00007',
178
+ title: 'Open',
179
+ options: { multiple: true },
180
+ });
181
+ ```
182
+
183
+ It subscribes to the response before calling, so the answer cannot be lost;
184
+ an abort sends `Request.Close()` and settles the promise locally (the portal
185
+ emits no `Response` for it); and only the initial call has a deadline — the
186
+ answer has none, because a dialog can legitimately be open for an hour.
187
+
188
+ The default argument shape, `(s parent_window, s title, a{sv} options)`, is
189
+ FileChooser's alone. Any other Request-shaped portal states its leading
190
+ arguments as `signature` and `args` together; the
191
+ [eyedropper](eyedropper.md)'s `PickColor`, which has no title:
192
+
193
+ ```js
194
+ await portalRequest(busRef, {
195
+ iface: 'org.freedesktop.portal.Screenshot',
196
+ member: 'PickColor',
197
+ signature: 'sa{sv}',
198
+ args: [parentWindow],
199
+ });
200
+ ```
201
+
202
+ The signature must end in `a{sv}` (the options dict carries `handle_token`); a
203
+ method without one does not answer through a `Request` and wants a plain
204
+ `bus.invoke` instead.
205
+
206
+ `hasService(name)` answers whether a service is reachable — owned now **or
207
+ activatable on demand**. (`NameHasOwner` alone is the wrong question: the
208
+ portal is D-Bus-activatable and often has no owner until first use.)
209
+
210
+ `portalVersion(iface)` answers whether the portal's backends provide one
211
+ _interface_, and at what version — `0` when it is not there at all. Gate on
212
+ this rather than on the service: desktops provide some portals and not others
213
+ (XFCE has FileChooser and no Screenshot).
214
+
215
+ See [dbus.md](dbus.md) for the connection these sit on.
216
+
217
+ ## Seeing it
218
+
219
+ - `npm run examples:menu` — a File menu whose Open/Save entries are real, with
220
+ the rung this machine landed on shown in the window.
221
+ Force the bottom rung anywhere with `backend: 'builtin'`, which is also how the
222
+ tests exercise it.