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
package/src/host.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `react-x11/host` — add a host element from outside the package.
|
|
3
|
+
*
|
|
4
|
+
* The element also has to be declared to JSX, which is module augmentation
|
|
5
|
+
* against `react-x11/jsx-runtime` (see docs/typescript.md and
|
|
6
|
+
* docs/extending.md):
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* declare module 'react-x11/jsx-runtime' {
|
|
10
|
+
* namespace JSX {
|
|
11
|
+
* interface IntrinsicElements {
|
|
12
|
+
* sparkline: { data: number[]; stroke?: string; style?: Style };
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { Node } from './node.js';
|
|
19
|
+
import type { NtkApp } from './types/nodes.js';
|
|
20
|
+
|
|
21
|
+
export interface HostContext {
|
|
22
|
+
isInsideText: boolean;
|
|
23
|
+
isInsideSvg: boolean;
|
|
24
|
+
isInside3d: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ElementDefinition {
|
|
28
|
+
/**
|
|
29
|
+
* Build the node. `app` is the ntk connection the tree renders through —
|
|
30
|
+
* the second argument every built-in node constructor takes. Must return
|
|
31
|
+
* a `Node` whose `kind` is the registered element name.
|
|
32
|
+
*/
|
|
33
|
+
create(
|
|
34
|
+
props: Record<string, unknown>,
|
|
35
|
+
app: NtkApp,
|
|
36
|
+
hostContext: HostContext,
|
|
37
|
+
): Node;
|
|
38
|
+
/**
|
|
39
|
+
* Lays out with yoga and paints into the owning window (default true).
|
|
40
|
+
* `false` is for a node owning a real child X window instead — see
|
|
41
|
+
* `GlAreaNode`.
|
|
42
|
+
*/
|
|
43
|
+
drawn?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Prop names this element owns even though they are also style names, so
|
|
46
|
+
* `<sparkline stroke="red">` is not reported as a flat style prop in
|
|
47
|
+
* development.
|
|
48
|
+
*/
|
|
49
|
+
semanticNames?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Prop names whose damage this element's own `applyProps` claims — so a
|
|
52
|
+
* commit that changes one of them contributes no damage of its own,
|
|
53
|
+
* instead of widening the frame to the whole node.
|
|
54
|
+
*
|
|
55
|
+
* For an element that draws a **scene** into one node: a graph view handed
|
|
56
|
+
* a new `nodes` array per drag step invalidates the box the dragged node
|
|
57
|
+
* moved through, and without this the commit claims the whole pane over
|
|
58
|
+
* the top of it. Everything left out keeps core's conservative answer, and
|
|
59
|
+
* an element that names a prop it does not actually claim shows stale
|
|
60
|
+
* pixels — see `Node.paintChanged` and docs/extending.md.
|
|
61
|
+
*/
|
|
62
|
+
selfDamagedProps?: string[];
|
|
63
|
+
/** Reject children, naming this element, instead of laying out something
|
|
64
|
+
* that will never paint. Default true. */
|
|
65
|
+
childrenAllowed?: boolean;
|
|
66
|
+
/** Replace an existing registration. Off by default. */
|
|
67
|
+
override?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function registerElement(
|
|
71
|
+
type: string,
|
|
72
|
+
definition: ElementDefinition,
|
|
73
|
+
): void;
|
|
74
|
+
|
|
75
|
+
/** Undo a registration; true if there was one. */
|
|
76
|
+
export function unregisterElement(type: string): boolean;
|
|
77
|
+
|
|
78
|
+
/** Registered element names, in registration order. */
|
|
79
|
+
export function registeredElements(): string[];
|
|
80
|
+
|
|
81
|
+
/** The built-in element names (a copy). */
|
|
82
|
+
export function hostTypes(): string[];
|
|
83
|
+
|
|
84
|
+
/** Every element name currently known, built-in and registered. */
|
|
85
|
+
export function knownElements(): string[];
|
|
86
|
+
|
|
87
|
+
/** Kinds that lay out with yoga and paint into the owning window (a copy). */
|
|
88
|
+
export function drawnKinds(): string[];
|
package/src/host.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// `react-x11/host` — the seam a package that is not react-x11 uses to add
|
|
2
|
+
// a host element. See docs/extending.md for the node contract; this file is
|
|
3
|
+
// only the entry point.
|
|
4
|
+
export {
|
|
5
|
+
registerElement,
|
|
6
|
+
unregisterElement,
|
|
7
|
+
registeredElements,
|
|
8
|
+
} from './registry.js';
|
|
9
|
+
|
|
10
|
+
import { DRAWN_KINDS as DRAWN } from './nodes.js';
|
|
11
|
+
import { registeredElements } from './registry.js';
|
|
12
|
+
|
|
13
|
+
const BUILT_IN = Object.freeze([
|
|
14
|
+
'window',
|
|
15
|
+
'popup',
|
|
16
|
+
'box',
|
|
17
|
+
'text',
|
|
18
|
+
'image',
|
|
19
|
+
'canvas',
|
|
20
|
+
'textinput',
|
|
21
|
+
'textarea',
|
|
22
|
+
'svg',
|
|
23
|
+
'glarea',
|
|
24
|
+
'foreign',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/** The built-in element names. A copy: the vocabulary grows through
|
|
28
|
+
* `registerElement`, never by mutating this. */
|
|
29
|
+
export function hostTypes() {
|
|
30
|
+
return [...BUILT_IN];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Every element name currently known, built-in and registered. */
|
|
34
|
+
export function knownElements() {
|
|
35
|
+
return [...BUILT_IN, ...registeredElements()];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Kinds that lay out with yoga and paint into the owning window. A copy —
|
|
39
|
+
* to add one, register the element with `drawn: true`. */
|
|
40
|
+
export function drawnKinds() {
|
|
41
|
+
return [...DRAWN];
|
|
42
|
+
}
|
package/src/idle.js
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
// Whether the user is still there, and how to tell the desktop not to blank
|
|
2
|
+
// the screen while something is playing.
|
|
3
|
+
//
|
|
4
|
+
// ## Idle: alarms, not polling
|
|
5
|
+
//
|
|
6
|
+
// X has a counter for this — `IDLETIME`, a SYNC system counter carrying
|
|
7
|
+
// milliseconds since the last input on any device — and SYNC *alarms* fire an
|
|
8
|
+
// event when a counter crosses a value. So "tell me when the user has been
|
|
9
|
+
// idle for five minutes" is one alarm and no timer at all, which is what
|
|
10
|
+
// every idle daemon on the desktop (xss-lock, KDE's, GNOME's) is built on.
|
|
11
|
+
//
|
|
12
|
+
// The idiom is a flip-flop, and it is the whole mechanism:
|
|
13
|
+
//
|
|
14
|
+
// idle ← alarm on IDLETIME >= timeout (PositiveComparison)
|
|
15
|
+
// active ← alarm on IDLETIME <= timeout (NegativeComparison)
|
|
16
|
+
//
|
|
17
|
+
// One fires, the state flips, and the alarm is changed to watch for the
|
|
18
|
+
// crossing back. Nothing runs in between — no interval, nothing waking the
|
|
19
|
+
// process up to ask a question whose answer is usually "no".
|
|
20
|
+
//
|
|
21
|
+
// **`delta` must be sent as 0.** It defaults to 1, and the protocol rejects a
|
|
22
|
+
// positive delta on a negative test with a Match error, so the half of the
|
|
23
|
+
// flip-flop that waits for the user to come back would fail — and fail
|
|
24
|
+
// silently, since a void request's error goes to the connection's error hook
|
|
25
|
+
// rather than to a callback.
|
|
26
|
+
//
|
|
27
|
+
// Where there is no `IDLETIME` counter the fallback is MIT-SCREEN-SAVER's
|
|
28
|
+
// `QueryInfo`, which answers the same number for one round trip but has no
|
|
29
|
+
// event behind it, so that rung polls. It schedules against the answer rather
|
|
30
|
+
// than on a fixed tick — with 12 seconds elapsed of a 300-second timeout the
|
|
31
|
+
// next check is 288 seconds away — so an active user costs one round trip per
|
|
32
|
+
// timeout period.
|
|
33
|
+
//
|
|
34
|
+
// ## Inhibition: three rungs that agree on what they do
|
|
35
|
+
//
|
|
36
|
+
// All three keep the **screen** awake and none of them stops a suspend. That
|
|
37
|
+
// is not a simplification, it is the intersection: `ScreenSaverSuspend` is
|
|
38
|
+
// an X request about blanking, `org.freedesktop.ScreenSaver` is the pre-portal
|
|
39
|
+
// desktop interface for the same thing, and only the portal can also inhibit
|
|
40
|
+
// logout and suspend. Exposing a `mode` that works on one rung out of three
|
|
41
|
+
// would be a seam that mostly does not.
|
|
42
|
+
|
|
43
|
+
import { sessionBus } from './bus.js';
|
|
44
|
+
import { PORTAL_NAME, PORTAL_PATH } from './portal.js';
|
|
45
|
+
|
|
46
|
+
import { requireExtension } from './extensions.js';
|
|
47
|
+
|
|
48
|
+
const IDLETIME = 'IDLETIME';
|
|
49
|
+
const SCREENSAVER_NAME = 'org.freedesktop.ScreenSaver';
|
|
50
|
+
const SCREENSAVER_PATH = '/org/freedesktop/ScreenSaver';
|
|
51
|
+
const INHIBIT_IFACE = 'org.freedesktop.portal.Inhibit';
|
|
52
|
+
const REQUEST_IFACE = 'org.freedesktop.portal.Request';
|
|
53
|
+
/** `org.freedesktop.portal.Inhibit`'s flag for "do not let the session idle". */
|
|
54
|
+
const INHIBIT_IDLE = 8;
|
|
55
|
+
|
|
56
|
+
const sessions = new WeakMap();
|
|
57
|
+
|
|
58
|
+
// --------------------------------------------------------------------------
|
|
59
|
+
// Idle
|
|
60
|
+
// --------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* One connection's idle machinery: the extensions it resolved once, and a
|
|
64
|
+
* watcher per distinct timeout. Two components asking for the same timeout
|
|
65
|
+
* share an alarm; asking for two different ones costs two.
|
|
66
|
+
*/
|
|
67
|
+
class IdleSession {
|
|
68
|
+
constructor(app) {
|
|
69
|
+
this.app = app;
|
|
70
|
+
this.watchers = new Map();
|
|
71
|
+
this.stopped = false;
|
|
72
|
+
this._sync = undefined;
|
|
73
|
+
this._saver = undefined;
|
|
74
|
+
this._counter = undefined;
|
|
75
|
+
this._handler = null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
stop() {
|
|
79
|
+
this.stopped = true;
|
|
80
|
+
for (const watcher of this.watchers.values()) watcher.stop();
|
|
81
|
+
this.watchers.clear();
|
|
82
|
+
if (this._handler && this.app?.X?.off) {
|
|
83
|
+
try {
|
|
84
|
+
this.app.X.off('event', this._handler);
|
|
85
|
+
} catch {
|
|
86
|
+
// an ntk old enough to hand back a client with no `off`
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
this._handler = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The SYNC extension, resolved once per connection. */
|
|
93
|
+
sync() {
|
|
94
|
+
if (this._sync === undefined)
|
|
95
|
+
this._sync = requireExtension(this.app, 'sync');
|
|
96
|
+
return this._sync;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** MIT-SCREEN-SAVER, likewise. */
|
|
100
|
+
saver() {
|
|
101
|
+
if (this._saver === undefined) {
|
|
102
|
+
this._saver = requireExtension(this.app, 'screen-saver');
|
|
103
|
+
}
|
|
104
|
+
return this._saver;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The `IDLETIME` counter's id, or null where the server has no such
|
|
109
|
+
* counter — which is every server without the XInput-driven idle tracking
|
|
110
|
+
* Xorg has, XQuartz among them.
|
|
111
|
+
*/
|
|
112
|
+
counter() {
|
|
113
|
+
if (this._counter !== undefined) return this._counter;
|
|
114
|
+
this._counter = this.sync().then((sync) => {
|
|
115
|
+
if (!sync?.ListSystemCounters) return null;
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
try {
|
|
118
|
+
sync.ListSystemCounters((err, counters) => {
|
|
119
|
+
if (err || !counters) return resolve(null);
|
|
120
|
+
resolve(counters.find((c) => c.name === IDLETIME)?.counter ?? null);
|
|
121
|
+
});
|
|
122
|
+
} catch {
|
|
123
|
+
resolve(null);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
return this._counter;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** One `event` listener for every alarm, installed once. */
|
|
131
|
+
listen(fn) {
|
|
132
|
+
if (this._handler) return;
|
|
133
|
+
const X = this.app?.X;
|
|
134
|
+
if (!X?.on) return;
|
|
135
|
+
this._handler = fn;
|
|
136
|
+
X.on('event', fn);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class IdleWatcher {
|
|
141
|
+
constructor(session, timeout) {
|
|
142
|
+
this.session = session;
|
|
143
|
+
this.timeout = timeout;
|
|
144
|
+
this.idle = false;
|
|
145
|
+
this.listeners = new Set();
|
|
146
|
+
this.stopped = false;
|
|
147
|
+
this.alarm = 0;
|
|
148
|
+
this._sync = null;
|
|
149
|
+
this._timer = null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
set(idle) {
|
|
153
|
+
if (this.stopped || this.idle === idle) return;
|
|
154
|
+
this.idle = idle;
|
|
155
|
+
for (const fn of [...this.listeners]) {
|
|
156
|
+
try {
|
|
157
|
+
fn();
|
|
158
|
+
} catch {
|
|
159
|
+
// one subscriber throwing must not take the others with it, nor the
|
|
160
|
+
// X event loop this runs on
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
stop() {
|
|
166
|
+
this.stopped = true;
|
|
167
|
+
this.listeners.clear();
|
|
168
|
+
clearTimeout(this._timer);
|
|
169
|
+
this._timer = null;
|
|
170
|
+
if (this.alarm) {
|
|
171
|
+
try {
|
|
172
|
+
this._sync?.DestroyAlarm(this.alarm);
|
|
173
|
+
} catch {
|
|
174
|
+
// the connection is going away with it
|
|
175
|
+
}
|
|
176
|
+
this.alarm = 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Arm a watcher: SYNC alarms where the counter exists, polling where it does
|
|
183
|
+
* not, and nothing at all where neither extension is present — on which the
|
|
184
|
+
* watcher reports "not idle" for good, which is the honest answer for a
|
|
185
|
+
* display that cannot be asked.
|
|
186
|
+
*/
|
|
187
|
+
async function armIdle(watcher) {
|
|
188
|
+
const session = watcher.session;
|
|
189
|
+
const counter = await session.counter();
|
|
190
|
+
if (watcher.stopped) return;
|
|
191
|
+
|
|
192
|
+
if (counter) {
|
|
193
|
+
const sync = await session.sync();
|
|
194
|
+
if (watcher.stopped || !sync) return;
|
|
195
|
+
watcher._sync = sync;
|
|
196
|
+
const X = session.app.X;
|
|
197
|
+
watcher.alarm = X.AllocID();
|
|
198
|
+
|
|
199
|
+
session.listen((ev) => {
|
|
200
|
+
if (session.stopped) return;
|
|
201
|
+
if (ev.type !== sync.firstEvent + sync.events.AlarmNotify) return;
|
|
202
|
+
for (const w of session.watchers.values()) {
|
|
203
|
+
if (w.alarm !== ev.alarm || w.stopped) continue;
|
|
204
|
+
// The counter value the alarm fired at says which way it crossed,
|
|
205
|
+
// rather than trusting the flip-flop's own idea of where it was: an
|
|
206
|
+
// alarm changed twice in a frame can deliver events out of order.
|
|
207
|
+
const idle = ev.counterValue >= w.timeout;
|
|
208
|
+
w.set(idle);
|
|
209
|
+
retest(sync, w, idle);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
sync.CreateAlarm(watcher.alarm, {
|
|
215
|
+
counter,
|
|
216
|
+
valueType: sync.ValueType.Absolute,
|
|
217
|
+
value: watcher.timeout,
|
|
218
|
+
testType: sync.TestType.PositiveComparison,
|
|
219
|
+
delta: 0,
|
|
220
|
+
events: 1,
|
|
221
|
+
});
|
|
222
|
+
} catch {
|
|
223
|
+
watcher.alarm = 0;
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// No counter: poll MIT-SCREEN-SAVER instead.
|
|
229
|
+
const saver = await session.saver();
|
|
230
|
+
if (watcher.stopped || !saver?.QueryInfo) return;
|
|
231
|
+
poll(watcher, saver);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Flip the alarm over to watch for the crossing back. */
|
|
235
|
+
function retest(sync, watcher, idle) {
|
|
236
|
+
if (!watcher.alarm) return;
|
|
237
|
+
try {
|
|
238
|
+
sync.ChangeAlarm(watcher.alarm, {
|
|
239
|
+
valueType: sync.ValueType.Absolute,
|
|
240
|
+
value: watcher.timeout,
|
|
241
|
+
testType: idle
|
|
242
|
+
? sync.TestType.NegativeComparison
|
|
243
|
+
: sync.TestType.PositiveComparison,
|
|
244
|
+
delta: 0,
|
|
245
|
+
events: 1,
|
|
246
|
+
});
|
|
247
|
+
} catch {
|
|
248
|
+
// the connection went away; nothing further will arrive either way
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The fallback rung. Sleeps for exactly as long as the answer says it can:
|
|
254
|
+
* an idle time of 12s against a 300s timeout cannot become idle for another
|
|
255
|
+
* 288s, so that is when to look again.
|
|
256
|
+
*
|
|
257
|
+
* Once idle, there is nothing to compute a wait from — the counter only goes
|
|
258
|
+
* up until input resets it, and no input reaches this process when the user
|
|
259
|
+
* is typing in another window. So that direction polls on an interval scaled
|
|
260
|
+
* to the timeout, floored at a second and capped at half a minute: a
|
|
261
|
+
* five-minute away marker clears within 30s of the user coming back, and a
|
|
262
|
+
* ten-second one clears within a second.
|
|
263
|
+
*/
|
|
264
|
+
function poll(watcher, saver) {
|
|
265
|
+
if (watcher.stopped) return;
|
|
266
|
+
const root = watcher.session.app.X.display?.screen?.[0]?.root;
|
|
267
|
+
if (root == null) return;
|
|
268
|
+
saver.QueryInfo(root, (err, info) => {
|
|
269
|
+
if (watcher.stopped) return;
|
|
270
|
+
if (err || !info) return schedule(watcher, saver, watcher.timeout);
|
|
271
|
+
const elapsed = info.idle ?? 0;
|
|
272
|
+
const idle = elapsed >= watcher.timeout;
|
|
273
|
+
watcher.set(idle);
|
|
274
|
+
schedule(
|
|
275
|
+
watcher,
|
|
276
|
+
saver,
|
|
277
|
+
idle
|
|
278
|
+
? Math.min(30_000, Math.max(1_000, watcher.timeout / 4))
|
|
279
|
+
: Math.max(250, watcher.timeout - elapsed),
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function schedule(watcher, saver, delay) {
|
|
285
|
+
clearTimeout(watcher._timer);
|
|
286
|
+
watcher._timer = setTimeout(() => poll(watcher, saver), delay);
|
|
287
|
+
// Never a reason for this to keep the process alive: an app whose windows
|
|
288
|
+
// have closed does not care whether anyone is at the keyboard.
|
|
289
|
+
watcher._timer.unref?.();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Whether this timeout is currently elapsed. Not public — `useIdle()` is. */
|
|
293
|
+
export function idleSnapshot(app, timeout) {
|
|
294
|
+
return sessions.get(app)?.watchers.get(timeout)?.idle ?? false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Subscribe to a timeout elapsing and un-elapsing. */
|
|
298
|
+
export function watchIdle(app, timeout, onChange) {
|
|
299
|
+
if (!app || !(timeout > 0)) return () => {};
|
|
300
|
+
let session = sessions.get(app);
|
|
301
|
+
if (!session) {
|
|
302
|
+
session = new IdleSession(app);
|
|
303
|
+
sessions.set(app, session);
|
|
304
|
+
}
|
|
305
|
+
let watcher = session.watchers.get(timeout);
|
|
306
|
+
if (!watcher) {
|
|
307
|
+
watcher = new IdleWatcher(session, timeout);
|
|
308
|
+
session.watchers.set(timeout, watcher);
|
|
309
|
+
armIdle(watcher).catch(() => {
|
|
310
|
+
// an extension that is not there is not an error, it is a rung
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
watcher.listeners.add(onChange);
|
|
314
|
+
return () => watcher.listeners.delete(onChange);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Tear down with the root that started it. */
|
|
318
|
+
export function endIdle(app) {
|
|
319
|
+
const session = sessions.get(app);
|
|
320
|
+
if (!session) return;
|
|
321
|
+
session.stop();
|
|
322
|
+
sessions.delete(app);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Test seam: state that a timeout has or has not elapsed, without a server
|
|
327
|
+
* that has SYNC. The watcher it creates is inert — nothing arms it — so a
|
|
328
|
+
* test drives the value itself.
|
|
329
|
+
*/
|
|
330
|
+
export function setIdleForTests(app, timeout, idle) {
|
|
331
|
+
let session = sessions.get(app);
|
|
332
|
+
if (!session) {
|
|
333
|
+
session = new IdleSession(app);
|
|
334
|
+
sessions.set(app, session);
|
|
335
|
+
}
|
|
336
|
+
let watcher = session.watchers.get(timeout);
|
|
337
|
+
if (!watcher) {
|
|
338
|
+
watcher = new IdleWatcher(session, timeout);
|
|
339
|
+
session.watchers.set(timeout, watcher);
|
|
340
|
+
}
|
|
341
|
+
watcher.set(idle);
|
|
342
|
+
return watcher;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// --------------------------------------------------------------------------
|
|
346
|
+
// Inhibition
|
|
347
|
+
// --------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Ask the desktop not to blank the screen, and get back the release.
|
|
351
|
+
*
|
|
352
|
+
* ```js
|
|
353
|
+
* const release = await keepAwake({ reason: 'Playing a video', app });
|
|
354
|
+
* // …later
|
|
355
|
+
* release();
|
|
356
|
+
* ```
|
|
357
|
+
*
|
|
358
|
+
* Never rejects and never throws: a desktop with no portal, no screensaver
|
|
359
|
+
* service and no MIT-SCREEN-SAVER hands back a release that does nothing,
|
|
360
|
+
* because "could not ask" and "asked and it was ignored" are the same outcome
|
|
361
|
+
* for the caller and neither is worth an error path in a video player.
|
|
362
|
+
*
|
|
363
|
+
* `useKeepAwake()` is the shape a component wants; this is for imperative
|
|
364
|
+
* code and for tests.
|
|
365
|
+
*/
|
|
366
|
+
export async function keepAwake({ reason = 'Busy', app = null } = {}) {
|
|
367
|
+
for (const rung of [portalInhibit, screenSaverInhibit, xInhibit]) {
|
|
368
|
+
try {
|
|
369
|
+
const release = await rung(reason, app);
|
|
370
|
+
if (release) return once(release);
|
|
371
|
+
} catch {
|
|
372
|
+
// the next rung down
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return () => {};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** A release that runs once however many times it is called — a double
|
|
379
|
+
* release would drop somebody else's inhibition on the counted X rung. */
|
|
380
|
+
function once(fn) {
|
|
381
|
+
let done = false;
|
|
382
|
+
return () => {
|
|
383
|
+
if (done) return;
|
|
384
|
+
done = true;
|
|
385
|
+
try {
|
|
386
|
+
fn();
|
|
387
|
+
} catch {
|
|
388
|
+
// releasing something already gone is the outcome we wanted
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Rung 1: `org.freedesktop.portal.Inhibit`.
|
|
395
|
+
*
|
|
396
|
+
* Not `portalRequest()`, though it is Request-shaped: this call's `Response`
|
|
397
|
+
* fires when the inhibition *ends*, so a helper that awaits one would hang
|
|
398
|
+
* for exactly as long as the feature is working. What is wanted is the
|
|
399
|
+
* handle, to `Close()` later.
|
|
400
|
+
*/
|
|
401
|
+
async function portalInhibit(reason, app) {
|
|
402
|
+
void app;
|
|
403
|
+
const ref = await sessionBus();
|
|
404
|
+
if (!ref) return null;
|
|
405
|
+
try {
|
|
406
|
+
const path = await ref.bus.invoke(
|
|
407
|
+
{
|
|
408
|
+
destination: PORTAL_NAME,
|
|
409
|
+
path: PORTAL_PATH,
|
|
410
|
+
interface: INHIBIT_IFACE,
|
|
411
|
+
member: 'Inhibit',
|
|
412
|
+
signature: 'sua{sv}',
|
|
413
|
+
// No parent window: an inhibition is not modal to anything, and the
|
|
414
|
+
// portal only uses the handle to place a dialog it does not show here.
|
|
415
|
+
body: ['', INHIBIT_IDLE, [['reason', ['s', reason]]]],
|
|
416
|
+
},
|
|
417
|
+
{ timeout: 5_000 },
|
|
418
|
+
);
|
|
419
|
+
if (!path) return null;
|
|
420
|
+
return () => {
|
|
421
|
+
ref.bus
|
|
422
|
+
.invoke({
|
|
423
|
+
destination: PORTAL_NAME,
|
|
424
|
+
path,
|
|
425
|
+
interface: REQUEST_IFACE,
|
|
426
|
+
member: 'Close',
|
|
427
|
+
signature: '',
|
|
428
|
+
body: [],
|
|
429
|
+
})
|
|
430
|
+
.catch(() => {});
|
|
431
|
+
};
|
|
432
|
+
} finally {
|
|
433
|
+
// The inhibition belongs to the connection, not to this reference, and
|
|
434
|
+
// the connection stays open — holding a ref would keep the socket
|
|
435
|
+
// `ref()`d and the process alive for as long as a video was playing,
|
|
436
|
+
// which the window on screen is already doing.
|
|
437
|
+
await ref.release();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Rung 2: the pre-portal desktop interface, which KDE, Xfce, MATE and
|
|
442
|
+
* every screensaver of that era implement. */
|
|
443
|
+
async function screenSaverInhibit(reason, app) {
|
|
444
|
+
void app;
|
|
445
|
+
const ref = await sessionBus();
|
|
446
|
+
if (!ref) return null;
|
|
447
|
+
try {
|
|
448
|
+
const cookie = await ref.bus.invoke(
|
|
449
|
+
{
|
|
450
|
+
destination: SCREENSAVER_NAME,
|
|
451
|
+
path: SCREENSAVER_PATH,
|
|
452
|
+
interface: SCREENSAVER_NAME,
|
|
453
|
+
member: 'Inhibit',
|
|
454
|
+
signature: 'ss',
|
|
455
|
+
body: [process.title || 'react-x11', reason],
|
|
456
|
+
},
|
|
457
|
+
{ timeout: 5_000 },
|
|
458
|
+
);
|
|
459
|
+
if (typeof cookie !== 'number') return null;
|
|
460
|
+
return () => {
|
|
461
|
+
ref.bus
|
|
462
|
+
.invoke({
|
|
463
|
+
destination: SCREENSAVER_NAME,
|
|
464
|
+
path: SCREENSAVER_PATH,
|
|
465
|
+
interface: SCREENSAVER_NAME,
|
|
466
|
+
member: 'UnInhibit',
|
|
467
|
+
signature: 'u',
|
|
468
|
+
body: [cookie],
|
|
469
|
+
})
|
|
470
|
+
.catch(() => {});
|
|
471
|
+
};
|
|
472
|
+
} finally {
|
|
473
|
+
await ref.release();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Rung 3: `ScreenSaverSuspend`, which needs no bus at all — the reason this
|
|
479
|
+
* ladder has a floor on a bare `startx` with no session services running.
|
|
480
|
+
*
|
|
481
|
+
* The server counts suspensions per client, so every `Suspend(true)` owes a
|
|
482
|
+
* `Suspend(false)`; `once()` above is what guarantees the pairing.
|
|
483
|
+
*/
|
|
484
|
+
async function xInhibit(reason, app) {
|
|
485
|
+
void reason;
|
|
486
|
+
const saver = app ? await requireExtension(app, 'screen-saver') : null;
|
|
487
|
+
if (!saver?.Suspend) return null;
|
|
488
|
+
saver.Suspend(true);
|
|
489
|
+
return () => saver.Suspend(false);
|
|
490
|
+
}
|