react-x11 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
// `org.freedesktop.Application`: being the app that a `myapp://…` link opens.
|
|
2
|
+
//
|
|
3
|
+
// The concrete flow this exists for: the app opens the system browser for an
|
|
4
|
+
// OAuth login, the provider redirects to `com.example.myapp://auth?code=…`,
|
|
5
|
+
// the desktop routes that URI back to the **already-running** app, and the
|
|
6
|
+
// app's window comes to the front with the code in hand.
|
|
7
|
+
//
|
|
8
|
+
// Four things have to be true and this module owns two of them — dispatch and
|
|
9
|
+
// delivery. Registration (a `.desktop` file claiming
|
|
10
|
+
// `x-scheme-handler/com.example.myapp`) is an install step rather than runtime
|
|
11
|
+
// code and is documented, not executed; the raise is `activate.js`, because it
|
|
12
|
+
// needs an X connection and a window id and this file deliberately has
|
|
13
|
+
// neither.
|
|
14
|
+
//
|
|
15
|
+
// ## Two dispatch paths, and why an app must survive both
|
|
16
|
+
//
|
|
17
|
+
// The desktop entry spec makes D-Bus activation sound like the whole story —
|
|
18
|
+
// `DBusActivatable=true` and implementations "should ignore the `Exec` key".
|
|
19
|
+
// *Should*, and the most common opener does not.
|
|
20
|
+
//
|
|
21
|
+
// - **GIO honours it.** `g_desktop_app_info_launch_uris_with_dbus` calls
|
|
22
|
+
// `Open`/`Activate` on the app's well-known name with a `platform_data`
|
|
23
|
+
// dict. `gio open`, and most of GNOME, take this path.
|
|
24
|
+
// - **`xdg-open`'s generic fallback does not.** Its
|
|
25
|
+
// `open_generic_xdg_x_scheme_handler` resolves the handler with `xdg-mime
|
|
26
|
+
// query default`, reads the `Exec` key and runs it — `DBusActivatable` is
|
|
27
|
+
// never read on that path. Chromium shells out to `xdg-open`, and `xdg-open`
|
|
28
|
+
// takes the generic branch on every desktop it cannot identify, which is
|
|
29
|
+
// every WM-only session: react-x11's own persona.
|
|
30
|
+
//
|
|
31
|
+
// So **a second copy of the app gets spawned with the URI in `argv`, and it
|
|
32
|
+
// has to hand that URI to the first copy and exit.** An app that only exports
|
|
33
|
+
// the interface opens a second window on half of all desktops. Both halves are
|
|
34
|
+
// `registerApplication()`, which is why it answers `role` rather than a
|
|
35
|
+
// boolean.
|
|
36
|
+
//
|
|
37
|
+
// ## The launch call arrives before the UI exists
|
|
38
|
+
//
|
|
39
|
+
// On the D-Bus path the bus *starts the process* because someone called
|
|
40
|
+
// `Open()`, and that call is outstanding while the app boots. Two consequences
|
|
41
|
+
// shape the code below:
|
|
42
|
+
//
|
|
43
|
+
// - registration happens **before `createRoot`**, not inside a component, or
|
|
44
|
+
// the launching call is answered with an unknown-method error;
|
|
45
|
+
// - the reply must not wait for the UI. `Open` is answered on the turn it
|
|
46
|
+
// arrives, the URIs are buffered, and they replay to the first handler that
|
|
47
|
+
// attaches. "Reply when the window is ready" is a design that gets slower
|
|
48
|
+
// until it breaks.
|
|
49
|
+
//
|
|
50
|
+
// ## What this file may not import
|
|
51
|
+
//
|
|
52
|
+
// No `react`, no X11. The bus half of this feature is genuinely orthogonal —
|
|
53
|
+
// it would work for any Node GUI toolkit — and it is written to that seam so
|
|
54
|
+
// that extracting it later is a move rather than a rewrite. What keeps it
|
|
55
|
+
// *here* for now is the connection: `RequestName` lands on one socket, and the
|
|
56
|
+
// app's name, its global menu and its portal Request paths have to be the same
|
|
57
|
+
// identity on the same connection (see docs/dbus.md).
|
|
58
|
+
//
|
|
59
|
+
// See docs/uri-schemes.md. Issue #173.
|
|
60
|
+
|
|
61
|
+
import { loadTransport, sessionBus } from './bus.js';
|
|
62
|
+
import { parseLaunchTime } from './startup.js';
|
|
63
|
+
|
|
64
|
+
/** The standard interface a D-Bus-activatable application exports. */
|
|
65
|
+
export const APPLICATION_IFACE = 'org.freedesktop.Application';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `RequestName` flags and replies, from the D-Bus specification.
|
|
69
|
+
*
|
|
70
|
+
* `DO_NOT_QUEUE` is what makes single-instance detection one round trip: with
|
|
71
|
+
* it, "somebody else has this name" comes back as a reply code instead of
|
|
72
|
+
* silently parking us in a queue behind them.
|
|
73
|
+
*/
|
|
74
|
+
const DO_NOT_QUEUE = 0x4;
|
|
75
|
+
const PRIMARY_OWNER = 1;
|
|
76
|
+
const EXISTS = 3;
|
|
77
|
+
const ALREADY_OWNER = 4;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* How long the *other* instance gets to accept a forwarded URI.
|
|
81
|
+
*
|
|
82
|
+
* Short and explicit, because dbus-native's default is 25 s and its reply
|
|
83
|
+
* timer is not unref'd — a process whose only remaining job is to exit would
|
|
84
|
+
* otherwise sit there for half a minute looking hung.
|
|
85
|
+
*/
|
|
86
|
+
const FORWARD_TIMEOUT = 5000;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Schemes an app may not claim through this API.
|
|
90
|
+
*
|
|
91
|
+
* Being the default browser or the default file handler is a different feature
|
|
92
|
+
* with a different threat model, and neither is what a deep link is for. The
|
|
93
|
+
* list is short on purpose: `mailto:` or `ftp:` are things an app might
|
|
94
|
+
* legitimately *be*, and refusing them would be this module inventing policy.
|
|
95
|
+
*/
|
|
96
|
+
const RESERVED_SCHEMES = new Set(['http', 'https', 'file']);
|
|
97
|
+
|
|
98
|
+
/** RFC 3986: `ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )`. */
|
|
99
|
+
const SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*$/;
|
|
100
|
+
|
|
101
|
+
/** A D-Bus well-known bus name: two or more dot-separated elements. */
|
|
102
|
+
const BUS_NAME_RE = /^[A-Za-z_-][A-Za-z0-9_-]*(\.[A-Za-z_-][A-Za-z0-9_-]*)+$/;
|
|
103
|
+
|
|
104
|
+
/** Anything that starts like an absolute URI. */
|
|
105
|
+
const HAS_SCHEME_RE = /^([A-Za-z][A-Za-z0-9+.-]*):/;
|
|
106
|
+
|
|
107
|
+
// --------------------------------------------------------------------------
|
|
108
|
+
// Derivations and validation
|
|
109
|
+
// --------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The object path the spec says an app id has, which is not a choice:
|
|
113
|
+
*
|
|
114
|
+
* > Starting with the well-known D-Bus name of the application, change all
|
|
115
|
+
* > dots to slashes, prefix a slash, and if a dash is found, convert it to an
|
|
116
|
+
* > underscore.
|
|
117
|
+
*
|
|
118
|
+
* `org.example.FooViewer` → `/org/example/FooViewer`;
|
|
119
|
+
* `com.example.my-app` → `/com/example/my_app`. The dash rule is the one
|
|
120
|
+
* everybody misses, and it fails as "the desktop launched us and then nothing
|
|
121
|
+
* happened" rather than as an error — a dash is legal in a bus name and
|
|
122
|
+
* illegal in an object path, so the call goes to a path we do not serve.
|
|
123
|
+
*/
|
|
124
|
+
export function objectPathForAppId(appId) {
|
|
125
|
+
return `/${appId}`.replaceAll('.', '/').replaceAll('-', '_');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* `appId` is three things at once and the spec ties them together: the
|
|
130
|
+
* well-known bus name, the `.desktop` file's basename, and — by convention and
|
|
131
|
+
* by RFC 8252 §7.1 — the URI scheme. So it is checked as the strictest of
|
|
132
|
+
* them, a bus name.
|
|
133
|
+
*
|
|
134
|
+
* This **throws**, and that is not a hole in the never-rejects rule: a
|
|
135
|
+
* malformed app id is a mistake in the source, not a fact about the machine.
|
|
136
|
+
* Answering `null` would hide it on every box without a session bus and
|
|
137
|
+
* surface it only on the developer's, which is the worst place for a bug to
|
|
138
|
+
* be discovered.
|
|
139
|
+
*/
|
|
140
|
+
function checkAppId(appId) {
|
|
141
|
+
if (typeof appId !== 'string' || !BUS_NAME_RE.test(appId)) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`react-x11: registerApplication({ appId: ${JSON.stringify(appId)} }) — ` +
|
|
144
|
+
'the app id is the well-known D-Bus name, the .desktop file name and ' +
|
|
145
|
+
'the URI scheme all at once, so it must be a valid bus name: two or ' +
|
|
146
|
+
'more dot-separated elements of [A-Za-z_-][A-Za-z0-9_-]*, e.g. ' +
|
|
147
|
+
'"com.example.myapp".',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (appId.length > 255) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`react-x11: registerApplication — the app id is ${appId.length} ` +
|
|
153
|
+
'characters; D-Bus caps a bus name at 255.',
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return appId;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Same contract as {@link checkAppId}: a bad scheme is a source mistake. */
|
|
160
|
+
function checkSchemes(schemes) {
|
|
161
|
+
if (schemes === undefined) return null;
|
|
162
|
+
if (!Array.isArray(schemes)) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
'react-x11: registerApplication({ schemes }) — expected an array of ' +
|
|
165
|
+
'scheme names, e.g. ["com.example.myapp"].',
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
for (const scheme of schemes) {
|
|
169
|
+
if (typeof scheme !== 'string' || !SCHEME_RE.test(scheme)) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`react-x11: registerApplication — ${JSON.stringify(scheme)} is not a ` +
|
|
172
|
+
'scheme name. RFC 3986 wants a letter followed by letters, digits, ' +
|
|
173
|
+
'"+", "-" or "." — and no colon.',
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (RESERVED_SCHEMES.has(scheme.toLowerCase())) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`react-x11: registerApplication — "${scheme}" cannot be registered ` +
|
|
179
|
+
'here. Being the default browser or file handler is a different ' +
|
|
180
|
+
'feature; a deep link wants a scheme derived from a domain you ' +
|
|
181
|
+
'control, written in reverse (RFC 8252 §7.1), e.g. ' +
|
|
182
|
+
'"com.example.myapp".',
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return schemes.map((scheme) => scheme.toLowerCase());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The scheme of a URI, lowercased, or `null` when the string is not one.
|
|
191
|
+
*
|
|
192
|
+
* Deliberately not `new URL()`: this runs on attacker-supplied input from an
|
|
193
|
+
* unauthenticated local peer, and a parser that throws on some inputs is a
|
|
194
|
+
* parser every caller has to wrap.
|
|
195
|
+
*/
|
|
196
|
+
export function schemeOf(uri) {
|
|
197
|
+
if (typeof uri !== 'string') return null;
|
|
198
|
+
const match = HAS_SCHEME_RE.exec(uri);
|
|
199
|
+
return match ? match[1].toLowerCase() : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The URIs of `list` this app answers for.
|
|
204
|
+
*
|
|
205
|
+
* `file:` is always allowed, whatever `schemes` says. `Open` is not only the
|
|
206
|
+
* deep-link entry point — it is also how a file manager says "open this
|
|
207
|
+
* document with you", and an app that declared a custom scheme must not
|
|
208
|
+
* thereby stop opening its own files. An app that declares no schemes at all
|
|
209
|
+
* gets everything, which is the honest reading of "it has not told us what it
|
|
210
|
+
* answers for".
|
|
211
|
+
*/
|
|
212
|
+
export function acceptUris(list, schemes) {
|
|
213
|
+
const uris = (Array.isArray(list) ? list : []).filter(
|
|
214
|
+
(uri) => typeof uri === 'string' && schemeOf(uri) !== null,
|
|
215
|
+
);
|
|
216
|
+
if (!schemes) return uris;
|
|
217
|
+
const allowed = new Set([...schemes, 'file']);
|
|
218
|
+
return uris.filter((uri) => {
|
|
219
|
+
if (allowed.has(schemeOf(uri))) return true;
|
|
220
|
+
debug(`ignoring ${redactUri(uri)}: not a scheme this app registered`);
|
|
221
|
+
return false;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A URI with everything secret taken out of it: no query, no fragment, no
|
|
227
|
+
* userinfo.
|
|
228
|
+
*
|
|
229
|
+
* The whole point of this feature is that an authorization code arrives inside
|
|
230
|
+
* a URI, so **the URI must never be logged whole** — not by us, and the docs
|
|
231
|
+
* say not by the app either. What is left identifies the link well enough to
|
|
232
|
+
* debug a routing problem.
|
|
233
|
+
*/
|
|
234
|
+
export function redactUri(uri) {
|
|
235
|
+
const text = String(uri);
|
|
236
|
+
const cut = text.search(/[?#]/);
|
|
237
|
+
const head = cut < 0 ? text : `${text.slice(0, cut)}…`;
|
|
238
|
+
// `scheme://user:password@host/…` — the credential is in the authority, and
|
|
239
|
+
// it survives stripping the query.
|
|
240
|
+
return head.replace(/(:\/\/)[^/@]*@/, '$1…@');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The debug channel for this module, off unless asked for.
|
|
245
|
+
*
|
|
246
|
+
* Its own switch rather than a general one, because *this* output is the one
|
|
247
|
+
* that has to be safe to paste into an issue: everything that goes through it
|
|
248
|
+
* has already been through {@link redactUri}.
|
|
249
|
+
*/
|
|
250
|
+
function debug(message) {
|
|
251
|
+
if (process.env.REACT_X11_DEBUG_URI === '1') {
|
|
252
|
+
console.error(`react-x11: ${message}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// --------------------------------------------------------------------------
|
|
257
|
+
// platform_data
|
|
258
|
+
// --------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Whatever the wire produced for an `a{sv}`, as a plain object.
|
|
262
|
+
*
|
|
263
|
+
* dbus-native's defaults already hand back exactly that, so this is normally
|
|
264
|
+
* one `Object.entries` pass. It tolerates the other shapes because
|
|
265
|
+
* `setValueShapes()` is process-global and app policy (docs/dbus.md): an app
|
|
266
|
+
* that flips it for its own service must not silently break the launch path.
|
|
267
|
+
*/
|
|
268
|
+
function plainDict(value) {
|
|
269
|
+
const out = Object.create(null);
|
|
270
|
+
if (!value || typeof value !== 'object') return out;
|
|
271
|
+
const entries = Array.isArray(value)
|
|
272
|
+
? value.filter((e) => Array.isArray(e) && e.length >= 2)
|
|
273
|
+
: Object.entries(value);
|
|
274
|
+
for (const [key, raw] of entries) out[String(key)] = plainValue(raw);
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A variant in any of the three shapes dbus-native can produce, unwrapped. */
|
|
279
|
+
function plainValue(value) {
|
|
280
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
281
|
+
// `new Variant(signature, value)`
|
|
282
|
+
if ('signature' in value && 'value' in value) return value.value;
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
285
|
+
// the classic `[signatureTree, [value]]` pair
|
|
286
|
+
if (Array.isArray(value) && value.length === 2 && Array.isArray(value[1])) {
|
|
287
|
+
return value[1].length === 1 ? value[1][0] : value[1];
|
|
288
|
+
}
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The launch context a handler is given.
|
|
294
|
+
*
|
|
295
|
+
* **Everything on it is untrusted.** `Open` is callable by any peer on the
|
|
296
|
+
* session bus — it is a per-user socket, not an authorization check — so a
|
|
297
|
+
* hostile local process can fabricate a `desktop-startup-id` and make the app
|
|
298
|
+
* raise itself. That is focus theft rather than data loss, and it is the
|
|
299
|
+
* reason the timestamp is documented as a hint rather than a credential.
|
|
300
|
+
*/
|
|
301
|
+
function launchContext(platformData) {
|
|
302
|
+
const data = plainDict(platformData);
|
|
303
|
+
const startupId =
|
|
304
|
+
typeof data['desktop-startup-id'] === 'string'
|
|
305
|
+
? data['desktop-startup-id']
|
|
306
|
+
: null;
|
|
307
|
+
const token =
|
|
308
|
+
typeof data['activation-token'] === 'string'
|
|
309
|
+
? data['activation-token']
|
|
310
|
+
: null;
|
|
311
|
+
return Object.freeze({
|
|
312
|
+
platformData: Object.freeze(data),
|
|
313
|
+
// The `_TIME` suffix of the startup id is the X timestamp of the user
|
|
314
|
+
// action that triggered the launch — the same parse startup notification
|
|
315
|
+
// does, and the value the window manager weighs before letting a window
|
|
316
|
+
// come forward.
|
|
317
|
+
timestamp: parseLaunchTime(startupId),
|
|
318
|
+
startupId,
|
|
319
|
+
/** Read, exposed, and ignored: there is no Wayland here. */
|
|
320
|
+
activationToken: token,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** The environment's version of the same thing, for a command-line launch. */
|
|
325
|
+
function environmentContext() {
|
|
326
|
+
const data = {};
|
|
327
|
+
// Read, never deleted. `beginStartup()` consumes DESKTOP_STARTUP_ID for the
|
|
328
|
+
// startup sequence and must still find it — registration runs first.
|
|
329
|
+
if (process.env.DESKTOP_STARTUP_ID) {
|
|
330
|
+
data['desktop-startup-id'] = process.env.DESKTOP_STARTUP_ID;
|
|
331
|
+
}
|
|
332
|
+
if (process.env.XDG_ACTIVATION_TOKEN) {
|
|
333
|
+
data['activation-token'] = process.env.XDG_ACTIVATION_TOKEN;
|
|
334
|
+
}
|
|
335
|
+
return launchContext(data);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// --------------------------------------------------------------------------
|
|
339
|
+
// Handlers, and the buffer in front of them
|
|
340
|
+
// --------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* One registration per process, because an application has one identity on the
|
|
344
|
+
* desktop. Module state rather than a context, for the same reason
|
|
345
|
+
* `launchTimestamp()` is: `useAppOpen()` has to reach it from inside a tree
|
|
346
|
+
* that was mounted after the launch it is asking about.
|
|
347
|
+
*/
|
|
348
|
+
let current = null;
|
|
349
|
+
|
|
350
|
+
const openHandlers = new Set();
|
|
351
|
+
const activateHandlers = new Set();
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Calls that arrived before anything was listening.
|
|
355
|
+
*
|
|
356
|
+
* Drained by the **first** handler to attach and then gone — replaying to
|
|
357
|
+
* every later subscriber would deliver one login twice.
|
|
358
|
+
*/
|
|
359
|
+
let buffered = [];
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Run app code detached from the D-Bus reply.
|
|
363
|
+
*
|
|
364
|
+
* Two things this buys, both of which are the difference between a working
|
|
365
|
+
* launch and a mysterious one: the reply goes out on the turn the call
|
|
366
|
+
* arrived, however long the handler takes; and a handler that throws stays the
|
|
367
|
+
* app's problem instead of becoming an error reply to a desktop that has no
|
|
368
|
+
* idea what to do with it.
|
|
369
|
+
*/
|
|
370
|
+
function dispatch(fn) {
|
|
371
|
+
queueMicrotask(() => {
|
|
372
|
+
try {
|
|
373
|
+
fn();
|
|
374
|
+
} catch (err) {
|
|
375
|
+
// Not swallowed: an app whose deep-link handler throws would otherwise
|
|
376
|
+
// see nothing at all, and there is no React boundary above a D-Bus call.
|
|
377
|
+
console.error('react-x11: a handler for an app launch threw', err);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function deliver(event) {
|
|
383
|
+
const handlers = event.kind === 'open' ? openHandlers : activateHandlers;
|
|
384
|
+
if (handlers.size === 0) {
|
|
385
|
+
buffered.push(event);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
for (const handler of [...handlers]) {
|
|
389
|
+
dispatch(() =>
|
|
390
|
+
event.kind === 'open'
|
|
391
|
+
? handler(event.uris, event.ctx)
|
|
392
|
+
: handler(event.ctx),
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function drainTo(kind, handler) {
|
|
398
|
+
if (buffered.length === 0) return;
|
|
399
|
+
const mine = buffered.filter((event) => event.kind === kind);
|
|
400
|
+
if (mine.length === 0) return;
|
|
401
|
+
buffered = buffered.filter((event) => event.kind !== kind);
|
|
402
|
+
for (const event of mine) {
|
|
403
|
+
dispatch(() =>
|
|
404
|
+
kind === 'open' ? handler(event.uris, event.ctx) : handler(event.ctx),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Be told when the desktop hands this app URIs to open.
|
|
411
|
+
*
|
|
412
|
+
* ```js
|
|
413
|
+
* const stop = onAppOpen((uris, ctx) => {
|
|
414
|
+
* activateWindow(null, { timestamp: ctx.timestamp });
|
|
415
|
+
* route(uris[0]);
|
|
416
|
+
* });
|
|
417
|
+
* ```
|
|
418
|
+
*
|
|
419
|
+
* Anything that arrived before the first handler attached is replayed to it,
|
|
420
|
+
* so an app whose React tree mounts a second after the launch loses nothing.
|
|
421
|
+
* Safe to call with no registration and no bus — it simply never fires.
|
|
422
|
+
*/
|
|
423
|
+
export function onAppOpen(handler) {
|
|
424
|
+
if (typeof handler !== 'function') return () => {};
|
|
425
|
+
openHandlers.add(handler);
|
|
426
|
+
drainTo('open', handler);
|
|
427
|
+
return () => openHandlers.delete(handler);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Be told when the desktop asks this app to come forward with nothing to open
|
|
432
|
+
* — a second launch from the panel, or `gio launch` on a running app.
|
|
433
|
+
*
|
|
434
|
+
* The handler is what raises the window; nothing is raised for you, because
|
|
435
|
+
* this module has no X connection and an app that wants to answer differently
|
|
436
|
+
* (a new document, say) should not have to undo a raise first.
|
|
437
|
+
*/
|
|
438
|
+
export function onAppActivate(handler) {
|
|
439
|
+
if (typeof handler !== 'function') return () => {};
|
|
440
|
+
activateHandlers.add(handler);
|
|
441
|
+
drainTo('activate', handler);
|
|
442
|
+
return () => activateHandlers.delete(handler);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// --------------------------------------------------------------------------
|
|
446
|
+
// The exported interface
|
|
447
|
+
// --------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* `org.freedesktop.Application`, as the desktop entry spec defines it.
|
|
451
|
+
*
|
|
452
|
+
* All three methods answer with nothing and answer immediately. `Open`'s
|
|
453
|
+
* `uris` are filtered before the app sees them; `ActivateAction` is a
|
|
454
|
+
* deliberate stub — the desktop-actions half wants a design alongside the
|
|
455
|
+
* global menu — but it is a stub that *replies correctly*, which is the
|
|
456
|
+
* difference between an unimplemented action and a shell that thinks the app
|
|
457
|
+
* is broken.
|
|
458
|
+
*/
|
|
459
|
+
function defineApplication(dbus, { schemes, onAction }) {
|
|
460
|
+
return dbus.defineInterface({
|
|
461
|
+
name: APPLICATION_IFACE,
|
|
462
|
+
methods: {
|
|
463
|
+
Activate: {
|
|
464
|
+
in: { platform_data: 'a{sv}' },
|
|
465
|
+
out: {},
|
|
466
|
+
handler: ({ platform_data: platformData }) => {
|
|
467
|
+
deliver({ kind: 'activate', ctx: launchContext(platformData) });
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
Open: {
|
|
471
|
+
in: { uris: 'as', platform_data: 'a{sv}' },
|
|
472
|
+
out: {},
|
|
473
|
+
handler: ({ uris, platform_data: platformData }) => {
|
|
474
|
+
const accepted = acceptUris(uris, schemes);
|
|
475
|
+
debug(
|
|
476
|
+
`Open(${accepted.map(redactUri).join(', ')}) from the session bus`,
|
|
477
|
+
);
|
|
478
|
+
deliver({
|
|
479
|
+
kind: 'open',
|
|
480
|
+
uris: accepted,
|
|
481
|
+
ctx: launchContext(platformData),
|
|
482
|
+
});
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
ActivateAction: {
|
|
486
|
+
in: {
|
|
487
|
+
action_name: 's',
|
|
488
|
+
parameter: 'av',
|
|
489
|
+
platform_data: 'a{sv}',
|
|
490
|
+
},
|
|
491
|
+
out: {},
|
|
492
|
+
handler: ({
|
|
493
|
+
action_name: name,
|
|
494
|
+
parameter,
|
|
495
|
+
platform_data: platformData,
|
|
496
|
+
}) => {
|
|
497
|
+
if (!onAction) return;
|
|
498
|
+
const ctx = launchContext(platformData);
|
|
499
|
+
dispatch(() => onAction(name, parameter ?? [], ctx));
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// --------------------------------------------------------------------------
|
|
507
|
+
// Registration
|
|
508
|
+
// --------------------------------------------------------------------------
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Own this app's name on the session bus, export
|
|
512
|
+
* `org.freedesktop.Application` on it, and answer whether this process is the
|
|
513
|
+
* app or a second copy of it.
|
|
514
|
+
*
|
|
515
|
+
* ```js
|
|
516
|
+
* const app = await registerApplication({
|
|
517
|
+
* appId: 'com.example.myapp',
|
|
518
|
+
* schemes: ['com.example.myapp'],
|
|
519
|
+
* });
|
|
520
|
+
* if (app?.role === 'secondary') process.exit(0); // we do not exit for you
|
|
521
|
+
* const root = await createRoot();
|
|
522
|
+
* ```
|
|
523
|
+
*
|
|
524
|
+
* **Call it before `createRoot`.** On the D-Bus path the bus started this
|
|
525
|
+
* process *because* someone called `Open`, and that call is outstanding while
|
|
526
|
+
* the app boots; registering from inside a component answers it with an
|
|
527
|
+
* unknown-method error.
|
|
528
|
+
*
|
|
529
|
+
* `null` means there is no session bus — ssh, a bare `startx`, a container,
|
|
530
|
+
* CI, Node 20 without the transport. The app runs as an ordinary
|
|
531
|
+
* single-window program, and a URI that arrived in `argv` is still delivered
|
|
532
|
+
* to {@link onAppOpen}: a cold-start deep link does not need a bus, only a
|
|
533
|
+
* second one does.
|
|
534
|
+
*
|
|
535
|
+
* It never rejects for anything about the machine. It does throw for a
|
|
536
|
+
* malformed `appId` or an unusable `scheme`, which are mistakes in the source
|
|
537
|
+
* — see {@link checkAppId}.
|
|
538
|
+
*/
|
|
539
|
+
export async function registerApplication(options = {}) {
|
|
540
|
+
const { onOpen, onActivate, onAction } = options;
|
|
541
|
+
const appId = checkAppId(options.appId);
|
|
542
|
+
const schemes = checkSchemes(options.schemes);
|
|
543
|
+
const objectPath = objectPathForAppId(appId);
|
|
544
|
+
|
|
545
|
+
if (current) {
|
|
546
|
+
// An app has one identity; two registrations would be two RequestNames and
|
|
547
|
+
// two exports on the same path, with the second silently replacing the
|
|
548
|
+
// first. The same id answers the existing registration and **ignores the
|
|
549
|
+
// rest of the options** — a double call cannot corrupt anything, and a
|
|
550
|
+
// second set of handlers belongs on `onAppOpen()` where adding one is what
|
|
551
|
+
// the function is for.
|
|
552
|
+
if (current.appId === appId) return current;
|
|
553
|
+
throw new Error(
|
|
554
|
+
`react-x11: registerApplication("${appId}") — this process is already ` +
|
|
555
|
+
`registered as "${current.appId}". An application has one identity ` +
|
|
556
|
+
'on the bus; release() the first registration to change it.',
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (typeof onOpen === 'function') onAppOpen(onOpen);
|
|
561
|
+
if (typeof onActivate === 'function') onAppActivate(onActivate);
|
|
562
|
+
|
|
563
|
+
// The URIs this launch carried on the command line, before any of the bus
|
|
564
|
+
// work: they are equally real whether we turn out to be the first copy of
|
|
565
|
+
// the app or the second, and whether or not there is a bus at all.
|
|
566
|
+
const argv = Array.isArray(options.argv)
|
|
567
|
+
? options.argv
|
|
568
|
+
: process.argv.slice(2);
|
|
569
|
+
const uris = acceptUris(argv, schemes);
|
|
570
|
+
|
|
571
|
+
const ref = await sessionBus();
|
|
572
|
+
if (!ref) {
|
|
573
|
+
// No bus is a first-class configuration, not a degraded one. This process
|
|
574
|
+
// is the app because it is the only one that can be.
|
|
575
|
+
if (uris.length > 0) {
|
|
576
|
+
debug(`no session bus; delivering ${uris.length} URI(s) from argv`);
|
|
577
|
+
deliver({ kind: 'open', uris, ctx: environmentContext() });
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
let dbus;
|
|
583
|
+
try {
|
|
584
|
+
dbus = await loadTransport();
|
|
585
|
+
} catch {
|
|
586
|
+
// Unreachable in practice — `sessionBus()` already loaded it — but this
|
|
587
|
+
// module must not be the one thing that turns a missing optional
|
|
588
|
+
// dependency into a crash.
|
|
589
|
+
await ref.release();
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const iface = defineApplication(dbus, { schemes, onAction });
|
|
594
|
+
|
|
595
|
+
// **Export before requesting the name.** The daemon queues the activating
|
|
596
|
+
// `Open` call against the well-known name and delivers it the instant we own
|
|
597
|
+
// it; an object exported one await later is an object that did not exist
|
|
598
|
+
// when the launch arrived, and the launcher gets UnknownMethod. Exporting
|
|
599
|
+
// first costs nothing if we turn out to be the second copy — nobody can
|
|
600
|
+
// route to us under a name we do not hold.
|
|
601
|
+
let registration;
|
|
602
|
+
try {
|
|
603
|
+
registration = await ref.bus.export(objectPath, iface);
|
|
604
|
+
} catch (cause) {
|
|
605
|
+
// The path is derived from an id that has already been validated, so this
|
|
606
|
+
// is a transport-level failure rather than a caller error — and a caller
|
|
607
|
+
// whose deep links do not work must still get an app that runs.
|
|
608
|
+
debug(`exporting ${objectPath} failed: ${cause?.message ?? cause}`);
|
|
609
|
+
await ref.release();
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let reply;
|
|
614
|
+
try {
|
|
615
|
+
reply = await ref.bus.requestName(appId, DO_NOT_QUEUE);
|
|
616
|
+
} catch (cause) {
|
|
617
|
+
debug(`RequestName(${appId}) failed: ${cause?.message ?? cause}`);
|
|
618
|
+
await registration.remove().catch(() => {});
|
|
619
|
+
await ref.release();
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const primary = reply === PRIMARY_OWNER || reply === ALREADY_OWNER;
|
|
624
|
+
|
|
625
|
+
if (!primary) {
|
|
626
|
+
// The first copy of the app is running; hand it what we were launched with
|
|
627
|
+
// and become nothing. `DO_NOT_QUEUE` is what makes this one round trip:
|
|
628
|
+
// without it the daemon parks us behind the owner and answers 2 instead,
|
|
629
|
+
// which is a state with no useful behaviour attached to it.
|
|
630
|
+
debug(
|
|
631
|
+
reply === EXISTS
|
|
632
|
+
? `${appId} is owned by another instance`
|
|
633
|
+
: `RequestName(${appId}) answered ${reply}; not the primary owner`,
|
|
634
|
+
);
|
|
635
|
+
await registration.remove().catch(() => {});
|
|
636
|
+
await forward(ref, dbus, { appId, objectPath, uris });
|
|
637
|
+
await ref.release();
|
|
638
|
+
const secondary = {
|
|
639
|
+
role: 'secondary',
|
|
640
|
+
appId,
|
|
641
|
+
objectPath,
|
|
642
|
+
async release() {},
|
|
643
|
+
};
|
|
644
|
+
secondary[Symbol.asyncDispose] = () => secondary.release();
|
|
645
|
+
return secondary;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (uris.length > 0) {
|
|
649
|
+
// Launched from the command line *with* a link — `xdg-open`'s path, and
|
|
650
|
+
// the cold start of the D-Bus one. Same delivery as a bus call, so an app
|
|
651
|
+
// handles one code path instead of two.
|
|
652
|
+
debug(`launched with ${uris.length} URI(s) in argv`);
|
|
653
|
+
deliver({ kind: 'open', uris, ctx: environmentContext() });
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
let released = false;
|
|
657
|
+
const primaryRegistration = {
|
|
658
|
+
role: 'primary',
|
|
659
|
+
appId,
|
|
660
|
+
objectPath,
|
|
661
|
+
/**
|
|
662
|
+
* Give the name back and stop serving. Rare: a registration normally lives
|
|
663
|
+
* as long as the process, and the bus ref it holds is what keeps the
|
|
664
|
+
* process alive while there is a name to answer for.
|
|
665
|
+
*
|
|
666
|
+
* Idempotent on its own flag rather than on `current`, so that a test seam
|
|
667
|
+
* that forgot the registration cannot turn this into a no-op that leaks
|
|
668
|
+
* the bus ref — a held ref is a `ref()`d socket and a process that never
|
|
669
|
+
* exits.
|
|
670
|
+
*/
|
|
671
|
+
async release() {
|
|
672
|
+
if (released) return;
|
|
673
|
+
released = true;
|
|
674
|
+
if (current === primaryRegistration) current = null;
|
|
675
|
+
await ref.bus.releaseName(appId).catch(() => {});
|
|
676
|
+
await registration?.remove().catch(() => {});
|
|
677
|
+
registration = null;
|
|
678
|
+
await ref.release();
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
primaryRegistration[Symbol.asyncDispose] = () =>
|
|
682
|
+
primaryRegistration.release();
|
|
683
|
+
current = primaryRegistration;
|
|
684
|
+
return primaryRegistration;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Hand this launch to the copy of the app that owns the name.
|
|
689
|
+
*
|
|
690
|
+
* `Open` when there is something to open, `Activate` when there is not —
|
|
691
|
+
* "the user started the app again" and "the user clicked a link" are different
|
|
692
|
+
* events, and an app that treats a second launch as an empty `Open` cannot
|
|
693
|
+
* tell them apart.
|
|
694
|
+
*
|
|
695
|
+
* **Auto-start is deliberately left on here**, which is the opposite of the
|
|
696
|
+
* rule `globalmenu.js` follows. There, starting the service we are talking to
|
|
697
|
+
* would be actively harmful; here, the owner exiting between our `RequestName`
|
|
698
|
+
* and this call is exactly the case where launching a fresh instance to handle
|
|
699
|
+
* the URI is the right outcome rather than a bug.
|
|
700
|
+
*/
|
|
701
|
+
async function forward(ref, dbus, { appId, objectPath, uris }) {
|
|
702
|
+
const platformData = {};
|
|
703
|
+
for (const [key, value] of Object.entries(
|
|
704
|
+
environmentContext().platformData,
|
|
705
|
+
)) {
|
|
706
|
+
platformData[key] = new dbus.Variant('s', String(value));
|
|
707
|
+
}
|
|
708
|
+
const call =
|
|
709
|
+
uris.length > 0
|
|
710
|
+
? { member: 'Open', signature: 'asa{sv}', body: [uris, platformData] }
|
|
711
|
+
: { member: 'Activate', signature: 'a{sv}', body: [platformData] };
|
|
712
|
+
debug(
|
|
713
|
+
`another instance owns ${appId}; forwarding ${call.member}` +
|
|
714
|
+
(uris.length ? ` (${uris.map(redactUri).join(', ')})` : ''),
|
|
715
|
+
);
|
|
716
|
+
try {
|
|
717
|
+
await ref.bus.invoke(
|
|
718
|
+
{
|
|
719
|
+
destination: appId,
|
|
720
|
+
path: objectPath,
|
|
721
|
+
interface: APPLICATION_IFACE,
|
|
722
|
+
member: call.member,
|
|
723
|
+
signature: call.signature,
|
|
724
|
+
body: call.body,
|
|
725
|
+
},
|
|
726
|
+
{ timeout: FORWARD_TIMEOUT },
|
|
727
|
+
);
|
|
728
|
+
} catch (cause) {
|
|
729
|
+
// The owner died in the window between the two calls, or refuses the
|
|
730
|
+
// interface. Nothing left to try: we do not own the name, so we cannot
|
|
731
|
+
// serve the link either. Reported rather than thrown — the caller's job is
|
|
732
|
+
// still to exit.
|
|
733
|
+
debug(`forwarding to ${appId} failed: ${cause?.message ?? cause}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** This process's registration, or `null`. Not public; the docs use `role`. */
|
|
738
|
+
export function currentRegistration() {
|
|
739
|
+
return current;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** Test seam, not public: forget every handler, buffer and registration. */
|
|
743
|
+
export function _resetApplicationState() {
|
|
744
|
+
current = null;
|
|
745
|
+
openHandlers.clear();
|
|
746
|
+
activateHandlers.clear();
|
|
747
|
+
buffered = [];
|
|
748
|
+
}
|