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,86 @@
1
+ // The React lid on `accelerators.js`: `useAccelerator`, and the internal
2
+ // registration the menus are built on.
3
+ //
4
+ // Split from the matcher for the reason `fonthooks.js` is split from
5
+ // `fonts.js` — the half that has to be right about X11 and the half that has
6
+ // to be right about React have different failure modes, and only one of them
7
+ // can be tested without a tree.
8
+
9
+ import { useEffect, useMemo, useRef } from 'react';
10
+
11
+ import { matchesShortcut } from './accelerators.js';
12
+ import { useTopLevelWindow } from './windowid.js';
13
+
14
+ /**
15
+ * Bind a chord for as long as this component is mounted, anchored at
16
+ * `anchorRef` — the node the binding belongs to, which is what decides
17
+ * whether it is on the screen and whether a modal has taken the keyboard
18
+ * from it (`EventManager._runAccelerators`).
19
+ *
20
+ * `handle(ev)` returns whether it took the key.
21
+ *
22
+ * The handler is read from a ref rather than captured, and is deliberately
23
+ * *not* a dependency: a chord pressed three minutes from now must run the
24
+ * handler from the current render, and re-registering on every render would
25
+ * also reorder the bindings under it.
26
+ */
27
+ export function useAcceleratorEntry(anchorRef, handle, enabled = true) {
28
+ const live = useRef(handle);
29
+ live.current = handle;
30
+ useEffect(() => {
31
+ if (!enabled) return undefined;
32
+ const manager = anchorRef.current?.root?.events;
33
+ if (!manager) return undefined;
34
+ return manager.registerAccelerator({
35
+ anchor: () => anchorRef.current ?? null,
36
+ handle: (ev) => live.current?.(ev) ?? false,
37
+ });
38
+ }, [anchorRef, enabled]);
39
+ }
40
+
41
+ /**
42
+ * `useAccelerator([['Control', 'K']], () => openPalette())` — a shortcut that
43
+ * is not in a menu.
44
+ *
45
+ * Same chord vocabulary as a menu item's `shortcut` and the same matching, so
46
+ * a shortcut can be moved into or out of a menu without being rewritten, and
47
+ * the same rules apply to it: exact on Control/Alt/Shift/Super, indifferent
48
+ * to Caps Lock and Num Lock, matched against the Latin keysym so it survives
49
+ * a layout switch, and behind whatever a focused element consumed with
50
+ * `preventDefault()`.
51
+ *
52
+ * The handler is called with the key event, and the key is consumed.
53
+ *
54
+ * By default the binding belongs to the window the component is in, which is
55
+ * what an application-wide shortcut wants. Two options for when it is not:
56
+ * `enabled: false` unbinds it without unmounting anything, and `scope` takes
57
+ * a ref to a node the binding hangs off instead — the way to give a modal
58
+ * `<Dialog>` a shortcut of its own, since a binding on the window behind it
59
+ * is one the modal has taken the keyboard from.
60
+ */
61
+ export function useAccelerator(shortcut, handler, options = {}) {
62
+ const { enabled = true, scope } = options;
63
+ const owner = useTopLevelWindow();
64
+ // `useTopLevelWindow` answers when it is read, so the fallback stays a
65
+ // live getter rather than a node captured on the render that mounted.
66
+ const anchorRef = useMemo(
67
+ () =>
68
+ scope ?? {
69
+ get current() {
70
+ return owner.current;
71
+ },
72
+ },
73
+ [scope, owner],
74
+ );
75
+ const live = useRef({ shortcut, handler });
76
+ live.current = { shortcut, handler };
77
+ useAcceleratorEntry(
78
+ anchorRef,
79
+ (ev) => {
80
+ if (!matchesShortcut(ev, live.current.shortcut)) return false;
81
+ live.current.handler?.(ev);
82
+ return true;
83
+ },
84
+ enabled,
85
+ );
86
+ }
@@ -0,0 +1,128 @@
1
+ // Accelerators: the half of a menu `shortcut` that was only ever drawn (#351).
2
+ //
3
+ // A `MenuBar` measured its items' shortcuts, drew them, announced them as
4
+ // `aria-keyshortcuts` and handed them to the panel as dbusmenu's `aas` — and
5
+ // bound none of them. An app wrote the binding a second time in an
6
+ // `onKeyDown`, so the promise on the screen and the behaviour behind it were
7
+ // two pieces of state that drifted.
8
+ //
9
+ // This is the matching half, and it is pure: an event and a chord in, a
10
+ // boolean out. The registry and the dispatch order live in `events.js`, next
11
+ // to the key path they are part of; the React lid is `acceleratorhooks.js`.
12
+ //
13
+ // ## Which keysym a chord is compared against
14
+ //
15
+ // `ev.keysym` — the **Latin** keysym for the physical key, whatever layout is
16
+ // live (`src/keyboard.js`, docs/events.md). That is what makes Ctrl+S keep
17
+ // saving while the user types Russian, and it is the reason this module has
18
+ // nothing to say about layouts at all.
19
+ //
20
+ // ## Exact on the four modifiers, indifferent to the locks
21
+ //
22
+ // Ctrl+S must not fire on Ctrl+Shift+S, and must still fire with Caps Lock or
23
+ // Num Lock on. Both fall out of comparing `ctrlKey`/`altKey`/`shiftKey`/
24
+ // `metaKey`, which are the four named modifier bits and nothing else: Lock is
25
+ // bit 1 and Num Lock is normally Mod2, so neither is in the comparison to
26
+ // begin with. This is the line every hand-rolled binding gets wrong, which is
27
+ // most of why it is here rather than in an application.
28
+
29
+ import {
30
+ isEnabled,
31
+ isSeparator,
32
+ splitChord,
33
+ visibleItems,
34
+ } from './menuitem.js';
35
+ import { keysymFromName } from './keysyms.js';
36
+
37
+ /** ASCII upper case folded down, so `'S'` and `'s'` are the same key. */
38
+ const fold = (keysym) =>
39
+ keysym >= 0x41 && keysym <= 0x5a ? keysym + 0x20 : keysym;
40
+
41
+ const isLetter = (keysym) => fold(keysym) >= 0x61 && fold(keysym) <= 0x7a;
42
+
43
+ /** The keysym one chord ends on, or `undefined` for one nothing can match. */
44
+ export function chordKeysym(chord) {
45
+ const { key } = splitChord(chord);
46
+ return key == null ? undefined : keysymFromName(key);
47
+ }
48
+
49
+ /**
50
+ * Does this key event *press* `chord` — `['Control', 'S']`?
51
+ *
52
+ * Two ways a chord's key can be recognised, and the second exists for the
53
+ * symbols:
54
+ *
55
+ * - **as the key it names.** `ev.keysym` is the Latin keysym for the physical
56
+ * key, case-folded here because dbusmenu carries the key and not the
57
+ * character it types, so `['Control', 's']` and `['Control', 'S']` are one
58
+ * binding. All four modifiers are compared exactly.
59
+ * - **as the character it typed.** `['Control', 'plus']` is a real shortcut
60
+ * that no US keyboard can press without Shift — `+` is the shifted `=` — so
61
+ * comparing the base keysym alone would draw `Ctrl++` and never fire it.
62
+ * Where the chord names a symbol, the character the key actually produced
63
+ * counts too, and Shift is then whatever it took to produce it rather than
64
+ * a modifier of its own. GTK reaches the same answer through XKB's consumed
65
+ * modifiers; this is that rule with the part we cannot ask the server for
66
+ * left out.
67
+ *
68
+ * The second path is **letters excluded** deliberately: it is exactly what
69
+ * would make Ctrl+Shift+S fire a Ctrl+S binding, which is the thing the
70
+ * modifier comparison is here to prevent.
71
+ */
72
+ export function matchesChord(ev, chord) {
73
+ const { modifiers, key } = splitChord(chord);
74
+ if (key == null) return false;
75
+ const wanted = keysymFromName(key);
76
+ if (wanted === undefined) return false;
77
+
78
+ const ctrl = modifiers.has('Control');
79
+ const alt = modifiers.has('Alt');
80
+ const shift = modifiers.has('Shift');
81
+ const meta = modifiers.has('Super');
82
+ if (
83
+ Boolean(ev?.ctrlKey) !== ctrl ||
84
+ Boolean(ev?.altKey) !== alt ||
85
+ Boolean(ev?.metaKey) !== meta
86
+ ) {
87
+ return false;
88
+ }
89
+
90
+ const pressed = ev?.keysym;
91
+ if (typeof pressed === 'number' && fold(pressed) === fold(wanted)) {
92
+ return Boolean(ev?.shiftKey) === shift;
93
+ }
94
+ if (isLetter(wanted)) return false;
95
+ // The character this key typed, which is where a Shift the layout needed
96
+ // has already been spent — so a chord that did not ask for Shift does not
97
+ // mind one here, and one that did still gets it.
98
+ return (!shift || Boolean(ev?.shiftKey)) && ev?.codepoint === wanted;
99
+ }
100
+
101
+ /** Does this key event press any alternative of an `aas` shortcut? */
102
+ export function matchesShortcut(ev, shortcut) {
103
+ if (!Array.isArray(shortcut)) return false;
104
+ return shortcut.some(
105
+ (chord) => Array.isArray(chord) && matchesChord(ev, chord),
106
+ );
107
+ }
108
+
109
+ /**
110
+ * The item in a menu descriptor whose shortcut this key event presses, or
111
+ * `null` — submenus included, since a shortcut is a way to reach a command
112
+ * *without* opening the menu it lives in.
113
+ *
114
+ * `enabled` and `visible` gate the search rather than the result, and they
115
+ * gate a parent's children with it: an item nobody can reach by opening the
116
+ * menu is not one a chord should reach either. A gated item is skipped and
117
+ * the walk goes on, so a disabled "Save" does not also swallow the key from
118
+ * whatever else claims it.
119
+ */
120
+ export function acceleratedItem(items, ev) {
121
+ for (const item of visibleItems(items)) {
122
+ if (isSeparator(item) || !isEnabled(item)) continue;
123
+ if (matchesShortcut(ev, item.shortcut)) return item;
124
+ const inner = acceleratedItem(item.items, ev);
125
+ if (inner) return inner;
126
+ }
127
+ return null;
128
+ }
@@ -0,0 +1,168 @@
1
+ // The raise: asking the window manager to bring one of this app's windows to
2
+ // the front, with the timestamp that makes the request legitimate.
3
+ //
4
+ // This is the half of the deep-link story that decides whether a user calls
5
+ // the feature working. The failure mode is not an exception — it is that the
6
+ // user finishes logging in, the browser redirects, the app receives the code
7
+ // correctly, sends `_NET_ACTIVE_WINDOW` with `CurrentTime` because nobody
8
+ // parsed the launch timestamp, the WM's focus-stealing prevention declines,
9
+ // and the taskbar entry blinks. Every layer reports success.
10
+ //
11
+ // So the timestamp is a first-class parameter here rather than an
12
+ // implementation detail, and its default is the value EWMH itself names:
13
+ //
14
+ // > `data.l[1]` — "Client's last user activity timestamp … at the time of the
15
+ // > request"
16
+ //
17
+ // which is exactly what `inputtime.js` already stashes off the event stream.
18
+ // A launch context has something better — the timestamp of the click in the
19
+ // *browser* that redirected here — and that is what a caller passes in.
20
+ //
21
+ // EWMH is explicit that the window manager may refuse, and that its fallback
22
+ // is `_NET_WM_STATE_DEMANDS_ATTENTION`. That is its call, so this reports what
23
+ // was **sent**, never what happened.
24
+ //
25
+ // See docs/uri-schemes.md. Issue #173.
26
+
27
+ import { lastInputTime } from './inputtime.js';
28
+ import { liveApps } from './trace-registry.js';
29
+ import { topLevelWindows, windowIdOf } from './windowid.js';
30
+
31
+ const ACTIVE_WINDOW = '_NET_ACTIVE_WINDOW';
32
+
33
+ /** `1` is an application asking; `2` is a pager, which we are not. */
34
+ const SOURCE_APPLICATION = 1;
35
+
36
+ let warnedAboutAmbiguity = false;
37
+
38
+ /** A ref object → what it holds. Anything else passes through. */
39
+ function deref(target) {
40
+ if (target && typeof target === 'object' && 'current' in target) {
41
+ return target.isWindow ? target : target.current;
42
+ }
43
+ return target;
44
+ }
45
+
46
+ /**
47
+ * The ntk window behind a node, a ref or an ntk window. Recognised by having
48
+ * both an id and a connection, which is the pair this file needs anyway.
49
+ */
50
+ function ntkWindowOf(node) {
51
+ if (!node || typeof node !== 'object') return null;
52
+ if (typeof node.id === 'number' && node.X) return node;
53
+ if (node.window?.X) return node.window;
54
+ if (node.root?.window?.X) return node.root.window;
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * The connection to send through, when the caller named a window that does not
60
+ * carry one — a raw XID, or nothing at all.
61
+ *
62
+ * One process usually has exactly one. Several is legal (a root per display,
63
+ * and a borrowed connection stays registered after its root unmounts), so the
64
+ * tie-break is which of them currently has a window: raising is about a window
65
+ * on screen, and a connection with none cannot be the one meant. Still
66
+ * ambiguous after that is a real `null` — an XID means nothing without the
67
+ * server that issued it.
68
+ */
69
+ function soleApp() {
70
+ const apps = liveApps();
71
+ if (apps.length <= 1) return apps[0] ?? null;
72
+ const showing = apps.filter((app) => topLevelWindows(app).length > 0);
73
+ return showing.length === 1 ? showing[0] : null;
74
+ }
75
+
76
+ /**
77
+ * Which of this app's windows to raise when the caller did not say.
78
+ *
79
+ * The same inference `useTopLevelWindow()` makes, for the same reason: one
80
+ * top-level window is exact, and the focused one is right when there are
81
+ * several, because it is the window the user was last in.
82
+ */
83
+ function inferWindow(app) {
84
+ const windows = topLevelWindows(app);
85
+ if (windows.length <= 1) return windows[0] ?? null;
86
+ const focused = windows.filter((w) => w.events?.windowFocused);
87
+ if (focused.length === 1) return focused[0];
88
+ if (process.env.NODE_ENV !== 'production' && !warnedAboutAmbiguity) {
89
+ warnedAboutAmbiguity = true;
90
+ console.warn(
91
+ `react-x11: activateWindow() with no window to raise, and this tree ` +
92
+ `has ${windows.length} top-level windows with none uniquely ` +
93
+ 'focused — raising the most recently opened. Name the window to be ' +
94
+ 'exact:\n' +
95
+ ' const win = useRef(null);\n' +
96
+ ' activateWindow(win, { timestamp });\n' +
97
+ ' return <window ref={win}>…</window>;',
98
+ );
99
+ }
100
+ return windows[windows.length - 1];
101
+ }
102
+
103
+ /** This app's currently active toplevel, for EWMH's `data.l[2]`. */
104
+ function currentActive(app) {
105
+ const focused = topLevelWindows(app).filter((w) => w.events?.windowFocused);
106
+ return focused.length === 1 ? (windowIdOf(focused[0]) ?? 0) : 0;
107
+ }
108
+
109
+ /**
110
+ * Ask the window manager to raise and focus a window.
111
+ *
112
+ * ```jsx
113
+ * useAppOpen((uris, ctx) => {
114
+ * activateWindow(win, { timestamp: ctx.timestamp });
115
+ * route(uris[0]);
116
+ * });
117
+ * ```
118
+ *
119
+ * `target` is anything {@link windowIdOf} accepts — a `<window>` ref, a ref to
120
+ * any node inside it, a live ntk window, a raw XID — or nothing, in which case
121
+ * this app's only top-level window is used (its focused one, when it has
122
+ * several).
123
+ *
124
+ * `timestamp` is the X server time of the user action being answered.
125
+ * **Getting this wrong is the whole failure mode of the feature**, so:
126
+ *
127
+ * - omitted — the last input this app saw, which is EWMH's own definition of
128
+ * the field;
129
+ * - a number — the launch context's timestamp, which is what a deep link
130
+ * should pass: the click in the browser is the user action, not whatever
131
+ * this app last saw;
132
+ * - `null` — `CurrentTime`. Legal, and most window managers will decline it.
133
+ *
134
+ * Returns whether the request was **issued**: `false` means there was no
135
+ * window or no connection to send it through. `true` is not a promise that the
136
+ * window came forward — EWMH lets the WM refuse and set
137
+ * `_NET_WM_STATE_DEMANDS_ATTENTION` instead, which is the blinking taskbar
138
+ * entry, and no client can tell the difference from here.
139
+ */
140
+ export function activateWindow(target, { timestamp, source } = {}) {
141
+ const node = deref(target);
142
+ const wnd = ntkWindowOf(node);
143
+ const app = node?.app ?? node?.root?.app ?? wnd?.app ?? soleApp();
144
+
145
+ const chosen = wnd ?? (windowIdOf(node) === null ? inferWindow(app) : null);
146
+ const xid = windowIdOf(chosen) ?? windowIdOf(node);
147
+ const X = ntkWindowOf(chosen)?.X ?? wnd?.X ?? app?.X;
148
+ const root = X?.display?.screen?.[0]?.root;
149
+
150
+ if (!xid || !root || !X.SendClientMessage || !X.InternAtom) return false;
151
+
152
+ const when =
153
+ timestamp === undefined ? (lastInputTime(app) ?? 0) : (timestamp ?? 0);
154
+
155
+ X.InternAtom(false, ACTIVE_WINDOW, (err, atom) => {
156
+ if (err || !atom) return;
157
+ // The message goes to the **root** and is *about* our window, which is why
158
+ // both are arguments. The default event mask is
159
+ // SubstructureRedirect|SubstructureNotify — what EWMH requires for
160
+ // messages sent to the root — so it is deliberately not passed.
161
+ X.SendClientMessage(root, xid, atom, 32, [
162
+ source ?? SOURCE_APPLICATION,
163
+ when >>> 0,
164
+ currentActive(app),
165
+ ]);
166
+ });
167
+ return true;
168
+ }