react-x11 2.51.0 → 2.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +87 -54
  2. package/docs/README.md +410 -0
  3. package/docs/accessibility.md +628 -0
  4. package/docs/appearance.md +416 -0
  5. package/docs/architecture/accessibility.md +470 -0
  6. package/docs/architecture/animation.md +707 -0
  7. package/docs/architecture/container-queries.md +525 -0
  8. package/docs/architecture/custom-layout.md +334 -0
  9. package/docs/architecture/drag-and-drop.md +941 -0
  10. package/docs/architecture/element-layer-contents.md +622 -0
  11. package/docs/architecture/frame-pacing.md +255 -0
  12. package/docs/architecture/grid-layout.md +358 -0
  13. package/docs/architecture/protocol-efficiency.md +357 -0
  14. package/docs/architecture/video.md +1149 -0
  15. package/docs/architecture/windowed-regions.md +854 -0
  16. package/docs/click-to-component.md +125 -0
  17. package/docs/clipboard.md +281 -0
  18. package/docs/components.md +1749 -0
  19. package/docs/dbus.md +277 -0
  20. package/docs/debugging.md +464 -0
  21. package/docs/desktop-calendar.md +284 -0
  22. package/docs/desktop.md +648 -0
  23. package/docs/devtools.md +250 -0
  24. package/docs/drag-and-drop.md +663 -0
  25. package/docs/ecosystem/animation.md +394 -0
  26. package/docs/ecosystem/data-fetching.md +201 -0
  27. package/docs/ecosystem/dev-tooling.md +436 -0
  28. package/docs/ecosystem/forms.md +313 -0
  29. package/docs/ecosystem/headless.md +219 -0
  30. package/docs/ecosystem/i18n.md +89 -0
  31. package/docs/ecosystem/icons.md +410 -0
  32. package/docs/ecosystem/layout.md +216 -0
  33. package/docs/ecosystem/routing.md +146 -0
  34. package/docs/ecosystem/state.md +361 -0
  35. package/docs/ecosystem/testing.md +500 -0
  36. package/docs/ecosystem/theming.md +540 -0
  37. package/docs/ecosystem.md +473 -0
  38. package/docs/elements.md +2762 -0
  39. package/docs/embedding.md +288 -0
  40. package/docs/events.md +1025 -0
  41. package/docs/extending.md +2242 -0
  42. package/docs/eyedropper.md +223 -0
  43. package/docs/filedialog.md +258 -0
  44. package/docs/frame.md +580 -0
  45. package/docs/gl.md +170 -0
  46. package/docs/globalmenu.md +278 -0
  47. package/docs/glx.md +137 -0
  48. package/docs/macos.md +3295 -0
  49. package/docs/notifications.md +164 -0
  50. package/docs/packaging.md +611 -0
  51. package/docs/permissions.md +148 -0
  52. package/docs/react-features.md +430 -0
  53. package/docs/remote.md +223 -0
  54. package/docs/scale.md +339 -0
  55. package/docs/security.md +132 -0
  56. package/docs/styling.md +1928 -0
  57. package/docs/system.md +599 -0
  58. package/docs/testing.md +514 -0
  59. package/docs/typescript.md +117 -0
  60. package/docs/uri-schemes.md +380 -0
  61. package/docs/wayland-backend.md +591 -0
  62. package/docs/wayland.md +545 -0
  63. package/docs/windows-embedding.md +271 -0
  64. package/docs/windows-gl.md +221 -0
  65. package/docs/windows-integrations.md +233 -0
  66. package/docs/windows.md +1404 -0
  67. package/package.json +10 -3
  68. package/src/bus.js +39 -26
  69. package/src/cli/create.d.ts +43 -0
  70. package/src/cli/create.js +305 -0
  71. package/src/cli/env.js +484 -0
  72. package/src/cli/index.js +29 -2
  73. package/src/cli/templates/AGENTS.template.md +195 -0
  74. package/src/cli/templates/DESIGN.template.md +269 -0
  75. package/src/desktopintegration.js +2 -1
  76. package/src/desktopsettings.js +193 -24
  77. package/src/filedialog.js +3 -3
  78. package/src/keyboardstate.js +28 -0
  79. package/src/node.d.ts +10 -10
  80. package/src/portal.js +31 -0
  81. package/src/screencolor.js +5 -10
  82. package/src/types/system.d.ts +8 -6
  83. package/src/wayland/app.js +59 -0
  84. package/src/wayland/clipboard.js +14 -7
  85. package/src/wayland/connection.js +3 -0
  86. package/src/wayland/context2d.js +129 -10
  87. package/src/wayland/protocols/index.json +6 -0
  88. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  89. package/src/wayland/seat.js +28 -1
  90. package/src/wayland/swapchain.js +23 -0
  91. package/src/wayland/xkb.js +137 -0
