react-x11 2.51.0 → 2.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +10 -3
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/xkb.js +137 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Click to component
|
|
2
|
+
|
|
3
|
+
Alt+Click any rendered element to jump straight to the JSX line that created
|
|
4
|
+
it, in your editor. On macOS that's **Option+Click**, on both backends: the
|
|
5
|
+
Cocoa backend maps ⌥ to Mod1 itself, and XQuartz maps it there by default
|
|
6
|
+
("Option keys send Alt_L and Alt_R" in its Input preferences). The
|
|
7
|
+
installed-feature log line says whichever one applies to your platform.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
Under `react-x11 dev` it is already on. Elsewhere:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
REACT_X11_EDITOR=code npm run examples:tasks # naming an editor enables it
|
|
15
|
+
# or, to use the default (cursor):
|
|
16
|
+
REACT_X11_CLICK_TO_COMPONENT=1 npm run examples:tasks
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Alt/Option+Click a task row, a button, anything — its owning component's source
|
|
20
|
+
opens at the exact line, the clicked element flashes blue for a moment, and
|
|
21
|
+
the console prints what was resolved:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
[click-to-component] TaskRow → examples/tasks.jsx:42:7
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Alt+Shift+Click also logs the full ownership chain (who rendered who, up to
|
|
28
|
+
the root) to the console — handy when the immediate owner is a shared
|
|
29
|
+
wrapper rather than the component you actually meant.
|
|
30
|
+
|
|
31
|
+
## Elements you didn't write
|
|
32
|
+
|
|
33
|
+
An element rendered by an installed component — a design system's
|
|
34
|
+
`<Toolbar>`, a chart library's internals — has its JSX call site inside that
|
|
35
|
+
package, not in your code. Clicking one resolves to the nearest owner up the
|
|
36
|
+
chain that _is_ your source: the `<Toolbar ... />` line in your file that put
|
|
37
|
+
it on screen. The console line says how far it had to climb, so a location
|
|
38
|
+
that doesn't match what you clicked is never a surprise:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
[click-to-component] App → src/ui/App.tsx:432:7 (1 owner up from the clicked <box>)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
When no owner in the chain is application source (a screen rendered entirely
|
|
45
|
+
by a package), the package's own file is opened instead and the line is
|
|
46
|
+
marked `— inside an installed package`. Only a tree with no React debug info
|
|
47
|
+
at all resolves to nothing.
|
|
48
|
+
|
|
49
|
+
With React DevTools attached as well (`REACT_X11_DEVTOOLS=1`, see
|
|
50
|
+
[devtools.md](devtools.md)), the same click also selects that element in the
|
|
51
|
+
DevTools tree — one click to the source _and_ to its props. Nothing changes
|
|
52
|
+
when DevTools is absent, which is the usual case.
|
|
53
|
+
|
|
54
|
+
`REACT_X11_EDITOR` picks the editor (`cursor` by default): `code`,
|
|
55
|
+
`code-insiders`, `windsurf`, `vim`, `nvim`, or any other value, treated as a
|
|
56
|
+
custom URI scheme. `react-x11 dev`'s error window opens a line the same way,
|
|
57
|
+
through the same variable (`src/editor.js`, shared by both).
|
|
58
|
+
|
|
59
|
+
## How it works
|
|
60
|
+
|
|
61
|
+
`src/ClickToComponent.js`, installed from `Reconciler.js` when either
|
|
62
|
+
`REACT_X11_CLICK_TO_COMPONENT` or `REACT_X11_EDITOR` is set, and
|
|
63
|
+
`REACT_X11_CLICK_TO_COMPONENT` is not `0` — the way to turn it off under
|
|
64
|
+
`react-x11 dev`, which sets it to `1` when it is unset. No
|
|
65
|
+
babel/jsx-source plugin, no build-time instrumentation:
|
|
66
|
+
|
|
67
|
+
- every host node already carries a reference to the fiber that created it
|
|
68
|
+
(`node._reactFiber`, set in `createInstance`). It is this feature's own
|
|
69
|
+
back-reference, not a DevTools one: `findFiberByHostInstance` is gone
|
|
70
|
+
from react-reconciler 0.33, and click-to-component works with DevTools
|
|
71
|
+
absent;
|
|
72
|
+
- React 19 captures a real `Error` at every JSX call site in development
|
|
73
|
+
mode (`fiber._debugStack`) and tracks who rendered what
|
|
74
|
+
(`fiber._debugOwner`) — no `__source`/`__self` babel transform needed;
|
|
75
|
+
- Node's `--enable-source-maps` (already on for both the plain `tsx`
|
|
76
|
+
examples and `examples:tasks:hot`'s loader) has rewritten that Error's
|
|
77
|
+
stack to original source before we ever see it, so resolving a location
|
|
78
|
+
is just parsing stack-trace text and skipping the frames React and this
|
|
79
|
+
renderer add on top of the call site (`react`, `react-reconciler`,
|
|
80
|
+
`scheduler`, `react-x11`'s own `src/`) — no source-map library required
|
|
81
|
+
(this is the one place
|
|
82
|
+
the design diverges from a browser DOM version of the same idea: a
|
|
83
|
+
browser doesn't remap `Error.stack` for you, so a tool like
|
|
84
|
+
[show-component](https://github.com/sidorares/show-component) has to
|
|
85
|
+
resolve source maps itself at runtime).
|
|
86
|
+
|
|
87
|
+
Frames from _other_ packages are deliberately kept rather than skipped: a
|
|
88
|
+
call site inside an installed component is a real location, it just isn't
|
|
89
|
+
one in your source, and telling those apart is what lets the resolver climb
|
|
90
|
+
the `_debugOwner` chain to the line that is. Skipping every `node_modules`
|
|
91
|
+
frame instead walks past the library component entirely and lands on
|
|
92
|
+
whichever frame of yours happens to be deeper in the render stack — usually
|
|
93
|
+
the `render()` call at startup, which looks like an answer and isn't.
|
|
94
|
+
|
|
95
|
+
Alt+Click is recognized in `EventManager._onMouseDown`
|
|
96
|
+
(`src/events.js`) — `native.buttons & MOD.Alt` is X11's `Mod1Mask` bit, the
|
|
97
|
+
same `buttons` bitmask every synthetic event's `altKey` reads — ahead of the
|
|
98
|
+
normal press handling, so it never also starts a drag or moves focus.
|
|
99
|
+
|
|
100
|
+
GUI editors (cursor/code/code-insiders/windsurf, or any unrecognized
|
|
101
|
+
`REACT_X11_EDITOR` value treated as a raw scheme) are opened by navigating
|
|
102
|
+
their registered URI scheme — `open cursor://file/abs/path:line:col` on
|
|
103
|
+
macOS, `xdg-open` elsewhere — the same idea show-component uses in a
|
|
104
|
+
browser via `window.location.href`, just handed to the OS instead. This is
|
|
105
|
+
noticeably faster than spawning the editor's CLI shim directly: a CLI like
|
|
106
|
+
`cursor -g ...` is a wrapper script that re-detects and re-execs the real
|
|
107
|
+
app on every call, while the URI scheme is delivered straight to the
|
|
108
|
+
already-running instance by Launch Services / xdg-open. vim and nvim have
|
|
109
|
+
no URI scheme to navigate, so those are still spawned directly with a
|
|
110
|
+
`+call cursor(line, column)` argument.
|
|
111
|
+
|
|
112
|
+
## Caveats
|
|
113
|
+
|
|
114
|
+
- Needs React running in development mode (fiber debug fields are stripped
|
|
115
|
+
in production builds).
|
|
116
|
+
- The editor is launched detached, so a failure is only visible as a
|
|
117
|
+
warning: an editor whose URI scheme nothing claims (`REACT_X11_EDITOR`
|
|
118
|
+
naming an editor that isn't installed) prints
|
|
119
|
+
`click-to-component — \`open cursor://...\` exited 1` next to the
|
|
120
|
+
resolved location rather than silently doing nothing.
|
|
121
|
+
- A node's `_reactFiber` is only refreshed on mount, not on every update —
|
|
122
|
+
usually harmless (the JSX call site of a given element rarely moves
|
|
123
|
+
between renders of the same tree shape), but a node that was
|
|
124
|
+
conditionally re-typed can point at a stale-but-structurally-similar
|
|
125
|
+
fiber.
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# Clipboard
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
import { useClipboard } from 'react-x11';
|
|
5
|
+
|
|
6
|
+
function Editor({ value }) {
|
|
7
|
+
const clipboard = useClipboard();
|
|
8
|
+
return (
|
|
9
|
+
<box
|
|
10
|
+
onKeyDown={(e) => {
|
|
11
|
+
if (e.ctrlKey && e.key === 'c') clipboard.writeText(value);
|
|
12
|
+
}}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`<textinput>` and `<textarea>` already do all of this for you — Ctrl+C/X/V,
|
|
19
|
+
middle-click paste, select-to-own. This page is for everything else: a
|
|
20
|
+
canvas that copies an image, a list that pastes files, a document view with
|
|
21
|
+
its own Copy item.
|
|
22
|
+
|
|
23
|
+
> **On the Cocoa backend** the clipboard is `NSPasteboard.general`, and the
|
|
24
|
+
> difference from X11 is structural rather than cosmetic: X's model is
|
|
25
|
+
> _ownership of a selection_, negotiated per paste, and macOS's is a
|
|
26
|
+
> _pasteboard the system holds_. Everything below works there — writing,
|
|
27
|
+
> [reading](#reading) any type the pasteboard carries, `targets()` and
|
|
28
|
+
> [`watch()`](#knowing-when-it-changes) — with three differences, each
|
|
29
|
+
> described where it applies: [`PRIMARY`](#primary-is-x11-only) is X11-only
|
|
30
|
+
> rather than missing, since nothing on macOS is filled by selecting text;
|
|
31
|
+
> `targets()` names the pasteboard's types rather than an owner's atoms; and
|
|
32
|
+
> `watch()` asks the pasteboard's change count while something is watching,
|
|
33
|
+
> since nothing announces a copy. A [multi-flavour payload](#writing) is
|
|
34
|
+
> written whole when the bridge has `pasteboardWrite` (windowkit/appkit#119);
|
|
35
|
+
> an older bridge keeps the text and says once, in development, what it
|
|
36
|
+
> dropped.
|
|
37
|
+
|
|
38
|
+
## Two clipboards
|
|
39
|
+
|
|
40
|
+
X has no clipboard buffer. "Copy" means owning a **selection** — a named
|
|
41
|
+
thing the server tracks — and answering conversion requests from whoever
|
|
42
|
+
pastes. There are two in everyday use, and they are independent:
|
|
43
|
+
|
|
44
|
+
| selection | filled by | pasted with |
|
|
45
|
+
| ----------- | ------------------------------------------ | -------------------- |
|
|
46
|
+
| `CLIPBOARD` | an explicit Copy or Cut | Ctrl+V, an Edit menu |
|
|
47
|
+
| `PRIMARY` | _selecting text at all_, with no Copy step | middle click |
|
|
48
|
+
|
|
49
|
+
Every call below takes `{ selection }` and defaults to `CLIPBOARD`. The
|
|
50
|
+
X11-native behaviour is to write `PRIMARY` whenever your selection changes,
|
|
51
|
+
which is what the built-in text controls do.
|
|
52
|
+
|
|
53
|
+
Two consequences worth knowing, because neither matches a browser:
|
|
54
|
+
|
|
55
|
+
- **The data lives in this process.** Nothing is stored by the server, so
|
|
56
|
+
when the app exits the clipboard goes with it — normal X behaviour, and
|
|
57
|
+
the reason desktops run clipboard managers. Nothing negotiates with one
|
|
58
|
+
yet.
|
|
59
|
+
- **Someone else copying takes it away.** Ownership is exclusive; there is
|
|
60
|
+
no "clipboard history" to fall back on.
|
|
61
|
+
|
|
62
|
+
### `PRIMARY` is X11-only
|
|
63
|
+
|
|
64
|
+
macOS has one pasteboard, and it is `CLIPBOARD`'s: nothing there is filled
|
|
65
|
+
by selecting text, and a middle click pastes nothing. So on the Cocoa
|
|
66
|
+
backend every selection but `CLIPBOARD` — `PRIMARY`, `SECONDARY`, a name of
|
|
67
|
+
your own — is one nobody can paste from:
|
|
68
|
+
|
|
69
|
+
- `write()` and `writeText()` resolve and leave the pasteboard alone;
|
|
70
|
+
- `read()` answers `null`, `readText()` rejects and `targets()` is `[]` —
|
|
71
|
+
X's answers for a selection nobody owns;
|
|
72
|
+
- `clear()` leaves the pasteboard alone, and `watch()` never fires.
|
|
73
|
+
|
|
74
|
+
The **win32** backend answers every selection but `CLIPBOARD` the same way,
|
|
75
|
+
since Windows has no equivalent either (and writes only text there so far).
|
|
76
|
+
The **Wayland** backend is the other X-like one: it has `PRIMARY` too, over
|
|
77
|
+
the primary-selection protocol where the compositor offers it
|
|
78
|
+
(`src/wayland/clipboard.js`), so "X11-only" here means "not on macOS or
|
|
79
|
+
Windows".
|
|
80
|
+
|
|
81
|
+
That is what keeps selecting from being copying. A drag across a
|
|
82
|
+
`selectable` surface, a select-all, shift+arrows in a field: each takes
|
|
83
|
+
`PRIMARY` on X11, and on macOS none of them replaces what the user copied
|
|
84
|
+
somewhere else. Code written to the X11 convention needs no platform check.
|
|
85
|
+
|
|
86
|
+
`PRIMARY` is deliberately not kept inside the app instead. Its point is the
|
|
87
|
+
paste in _another_ application; all an in-process copy could keep is a
|
|
88
|
+
middle click that types into a text field, which no Mac text field does. An
|
|
89
|
+
app that wants select-to-copy on macOS anyway — a terminal emulator, say —
|
|
90
|
+
can write `CLIPBOARD` itself: a surface's `selectedText()` is the text, and
|
|
91
|
+
`onSelectionChange` says when it moved.
|
|
92
|
+
|
|
93
|
+
## `useClipboard()`
|
|
94
|
+
|
|
95
|
+
Returns the clipboard for the connection this tree renders onto. Stable, so
|
|
96
|
+
it is safe in a dependency array. Everything on it returns a promise.
|
|
97
|
+
|
|
98
|
+
### Writing
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
await clipboard.writeText('hello');
|
|
102
|
+
await clipboard.write({
|
|
103
|
+
'text/html': '<b>hello</b>',
|
|
104
|
+
'text/plain': 'hello',
|
|
105
|
+
'image/png': pngBytes,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`write()` takes a map of **type name to payload** — strings are encoded
|
|
110
|
+
UTF-8, `Buffer`s and typed arrays are served as they are. Offering several
|
|
111
|
+
flavours of the same thing is the point: a rich-text editor takes the HTML,
|
|
112
|
+
a terminal takes the plain text, and each asks for what it understands.
|
|
113
|
+
There is no size limit worth naming; large payloads transfer incrementally.
|
|
114
|
+
|
|
115
|
+
`clear()` gives the selection back, so nothing is served for it any more.
|
|
116
|
+
|
|
117
|
+
### Reading
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
const text = await clipboard.read('text'); // 'hello' | null
|
|
121
|
+
const files = await clipboard.readFiles(); // [{ uri, path? }]
|
|
122
|
+
const png = await clipboard.read('image/png'); // Uint8Array | null
|
|
123
|
+
const offered = await clipboard.targets(); // ['text/html', …]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**`read(type)` is the one to reach for.** It accepts a concrete type name or
|
|
127
|
+
a **group**, resolves it against what the owner actually offers, and decodes
|
|
128
|
+
text-ish types to a string. It answers `null` when the owner has nothing of
|
|
129
|
+
that kind, because "is there an image on the clipboard?" is a question, not
|
|
130
|
+
an error.
|
|
131
|
+
|
|
132
|
+
The groups exist because the same payload arrives under different names
|
|
133
|
+
depending on who copied it, and exact-match filtering silently misses most
|
|
134
|
+
of the desktop:
|
|
135
|
+
|
|
136
|
+
| group | matches |
|
|
137
|
+
| --------- | ---------------------------------------------------------------------------------------------------------- |
|
|
138
|
+
| `'text'` | `text/plain;charset=utf-8`, `UTF8_STRING`, `text/plain`, `STRING`, `TEXT`, `COMPOUND_TEXT` (in that order) |
|
|
139
|
+
| `'files'` | `text/uri-list`, `application/x-kde4-urilist`, `x-special/gnome-copied-files` |
|
|
140
|
+
| `'uris'` | `text/uri-list`, `text/x-moz-url`, `_NETSCAPE_URL` |
|
|
141
|
+
|
|
142
|
+
This is the same table [drag and drop](drag-and-drop.md) uses, and
|
|
143
|
+
deliberately so: the flavours a file manager offers on a drop are the ones
|
|
144
|
+
it offers on a copy. Groups do not overlap — a Firefox link is
|
|
145
|
+
`text/x-moz-url`, which is in `uris`, not `text`.
|
|
146
|
+
|
|
147
|
+
On the Cocoa backend `targets()` lists the pasteboard's types in these
|
|
148
|
+
names: a type with a MIME type under that (`public.png` is `image/png`,
|
|
149
|
+
Finder's `public.file-url` is `text/uri-list`), text under both its X and
|
|
150
|
+
MIME names, and any other type under its own identifier. That last rule is
|
|
151
|
+
what lets an app honour a password manager: one that copies a secret puts
|
|
152
|
+
[`org.nspasteboard.ConcealedType`](http://nspasteboard.org) beside it, the
|
|
153
|
+
macOS convention for "keep this out of clipboard history", and a clipboard
|
|
154
|
+
history should skip any copy whose `targets()` include it (or
|
|
155
|
+
`org.nspasteboard.TransientType`). Files copied in Finder are one item each,
|
|
156
|
+
and `read('files')` reads them as one uri-list.
|
|
157
|
+
|
|
158
|
+
Text is read through the bridge's own text verb. Anything else is read from
|
|
159
|
+
the pasteboard's items: through `pasteboardItems`/`pasteboardItemData` on a
|
|
160
|
+
bridge that has them, and otherwise through `osascript -l JavaScript`, one
|
|
161
|
+
short-lived child of about 60 ms per `targets()` or `read()`. The type list
|
|
162
|
+
is kept against the pasteboard's change count, so asking again with nothing
|
|
163
|
+
copied in between costs nothing.
|
|
164
|
+
|
|
165
|
+
`readFiles()` is `read('files')` with the RFC 2483 parsing applied: CRLF
|
|
166
|
+
separators, `#` comment lines, percent-decoding. `path` is present only for
|
|
167
|
+
genuinely local `file:` URIs — a remote `file://otherhost/…` gets a `uri`
|
|
168
|
+
and no `path` rather than a path that does not exist here.
|
|
169
|
+
|
|
170
|
+
`readText()` is the plain-text shortcut and asks only for `UTF8_STRING` then
|
|
171
|
+
`STRING`. It **rejects** when there is no owner or the owner offers neither,
|
|
172
|
+
where `read('text')` returns `null` and walks the whole preference list. If
|
|
173
|
+
you are not sure which you want, use `read('text')`.
|
|
174
|
+
|
|
175
|
+
### Knowing when it changes
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
let stop;
|
|
180
|
+
clipboard
|
|
181
|
+
.watch((ev) => setCanPaste(ev.owner !== 0))
|
|
182
|
+
.then((fn) => (stop = fn));
|
|
183
|
+
return () => stop?.();
|
|
184
|
+
}, [clipboard]);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The built-in edit menu on `<textinput>`/`<textarea>` already does this for
|
|
188
|
+
its Paste row — see [elements.md](elements.md#the-right-click-menu). This is
|
|
189
|
+
the same mechanism, for your own menus. If what you are building _is_ an edit
|
|
190
|
+
menu, open the standard one instead and the Paste row comes with it
|
|
191
|
+
([extending.md](extending.md#the-standard-edit-menu)).
|
|
192
|
+
|
|
193
|
+
`watch()` is a server-side subscription, not a poll — it costs nothing until
|
|
194
|
+
something changes. `owner === 0` means nothing is on the clipboard, which is
|
|
195
|
+
the case a Paste menu item wants. It rejects on a server without XFixes;
|
|
196
|
+
every X server since about 2004 has it.
|
|
197
|
+
|
|
198
|
+
On the Cocoa backend nothing announces a copy, so `watch()` is a poll after
|
|
199
|
+
all: it asks the pasteboard's change count, which every write bumps, every
|
|
200
|
+
250 ms while anything is watching, and stops with the last unsubscribe. The
|
|
201
|
+
count is a published number on the bridge, under a microsecond to read, and
|
|
202
|
+
a change costs one look at what the pasteboard now holds, to say whether
|
|
203
|
+
`owner` is `0`. No window owns the pasteboard, so `owner` is `-1` when it
|
|
204
|
+
holds anything; `ev.changeCount` is the pasteboard's own count. Two copies
|
|
205
|
+
inside one poll are one change, and the app's own copies are changes too, as
|
|
206
|
+
they are on X11. It rejects on a bridge without `pasteboardChangeCount`.
|
|
207
|
+
|
|
208
|
+
## Timestamps
|
|
209
|
+
|
|
210
|
+
X arbitrates between two clients copying at the same moment by **timestamp**,
|
|
211
|
+
and ICCCM is explicit that the right one is the event that caused the copy —
|
|
212
|
+
never "now". You do not have to pass one: react-x11 remembers the last input
|
|
213
|
+
event on the connection and uses it, so a copy from a Ctrl+C handler is
|
|
214
|
+
stamped with that keystroke. Pass `{ time }` if you have a better answer.
|
|
215
|
+
|
|
216
|
+
This is also why a `write()` can fail: if another client copied with a newer
|
|
217
|
+
timestamp, the server refuses ours, and the promise rejects rather than
|
|
218
|
+
pretending.
|
|
219
|
+
|
|
220
|
+
### Owning a selection of your own
|
|
221
|
+
|
|
222
|
+
The same rule governs every other selection — a system tray takes
|
|
223
|
+
`_NET_SYSTEM_TRAY_S<screen>`, a clipboard manager takes `CLIPBOARD_MANAGER`,
|
|
224
|
+
an input method takes `XIM_SERVERS` — and there the timestamp is yours to
|
|
225
|
+
supply. Both of the values worth supplying are exported:
|
|
226
|
+
|
|
227
|
+
```jsx
|
|
228
|
+
import { lastInputTime, serverTime, useApp } from 'react-x11';
|
|
229
|
+
|
|
230
|
+
const app = useApp();
|
|
231
|
+
|
|
232
|
+
// the user did something, and ICCCM wants that event's own time
|
|
233
|
+
const take = (wid, selection) =>
|
|
234
|
+
app.X.SetSelectionOwner(wid, selection, lastInputTime(app));
|
|
235
|
+
|
|
236
|
+
// nothing did — a tray takes its selection when it starts
|
|
237
|
+
const claim = async (wid, selection) =>
|
|
238
|
+
app.X.SetSelectionOwner(wid, selection, await serverTime(app));
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`lastInputTime(app)` is free: it is the timestamp of the last input event
|
|
242
|
+
this connection saw, which is what react-x11 already stamps copies with, and
|
|
243
|
+
what EWMH calls `_NET_WM_USER_TIME`. It is `undefined` before any input has
|
|
244
|
+
arrived.
|
|
245
|
+
|
|
246
|
+
`serverTime(app)` is one round trip and always answers. Reach for it when
|
|
247
|
+
there is no user action to name — and **do not** write `lastInputTime(app) ??
|
|
248
|
+
0` instead. Zero is `CurrentTime`, which ICCCM 2.1 forbids for exactly the
|
|
249
|
+
reason above: the server accepts it, two clients racing for one selection
|
|
250
|
+
cannot be ordered by it, and the loser is never told it lost.
|
|
251
|
+
|
|
252
|
+
`serverTime` resolves `0` if the server has not answered within five seconds.
|
|
253
|
+
It never rejects and never hangs.
|
|
254
|
+
|
|
255
|
+
## Reaching ntk directly
|
|
256
|
+
|
|
257
|
+
`useClipboard()` is a thin layer over ntk's `app.clipboard` and adds only the
|
|
258
|
+
vocabulary, the timestamps and the React access. Anything it does not cover
|
|
259
|
+
is one step away:
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
const app = useApp();
|
|
263
|
+
await app.clipboard.write(data, { selection: 'MY_SELECTION', time });
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`app` is the whole ntk connection — `app.fonts`, `app.cursors`, `app.X` for
|
|
267
|
+
raw protocol. See ntk's own clipboard documentation for the ICCCM details.
|
|
268
|
+
|
|
269
|
+
## Limits
|
|
270
|
+
|
|
271
|
+
- **On the Cocoa backend `watch()` polls**, a quarter second behind a copy
|
|
272
|
+
made elsewhere, and a read of anything but text spawns `osascript` until
|
|
273
|
+
the bridge has `pasteboardItems`. `PRIMARY` is not a limit there but a
|
|
274
|
+
platform difference: see [`PRIMARY` is X11-only](#primary-is-x11-only).
|
|
275
|
+
- **No clipboard-manager handoff** (`SAVE_TARGETS`), so the data really does
|
|
276
|
+
vanish when the app exits.
|
|
277
|
+
- **`STRING` is latin-1 by definition**, so codepoints above U+00FF are lossy
|
|
278
|
+
in that target. Modern requestors ask for `UTF8_STRING`, which is not — and
|
|
279
|
+
a bare `writeText()` offers both.
|
|
280
|
+
- **No image decoding.** `read('image/png')` hands back the bytes; turning
|
|
281
|
+
them into something drawable is yours.
|