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.
- package/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// Caps Lock, Num Lock, and which keyboard layout is live.
|
|
2
|
+
//
|
|
3
|
+
// The one thing an application cannot work out for itself. A key event
|
|
4
|
+
// carries the modifier state at the moment it was pressed, so an app can see
|
|
5
|
+
// that Caps Lock was on for *a key it received* — which is exactly no help in
|
|
6
|
+
// the case that matters, a password field that wants to warn before the first
|
|
7
|
+
// character is typed and while the field is merely focused.
|
|
8
|
+
//
|
|
9
|
+
// XKB answers both halves directly: `GetState` for the state now,
|
|
10
|
+
// `StateNotify` for every change after that, including the ones that happen
|
|
11
|
+
// while another window has the keyboard. There is no polling and no timer.
|
|
12
|
+
//
|
|
13
|
+
// ## Which bit is which lock
|
|
14
|
+
//
|
|
15
|
+
// Caps Lock is `LockMask`, fixed by the core protocol. **Num Lock is not
|
|
16
|
+
// fixed by anything** — it is wherever the modifier map puts it, and that is
|
|
17
|
+
// `Mod2` on every Linux and BSD desktop, on XQuartz, and in every toolkit
|
|
18
|
+
// that hardcodes it (GTK reads the modmap; Qt hardcodes Mod2). Reading the
|
|
19
|
+
// modifier map to be sure costs a round trip and a keysym search to arrive at
|
|
20
|
+
// Mod2 in every real case, so this takes the convention — the same one
|
|
21
|
+
// `MOD.Mod2` in `keysyms.js` already documents for Alt and Super.
|
|
22
|
+
//
|
|
23
|
+
// ## Where the layout names come from
|
|
24
|
+
//
|
|
25
|
+
// `GetState().group` is an *index*, 0 to 3, and says nothing about what is in
|
|
26
|
+
// the group. The names live in `_XKB_RULES_NAMES` on the root window — the
|
|
27
|
+
// property `setxkbmap` writes and `setxkbmap -query` reads back — as
|
|
28
|
+
// NUL-separated rules, model, layouts, variants and options. So `us,ru` plus
|
|
29
|
+
// group 1 is `'ru'`.
|
|
30
|
+
//
|
|
31
|
+
// XKB's own `GetNames(GroupNames)` is the other route and is not used here:
|
|
32
|
+
// it answers atoms that have to be interned back one round trip each, and
|
|
33
|
+
// what it returns is a description like `English (US)` where an indicator in
|
|
34
|
+
// a status bar wants `us`.
|
|
35
|
+
|
|
36
|
+
import { requireExtension } from './extensions.js';
|
|
37
|
+
|
|
38
|
+
const sessions = new WeakMap();
|
|
39
|
+
|
|
40
|
+
/** `1 << xkbType` for the two events this needs. */
|
|
41
|
+
const NEW_KEYBOARD_NOTIFY = 1 << 0;
|
|
42
|
+
const STATE_NOTIFY = 1 << 2;
|
|
43
|
+
|
|
44
|
+
const XKB_USE_CORE_KBD = 0x100;
|
|
45
|
+
const LOCK_MASK = 2; // MOD.Lock — Caps Lock, per the core protocol
|
|
46
|
+
const MOD2_MASK = 16; // MOD.Mod2 — Num Lock, per universal convention
|
|
47
|
+
const RULES_PROPERTY = '_XKB_RULES_NAMES';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What is known before XKB has answered, and what stays true on a display
|
|
51
|
+
* without the extension.
|
|
52
|
+
*
|
|
53
|
+
* Every field is the "off" answer rather than a null, because every caller of
|
|
54
|
+
* this is drawing something: a Caps Lock warning that renders `null` as truthy
|
|
55
|
+
* is worse than one that is briefly absent, and it is absent for one frame.
|
|
56
|
+
* `layouts` being empty is what distinguishes "not known" from "no lock on".
|
|
57
|
+
*/
|
|
58
|
+
const UNKNOWN = Object.freeze({
|
|
59
|
+
capsLock: false,
|
|
60
|
+
numLock: false,
|
|
61
|
+
group: 0,
|
|
62
|
+
layout: null,
|
|
63
|
+
layouts: Object.freeze([]),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
class KeyboardSession {
|
|
67
|
+
constructor(app) {
|
|
68
|
+
this.app = app;
|
|
69
|
+
this.snapshot = UNKNOWN;
|
|
70
|
+
this.listeners = new Set();
|
|
71
|
+
this.armed = false;
|
|
72
|
+
this.stopped = false;
|
|
73
|
+
this._handler = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
publish(values) {
|
|
77
|
+
const next = { ...this.snapshot, ...values };
|
|
78
|
+
// `layout` is derived in one place so it cannot disagree with the group
|
|
79
|
+
// and the list it comes from — a group index past the end of a
|
|
80
|
+
// just-reconfigured list is null rather than undefined.
|
|
81
|
+
next.layout = next.layouts[next.group] ?? null;
|
|
82
|
+
const prev = this.snapshot;
|
|
83
|
+
if (
|
|
84
|
+
next.capsLock === prev.capsLock &&
|
|
85
|
+
next.numLock === prev.numLock &&
|
|
86
|
+
next.group === prev.group &&
|
|
87
|
+
next.layout === prev.layout &&
|
|
88
|
+
// by value: `readLayouts` builds a fresh array every time it runs, and
|
|
89
|
+
// it runs again on every keymap replacement, most of which change
|
|
90
|
+
// nothing about the list
|
|
91
|
+
next.layouts.length === prev.layouts.length &&
|
|
92
|
+
next.layouts.every((l, i) => l === prev.layouts[i])
|
|
93
|
+
) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.snapshot = Object.freeze(next);
|
|
97
|
+
for (const fn of [...this.listeners]) {
|
|
98
|
+
try {
|
|
99
|
+
fn();
|
|
100
|
+
} catch {
|
|
101
|
+
// one subscriber throwing must not take the others with it, nor the
|
|
102
|
+
// X event loop this runs on
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
stop() {
|
|
108
|
+
this.stopped = true;
|
|
109
|
+
if (this._handler && this.app?.X?.off) {
|
|
110
|
+
try {
|
|
111
|
+
this.app.X.off('event', this._handler);
|
|
112
|
+
} catch {
|
|
113
|
+
// an ntk old enough to hand back a client with no `off`
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
this._handler = null;
|
|
117
|
+
this.listeners.clear();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `_XKB_RULES_NAMES` → the configured layout codes.
|
|
123
|
+
*
|
|
124
|
+
* Five NUL-terminated strings — rules, model, layouts, variants, options —
|
|
125
|
+
* of which the third is `us,ru`. Pure, and exported for the test: the
|
|
126
|
+
* in-process X server has no XKB, so this is the part of the path that can
|
|
127
|
+
* be pinned without one.
|
|
128
|
+
*
|
|
129
|
+
* A trailing NUL leaves an empty final field, and a layout list can
|
|
130
|
+
* legitimately have an empty entry (`us,,ru` from a hand-edited config), so
|
|
131
|
+
* the split keeps position and drops only what is empty at the ends.
|
|
132
|
+
*/
|
|
133
|
+
export function layoutsFromRules(data) {
|
|
134
|
+
if (!data?.length) return [];
|
|
135
|
+
const fields = data.toString('latin1').split('\0');
|
|
136
|
+
const layouts = fields[2];
|
|
137
|
+
if (!layouts) return [];
|
|
138
|
+
const list = layouts.split(',').map((s) => s.trim());
|
|
139
|
+
while (list.length && !list[list.length - 1]) list.pop();
|
|
140
|
+
// `empty` is a real xkeyboard-config layout, and what it is defined as is a
|
|
141
|
+
// layout with no symbols in it — which is exactly what XQuartz writes here,
|
|
142
|
+
// because it synthesizes its keymap from macOS and has no XKB layout to
|
|
143
|
+
// name. Reporting it would put "EMPTY" in a status-bar indicator; an empty
|
|
144
|
+
// list is what "this display cannot say" already means everywhere else here.
|
|
145
|
+
return list.every((name) => name === 'empty') ? [] : list;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Caps and Num out of a locked-modifier mask. */
|
|
149
|
+
export function locksFromMods(lockedMods) {
|
|
150
|
+
return {
|
|
151
|
+
capsLock: Boolean(lockedMods & LOCK_MASK),
|
|
152
|
+
numLock: Boolean(lockedMods & MOD2_MASK),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function arm(session) {
|
|
157
|
+
if (session.armed) return;
|
|
158
|
+
session.armed = true;
|
|
159
|
+
const app = session.app;
|
|
160
|
+
const xkb = await requireExtension(app, 'xkb');
|
|
161
|
+
if (!xkb || session.stopped) return;
|
|
162
|
+
|
|
163
|
+
// Subscribe before reading. A lock toggled between the two would otherwise
|
|
164
|
+
// be lost for the life of the connection — the same ordering rule the
|
|
165
|
+
// appearance ladder and the clipboard watch both follow.
|
|
166
|
+
const handler = (ev) => {
|
|
167
|
+
if (session.stopped || ev.type !== xkb.firstEvent) return;
|
|
168
|
+
if (ev.xkbType === xkb.events.StateNotify) {
|
|
169
|
+
session.publish({ ...locksFromMods(ev.lockedMods), group: ev.group });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// NewKeyboardNotify: the keymap itself was replaced, which is what
|
|
173
|
+
// `setxkbmap` does when the *set* of layouts changes rather than which
|
|
174
|
+
// one is active. Rare, and cheap to answer — one property read.
|
|
175
|
+
if (ev.xkbType === 0) readLayouts(session);
|
|
176
|
+
};
|
|
177
|
+
session._handler = handler;
|
|
178
|
+
app.X.on('event', handler);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
xkb.SelectEvents(
|
|
182
|
+
XKB_USE_CORE_KBD,
|
|
183
|
+
STATE_NOTIFY | NEW_KEYBOARD_NOTIFY,
|
|
184
|
+
0,
|
|
185
|
+
STATE_NOTIFY | NEW_KEYBOARD_NOTIFY,
|
|
186
|
+
0,
|
|
187
|
+
0,
|
|
188
|
+
);
|
|
189
|
+
} catch {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
readLayouts(session);
|
|
194
|
+
|
|
195
|
+
const state = await new Promise((resolve) => {
|
|
196
|
+
try {
|
|
197
|
+
xkb.GetState(XKB_USE_CORE_KBD, (err, value) =>
|
|
198
|
+
resolve(err ? null : value),
|
|
199
|
+
);
|
|
200
|
+
} catch {
|
|
201
|
+
resolve(null);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
if (state && !session.stopped) {
|
|
205
|
+
session.publish({ ...locksFromMods(state.lockedMods), group: state.group });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function readLayouts(session) {
|
|
210
|
+
const X = session.app.X;
|
|
211
|
+
const root = X.display?.screen?.[0]?.root;
|
|
212
|
+
if (root == null) return;
|
|
213
|
+
try {
|
|
214
|
+
const atom = await new Promise((resolve, reject) =>
|
|
215
|
+
X.InternAtom(false, RULES_PROPERTY, (err, value) =>
|
|
216
|
+
err ? reject(err) : resolve(value),
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
const prop = await new Promise((resolve) =>
|
|
220
|
+
X.GetProperty(0, root, atom, 0, 0, 0x1fffffff, (err, value) =>
|
|
221
|
+
resolve(err ? null : value),
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
if (session.stopped) return;
|
|
225
|
+
const layouts = layoutsFromRules(prop?.data);
|
|
226
|
+
if (layouts.length) session.publish({ layouts: Object.freeze(layouts) });
|
|
227
|
+
} catch {
|
|
228
|
+
// no rules property: a server configured by other means, or XQuartz,
|
|
229
|
+
// where the group index is all there is. `layouts` stays empty and
|
|
230
|
+
// `layout` stays null, which is what those say.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** What is known right now. Not public — `useKeyboardState()` is. */
|
|
235
|
+
export function keyboardStateSnapshot(app) {
|
|
236
|
+
return sessions.get(app)?.snapshot ?? UNKNOWN;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Subscribe to the lock and layout state changing. */
|
|
240
|
+
export function watchKeyboardState(app, onChange) {
|
|
241
|
+
if (!app) return () => {};
|
|
242
|
+
let session = sessions.get(app);
|
|
243
|
+
if (!session) {
|
|
244
|
+
session = new KeyboardSession(app);
|
|
245
|
+
sessions.set(app, session);
|
|
246
|
+
}
|
|
247
|
+
arm(session).catch(() => {
|
|
248
|
+
// an extension that is not there is not an error
|
|
249
|
+
});
|
|
250
|
+
session.listeners.add(onChange);
|
|
251
|
+
return () => session.listeners.delete(onChange);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Tear down with the root that started it. */
|
|
255
|
+
export function endKeyboardState(app) {
|
|
256
|
+
const session = sessions.get(app);
|
|
257
|
+
if (!session) return;
|
|
258
|
+
session.stop();
|
|
259
|
+
sessions.delete(app);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Test seam: state the keyboard without a server that has XKB. Marks the
|
|
264
|
+
* session armed, so nothing tries to reach the extension behind the value.
|
|
265
|
+
*/
|
|
266
|
+
export function setKeyboardStateForTests(app, values) {
|
|
267
|
+
let session = sessions.get(app);
|
|
268
|
+
if (!session) {
|
|
269
|
+
session = new KeyboardSession(app);
|
|
270
|
+
sessions.set(app, session);
|
|
271
|
+
}
|
|
272
|
+
session.armed = true;
|
|
273
|
+
session.publish({
|
|
274
|
+
...values,
|
|
275
|
+
layouts: Object.freeze(values.layouts ?? UNKNOWN.layouts),
|
|
276
|
+
});
|
|
277
|
+
return session;
|
|
278
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// `useKeyboardState()` — the locks and the active layout, as something a
|
|
2
|
+
// component re-renders on.
|
|
3
|
+
//
|
|
4
|
+
// The store lives in `keyboardstate.js`, keyed by connection: there is one
|
|
5
|
+
// keyboard per display, and its state is the same for every window on it.
|
|
6
|
+
|
|
7
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
8
|
+
|
|
9
|
+
import { useApp } from './appcontext.js';
|
|
10
|
+
import { keyboardStateSnapshot, watchKeyboardState } from './keyboardstate.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The keyboard's locks and layout, live.
|
|
14
|
+
*
|
|
15
|
+
* ```jsx
|
|
16
|
+
* const { capsLock, layout } = useKeyboardState();
|
|
17
|
+
*
|
|
18
|
+
* <PasswordInput value={password} onChange={setPassword} />
|
|
19
|
+
* {capsLock && <text style={{ color: theme.warning }}>Caps Lock is on</text>}
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* | | |
|
|
23
|
+
* | --- | --- |
|
|
24
|
+
* | `capsLock` `numLock` | on or off, **now** — not "was on for the last key" |
|
|
25
|
+
* | `group` | the active XKB group, `0`–`3` |
|
|
26
|
+
* | `layout` | that group's layout code, `'ru'` — or null |
|
|
27
|
+
* | `layouts` | every configured layout, `['us', 'ru']` — `[]` where unknown |
|
|
28
|
+
*
|
|
29
|
+
* The point of the first row is that it is true of the keyboard rather than
|
|
30
|
+
* of an event: a password field can warn about Caps Lock **before** the first
|
|
31
|
+
* character is typed, which is the only time the warning is worth anything.
|
|
32
|
+
* A key event's own modifier state cannot do that, because it needs a key.
|
|
33
|
+
*
|
|
34
|
+
* The second is for a status-bar indicator, and for the case that costs
|
|
35
|
+
* people real time — a password typed in the wrong script, which looks
|
|
36
|
+
* identical to a wrong password. `layout` is a code rather than a
|
|
37
|
+
* description (`'ru'`, not `'Russian'`) because that is what an indicator
|
|
38
|
+
* shows and what a lookup key wants; uppercase it for display.
|
|
39
|
+
*
|
|
40
|
+
* Everything here needs **XKB**, which Xorg and XQuartz both have. Where it
|
|
41
|
+
* is missing the locks read false and `layouts` is empty, and an app that
|
|
42
|
+
* renders a warning only when `capsLock` is true degrades to not warning.
|
|
43
|
+
* `layouts` is the field to check when the difference between "off" and "not
|
|
44
|
+
* known" matters.
|
|
45
|
+
*
|
|
46
|
+
* Nothing is asked of the server until a component calls this, and the change
|
|
47
|
+
* is delivered by `XkbStateNotify` — no polling, and it arrives even while
|
|
48
|
+
* another application has the keyboard.
|
|
49
|
+
*/
|
|
50
|
+
export function useKeyboardState() {
|
|
51
|
+
const app = useApp();
|
|
52
|
+
const subscribe = useCallback(
|
|
53
|
+
(onChange) => watchKeyboardState(app, onChange),
|
|
54
|
+
[app],
|
|
55
|
+
);
|
|
56
|
+
const snapshot = useCallback(() => keyboardStateSnapshot(app), [app]);
|
|
57
|
+
return useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
58
|
+
}
|
package/src/keysyms.d.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X11 keysyms — `react-x11/keysyms`. One vocabulary for app code, the
|
|
3
|
+
* widgets and `fireEvent.key`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The keysym for a character. Latin-1 (U+0020–U+00FF) is identity, so `'a'`
|
|
8
|
+
* is `0x61` and `'é'` is `0xe9`; anything else is `0x01000000 + codePoint`.
|
|
9
|
+
*/
|
|
10
|
+
export function keysymOf(char: string): number;
|
|
11
|
+
|
|
12
|
+
/** The character a keysym produces, or `''` for a non-printing key. */
|
|
13
|
+
export function charOf(keysym: number): string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The letter of a Ctrl chord, independent of Shift — the keysym for its
|
|
17
|
+
* lowercase form, so `keysymOf('z')` matches both Ctrl+Z and Ctrl+Shift+Z.
|
|
18
|
+
* Null when the event carries neither a keysym nor a codepoint.
|
|
19
|
+
*
|
|
20
|
+
* ntk derives `codepoint` from the *shifted* keysym, so a handler that
|
|
21
|
+
* compared code points would see `Z` for Ctrl+Shift+Z and miss the chord.
|
|
22
|
+
* Here rather than inside `<textinput>` because both layers need it: the
|
|
23
|
+
* built-in editors read Ctrl+C/V/Z, and so does any widget that answers a
|
|
24
|
+
* chord of its own.
|
|
25
|
+
*
|
|
26
|
+
* ```js
|
|
27
|
+
* if (ev.ctrlKey && ctrlChordLetter(ev) === keysymOf('d')) duplicateLine();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function ctrlChordLetter(ev: {
|
|
31
|
+
keysym?: number | null;
|
|
32
|
+
codepoint?: number | null;
|
|
33
|
+
}): number | null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The keysym an X11 key *name* stands for — `'Return'`, `'plus'`, `'F5'`,
|
|
37
|
+
* `'s'` — or `undefined` for a name nothing here knows.
|
|
38
|
+
*
|
|
39
|
+
* This is the vocabulary a menu item's `shortcut` chords are written in
|
|
40
|
+
* (X11's names, which is what `gdk_keyval_name()` emits and what a panel's
|
|
41
|
+
* dbusmenu importer parses), so a chord can be matched, drawn and exported
|
|
42
|
+
* from one spelling. A one-character name is the character itself.
|
|
43
|
+
*/
|
|
44
|
+
export function keysymFromName(name: string): number | undefined;
|
|
45
|
+
|
|
46
|
+
export const XK_BACKSPACE: 0xff08;
|
|
47
|
+
export const XK_TAB: 0xff09;
|
|
48
|
+
export const XK_RETURN: 0xff0d;
|
|
49
|
+
export const XK_ESCAPE: 0xff1b;
|
|
50
|
+
export const XK_DELETE: 0xffff;
|
|
51
|
+
export const XK_INSERT: 0xff63;
|
|
52
|
+
|
|
53
|
+
export const XK_HOME: 0xff50;
|
|
54
|
+
export const XK_LEFT: 0xff51;
|
|
55
|
+
export const XK_UP: 0xff52;
|
|
56
|
+
export const XK_RIGHT: 0xff53;
|
|
57
|
+
export const XK_DOWN: 0xff54;
|
|
58
|
+
export const XK_PAGE_UP: 0xff55;
|
|
59
|
+
export const XK_PAGE_DOWN: 0xff56;
|
|
60
|
+
export const XK_END: 0xff57;
|
|
61
|
+
|
|
62
|
+
export const XK_KP_ENTER: 0xff8d;
|
|
63
|
+
export const XK_MENU: 0xff67;
|
|
64
|
+
export const XK_SPACE: 0x0020;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The Compose key. It types nothing on its own: it opens a sequence that
|
|
68
|
+
* the next keys finish — `Compose o c` is `©`. See
|
|
69
|
+
* [docs/events.md](events.md#composition).
|
|
70
|
+
*/
|
|
71
|
+
export const XK_MULTI_KEY: 0xff20;
|
|
72
|
+
|
|
73
|
+
// The dead keys. Each waits for the character it decorates, so `dead_acute`
|
|
74
|
+
// then `e` is `é`; on its own — followed by a space, or pressed twice — it
|
|
75
|
+
// types the accent.
|
|
76
|
+
export const XK_DEAD_GRAVE: 0xfe50;
|
|
77
|
+
export const XK_DEAD_ACUTE: 0xfe51;
|
|
78
|
+
export const XK_DEAD_CIRCUMFLEX: 0xfe52;
|
|
79
|
+
export const XK_DEAD_TILDE: 0xfe53;
|
|
80
|
+
export const XK_DEAD_MACRON: 0xfe54;
|
|
81
|
+
export const XK_DEAD_BREVE: 0xfe55;
|
|
82
|
+
export const XK_DEAD_ABOVEDOT: 0xfe56;
|
|
83
|
+
export const XK_DEAD_DIAERESIS: 0xfe57;
|
|
84
|
+
export const XK_DEAD_ABOVERING: 0xfe58;
|
|
85
|
+
export const XK_DEAD_DOUBLEACUTE: 0xfe59;
|
|
86
|
+
export const XK_DEAD_CARON: 0xfe5a;
|
|
87
|
+
export const XK_DEAD_CEDILLA: 0xfe5b;
|
|
88
|
+
export const XK_DEAD_OGONEK: 0xfe5c;
|
|
89
|
+
export const XK_DEAD_IOTA: 0xfe5d;
|
|
90
|
+
export const XK_DEAD_VOICED_SOUND: 0xfe5e;
|
|
91
|
+
export const XK_DEAD_SEMIVOICED_SOUND: 0xfe5f;
|
|
92
|
+
export const XK_DEAD_BELOWDOT: 0xfe60;
|
|
93
|
+
export const XK_DEAD_HOOK: 0xfe61;
|
|
94
|
+
export const XK_DEAD_HORN: 0xfe62;
|
|
95
|
+
export const XK_DEAD_STROKE: 0xfe63;
|
|
96
|
+
export const XK_DEAD_ABOVECOMMA: 0xfe64;
|
|
97
|
+
export const XK_DEAD_ABOVEREVERSEDCOMMA: 0xfe65;
|
|
98
|
+
export const XK_DEAD_DOUBLEGRAVE: 0xfe66;
|
|
99
|
+
export const XK_DEAD_BELOWRING: 0xfe67;
|
|
100
|
+
export const XK_DEAD_BELOWMACRON: 0xfe68;
|
|
101
|
+
export const XK_DEAD_BELOWCIRCUMFLEX: 0xfe69;
|
|
102
|
+
export const XK_DEAD_BELOWTILDE: 0xfe6a;
|
|
103
|
+
export const XK_DEAD_BELOWBREVE: 0xfe6b;
|
|
104
|
+
export const XK_DEAD_BELOWDIAERESIS: 0xfe6c;
|
|
105
|
+
export const XK_DEAD_INVERTEDBREVE: 0xfe6d;
|
|
106
|
+
export const XK_DEAD_BELOWCOMMA: 0xfe6e;
|
|
107
|
+
export const XK_DEAD_CURRENCY: 0xfe6f;
|
|
108
|
+
|
|
109
|
+
/** Whether a keysym is one of the `XK_dead_*` block. */
|
|
110
|
+
export function isDeadKeysym(keysym: number): boolean;
|
|
111
|
+
|
|
112
|
+
export const XK_SHIFT_L: 0xffe1;
|
|
113
|
+
export const XK_SHIFT_R: 0xffe2;
|
|
114
|
+
export const XK_CONTROL_L: 0xffe3;
|
|
115
|
+
export const XK_CONTROL_R: 0xffe4;
|
|
116
|
+
export const XK_CAPS_LOCK: 0xffe5;
|
|
117
|
+
export const XK_ALT_L: 0xffe9;
|
|
118
|
+
export const XK_ALT_R: 0xffea;
|
|
119
|
+
export const XK_SUPER_L: 0xffeb;
|
|
120
|
+
export const XK_SUPER_R: 0xffec;
|
|
121
|
+
|
|
122
|
+
export const XK_F1: 0xffbe;
|
|
123
|
+
export const XK_F2: 0xffbf;
|
|
124
|
+
export const XK_F3: 0xffc0;
|
|
125
|
+
export const XK_F4: 0xffc1;
|
|
126
|
+
export const XK_F5: 0xffc2;
|
|
127
|
+
export const XK_F6: 0xffc3;
|
|
128
|
+
export const XK_F7: 0xffc4;
|
|
129
|
+
export const XK_F8: 0xffc5;
|
|
130
|
+
export const XK_F9: 0xffc6;
|
|
131
|
+
export const XK_F10: 0xffc7;
|
|
132
|
+
export const XK_F11: 0xffc8;
|
|
133
|
+
export const XK_F12: 0xffc9;
|
|
134
|
+
|
|
135
|
+
/** X11 modifier mask bits, as `ev.nativeEvent.buttons` carries them. */
|
|
136
|
+
export const MOD: {
|
|
137
|
+
Shift: 1;
|
|
138
|
+
Lock: 2;
|
|
139
|
+
Control: 4;
|
|
140
|
+
Mod1: 8;
|
|
141
|
+
Alt: 8;
|
|
142
|
+
Mod2: 16;
|
|
143
|
+
Mod3: 32;
|
|
144
|
+
Mod4: 64;
|
|
145
|
+
Super: 64;
|
|
146
|
+
Mod5: 128;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export type ModifierName = keyof typeof MOD;
|