react-x11 1.2.0 → 2.0.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 (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. package/src/scene3d.js +0 -683
@@ -0,0 +1,206 @@
1
+ // The server timestamp of the last X input event, per app — and a way to ask
2
+ // the server for a fresh one when there has not been an input to name.
3
+ //
4
+ // X arbitrates between clients by timestamp, and every selection operation
5
+ // wants one: taking a selection (ICCCM 2.1), converting one (ICCCM 2.4) and
6
+ // giving one back (ICCCM 2.3.1) all say to use "the timestamp of the event
7
+ // that caused this" and not CurrentTime. The only place that value exists is
8
+ // the X event being dispatched, which is nowhere near the code that copies —
9
+ // `_copySelection` is four frames down from the keystroke that triggered it.
10
+ //
11
+ // So it is stashed on the way past, the same trick GTK plays with
12
+ // `gtk_get_current_event_time()`. Without it ntk asks the server for a
13
+ // timestamp on every acquisition, which is a round trip per copy — and
14
+ // select-to-own PRIMARY copies on every selection-extending keystroke.
15
+ //
16
+ // Keyed on the app rather than stored on it: one process can drive several
17
+ // roots on several connections, and a timestamp is only meaningful against
18
+ // the server clock that issued it.
19
+ //
20
+ // ## Why both halves are public
21
+ //
22
+ // react-x11 owns the clipboard, so for a long time nothing outside this file
23
+ // needed either. An application that owns a selection of its own does — a
24
+ // system tray takes `_NET_SYSTEM_TRAY_S<screen>`, a clipboard manager takes
25
+ // `CLIPBOARD_MANAGER`, an input method takes `XIM_SERVERS` — and the choice
26
+ // it faces is exactly the one the two exports name:
27
+ //
28
+ // - There **was** a user action behind this, and ICCCM wants that event's
29
+ // own time: `lastInputTime(app)`, free, already stashed.
30
+ // - There was **not** — a tray acquires its selection when it starts, not
31
+ // because anyone clicked — so there is no causing event and any current
32
+ // server time is correct: `await serverTime(app)`, one round trip.
33
+ //
34
+ // Both beat `CurrentTime`, which is the failure this exists to prevent: the
35
+ // server takes it, two clients racing for one selection cannot be ordered by
36
+ // it, and the loser is never told it lost.
37
+
38
+ const lastInput = new WeakMap();
39
+
40
+ // Motion is deliberately not tracked. Nothing converts or acquires a
41
+ // selection from a motion event — a drag-select owns PRIMARY on the mouseup
42
+ // that ends it, and a drag publishes on the mousedown that began it — so
43
+ // tracking motion would add a write per motion event to buy nothing.
44
+ const TRACKED = new Set(['mousedown', 'mouseup', 'keydown', 'keyup']);
45
+
46
+ /** Remember the server time an input event carried, if it carried one. */
47
+ export function noteInputTime(app, name, native) {
48
+ if (!app || !TRACKED.has(name)) return;
49
+ const time = native?.time;
50
+ if (typeof time === 'number') lastInput.set(app, time >>> 0);
51
+ }
52
+
53
+ /**
54
+ * The last input timestamp seen on this app's connection, or `undefined`
55
+ * when no input has arrived yet.
56
+ *
57
+ * ```jsx
58
+ * const app = useApp();
59
+ * // acquiring PRIMARY because the user just selected something
60
+ * app.X.SetSelectionOwner(wid, atom, lastInputTime(app));
61
+ * ```
62
+ *
63
+ * This is EWMH's own definition of `_NET_WM_USER_TIME` and ICCCM's "the
64
+ * timestamp of the event that caused this", so it is the right value for
65
+ * anything happening *because the user did something* — which includes every
66
+ * selection a keystroke or a click acquires, and the `_NET_ACTIVE_WINDOW`
67
+ * request behind {@link activateWindow}.
68
+ *
69
+ * `undefined` is passed straight through to ntk rather than replaced with a
70
+ * zero: ntk's `write()` turns it into a real server timestamp, and
71
+ * CurrentTime is what ICCCM 2.1 forbids. Elsewhere `undefined` means there
72
+ * is no user action to name and {@link serverTime} is the answer — a caller
73
+ * who reaches for `?? 0` has written CurrentTime back in.
74
+ */
75
+ export function lastInputTime(app) {
76
+ return app ? lastInput.get(app) : undefined;
77
+ }
78
+
79
+ /** ms before `serverTime()` gives up on the round trip. */
80
+ const TIMEOUT = 5000;
81
+
82
+ /** x11.eventMask.PropertyChange, and PropertyNotify's event code. */
83
+ const PROPERTY_CHANGE_MASK = 4194304;
84
+ const PROPERTY_NOTIFY = 28;
85
+
86
+ /** ChangeProperty's Append mode, and PropertyNotify's NewValue state. */
87
+ const APPEND = 2;
88
+ const NEW_VALUE = 0;
89
+
90
+ const TIME_PROPERTY = '_REACT_X11_TIMESTAMP';
91
+
92
+ /** The 1x1 window and property atom each connection derives times through. */
93
+ const timeSources = new WeakMap();
94
+
95
+ /**
96
+ * A current timestamp from this connection's server.
97
+ *
98
+ * ```jsx
99
+ * // a tray taking its manager selection at startup: nothing the user did
100
+ * // caused this, so there is no event time to use
101
+ * app.X.SetSelectionOwner(manager, selection, await serverTime(app));
102
+ * ```
103
+ *
104
+ * There is no request that just asks for the time, so this is the trick every
105
+ * X client uses instead: append zero bytes to a property on a window we own
106
+ * and watch, and read the time off the `PropertyNotify` that generates. One
107
+ * round trip, no visible side effect — the window is 1x1, never mapped, and
108
+ * created once per connection.
109
+ *
110
+ * Prefer {@link lastInputTime} where a user action caused the operation: it
111
+ * is free, and ICCCM asks for the causing event's time specifically. Reach
112
+ * for this where nothing did, and where a stale value would be wrong anyway —
113
+ * `SetSelectionOwner` is refused outright if the timestamp predates the
114
+ * current owner's acquisition, so "the last thing the user touched, an hour
115
+ * ago" is not a safe substitute for "now".
116
+ *
117
+ * Resolves `0` — CurrentTime — if the server has not answered within five
118
+ * seconds, or if this app has no connection to ask. Never rejects and never
119
+ * hangs: a caller stuck waiting for a timestamp is worse than one told the
120
+ * connection is not answering.
121
+ */
122
+ export async function serverTime(app) {
123
+ const X = app?.X;
124
+ const source = await timeSource(app);
125
+ if (!source) return 0;
126
+ return new Promise((resolve) => {
127
+ const done = (time) => {
128
+ clearTimeout(timer);
129
+ X.removeListener('event', onEvent);
130
+ resolve(time);
131
+ };
132
+ const onEvent = (ev) => {
133
+ if (
134
+ ev.type === PROPERTY_NOTIFY &&
135
+ ev.wid === source.window &&
136
+ ev.atom === source.atom &&
137
+ ev.state === NEW_VALUE
138
+ ) {
139
+ done(ev.time >>> 0);
140
+ }
141
+ };
142
+ const timer = setTimeout(() => done(0), TIMEOUT);
143
+ timer.unref?.();
144
+ X.on('event', onEvent);
145
+ // Appending nothing still rewrites the property, which is what generates
146
+ // the event; replacing would too, but appending cannot grow the property
147
+ // however many times this is called.
148
+ X.ChangeProperty(
149
+ APPEND,
150
+ source.window,
151
+ source.atom,
152
+ X.atoms.STRING,
153
+ 8,
154
+ Buffer.alloc(0),
155
+ );
156
+ });
157
+ }
158
+
159
+ /**
160
+ * The window and atom to bounce a timestamp off, created on first use and
161
+ * shared by every later call on the same connection.
162
+ *
163
+ * Its own window rather than one of the app's: selecting PropertyChange on a
164
+ * `<window>` would put every property the window manager writes on it — and
165
+ * `_NET_WM_STATE` changes constantly — through this connection's event stream
166
+ * for the life of the app, to serve a call that happens once.
167
+ *
168
+ * A promise because the property atom has to be interned before anything can
169
+ * be appended to it; awaiting it is also what keeps two calls racing at
170
+ * startup from creating two windows. Resolves null on a connection that
171
+ * cannot do this at all, which is a headless mock rather than any real
172
+ * server.
173
+ */
174
+ function timeSource(app) {
175
+ if (!app || typeof app !== 'object') return null;
176
+ const cached = timeSources.get(app);
177
+ if (cached !== undefined) return cached;
178
+
179
+ const X = app.X;
180
+ const root = X?.display?.screen?.[0]?.root;
181
+ if (
182
+ root == null ||
183
+ typeof X.AllocID !== 'function' ||
184
+ typeof X.CreateWindow !== 'function' ||
185
+ typeof X.ChangeProperty !== 'function' ||
186
+ typeof X.on !== 'function'
187
+ ) {
188
+ timeSources.set(app, null);
189
+ return null;
190
+ }
191
+
192
+ const window = X.AllocID();
193
+ // InputOutput rather than InputOnly: properties are legal on both, but a
194
+ // 1x1 window that is never mapped costs the same either way, and this is
195
+ // the shape every other toolkit's timestamp window has.
196
+ X.CreateWindow(window, root, -100, -100, 1, 1, 0, 0, 1, 0, {
197
+ eventMask: PROPERTY_CHANGE_MASK,
198
+ });
199
+ const pending = new Promise((resolve) => {
200
+ X.InternAtom(false, TIME_PROPERTY, (err, atom) =>
201
+ resolve(err || !atom ? null : { window, atom }),
202
+ );
203
+ });
204
+ timeSources.set(app, pending);
205
+ return pending;
206
+ }
@@ -0,0 +1,21 @@
1
+ /** Development counterpart of jsx-runtime.d.ts — the same JSX namespace. */
2
+
3
+ import type * as React from 'react';
4
+ import type { ReactX11Elements } from './types/elements.js';
5
+
6
+ export { Fragment, jsxDEV } from 'react/jsx-dev-runtime';
7
+
8
+ export namespace JSX {
9
+ type ElementType = React.JSX.ElementType;
10
+ type Element = React.JSX.Element;
11
+ type ElementClass = React.JSX.ElementClass;
12
+ type ElementAttributesProperty = React.JSX.ElementAttributesProperty;
13
+ type ElementChildrenAttribute = React.JSX.ElementChildrenAttribute;
14
+ type LibraryManagedAttributes<C, P> = React.JSX.LibraryManagedAttributes<
15
+ C,
16
+ P
17
+ >;
18
+ type IntrinsicAttributes = React.JSX.IntrinsicAttributes;
19
+ type IntrinsicClassAttributes<T> = React.JSX.IntrinsicClassAttributes<T>;
20
+ interface IntrinsicElements extends ReactX11Elements {}
21
+ }
@@ -0,0 +1,2 @@
1
+ // Development counterpart of jsx-runtime.js — see the note there.
2
+ export { Fragment, jsxDEV } from 'react/jsx-dev-runtime';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The JSX namespace for react-x11. Set
3
+ *
4
+ * { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "react-x11" } }
5
+ *
6
+ * and `<window>`, `<box>`, `<text>` … type-check, while `<div>` does not.
7
+ *
8
+ * This has to be a JSX *source* rather than an augmentation of React's own
9
+ * namespace: five element names — `text`, `image`, `canvas`, `html`, `svg` —
10
+ * also exist in `@types/react` as HTML or SVG elements with incompatible
11
+ * props, and declaration merging cannot replace an existing member. Owning
12
+ * the namespace is also what makes `<div>` an error here, which augmenting
13
+ * could never do.
14
+ */
15
+
16
+ import type * as React from 'react';
17
+ import type { ReactX11Elements } from './types/elements.js';
18
+
19
+ export { Fragment, jsx, jsxs } from 'react/jsx-runtime';
20
+
21
+ export namespace JSX {
22
+ type ElementType = React.JSX.ElementType;
23
+ type Element = React.JSX.Element;
24
+ type ElementClass = React.JSX.ElementClass;
25
+ type ElementAttributesProperty = React.JSX.ElementAttributesProperty;
26
+ type ElementChildrenAttribute = React.JSX.ElementChildrenAttribute;
27
+ type LibraryManagedAttributes<C, P> = React.JSX.LibraryManagedAttributes<
28
+ C,
29
+ P
30
+ >;
31
+ type IntrinsicAttributes = React.JSX.IntrinsicAttributes;
32
+ type IntrinsicClassAttributes<T> = React.JSX.IntrinsicClassAttributes<T>;
33
+
34
+ /**
35
+ * The X11 host elements — and only those. An interface so a project can
36
+ * still declare extra elements of its own by merging into it.
37
+ */
38
+ interface IntrinsicElements extends ReactX11Elements {}
39
+ }
@@ -0,0 +1,6 @@
1
+ // The automatic JSX runtime, so a project can set
2
+ // `"jsxImportSource": "react-x11"` and have JSX type-check against the X11
3
+ // elements instead of the DOM's. At runtime this is React's own runtime —
4
+ // the elements are host components either way; only the *types* differ, and
5
+ // they have to come from somewhere TypeScript will look.
6
+ export { Fragment, jsx, jsxs } from 'react/jsx-runtime';
@@ -0,0 +1,256 @@
1
+ // The accelerator half of a key event: which keysym a shortcut matches
2
+ // against when the user is typing in another script (issue #85).
3
+ //
4
+ // A key event answers two different questions and they have different
5
+ // answers the moment a non-Latin layout is active:
6
+ //
7
+ // - **text input** — the active group, the active level. `ev.codepoint` and
8
+ // `ev.key`, both decoded by ntk against the keymap and the event's state
9
+ // field, so a Cyrillic layout types Cyrillic.
10
+ // - **accelerators** — the *Latin* keysym for that physical key, whichever
11
+ // group happens to hold it. `ev.keysym`, resolved here. Shortcuts do not
12
+ // move with the layout: GTK, Qt and the browsers all resolve them this
13
+ // way, and Ctrl+Z has to keep undoing while the user types Russian.
14
+ //
15
+ // ntk hands us group 1 level 1 as `baseKeysym`, which is the right answer
16
+ // whenever group 1 is Latin — the `us,ru` ordering everybody on Linux
17
+ // actually uses. It is the wrong answer in the two cases this module is
18
+ // for:
19
+ //
20
+ // - **`ru,us`** — the Latin layout is group 2, so group 1 level 1 is
21
+ // `Cyrillic_ya` and a chord matched against it matches nothing. The
22
+ // keysym we want is on the same keycode, two entries along.
23
+ // - **XQuartz** — there are no groups at all. XQuartz synthesizes the keymap
24
+ // from the active macOS layout and *overwrites* it when the input menu
25
+ // changes (Preferences → Input → "Follow system keyboard layout"), so
26
+ // after a switch to Russian no group anywhere holds Latin. Nothing in the
27
+ // keymap can answer, and the only thing left that still means "the Z key"
28
+ // is the keycode itself.
29
+ //
30
+ // So: the base keysym if it is already Latin, else the Latin letter another
31
+ // group puts on the same keycode, else the letter the *physical position*
32
+ // carries on a US keyboard. The last step needs to know which keycodes the
33
+ // server is speaking, which is what `SCHEMES` fingerprints.
34
+
35
+ import { keysymOf } from './keysyms.js';
36
+
37
+ // Keysyms from 0xfd00 to 0xffff are keys rather than characters — Return,
38
+ // the arrows, the function row, the modifiers, ISO_Level3_Shift, the dead
39
+ // keys. They are the same in every layout and never need resolving.
40
+ const KEY_LOW = 0xfd00;
41
+
42
+ // Direct Unicode keysyms: 0x01000000 + code point.
43
+ const UNICODE_LOW = 0x01000000;
44
+ const UNICODE_HIGH = 0x0110ffff;
45
+
46
+ // The legacy keysym blocks that are not Latin script. Below 0x400 is
47
+ // Latin-1 through Latin-4; 0x800-0xbff are technical, publishing and APL
48
+ // symbols; 0x1200-0x13ff are Latin-8 and Latin-9 and 0x1e00-0x1eff is
49
+ // Vietnamese, all of them Latin. What is left is the scripts a shortcut
50
+ // cannot be written in.
51
+ const NON_LATIN_BLOCKS = [
52
+ [0x0400, 0x04ff], // Kana
53
+ [0x0500, 0x05ff], // Arabic
54
+ [0x0600, 0x06ff], // Cyrillic
55
+ [0x0700, 0x07ff], // Greek
56
+ [0x0c00, 0x0cff], // Hebrew
57
+ [0x0d00, 0x0dff], // Thai
58
+ [0x0e00, 0x0eff], // Korean
59
+ [0x1400, 0x16ff], // Armenian, Georgian, Caucasus
60
+ ];
61
+
62
+ // For the direct-Unicode block, where there is no block structure to read:
63
+ // anything that is not Latin script and not script-neutral (digits,
64
+ // punctuation, symbols, spaces) is a letter of some other alphabet.
65
+ const LATIN_OR_NEUTRAL =
66
+ /^[\p{Script=Latin}\p{Script=Common}\p{Script=Inherited}]$/u;
67
+
68
+ /**
69
+ * Whether a keysym is a letter of a script an accelerator cannot be written
70
+ * in — the question that decides whether anything below runs at all.
71
+ */
72
+ export function isNonLatinKeysym(keysym) {
73
+ if (typeof keysym !== 'number' || keysym <= 0xff) return false;
74
+ if (keysym >= KEY_LOW && keysym <= 0xffff) return false;
75
+ if (keysym >= UNICODE_LOW) {
76
+ if (keysym > UNICODE_HIGH) return false;
77
+ return !LATIN_OR_NEUTRAL.test(String.fromCodePoint(keysym - UNICODE_LOW));
78
+ }
79
+ return NON_LATIN_BLOCKS.some(([lo, hi]) => keysym >= lo && keysym <= hi);
80
+ }
81
+
82
+ // An ASCII keysym is its own code point, so `a`-`z` is a range.
83
+ const LOWER_A = 0x61;
84
+ const LOWER_Z = 0x7a;
85
+ const TO_UPPER = -0x20;
86
+
87
+ /**
88
+ * The Latin letter another group puts on this keycode, if one does.
89
+ *
90
+ * The core keyboard map flattens XKB's groups into pairs —
91
+ * `[g1l1, g1l2, g2l1, g2l2, ...]` — so the second group starts at index 2.
92
+ * What the core map cannot say is whether those two entries are a second
93
+ * *group* or levels 3 and 4 of the first one: four keysyms on a keycode are
94
+ * `us,ru` under one reading and `us(intl)` under the other, and the request
95
+ * that would settle it (XkbGetMap) is not implemented downstream. ntk's
96
+ * `decodeKey` refuses to guess and reads groups only; this refuses in the
97
+ * other direction, by taking a later pair only when it looks like a Latin
98
+ * letter and its own capital.
99
+ *
100
+ * That is deliberately narrow. An AltGr row that happens to reach an ASCII
101
+ * symbol — macOS's Option layer is full of them — must not be mistaken for
102
+ * a Latin layout, and a Latin *letter* pair is both the thing that is
103
+ * unlikely to be an accident and the thing accelerators are made of.
104
+ */
105
+ function latinInLaterGroup(syms) {
106
+ for (let i = 2; i < syms.length; i += 2) {
107
+ const lower = syms[i];
108
+ const upper = syms[i + 1];
109
+ if (lower < LOWER_A || lower > LOWER_Z) continue;
110
+ // "if the second element is NoSymbol, the group is treated as the lower
111
+ // and upper case of the first" — X core protocol
112
+ if (!upper || upper === lower + TO_UPPER) return lower;
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ /** `{ 10: 0x31, 11: 0x32, … }` from runs of consecutive keycodes. */
118
+ function positions(runs) {
119
+ const table = {};
120
+ for (const [first, chars] of runs) {
121
+ for (let i = 0; i < chars.length; i++)
122
+ table[first + i] = chars.charCodeAt(i);
123
+ }
124
+ return table;
125
+ }
126
+
127
+ /**
128
+ * What each physical key carries on a US keyboard, per keycode scheme, and
129
+ * the layout-independent keys that identify the scheme.
130
+ *
131
+ * Keycodes are not a standard: they are whatever the server's input driver
132
+ * assigns. Two schemes cover everything this renderer runs on — Linux's
133
+ * evdev, and the macOS virtual keycodes XQuartz passes through with 8 added
134
+ * — and they disagree everywhere, which is exactly what makes the probe
135
+ * reliable. Return alone separates them (36 on evdev is `8` on macOS), and
136
+ * five keys make it certain.
137
+ */
138
+ const SCHEMES = [
139
+ {
140
+ name: 'evdev',
141
+ probe: { 9: 0xff1b, 22: 0xff08, 23: 0xff09, 36: 0xff0d, 65: 0x20 },
142
+ latin: positions([
143
+ [10, '1234567890-='],
144
+ [24, 'qwertyuiop[]'],
145
+ [38, "asdfghjkl;'"],
146
+ [49, '`'],
147
+ [51, '\\'],
148
+ [52, 'zxcvbnm,./'],
149
+ [65, ' '],
150
+ ]),
151
+ },
152
+ {
153
+ name: 'macos',
154
+ probe: { 44: 0xff0d, 56: 0xff09, 57: 0x20, 59: 0xff08, 61: 0xff1b },
155
+ latin: positions([
156
+ [8, 'asdfhgzxcv'],
157
+ [19, 'b'],
158
+ [20, 'qweryt'],
159
+ [26, '123465=97-80'],
160
+ [38, ']ou[ip'],
161
+ [45, "lj'k;\\,/nm."],
162
+ [57, ' '],
163
+ [58, '`'],
164
+ ]),
165
+ },
166
+ ];
167
+
168
+ /**
169
+ * Which keycode scheme this keymap is speaking, or null if it is neither.
170
+ *
171
+ * The probe reads keys whose keysym no layout moves, so it answers the same
172
+ * before and after a switch — which matters, because on XQuartz the keymap
173
+ * it is reading is rewritten under it.
174
+ */
175
+ export function physicalLatinTable(keycode2keysyms) {
176
+ if (!keycode2keysyms) return null;
177
+ for (const scheme of SCHEMES) {
178
+ const matches = Object.entries(scheme.probe).every(
179
+ ([keycode, keysym]) => keycode2keysyms[keycode]?.[0] === keysym,
180
+ );
181
+ if (matches) return scheme.latin;
182
+ }
183
+ return null;
184
+ }
185
+
186
+ /**
187
+ * The keysym a shortcut matches against, given ntk's group 1 level 1 and
188
+ * the keycode's whole row.
189
+ *
190
+ * `physical` is the keycode→Latin table for this server, or null when the
191
+ * scheme is unknown — in which case a key with no Latin anywhere on it
192
+ * keeps its own keysym, since a wrong shortcut is worse than a missing one.
193
+ */
194
+ export function latinKeysym(base, syms, keycode, physical) {
195
+ if (!isNonLatinKeysym(base)) return base;
196
+ const fromGroup = syms ? latinInLaterGroup(syms) : undefined;
197
+ if (fromGroup !== undefined) return fromGroup;
198
+ return physical?.[keycode] ?? base;
199
+ }
200
+
201
+ /**
202
+ * Record what `createRoot({ accelerators })` asked for.
203
+ *
204
+ * The default resolves; `'layout'` does not, for an application that wants
205
+ * the keysym the key actually typed and will do its own matching. An object
206
+ * is a keycode→Latin keysym table of your own, for a server whose keycodes
207
+ * are neither of the two schemes above (and it still prefers a Latin group
208
+ * on the key itself, which is the more accurate answer where there is one).
209
+ */
210
+ export function beginKeyboard(app, accelerators) {
211
+ if (!app || accelerators === undefined) return;
212
+ if (accelerators === 'latin') {
213
+ app._reactX11Accelerators = undefined;
214
+ return;
215
+ }
216
+ if (accelerators === 'layout' || accelerators === false) {
217
+ app._reactX11Accelerators = null;
218
+ return;
219
+ }
220
+ if (typeof accelerators !== 'object') {
221
+ throw new TypeError(
222
+ `react-x11: createRoot({ accelerators }) takes 'latin', 'layout' or a ` +
223
+ `keycode table, not ${JSON.stringify(accelerators)}.`,
224
+ );
225
+ }
226
+ const table = {};
227
+ for (const [keycode, value] of Object.entries(accelerators)) {
228
+ table[keycode] = typeof value === 'string' ? keysymOf(value) : value;
229
+ }
230
+ app._reactX11Accelerators = { table };
231
+ }
232
+
233
+ /**
234
+ * The accelerator keysym for one key event, cached per connection.
235
+ *
236
+ * Only the *scheme* is cached: the keymap itself is re-read on every
237
+ * MappingNotify (ntk does that), and on XQuartz a layout switch is a
238
+ * rewritten keymap, so the row has to be looked at fresh each time. The
239
+ * scheme is fixed by the server's input driver and cannot change under a
240
+ * running connection — but it is resolved lazily and not remembered until
241
+ * it answers, because the first key can beat `GetKeyboardMapping`'s reply.
242
+ */
243
+ export function acceleratorKeysym(app, keycode, base) {
244
+ const config = app?._reactX11Accelerators;
245
+ if (config === null) return base;
246
+ // The common case, and every key event pays it: a Latin layout, or a key
247
+ // that is a key rather than a character. One comparison and out.
248
+ if (!isNonLatinKeysym(base)) return base;
249
+ const syms = app?.X?.keycode2keysyms?.[keycode];
250
+ if (config?.table) return latinKeysym(base, syms, keycode, config.table);
251
+ if (app && !app._reactX11PhysicalLatin) {
252
+ const table = physicalLatinTable(app.X?.keycode2keysyms);
253
+ if (table) app._reactX11PhysicalLatin = table;
254
+ }
255
+ return latinKeysym(base, syms, keycode, app?._reactX11PhysicalLatin);
256
+ }