@@ -67,6 +67,8 @@ const MODE_RRECT = 3;
67
67
  const MODE_GRADIENT = 4;
68
68
  /** A texture read through a projection: `vParams` is (s/w, t/w, 1/w). */
69
69
  const MODE_PROJECTIVE = 5;
70
+ /** A pattern: an image laid down from the fragment's device position. */
71
+ const MODE_PATTERN = 6;
70
72
 
71
73
  /**
72
74
  * The stencil buffer, shared between two uses: bit 7 is the clip (set where
@@ -132,6 +134,12 @@ uniform highp vec2 uViewport;
132
134
  uniform int uGradKind;
133
135
  uniform vec4 uGradA;
134
136
  uniform vec4 uGradB;
137
+ // a pattern: device position to its pixels (the inverse transform it is
138
+ // filled under, a b c d and then e f), its size, and which axes repeat (1 x,
139
+ // 2 y) and whether its rows run bottom up (4: a render target)
140
+ uniform highp vec4 uPatInv;
141
+ uniform highp vec4 uPatOff;
142
+ uniform int uPatFlags;
135
143
 
136
144
  float sdRoundBox(vec2 p, vec2 b, float r) {
137
145
  vec2 q = abs(p) - b + r;
@@ -167,6 +175,28 @@ void main() {
167
175
  // Interpolated across the quad in the plane of the screen, s/w, t/w and
168
176
  // 1/w are each exact, and so is their quotient: a texture in perspective.
169
177
  gl_FragColor = vColor * sampleTex(vParams.xy / vParams.z);
178
+ } else if (uMode == 6) {
179
+ // A pattern, from the fragment's device position as a gradient is, so
180
+ // any shape can be filled with one: back through the transform it was
181
+ // filled under to its own pixels, wrapped along the axes it repeats on
182
+ // and clear off the ones it does not, and read at the centre of the
183
+ // texel it lands in, so a tile drawn at its own size lands pixel for
184
+ // pixel — a grid's dots do not smear across a seam.
185
+ highp vec2 p = vec2(gl_FragCoord.x, uViewport.y - gl_FragCoord.y);
186
+ highp vec2 q = vec2(uPatInv.x * p.x + uPatInv.z * p.y + uPatOff.x,
187
+ uPatInv.y * p.x + uPatInv.w * p.y + uPatOff.y);
188
+ highp vec2 size = uPatOff.zw;
189
+ highp vec2 t = q / size;
190
+ float flags = float(uPatFlags);
191
+ if (mod(flags, 2.0) >= 1.0) t.x = fract(t.x);
192
+ if (mod(floor(flags / 2.0), 2.0) >= 1.0) t.y = fract(t.y);
193
+ if (t.x < 0.0 || t.x >= 1.0 || t.y < 0.0 || t.y >= 1.0) {
194
+ gl_FragColor = vec4(0.0);
195
+ } else {
196
+ t = (floor(t * size) + 0.5) / size;
197
+ if (flags >= 4.0) t.y = 1.0 - t.y;
198
+ gl_FragColor = vColor * sampleTex(t);
199
+ }
170
200
  } else {
171
201
  // Gradients are evaluated per fragment from its device position, so any
172
202
  // shape — a cover quad over a path included — can be filled with one.
@@ -415,6 +445,43 @@ export function parseFont(font) {
415
445
  */
416
446
  const fontIds = new WeakMap();
417
447
  let nextFontId = 1;
