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
package/src/compose.js ADDED
@@ -0,0 +1,868 @@
1
+ // Composition: dead keys and the Compose key, client-side.
2
+ //
3
+ // A key event carries one keysym and at most one code point, so the path
4
+ // from `EventManager._onKey` to `TextInputNode._insert` could only ever type
5
+ // what one key produces. That is enough for a US layout and for nothing
6
+ // else: `é` on a French, German or us-intl layout is `dead_acute` then `e`,
7
+ // and `dead_acute` is a keysym with no code point at all — it used to arrive
8
+ // and be dropped, so the accent vanished and the letter came out bare
9
+ // (issue #272).
10
+ //
11
+ // This is the state that was missing between those two points. It is
12
+ // entirely client-side: no X extension, no input-method process, no
13
+ // protocol. `XK_dead_*` and `XK_Multi_key` are ordinary keysyms that already
14
+ // arrive; what turns a *sequence* of them into a character is a table and a
15
+ // small machine in front of the insert.
16
+ //
17
+ // ## The table is Unicode's, not ours
18
+ //
19
+ // The obvious implementation is X's `Compose` file: six thousand lines
20
+ // mapping keysym sequences to strings. Bundling it would be 300 kB of data
21
+ // for the Latin coverage alone, and it would still be a snapshot of one
22
+ // machine's locale.
23
+ //
24
+ // A dead key *is* a combining mark, so the composition is the one Unicode
25
+ // already specifies: `dead_acute` is U+0301, and `dead_acute` + `e` is
26
+ // `'é'.normalize('NFC')`. That is 30 entries instead of 6000, it
27
+ // covers every base letter in every script — Cyrillic, Greek, Vietnamese
28
+ // with two stacked marks — and it cannot drift, because the data is the
29
+ // runtime's. Only the sequences Unicode has no canonical composition for
30
+ // need naming by hand: `dead_stroke` + `o` is `ø`, whose decomposition
31
+ // Unicode deliberately does not define, and the `Multi_key` symbol
32
+ // sequences (`Compose o c` is `©`), which are conventions rather than
33
+ // characters.
34
+ //
35
+ // ## Default, and the seam
36
+ //
37
+ // The built-in table is what an app gets with no configuration, and it is
38
+ // chosen to cover what a Latin-script keyboard can type: every dead key on
39
+ // every common layout, plus the punctuation and symbol sequences people
40
+ // actually reach for. The seam is `createRoot({ compose })` — `'system'`
41
+ // adds the machine's own Compose file (which is what picks up a personal
42
+ // `~/.XCompose`), `{ sequences }` adds or overrides individual ones, and
43
+ // `false` turns the whole thing off for an app that means to do its own.
44
+ //
45
+ // What this does *not* do is an input method: there is no preedit coming
46
+ // from another process, no candidate window, and therefore no CJK. That is
47
+ // XIM or an IBus/Fcitx D-Bus client, and it is the next tier of #272 — but
48
+ // it lands on this machinery rather than beside it, because the preedit
49
+ // buffer and the composition events it needs are the ones below.
50
+ import { readFileSync } from 'node:fs';
51
+ import { homedir } from 'node:os';
52
+ import { join } from 'node:path';
53
+ import {
54
+ charOf,
55
+ keysymOf,
56
+ isDeadKeysym,
57
+ XK_MULTI_KEY,
58
+ XK_ESCAPE,
59
+ XK_BACKSPACE,
60
+ XK_SPACE,
61
+ XK_DEAD_GRAVE,
62
+ XK_DEAD_ACUTE,
63
+ XK_DEAD_CIRCUMFLEX,
64
+ XK_DEAD_TILDE,
65
+ XK_DEAD_MACRON,
66
+ XK_DEAD_BREVE,
67
+ XK_DEAD_ABOVEDOT,
68
+ XK_DEAD_DIAERESIS,
69
+ XK_DEAD_ABOVERING,
70
+ XK_DEAD_DOUBLEACUTE,
71
+ XK_DEAD_CARON,
72
+ XK_DEAD_CEDILLA,
73
+ XK_DEAD_OGONEK,
74
+ XK_DEAD_IOTA,
75
+ XK_DEAD_VOICED_SOUND,
76
+ XK_DEAD_SEMIVOICED_SOUND,
77
+ XK_DEAD_BELOWDOT,
78
+ XK_DEAD_HOOK,
79
+ XK_DEAD_HORN,
80
+ XK_DEAD_STROKE,
81
+ XK_DEAD_ABOVECOMMA,
82
+ XK_DEAD_ABOVEREVERSEDCOMMA,
83
+ XK_DEAD_DOUBLEGRAVE,
84
+ XK_DEAD_BELOWRING,
85
+ XK_DEAD_BELOWMACRON,
86
+ XK_DEAD_BELOWCIRCUMFLEX,
87
+ XK_DEAD_BELOWTILDE,
88
+ XK_DEAD_BELOWBREVE,
89
+ XK_DEAD_BELOWDIAERESIS,
90
+ XK_DEAD_INVERTEDBREVE,
91
+ XK_DEAD_BELOWCOMMA,
92
+ XK_DEAD_CURRENCY,
93
+ } from './keysyms.js';
94
+
95
+ /**
96
+ * The combining mark each dead key applies. This is the whole dead-key
97
+ * table: everything else about `dead_acute` + `e` follows from Unicode
98
+ * normalisation.
99
+ */
100
+ const MARKS = {
101
+ [XK_DEAD_GRAVE]: '̀',
102
+ [XK_DEAD_ACUTE]: '́',
103
+ [XK_DEAD_CIRCUMFLEX]: '̂',
104
+ [XK_DEAD_TILDE]: '̃',
105
+ [XK_DEAD_MACRON]: '̄',
106
+ [XK_DEAD_BREVE]: '̆',
107
+ [XK_DEAD_ABOVEDOT]: '̇',
108
+ [XK_DEAD_DIAERESIS]: '̈',
109
+ [XK_DEAD_HOOK]: '̉',
110
+ [XK_DEAD_ABOVERING]: '̊',
111
+ [XK_DEAD_DOUBLEACUTE]: '̋',
112
+ [XK_DEAD_CARON]: '̌',
113
+ [XK_DEAD_DOUBLEGRAVE]: '̏',
114
+ [XK_DEAD_INVERTEDBREVE]: '̑',
115
+ [XK_DEAD_ABOVECOMMA]: '̓',
116
+ [XK_DEAD_ABOVEREVERSEDCOMMA]: '̔',
117
+ [XK_DEAD_HORN]: '̛',
118
+ [XK_DEAD_BELOWDOT]: '̣',
119
+ [XK_DEAD_BELOWDIAERESIS]: '̤',
120
+ [XK_DEAD_BELOWRING]: '̥',
121
+ [XK_DEAD_BELOWCOMMA]: '̦',
122
+ [XK_DEAD_CEDILLA]: '̧',
123
+ [XK_DEAD_OGONEK]: '̨',
124
+ [XK_DEAD_BELOWCIRCUMFLEX]: '̭',
125
+ [XK_DEAD_BELOWBREVE]: '̮',
126
+ [XK_DEAD_BELOWTILDE]: '̰',
127
+ [XK_DEAD_BELOWMACRON]: '̱',
128
+ [XK_DEAD_STROKE]: '̸',
129
+ [XK_DEAD_IOTA]: 'ͅ',
130
+ [XK_DEAD_VOICED_SOUND]: '゙',
131
+ [XK_DEAD_SEMIVOICED_SOUND]: '゚',
132
+ };
133
+
134
+ /**
135
+ * What a dead key shows while it is pending, and what it types on its own —
136
+ * `dead_acute` then space is `´`, which is how the accent character itself
137
+ * is typed on a layout that has no separate key for it.
138
+ *
139
+ * A mark with no spacing form of its own falls back to the mark on a space,
140
+ * which renders as the mark: not beautiful in every font, but it is the
141
+ * accent, which is what the user needs to see.
142
+ */
143
+ const SPACING = {
144
+ [XK_DEAD_GRAVE]: '`',
145
+ [XK_DEAD_ACUTE]: '´',
146
+ [XK_DEAD_CIRCUMFLEX]: '^',
147
+ [XK_DEAD_TILDE]: '~',
148
+ [XK_DEAD_MACRON]: '¯',
149
+ [XK_DEAD_BREVE]: '˘',
150
+ [XK_DEAD_ABOVEDOT]: '˙',
151
+ [XK_DEAD_DIAERESIS]: '¨',
152
+ [XK_DEAD_ABOVERING]: '°',
153
+ [XK_DEAD_DOUBLEACUTE]: '˝',
154
+ [XK_DEAD_CARON]: 'ˇ',
155
+ [XK_DEAD_CEDILLA]: '¸',
156
+ [XK_DEAD_OGONEK]: '˛',
157
+ [XK_DEAD_STROKE]: '/',
158
+ [XK_DEAD_CURRENCY]: '¤',
159
+ };
160
+
161
+ /**
162
+ * The pairs Unicode will not compose, because the character has no
163
+ * canonical decomposition: a stroke through a letter is a different letter,
164
+ * not a decorated one, and a currency sign is not a decorated letter at all.
165
+ * Small and closed, which is the test for what belongs here.
166
+ */
167
+ const UNCOMPOSED = {
168
+ [XK_DEAD_STROKE]: {
169
+ o: 'ø',
170
+ O: 'Ø',
171
+ d: 'đ',
172
+ D: 'Đ',
173
+ l: 'ł',
174
+ L: 'Ł',
175
+ t: 'ŧ',
176
+ T: 'Ŧ',
177
+ h: 'ħ',
178
+ H: 'Ħ',
179
+ b: 'ƀ',
180
+ B: 'Ƀ',
181
+ g: 'ǥ',
182
+ G: 'Ǥ',
183
+ i: 'ɨ',
184
+ I: 'Ɨ',
185
+ },
186
+ [XK_DEAD_CURRENCY]: {
187
+ e: '€',
188
+ E: '€',
189
+ l: '£',
190
+ L: '£',
191
+ y: '¥',
192
+ Y: '¥',
193
+ c: '¢',
194
+ C: '¢',
195
+ r: '₹',
196
+ R: '₹',
197
+ w: '₩',
198
+ W: '₩',
199
+ f: '₣',
200
+ F: '₣',
201
+ d: '$',
202
+ D: '$',
203
+ },
204
+ };
205
+
206
+ /**
207
+ * A character that names an accent when it follows `Multi_key`. This is
208
+ * what makes `Compose ' e` type `é` without a table entry per letter: the
209
+ * quote resolves to `dead_acute` and the ordinary dead-key path takes it
210
+ * from there, in either order (`Compose e '` too, which is how half the
211
+ * standard Compose file is written).
212
+ */
213
+ const ACCENTS = {
214
+ "'": XK_DEAD_ACUTE,
215
+ '`': XK_DEAD_GRAVE,
216
+ '^': XK_DEAD_CIRCUMFLEX,
217
+ '~': XK_DEAD_TILDE,
218
+ '"': XK_DEAD_DIAERESIS,
219
+ ',': XK_DEAD_CEDILLA,
220
+ ';': XK_DEAD_OGONEK,
221
+ _: XK_DEAD_MACRON,
222
+ '.': XK_DEAD_ABOVEDOT,
223
+ '/': XK_DEAD_STROKE,
224
+ o: XK_DEAD_ABOVERING,
225
+ v: XK_DEAD_CARON,
226
+ U: XK_DEAD_BREVE,
227
+ };
228
+
229
+ /**
230
+ * The `Multi_key` sequences, keyed by the characters that follow it. These
231
+ * are the ones that are conventions rather than compositions — no rule
232
+ * derives `©` from `o` and `c` — so they are named, and the list is the
233
+ * part of X's Compose file people actually press. Anything reachable
234
+ * through an accent (`Compose ' e`) is deliberately absent: `ACCENTS`
235
+ * covers those by rule.
236
+ */
237
+ const SYMBOLS = {
238
+ oc: '©',
239
+ oC: '©',
240
+ Oc: '©',
241
+ OC: '©',
242
+ or: '®',
243
+ oR: '®',
244
+ Or: '®',
245
+ OR: '®',
246
+ tm: '™',
247
+ TM: '™',
248
+ '+-': '±',
249
+ xx: '×',
250
+ ':-': '÷',
251
+ '-:': '÷',
252
+ oo: '°',
253
+ '..': '…',
254
+ '--.': '–',
255
+ '---': '—',
256
+ '<<': '«',
257
+ '>>': '»',
258
+ '"<': '“',
259
+ '<"': '“',
260
+ '">': '”',
261
+ '>"': '”',
262
+ "'<": '‘',
263
+ "<'": '‘',
264
+ "'>": '’',
265
+ ">'": '’',
266
+ '??': '¿',
267
+ '!!': '¡',
268
+ ss: 'ß',
269
+ SS: 'ẞ',
270
+ ae: 'æ',
271
+ AE: 'Æ',
272
+ oe: 'œ',
273
+ OE: 'Œ',
274
+ 12: '½',
275
+ 13: '⅓',
276
+ 14: '¼',
277
+ 34: '¾',
278
+ '=e': '€',
279
+ '=E': '€',
280
+ '=l': '£',
281
+ '=L': '£',
282
+ '=y': '¥',
283
+ '=Y': '¥',
284
+ '=r': '₹',
285
+ '=R': '₹',
286
+ '=w': '₩',
287
+ '=W': '₩',
288
+ 'c/': '¢',
289
+ '/c': '¢',
290
+ '->': '→',
291
+ '<-': '←',
292
+ '<=': '≤',
293
+ '>=': '≥',
294
+ '%o': '‰',
295
+ '/u': 'µ',
296
+ mu: 'µ',
297
+ '.=': '•',
298
+ so: '§',
299
+ 'p!': '¶',
300
+ 'P!': '¶',
301
+ '+z': '†',
302
+ };
303
+
304
+ /**
305
+ * What the Compose key itself shows while a sequence is open.
306
+ *
307
+ * Not nothing: pressing Compose and seeing the field sit there is the
308
+ * "answer the input, not the outcome" failure (AGENTS.md) in its purest
309
+ * form — the key did something, and the only evidence is that the next
310
+ * two keys will behave strangely.
311
+ */
312
+ const COMPOSE_MARK = '·';
313
+
314
+ /** A sequence trie: keysym arrays in, `{ text, prefix }` out. */
315
+ class ComposeTable {
316
+ constructor() {
317
+ this.root = { children: new Map(), text: undefined };
318
+ }
319
+
320
+ /** Later definitions win, which is what makes a Compose file an override
321
+ * of the built-ins rather than an addition beside them. */
322
+ add(keysyms, text) {
323
+ let node = this.root;
324
+ for (const k of keysyms) {
325
+ let next = node.children.get(k);
326
+ if (!next) {
327
+ next = { children: new Map(), text: undefined };
328
+ node.children.set(k, next);
329
+ }
330
+ node = next;
331
+ }
332
+ node.text = text;
333
+ }
334
+
335
+ lookup(keysyms) {
336
+ let node = this.root;
337
+ for (const k of keysyms) {
338
+ node = node.children.get(k);
339
+ if (!node) return { text: undefined, prefix: false };
340
+ }
341
+ return { text: node.text, prefix: node.children.size > 0 };
342
+ }
343
+ }
344
+
345
+ let builtinTable = null;
346
+
347
+ /** The table every app gets without asking. Built once, lazily. */
348
+ export function builtinCompose() {
349
+ if (builtinTable) return builtinTable;
350
+ const table = new ComposeTable();
351
+ for (const [seq, text] of Object.entries(SYMBOLS)) {
352
+ table.add([XK_MULTI_KEY, ...Array.from(seq, keysymOf)], text);
353
+ }
354
+ builtinTable = table;
355
+ return table;
356
+ }
357
+
358
+ // --- Compose files ---------------------------------------------------------
359
+
360
+ /**
361
+ * X keysym names for the ASCII range, which is what a Compose file's input
362
+ * side is written in. Generated rather than listed: the letters and digits
363
+ * name themselves, and only the punctuation has names to remember.
364
+ */
365
+ const ASCII_NAMES = (() => {
366
+ const names = new Map();
367
+ const punctuation = {
368
+ 0x20: 'space',
369
+ 0x21: 'exclam',
370
+ 0x22: 'quotedbl',
371
+ 0x23: 'numbersign',
372
+ 0x24: 'dollar',
373
+ 0x25: 'percent',
374
+ 0x26: 'ampersand',
375
+ 0x27: 'apostrophe',
376
+ 0x28: 'parenleft',
377
+ 0x29: 'parenright',
378
+ 0x2a: 'asterisk',
379
+ 0x2b: 'plus',
380
+ 0x2c: 'comma',
381
+ 0x2d: 'minus',
382
+ 0x2e: 'period',
383
+ 0x2f: 'slash',
384
+ 0x3a: 'colon',
385
+ 0x3b: 'semicolon',
386
+ 0x3c: 'less',
387
+ 0x3d: 'equal',
388
+ 0x3e: 'greater',
389
+ 0x3f: 'question',
390
+ 0x40: 'at',
391
+ 0x5b: 'bracketleft',
392
+ 0x5c: 'backslash',
393
+ 0x5d: 'bracketright',
394
+ 0x5e: 'asciicircum',
395
+ 0x5f: 'underscore',
396
+ 0x60: 'grave',
397
+ 0x7b: 'braceleft',
398
+ 0x7c: 'bar',
399
+ 0x7d: 'braceright',
400
+ 0x7e: 'asciitilde',
401
+ };
402
+ for (const [code, name] of Object.entries(punctuation)) {
403
+ names.set(name, Number(code));
404
+ }
405
+ for (let c = 0x30; c <= 0x39; c++) names.set(String.fromCharCode(c), c);
406
+ for (let c = 0x41; c <= 0x5a; c++) names.set(String.fromCharCode(c), c);
407
+ for (let c = 0x61; c <= 0x7a; c++) names.set(String.fromCharCode(c), c);
408
+ return names;
409
+ })();
410
+
411
+ const DEAD_NAMES = new Map([
412
+ ['Multi_key', XK_MULTI_KEY],
413
+ ['dead_grave', XK_DEAD_GRAVE],
414
+ ['dead_acute', XK_DEAD_ACUTE],
415
+ ['dead_circumflex', XK_DEAD_CIRCUMFLEX],
416
+ ['dead_tilde', XK_DEAD_TILDE],
417
+ ['dead_macron', XK_DEAD_MACRON],
418
+ ['dead_breve', XK_DEAD_BREVE],
419
+ ['dead_abovedot', XK_DEAD_ABOVEDOT],
420
+ ['dead_diaeresis', XK_DEAD_DIAERESIS],
421
+ ['dead_abovering', XK_DEAD_ABOVERING],
422
+ ['dead_doubleacute', XK_DEAD_DOUBLEACUTE],
423
+ ['dead_caron', XK_DEAD_CARON],
424
+ ['dead_cedilla', XK_DEAD_CEDILLA],
425
+ ['dead_ogonek', XK_DEAD_OGONEK],
426
+ ['dead_iota', XK_DEAD_IOTA],
427
+ ['dead_voiced_sound', XK_DEAD_VOICED_SOUND],
428
+ ['dead_semivoiced_sound', XK_DEAD_SEMIVOICED_SOUND],
429
+ ['dead_belowdot', XK_DEAD_BELOWDOT],
430
+ ['dead_hook', XK_DEAD_HOOK],
431
+ ['dead_horn', XK_DEAD_HORN],
432
+ ['dead_stroke', XK_DEAD_STROKE],
433
+ ['dead_abovecomma', XK_DEAD_ABOVECOMMA],
434
+ ['dead_psili', XK_DEAD_ABOVECOMMA],
435
+ ['dead_abovereversedcomma', XK_DEAD_ABOVEREVERSEDCOMMA],
436
+ ['dead_dasia', XK_DEAD_ABOVEREVERSEDCOMMA],
437
+ ['dead_doublegrave', XK_DEAD_DOUBLEGRAVE],
438
+ ['dead_belowring', XK_DEAD_BELOWRING],
439
+ ['dead_belowmacron', XK_DEAD_BELOWMACRON],
440
+ ['dead_belowcircumflex', XK_DEAD_BELOWCIRCUMFLEX],
441
+ ['dead_belowtilde', XK_DEAD_BELOWTILDE],
442
+ ['dead_belowbreve', XK_DEAD_BELOWBREVE],
443
+ ['dead_belowdiaeresis', XK_DEAD_BELOWDIAERESIS],
444
+ ['dead_invertedbreve', XK_DEAD_INVERTEDBREVE],
445
+ ['dead_belowcomma', XK_DEAD_BELOWCOMMA],
446
+ ['dead_currency', XK_DEAD_CURRENCY],
447
+ ]);
448
+
449
+ /** `<name>` → keysym, or undefined for a name outside what we can resolve. */
450
+ function keysymNamed(name) {
451
+ const dead = DEAD_NAMES.get(name);
452
+ if (dead !== undefined) return dead;
453
+ const ascii = ASCII_NAMES.get(name);
454
+ if (ascii !== undefined) return ascii;
455
+ // the two escape hatches X's own format has for everything unnamed
456
+ if (/^U[0-9A-Fa-f]{4,6}$/.test(name)) {
457
+ return keysymOf(String.fromCodePoint(parseInt(name.slice(1), 16)));
458
+ }
459
+ if (/^0x[0-9A-Fa-f]+$/.test(name)) return Number(name);
460
+ return undefined;
461
+ }
462
+
463
+ const LINE =
464
+ /^\s*((?:<[A-Za-z_0-9]+>\s*)+):\s*(?:"((?:[^"\\]|\\.)*)"|([A-Za-z_0-9]+))/;
465
+
466
+ function unescape(text) {
467
+ return text.replace(/\\(x[0-9A-Fa-f]{2}|[0-7]{1,3}|.)/g, (_, esc) => {
468
+ if (esc[0] === 'x') return String.fromCharCode(parseInt(esc.slice(1), 16));
469
+ if (/^[0-7]+$/.test(esc)) return String.fromCharCode(parseInt(esc, 8));
470
+ return { n: '\n', t: '\t', r: '\r' }[esc] ?? esc;
471
+ });
472
+ }
473
+
474
+ /**
475
+ * Parse X's Compose format — the one in `/usr/share/X11/locale/<locale>/Compose`
476
+ * and in `~/.XCompose`:
477
+ *
478
+ * ```
479
+ * <Multi_key> <o> <c> : "©" copyright
480
+ * <dead_acute> <e> : "é" eacute
481
+ * ```
482
+ *
483
+ * Returns the sequences it understood and a count of the lines it did not.
484
+ * Two things it does not do, both stated rather than silently absorbed:
485
+ * `include` directives are ignored (the system file's first line includes
486
+ * the locale's, so a bare `~/.XCompose` may parse to almost nothing), and a
487
+ * line whose input side names a keysym outside ASCII and the dead-key block
488
+ * — `<Greek_alpha>`, `<Cyrillic_a>` — is skipped, because resolving those
489
+ * names needs a table this package does not carry.
490
+ */
491
+ export function parseCompose(text) {
492
+ const sequences = [];
493
+ let skipped = 0;
494
+ for (const line of String(text).split('\n')) {
495
+ if (!line.trim() || line.trim().startsWith('#')) continue;
496
+ const match = LINE.exec(line);
497
+ if (!match) {
498
+ if (!/^\s*include\b/.test(line)) skipped++;
499
+ continue;
500
+ }
501
+ const [, lhs, quoted, named] = match;
502
+ const keysyms = [];
503
+ let ok = true;
504
+ for (const [, name] of lhs.matchAll(/<([A-Za-z_0-9]+)>/g)) {
505
+ const keysym = keysymNamed(name);
506
+ if (keysym === undefined) {
507
+ ok = false;
508
+ break;
509
+ }
510
+ keysyms.push(keysym);
511
+ }
512
+ // the right-hand side may be a string, a keysym name, or both; the
513
+ // string wins, and a bare name is the character that keysym types
514
+ const result =
515
+ quoted !== undefined ? unescape(quoted) : charOf(keysymNamed(named) ?? 0);
516
+ if (!ok || !result) {
517
+ skipped++;
518
+ continue;
519
+ }
520
+ sequences.push([keysyms, result]);
521
+ }
522
+ return { sequences, skipped };
523
+ }
524
+
525
+ /**
526
+ * The Compose file this machine would use, or `null` when there is none —
527
+ * which is the normal answer on macOS, where XQuartz ships no locale tree.
528
+ *
529
+ * The search is Xlib's: `$XCOMPOSEFILE`, then a personal `~/.XCompose`,
530
+ * then the locale's file under the X11 tree.
531
+ */
532
+ export function systemComposeFile() {
533
+ const candidates = [];
534
+ if (process.env.XCOMPOSEFILE) candidates.push(process.env.XCOMPOSEFILE);
535
+ const home = process.env.HOME || homedir();
536
+ if (home) candidates.push(join(home, '.XCompose'));
537
+ const locale =
538
+ process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || '';
539
+ const name = locale.split(':')[0] || 'en_US.UTF-8';
540
+ for (const dir of ['/usr/share/X11/locale', '/opt/X11/share/X11/locale']) {
541
+ candidates.push(join(dir, name, 'Compose'));
542
+ candidates.push(join(dir, 'en_US.UTF-8', 'Compose'));
543
+ }
544
+ for (const path of candidates) {
545
+ try {
546
+ readFileSync(path);
547
+ return path;
548
+ } catch {
549
+ // not there, or not readable: try the next rung
550
+ }
551
+ }
552
+ return null;
553
+ }
554
+
555
+ /**
556
+ * Build the table a root composes with.
557
+ *
558
+ * `option` is `createRoot`'s `compose`:
559
+ * `undefined` — the built-ins; `false` — no composition at all;
560
+ * `'system'` — the built-ins plus this machine's Compose file;
561
+ * `{ file, sequences }` — the built-ins plus a file and/or explicit
562
+ * sequences, each overriding what came before it.
563
+ */
564
+ export function composeTable(option) {
565
+ if (option === false) return null;
566
+ const settings =
567
+ option === 'system'
568
+ ? { file: 'system' }
569
+ : typeof option === 'object' && option !== null
570
+ ? option
571
+ : {};
572
+ const base = builtinCompose();
573
+ if (!settings.file && !settings.sequences) return base;
574
+
575
+ // a copy, so one root's Compose file is not every root's
576
+ const extended = new ComposeTable();
577
+ copyInto(extended, base.root, []);
578
+
579
+ if (settings.file) {
580
+ const path =
581
+ settings.file === 'system' ? systemComposeFile() : settings.file;
582
+ if (path) {
583
+ let text = null;
584
+ try {
585
+ text = readFileSync(path, 'utf8');
586
+ } catch (err) {
587
+ // Named explicitly and not there is a mistake worth hearing about;
588
+ // 'system' finding nothing is Tuesday on macOS and says nothing.
589
+ if (settings.file !== 'system') {
590
+ console.warn(
591
+ `react-x11: could not read the Compose file ${path} ` +
592
+ `(${err?.code ?? err?.message}). Composition falls back to the ` +
593
+ 'built-in sequences; pass compose: false to turn it off.',
594
+ );
595
+ }
596
+ }
597
+ if (text) {
598
+ for (const [keysyms, result] of parseCompose(text).sequences) {
599
+ extended.add(keysyms, result);
600
+ }
601
+ }
602
+ }
603
+ }
604
+ for (const [keysyms, result] of settings.sequences ?? []) {
605
+ extended.add(
606
+ Array.from(keysyms, (k) => (typeof k === 'string' ? keysymOf(k) : k)),
607
+ result,
608
+ );
609
+ }
610
+ return extended;
611
+ }
612
+
613
+ function copyInto(table, node, path) {
614
+ if (node.text !== undefined) table.add(path, node.text);
615
+ for (const [keysym, child] of node.children) {
616
+ copyInto(table, child, [...path, keysym]);
617
+ }
618
+ }
619
+
620
+ /**
621
+ * Install a root's table on its connection, the way the other per-root
622
+ * settings ride the app object. Returns it so a caller can tell composition
623
+ * off from composition on.
624
+ */
625
+ export function beginCompose(app, option) {
626
+ const table = composeTable(option);
627
+ if (app) app._reactX11Compose = table;
628
+ return table;
629
+ }
630
+
631
+ /** The table a window composes with: its root's, or the built-ins for an
632
+ * app that never went through `createRoot` (a unit test, a mock). */
633
+ export function composeTableFor(app) {
634
+ const table = app?._reactX11Compose;
635
+ return table === undefined ? builtinCompose() : table;
636
+ }
637
+
638
+ // --- the machine -----------------------------------------------------------
639
+
640
+ // Modifier keysyms, plus the two group/level switches. A sequence has to
641
+ // survive them: reaching an uppercase letter means pressing Shift, and
642
+ // AltGr is how half of Europe reaches a dead key in the first place.
643
+ const MODIFIER_LOW = 0xffe1;
644
+ const MODIFIER_HIGH = 0xffee;
645
+ const XK_MODE_SWITCH = 0xff7e;
646
+ const XK_ISO_LEVEL3_SHIFT = 0xfe03;
647
+ const XK_ISO_LEVEL5_SHIFT = 0xfe11;
648
+
649
+ function isModifier(keysym) {
650
+ return (
651
+ (keysym >= MODIFIER_LOW && keysym <= MODIFIER_HIGH) ||
652
+ keysym === XK_MODE_SWITCH ||
653
+ keysym === XK_ISO_LEVEL3_SHIFT ||
654
+ keysym === XK_ISO_LEVEL5_SHIFT
655
+ );
656
+ }
657
+
658
+ /** The character a pending keysym stands for. `Multi_key` is the one that
659
+ * differs between the two readers: it *shows* as a mark, and it *types*
660
+ * nothing, because `·` is a note about the keyboard rather than something
661
+ * anybody pressed a key to say. */
662
+ function markOf(keysym) {
663
+ return SPACING[keysym] ?? MARKS[keysym] ?? charOf(keysym);
664
+ }
665
+
666
+ /** What an open sequence shows in the preedit. */
667
+ function preeditOf(keys) {
668
+ return keys
669
+ .map((k) => (k === XK_MULTI_KEY ? COMPOSE_MARK : markOf(k)))
670
+ .join('');
671
+ }
672
+
673
+ /** What an open sequence types when it turns out not to be one. */
674
+ function typedOf(keys) {
675
+ return keys.map((k) => (k === XK_MULTI_KEY ? '' : markOf(k))).join('');
676
+ }
677
+
678
+ /** The dead key a pending keysym stands for, if any — a `dead_*` keysym is
679
+ * itself, and a character after `Multi_key` may name one. */
680
+ function deadOf(keysym, viaMulti) {
681
+ if (isDeadKeysym(keysym)) return keysym;
682
+ if (!viaMulti) return undefined;
683
+ return ACCENTS[charOf(keysym)];
684
+ }
685
+
686
+ /** Apply dead keys to a base keysym, or undefined if they do not compose. */
687
+ function applyDead(deads, base) {
688
+ if (base === XK_SPACE) {
689
+ // the accent on its own, which is how `´` is typed at all on a layout
690
+ // whose only acute is a dead key
691
+ return deads.map((d) => SPACING[d] ?? MARKS[d] ?? '').join('');
692
+ }
693
+ const char = charOf(base);
694
+ if (!char) return undefined;
695
+ if (deads.length === 1) {
696
+ const named = UNCOMPOSED[deads[0]]?.[char];
697
+ if (named) return named;
698
+ }
699
+ let text = char;
700
+ for (const dead of deads) {
701
+ const mark = MARKS[dead];
702
+ if (!mark) return undefined;
703
+ text += mark;
704
+ }
705
+ const composed = text.normalize('NFC');
706
+ // A sequence that stays decomposed is one Unicode has no character for:
707
+ // `dead_acute` + `q` normalises to `q` + U+0301 and is not a `q́` anybody
708
+ // meant to type. Two code points out means no.
709
+ return Array.from(composed).length === 1 ? composed : undefined;
710
+ }
711
+
712
+ /**
713
+ * Everything the algorithmic path can make of a sequence — the dead-key
714
+ * table, in both the orders the Compose file writes them.
715
+ */
716
+ function derive(keys) {
717
+ const viaMulti = keys[0] === XK_MULTI_KEY;
718
+ const body = viaMulti ? keys.slice(1) : keys;
719
+ if (body.length < 2) return undefined;
720
+ const direct = inOrder(body, viaMulti);
721
+ if (direct !== undefined) return direct;
722
+ // `Compose e '` as well as `Compose ' e`: the standard file spells the
723
+ // common ones both ways round, and a user who learned one is not going to
724
+ // be told the other is the real one.
725
+ if (viaMulti && body.length === 2) return inOrder([body[1], body[0]], true);
726
+ return undefined;
727
+ }
728
+
729
+ /** Every key but the last read as a dead key, applied to the last. */
730
+ function inOrder(body, viaMulti) {
731
+ const deads = [];
732
+ for (const keysym of body.slice(0, -1)) {
733
+ const dead = deadOf(keysym, viaMulti);
734
+ if (dead === undefined) return undefined;
735
+ deads.push(dead);
736
+ }
737
+ return applyDead(deads, body[body.length - 1]);
738
+ }
739
+
740
+ /**
741
+ * The composition state machine for one keyboard focus.
742
+ *
743
+ * `probe` is pure and `apply` is what commits the transition, deliberately:
744
+ * the key event is dispatched to the application *before* the composer eats
745
+ * it, so an `onKeyDown` that calls `preventDefault()` gets to keep its
746
+ * chord — and it can only do that if asking what would happen has not
747
+ * already changed anything.
748
+ */
749
+ export class Composer {
750
+ constructor(table = builtinCompose()) {
751
+ this.table = table;
752
+ this.keys = [];
753
+ }
754
+
755
+ get composing() {
756
+ return this.keys.length > 0;
757
+ }
758
+
759
+ get preedit() {
760
+ return preeditOf(this.keys);
761
+ }
762
+
763
+ reset() {
764
+ this.keys = [];
765
+ }
766
+
767
+ /**
768
+ * What this keysym would do, without doing it.
769
+ *
770
+ * @returns {{keys: number[], consumed: boolean,
771
+ * preedit: string|null, text: string|null}}
772
+ * `consumed` is whether the key belongs to the composition rather than
773
+ * to the application, `preedit` is what to show (null for "this key is
774
+ * nothing to do with composition"), and `text` is what to insert.
775
+ */
776
+ probe(keysym) {
777
+ const idle = {
778
+ keys: this.keys,
779
+ consumed: false,
780
+ preedit: null,
781
+ text: null,
782
+ };
783
+ if (keysym == null || isModifier(keysym)) return idle;
784
+
785
+ if (!this.composing) {
786
+ if (keysym === XK_MULTI_KEY || isDeadKeysym(keysym)) {
787
+ return this._pending([keysym]);
788
+ }
789
+ return idle;
790
+ }
791
+
792
+ // Escape abandons the sequence: nothing typed, and the accent that was
793
+ // showing goes away. The one gesture every composing user needs and the
794
+ // only one that cannot be a fallback, since Escape has no character.
795
+ if (keysym === XK_ESCAPE) {
796
+ return { keys: [], consumed: true, preedit: '', text: null };
797
+ }
798
+ // Backspace un-presses the last key of the sequence, the way it undoes
799
+ // anything else half-typed.
800
+ if (keysym === XK_BACKSPACE) {
801
+ return this._pending(this.keys.slice(0, -1));
802
+ }
803
+
804
+ const candidate = [...this.keys, keysym];
805
+ const hit = this.table.lookup(candidate);
806
+ if (hit.text !== undefined) return this._commit(hit.text);
807
+ const derived = derive(candidate);
808
+ // A table entry that is also a prefix (`Compose - -` before
809
+ // `Compose - - -`) commits the shorter one, because there is nothing to
810
+ // wait for: X's own tables do not nest that way.
811
+ if (derived !== undefined) return this._commit(derived);
812
+ if (hit.prefix) return this._pending(candidate);
813
+ // A dead key on a dead key stacks — Vietnamese `ế` is circumflex then
814
+ // acute then `e` — except for the same one twice, which every layout
815
+ // uses to type the accent itself.
816
+ if (isDeadKeysym(keysym)) {
817
+ if (keysym === this.keys[this.keys.length - 1]) {
818
+ return this._commit(markOf(keysym));
819
+ }
820
+ return this._pending(candidate);
821
+ }
822
+ // The key straight after Compose always waits for one more, whatever the
823
+ // table says. That is what makes `Compose e '` work as well as
824
+ // `Compose ' e`: the standard file spells the common accents both ways
825
+ // round, and the letter-first order cannot be recognised until the
826
+ // accent after it has arrived.
827
+ if (
828
+ this.keys.length === 1 &&
829
+ this.keys[0] === XK_MULTI_KEY &&
830
+ charOf(keysym)
831
+ ) {
832
+ return this._pending(candidate);
833
+ }
834
+
835
+ // No sequence, so the keys are typed as they came: X's rule for a failed
836
+ // composition is that nothing is swallowed. A key with no character of
837
+ // its own — an arrow, Return — hands the accent over and then takes its
838
+ // normal turn, which is why `consumed` is false while `text` is not null.
839
+ const accent = typedOf(this.keys);
840
+ const char = charOf(keysym);
841
+ if (!char) {
842
+ return { keys: [], consumed: false, preedit: '', text: accent };
843
+ }
844
+ return this._commit(accent + char);
845
+ }
846
+
847
+ _pending(keys) {
848
+ if (keys.length === 0) {
849
+ return { keys, consumed: true, preedit: '', text: null };
850
+ }
851
+ return { keys, consumed: true, preedit: preeditOf(keys), text: null };
852
+ }
853
+
854
+ _commit(text) {
855
+ return { keys: [], consumed: true, preedit: '', text };
856
+ }
857
+
858
+ /** Commit a `probe` result. */
859
+ apply(result) {
860
+ this.keys = result.keys;
861
+ return result;
862
+ }
863
+
864
+ /** Probe and apply in one step — what a test or a non-event caller wants. */
865
+ feed(keysym) {
866
+ return this.apply(this.probe(keysym));
867
+ }
868
+ }