448
+ const REPETITIONS = new Set(['repeat', 'repeat-x', 'repeat-y', 'no-repeat']);
449
+
450
+ /**
451
+ * A pattern: an image or a surface, and the axes it repeats along. Its
452
+ * texture is looked up as it is filled rather than made here, so a surface
453
+ * drawn on again after the pattern was made fills with what it holds then,
454
+ * as a canvas pattern of a canvas does.
455
+ */
456
+ class Pattern {
457
+ constructor(source, repetition) {
458
+ this.source = source;
459
+ this.repetition = repetition;
460
+ /** its own transform, under the one it is filled with (`setTransform`) */
461
+ this.m = null;
462
+ }
463
+
464
+ setTransform(m) {
465
+ this.m = m
466
+ ? [m.a ?? 1, m.b ?? 0, m.c ?? 0, m.d ?? 1, m.e ?? 0, m.f ?? 0]
467
+ : null;
468
+ }
469
+ }
470
+
471
+ function invert(m) {
472
+ const det = m[0] * m[3] - m[1] * m[2];
473
+ if (!det) return null;
474
+ const a = m[3] / det;
475
+ const b = -m[1] / det;
476
+ const c = -m[2] / det;
477
+ const d = m[0] / det;
478
+ return [a, b, c, d, -(a * m[4] + c * m[5]), -(b * m[4] + d * m[5])];
479
+ }
480
+
481
+ /** A paint that is not a colour: drawn from the fragment's position. */
482
+ const isPaint = (style) =>
483
+ style instanceof Gradient || style instanceof Pattern;
484
+
418
485
  function fontKey(font) {
419
486
  let id = fontIds.get(font);
420
487
  if (!id) fontIds.set(font, (id = `f${nextFontId++}`));
@@ -571,6 +638,9 @@ export class WaylandContext2D {
571
638
  uGradKind: u('uGradKind'),
572
639
  uGradA: u('uGradA'),
573
640
  uGradB: u('uGradB'),
641
+ uPatInv: u('uPatInv'),
642
+ uPatOff: u('uPatOff'),
643
+ uPatFlags: u('uPatFlags'),
574
644
  };
575
645
  shared.buffer = gl.createBuffer();
576
646
  gl.bindBuffer(gl.ARRAY_BUFFER, shared.buffer);
@@ -847,9 +917,23 @@ export class WaylandContext2D {
847
917
  return new Gradient('radial', [x0, y0, r0, x1, y1, r1]);
848
918
  }
849
919
 
850
- createPattern() {
851
- // Patterns need a repeating texture mode; not yet.
852
- return null;
920
+ /**
921
+ * A pattern of `image` — an ImageData, an ntk `Image`, a surface or a
922
+ * render target. Null for one with nothing in it, as canvas answers for an
923
+ * image it cannot use; `repetition` is canvas's, null and `''` meaning
924
+ * `'repeat'`, and anything else a `SyntaxError`.
925
+ */
926
+ createPattern(image, repetition) {
927
+ const rep =
928
+ repetition == null || repetition === '' ? 'repeat' : String(repetition);
929
+ if (!REPETITIONS.has(rep)) {
930
+ throw new SyntaxError(
931
+ `createPattern: ${JSON.stringify(rep)} is not a repetition — ` +
932
+ "expected 'repeat', 'repeat-x', 'repeat-y' or 'no-repeat'.",
933
+ );
934
+ }
935
+ if (!((image?.width | 0) > 0 && (image?.height | 0) > 0)) return null;
936
+ return new Pattern(image, rep);
853
937
  }
854
938
 
855
939
  // ---- paths ------------------------------------------------------------
@@ -1088,7 +1172,7 @@ export class WaylandContext2D {
1088
1172
 
1089
1173
  _rect(x, y, w, h, radius, style, strokeWidth) {
1090
1174
  if (!(w > 0 && h > 0)) return;
1091
- if (style instanceof Gradient) {
1175
+ if (isPaint(style)) {
1092
1176
  if (radius <= 0 && strokeWidth === 0)
1093
1177
  return this._gradientQuad(x, y, w, h, style);
1094
1178
  // rounded/stroked gradient: stencil the shape, cover with the gradient
@@ -1226,8 +1310,40 @@ export class WaylandContext2D {
1226
1310
  }
1227
1311
  }
1228
1312
 
1313
+ /** Bind a gradient or a pattern for the next quads; false where there is
1314
+ * nothing to draw with — a pattern of an image that has gone. */
1315
+ _usePaint(paint) {
1316
+ if (paint instanceof Pattern) return this._usePattern(paint);
1317
+ this._useGradient(paint);
1318
+ return true;
1319
+ }
1320
+
1321
+ /** Bind a pattern's texture, and the way from a device pixel to one of
1322
+ * its own, as the current batch's uniforms. */
1323
+ _usePattern(pat) {
1324
+ const src = this._textureFor(pat.source);
1325
+ if (!src) return false;
1326
+ const inv = invert(pat.m ? mul(this._m, pat.m) : this._m);
1327
+ if (!inv) return false;
1328
+ const gl = this.gl;
1329
+ this._flush();
1330
+ this._mode = MODE_PATTERN;
1331
+ this._texture = src.tex;
1332
+ this._swizzle = src.swizzle;
1333
+ const rep = pat.repetition;
1334
+ const flags =
1335
+ (rep === 'repeat' || rep === 'repeat-x' ? 1 : 0) +
1336
+ (rep === 'repeat' || rep === 'repeat-y' ? 2 : 0) +
1337
+ (src.flipY ? 4 : 0);
1338
+ gl.useProgram(this._program);
1339
+ gl.uniform4f(this._loc.uPatInv, inv[0], inv[1], inv[2], inv[3]);
1340
+ gl.uniform4f(this._loc.uPatOff, inv[4], inv[5], src.w, src.h);
1341
+ gl.uniform1i(this._loc.uPatFlags, flags);
1342
+ return true;
1343
+ }
1344
+
1229
1345
  _gradientQuad(x, y, w, h, grad) {
1230
- this._useGradient(grad);
1346
+ if (!this._usePaint(grad)) return;
1231
1347
  const m = this._m;
1232
1348
  const alpha = this.globalAlpha;
1233
1349
  this._quad(
@@ -1277,7 +1393,7 @@ export class WaylandContext2D {
1277
1393
  // only once the device has been claimed.
1278
1394
  this._claim();
1279
1395
  let color = null;
1280
- if (!(style instanceof Gradient)) {
1396
+ if (!isPaint(style)) {
1281
1397
  color = this._color(style);
1282
1398
  if (color[3] === 0) return;
1283
1399
  if (this._fillCoverage(polys, rule, bounds, color)) return;
@@ -1297,15 +1413,18 @@ export class WaylandContext2D {
1297
1413
  const { x, y, w, h } = bounds;
1298
1414
  const saved = this._m;
1299
1415
  this._m = IDENTITY;
1300
- if (style instanceof Gradient) {
1301
- this._m = saved; // gradient endpoints are in user space
1302
- this._useGradient(style);
1416
+ if (isPaint(style)) {
1417
+ this._m = saved; // a gradient's endpoints, a pattern's origin: user space
1418
+ const painted = this._usePaint(style);
1303
1419
  this._m = IDENTITY;
1304
1420
  const a = this.globalAlpha;
1421
+ // a pattern with nothing to draw with still covers, clear, so the
1422
+ // counts the winding pass left are zeroed as the cover always does
1423
+ if (!painted) this._setMode(MODE_SOLID, null, SWIZZLE_RGBA);
1305
1424
  this._quad(
1306
1425
  [x, y, x + w, y, x + w, y + h, x, y + h],
1307
1426
  UV_UNIT,
1308
- [a, a, a, a],
1427
+ painted ? [a, a, a, a] : CLEAR,
1309
1428
  ZERO4,
1310
1429
  );
1311
1430
  } else {
@@ -127,5 +127,11 @@
127
127
  "keyboard-shortcuts-inhibit-unstable-v1": [
128
128
  "zwp_keyboard_shortcuts_inhibit_manager_v1",
129
129
  "zwp_keyboard_shortcuts_inhibitor_v1"
130
+ ],
131
+ "xdg-foreign-unstable-v2": [
132
+ "zxdg_exporter_v2",
133
+ "zxdg_importer_v2",
134
+ "zxdg_exported_v2",
135
+ "zxdg_imported_v2"
130
136
  ]
131
137
  }
@@ -0,0 +1 @@
1
+ [{"name":"zxdg_exporter_v2","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"export_toplevel","args":[{"name":"id","type":"new_id","interface":"zxdg_exported_v2"},{"name":"surface","type":"object","interface":"wl_surface"}]}],"events":[],"enums":{"error":{"entries":[{"name":"invalid_surface","value":0}]}}},{"name":"zxdg_importer_v2","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"import_toplevel","args":[{"name":"id","type":"new_id","interface":"zxdg_imported_v2"},{"name":"handle","type":"string"}]}],"events":[],"enums":{}},{"name":"zxdg_exported_v2","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]}],"events":[{"name":"handle","args":[{"name":"handle","type":"string"}]}],"enums":{}},{"name":"zxdg_imported_v2","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"set_parent_of","args":[{"name":"surface","type":"object","interface":"wl_surface"}]}],"events":[{"name":"destroyed","args":[]}],"enums":{"error":{"entries":[{"name":"invalid_surface","value":0}]}}}]
@@ -26,7 +26,7 @@
26
26
 
27
27
  import { EventEmitter } from 'node:events';
28
28
  import fs from 'node:fs';
29
- import { XkbKeymap } from './xkb.js';
29
+ import { XkbKeymap, REAL_MODS, layoutsFromKeymap } from './xkb.js';
30
30
 
31
31
  /** `wl_seat.capability` bits. */
32
32
  export const SEAT_CAP = { POINTER: 1, KEYBOARD: 2, TOUCH: 4 };
@@ -185,7 +185,10 @@ export class WaylandSeat extends EventEmitter {
185
185
  this.xkb = null;
186
186
  /** X-style modifier mask: depressed | latched | locked */
187
187
  this.mods = 0;
188
+ /** the locked half alone — Caps Lock and Num Lock (keyboardState) */
189
+ this.locked = 0;
188
190
  this.group = 0;
191
+ this._layouts = null;
189
192
  /** pointer buttons currently held, as an X state mask */
190
193
  this.buttonMask = 0;
191
194
  this.repeat = { rate: 25, delay: 600 };
@@ -437,6 +440,7 @@ export class WaylandSeat extends EventEmitter {
437
440
  : null;
438
441
  this.keymap = { format, size, text };
439
442
  this.xkb = text ? XkbKeymap.parse(text) : null;
443
+ this._layouts = null;
440
444
  this.emit('keymap', this.keymap, this.xkb);
441
445
  } catch (err) {
442
446
  this.emit(
@@ -473,6 +477,7 @@ export class WaylandSeat extends EventEmitter {
473
477
  k.on('modifiers', (serial, depressed, latched, locked, group) => {
474
478
  this.lastSerial = serial;
475
479
  this.mods = (depressed | latched | locked) & 0xff;
480
+ this.locked = locked;
476
481
  this.group = group;
477
482
  this.emit('modifiers', {
478
483
  mods: this.mods,
@@ -501,6 +506,28 @@ export class WaylandSeat extends EventEmitter {
501
506
  });
502
507
  }
503
508
 
509
+ /**
510
+ * The locks and the layout, in `useKeyboardState()`'s shape
511
+ * (src/keyboardstate.js). Caps Lock is Lock, which no keymap can move;
512
+ * Num Lock is wherever this keymap binds the virtual `NumLock`, with the
513
+ * X convention's Mod2 where it does not say.
514
+ *
515
+ * Only as fresh as the last `wl_keyboard.modifiers`, and a compositor sends
516
+ * those to the client with keyboard focus alone: a lock toggled while
517
+ * another app has the keyboard arrives when focus comes back. That is the
518
+ * protocol, not a shortcut — X11's StateNotify reaches every client.
519
+ */
520
+ keyboardState() {
521
+ const numLock = this.xkb?.maskOf('NumLock') || REAL_MODS.Mod2;
522
+ this._layouts ??= Object.freeze(layoutsFromKeymap(this.keymap?.text));
523
+ return {
524
+ capsLock: Boolean(this.locked & REAL_MODS.Lock),
525
+ numLock: Boolean(this.locked & numLock),
526
+ group: this.group,
527
+ layouts: this._layouts,
528
+ };
529
+ }
530
+
504
531
  /** A key event in the shape events.js reads, decoded against the keymap. */
505
532
  _keyEvent(type, keycode, time, serial, repeat = false) {
506
533
  const state = XkbKeymap.stateOf(this.mods, this.group);
@@ -191,6 +191,17 @@ export class WaylandSwapchain {
191
191
 
192
192
  /** Give the chain buffers of this size, keeping its GBM surface. */
193
193
  _resize(w, h) {
194
+ // The size they already are keeps them: x11-dri makes nothing again and
195
+ // moves no generation, so the wl_buffers stay too. Dropped, the next
196
+ // swap answered a buffer the compositor already had — `isNew: false`, no
197
+ // descriptor — that this chain no longer knew, and imported it with
198
+ // none. A small surface asks for this on every change of size: `slackFor`
199
+ // rounds both sides up to the same 128. What each buffer holds is of the
200
+ // old size, so all of it is owed.
201
+ if (w === this.allocWidth && h === this.allocHeight) {
202
+ for (const entry of this.buffers.values()) entry.damage = 'all';
203
+ return;
204
+ }
194
205
  // In place: the handle stays valid, the context keeps its GL objects and
195
206
  // stays current. Buffers from the old generation are gone, so their
196
207
  // `wl_buffer`s go with them — the compositor may still be showing one, so
@@ -299,6 +310,18 @@ export class WaylandSwapchain {
299
310
  return true;
300
311
  }
301
312
  this._swapsSinceNew = 0;
313
+ if (!out.isNew) {
314
+ // GBM exported this one already, so it carries no descriptor, and the
315
+ // chain has no wl_buffer for it: the two have lost track of each
316
+ // other, which no compositor did.
317
+ this._fail(
318
+ new Error(
319
+ `GPU buffer ${id} came round again with no wl_buffer for it ` +
320
+ '(react-x11 bug: the swapchain dropped a buffer GBM still holds)',
321
+ ),
322
+ );
323
+ return false;
324
+ }
302
325
  return await this._import(id, out, generation, damage);
303
326
  }
304
327
 
@@ -331,6 +331,14 @@ export class XkbKeymap {
331
331
  }
332
332
  }
333
333
 
334
+ /**
335
+ * A modifier by name, as the real mask the compositor reports it under —
336
+ * `'NumLock'` is whichever ModN this keymap binds it to, `'Lock'` is Lock.
337
+ */
338
+ maskOf(name) {
339
+ return this._modMask(name);
340
+ }
341
+
334
342
  /** A modifier name (real or virtual) as a real mask. */
335
343
  _modMask(name) {
336
344
  if (name in REAL_MODS) return REAL_MODS[name];
@@ -461,6 +469,135 @@ export class XkbKeymap {
461
469
  }
462
470
  }
463
471
 
472
+ /**
473
+ * Files the rules add to `xkb_symbols` that are not a layout: the model's
474
+ * extras (`inet(evdev)`) and every option's (`group(alt_shift_toggle)`,
475
+ * `compose(rctrl)`). Only consulted where a keymap has one group and so no
476
+ * group suffix to anchor on.
477
+ */
478
+ const NOT_LAYOUTS = new Set([
479
+ 'altwin',
480
+ 'capslock',
481
+ 'compose',
482
+ 'ctrl',
483
+ 'eurosign',
484
+ 'fkeys',
485
+ 'grab',
486
+ 'group',
487
+ 'hyper',
488
+ 'inet',
489
+ 'keypad',
490
+ 'kpdl',
491
+ 'level2',
492
+ 'level3',
493
+ 'level5',
494
+ 'nbsp',
495
+ 'parens',
496
+ 'rupeesign',
497
+ 'scrolllock',
498
+ 'shift',
499
+ 'srvr_ctrl',
500
+ 'srvrkeys',
501
+ 'terminate',
502
+ 'typo',
503
+ ]);
504
+
505
+ /** `macintosh_vndr/us(alt)` → `us`: the file, without directory or variant. */
506
+ const fileOf = (token) => token.replace(/\(.*$/, '').replace(/^.*\//, '');
507
+
508
+ /**
509
+ * The layout codes a keymap was compiled from — `['us', 'ru']`, what X11
510
+ * reads out of `_XKB_RULES_NAMES` and what a Wayland client has nothing
511
+ * like. Pure, and exported for the test.
512
+ *
513
+ * The one place a compositor's keymap still says is the **name** of its
514
+ * `xkb_symbols` section. libxkbcommon names it after the include string the
515
+ * rules built — `pc+us+ru:2+inet(evdev)+group(alt_shift_toggle)` — with both
516
+ * `+` and `:` written as `_`:
517
+ *
518
+ * xkb_symbols "pc_us_ru_2_inet(evdev)_group(alt_shift_toggle)" {
519
+ * name[1]="English (US)";
520
+ * name[2]="Russian";
521
+ *
522
+ * The rules put the model's file first, then the layouts in order — each one
523
+ * after the first followed by its group number — then the model's extras and
524
+ * the options. So group N is the file right before the bare `N`, group 1 is
525
+ * the file right before group 2, and with a single group it is the file
526
+ * before the first extra. `name[N]` is how many groups there are, and an
527
+ * answer that cannot account for every one of them is no answer: the list
528
+ * comes back empty, which is what "not known" means in src/keyboardstate.js.
529
+ *
530
+ * The `name[N]` strings are not the answer themselves: they are
531
+ * descriptions, `"Russian"`, where an indicator wants `ru` — the same reason
532
+ * the X11 path reads the rules rather than XKB's group names.
533
+ */
534
+ export function layoutsFromKeymap(text) {
535
+ if (typeof text !== 'string') return [];
536
+ const at = text.search(/xkb_symbols\s+"/);
537
+ if (at < 0) return [];
538
+ const section = text.slice(at);
539
+ const name = /^xkb_symbols\s+"([^"]*)"/.exec(section)?.[1];
540
+ let groups = 0;
541
+ for (const m of section.matchAll(/\bname\[(?:group)?(\d+)\]\s*=/gi)) {
542
+ groups = Math.max(groups, Number(m[1]));
543
+ }
544
+ if (!name || !groups || groups > 4) return [];
545
+
546
+ const tokens = splitIncludes(name);
547
+ const layouts = [];
548
+ if (groups === 1) {
549
+ let i = tokens.findIndex((t, n) => n > 0 && NOT_LAYOUTS.has(fileOf(t)));
550
+ if (i < 0) i = tokens.length;
551
+ if (i < 2) return [];
552
+ layouts.push(tokens[i - 1]);
553
+ } else {
554
+ let prev = -1;
555
+ for (let g = 2; g <= groups; g++) {
556
+ const i = tokens.indexOf(String(g), prev + 1);
557
+ if (i < 2) return [];
558
+ if (g === 2) layouts.push(tokens[i - 2]);
559
+ layouts.push(tokens[i - 1]);
560
+ prev = i;
561
+ }
562
+ }
563
+ const codes = layouts.map(fileOf);
564
+ if (codes.some((c) => !/^[a-z][a-z0-9-]*$/i.test(c) || NOT_LAYOUTS.has(c))) {
565
+ return [];
566
+ }
567
+ // `empty` is what a keymap with no layout to name is built from; reporting
568
+ // it would put "EMPTY" in an indicator (the X11 path drops it too)
569
+ return codes.every((c) => c === 'empty') ? [] : codes;
570
+ }
571
+
572
+ /**
573
+ * `pc_us(dvorak)_ru(phonetic_winkeys)_2` → `['pc', 'us(dvorak)',
574
+ * 'ru(phonetic_winkeys)', '2']`. Split where a `_`, `+` or `:` stands outside
575
+ * parentheses — the raw include string spells them that way, and a
576
+ * variant's own underscores are inside them. Two file names have an
577
+ * underscore of their own, the `*_vndr/` directories and `srvr_ctrl`, and are
578
+ * kept whole.
579
+ */
580
+ function splitIncludes(name) {
581
+ const guarded = name
582
+ .replace(/([a-z0-9]+)_vndr\//gi, '$1\u0001vndr/')
583
+ .replace(/srvr_ctrl/g, 'srvr\u0001ctrl');
584
+ const out = [];
585
+ let depth = 0;
586
+ let cur = '';
587
+ for (const ch of guarded) {
588
+ if (ch === '(') depth++;
589
+ else if (ch === ')') depth = Math.max(0, depth - 1);
590
+ if (depth === 0 && (ch === '_' || ch === '+' || ch === ':')) {
591
+ if (cur) out.push(cur);
592
+ cur = '';
593
+ } else {
594
+ cur += ch;
595
+ }
596
+ }
597
+ if (cur) out.push(cur);
598
+ return out.map((t) => t.replaceAll('\u0001', '_'));
599
+ }
600
+
464
601
  /**
465
602
  * `XkbKSIsKeypad`: the one run of keysyms the keypad produces, `KP_Space`
466
603
  * through `KP_Equal`. Every keypad key is in it on both of its levels — the