@quickgui/native 0.0.1
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 +49 -0
- package/binding.d.ts +746 -0
- package/binding.js +834 -0
- package/dialog.ts +176 -0
- package/host.ts +58 -0
- package/index.ts +1603 -0
- package/integrations.ts +258 -0
- package/native-tree.ts +377 -0
- package/package.json +63 -0
- package/protocol.ts +287 -0
- package/quickgui-native.darwin-arm64.node +0 -0
- package/quickgui-native.darwin-x64.node +0 -0
- package/scripts/build.ts +74 -0
- package/single-instance.ts +31 -0
- package/system.ts +1653 -0
- package/tray.ts +399 -0
package/system.ts
ADDED
|
@@ -0,0 +1,1653 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
import * as binding from "./binding.js";
|
|
4
|
+
import type { Window } from "./index.ts";
|
|
5
|
+
import {
|
|
6
|
+
configureTrayContext,
|
|
7
|
+
dispatchTrayEvent,
|
|
8
|
+
rejectPendingTrayRequests,
|
|
9
|
+
} from "./tray.ts";
|
|
10
|
+
|
|
11
|
+
export { Tray, TrayIcon } from "./tray.ts";
|
|
12
|
+
export type {
|
|
13
|
+
TrayEvent,
|
|
14
|
+
TrayEventType,
|
|
15
|
+
TrayIconOptions,
|
|
16
|
+
TrayIconSource,
|
|
17
|
+
TrayMenuActionItem,
|
|
18
|
+
TrayMenuItem,
|
|
19
|
+
TrayMenuSeparatorItem,
|
|
20
|
+
TrayMenuSubmenuItem,
|
|
21
|
+
} from "./tray.ts";
|
|
22
|
+
|
|
23
|
+
export interface Rectangle {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Display {
|
|
31
|
+
/** Process-stable display identifier. Persist `uuid`, when available, across launches. */
|
|
32
|
+
id: string;
|
|
33
|
+
uuid?: string;
|
|
34
|
+
name: string;
|
|
35
|
+
bounds: Rectangle;
|
|
36
|
+
workArea: Rectangle;
|
|
37
|
+
scaleFactor: number;
|
|
38
|
+
refreshRate?: number;
|
|
39
|
+
primary: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DesktopIntegrationSupport {
|
|
43
|
+
systemNotifications: boolean;
|
|
44
|
+
scheduledNotifications: boolean;
|
|
45
|
+
notificationReplies: boolean;
|
|
46
|
+
nativeApplicationMenus: boolean;
|
|
47
|
+
nativePopupMenus: boolean;
|
|
48
|
+
trayIcons: boolean;
|
|
49
|
+
programmableTrayPopup: boolean;
|
|
50
|
+
globalShortcuts: boolean;
|
|
51
|
+
singleInstance: boolean;
|
|
52
|
+
dynamicProtocolRegistration: boolean;
|
|
53
|
+
autostart: boolean;
|
|
54
|
+
windowIcons: boolean;
|
|
55
|
+
windowFocusability: boolean;
|
|
56
|
+
windowOpacity: boolean;
|
|
57
|
+
skipTaskbar: boolean;
|
|
58
|
+
visibleOnAllWorkspaces: boolean;
|
|
59
|
+
cursorControl: boolean;
|
|
60
|
+
cursorScreenPosition: boolean;
|
|
61
|
+
taskbarProgress: boolean;
|
|
62
|
+
taskbarOverlayIcons: boolean;
|
|
63
|
+
dockBadges: boolean;
|
|
64
|
+
dockIcons: boolean;
|
|
65
|
+
dockMenus: boolean;
|
|
66
|
+
recentDocuments: boolean;
|
|
67
|
+
fileIcons: boolean;
|
|
68
|
+
nativeAboutPanel: boolean;
|
|
69
|
+
userTasks: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AboutPanelOptions {
|
|
73
|
+
applicationName?: string;
|
|
74
|
+
applicationVersion?: string;
|
|
75
|
+
version?: string;
|
|
76
|
+
copyright?: string;
|
|
77
|
+
credits?: string;
|
|
78
|
+
icon?: ImageSource;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface UserTask {
|
|
82
|
+
title: string;
|
|
83
|
+
/** Platform command-line argument string retained by the Windows Jump List. */
|
|
84
|
+
arguments: string;
|
|
85
|
+
program?: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
workingDirectory?: string;
|
|
88
|
+
icon?: { path: string; index: number };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface NativeImage {
|
|
92
|
+
data: Uint8Array;
|
|
93
|
+
width: number;
|
|
94
|
+
height: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface KeyboardLayout {
|
|
98
|
+
id: string;
|
|
99
|
+
name: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type AppearanceMode = "light" | "dark";
|
|
103
|
+
export type AppearancePreference = AppearanceMode | "system";
|
|
104
|
+
export type WindowKind = "normal" | "popover" | "system-popover" | "floating" | "dialog";
|
|
105
|
+
export type WindowLevel = "always-on-bottom" | "normal" | "always-on-top";
|
|
106
|
+
export type CursorGrabMode = "none" | "confined" | "locked";
|
|
107
|
+
export type TaskbarProgressState = "none" | "normal" | "indeterminate" | "paused" | "error";
|
|
108
|
+
export type WindowBackgroundAppearance = "opaque" | "transparent" | "blurred";
|
|
109
|
+
export type ImageSource =
|
|
110
|
+
| string
|
|
111
|
+
| {
|
|
112
|
+
/** Encoded image bytes, or tightly packed RGBA8 when both dimensions are supplied. */
|
|
113
|
+
data: Uint8Array;
|
|
114
|
+
width?: number;
|
|
115
|
+
height?: number;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export interface WindowState {
|
|
119
|
+
displayId?: string;
|
|
120
|
+
kind: WindowKind;
|
|
121
|
+
bounds: Rectangle;
|
|
122
|
+
viewportSize: { width: number; height: number };
|
|
123
|
+
minimumSize?: { width: number; height: number };
|
|
124
|
+
maximumSize?: { width: number; height: number };
|
|
125
|
+
scaleFactor: number;
|
|
126
|
+
appearance: AppearanceMode;
|
|
127
|
+
backgroundAppearance: WindowBackgroundAppearance;
|
|
128
|
+
focused: boolean;
|
|
129
|
+
focusable: boolean;
|
|
130
|
+
visible: boolean;
|
|
131
|
+
minimized: boolean;
|
|
132
|
+
maximized: boolean;
|
|
133
|
+
fullscreen: boolean;
|
|
134
|
+
occluded: boolean;
|
|
135
|
+
movable: boolean;
|
|
136
|
+
resizable: boolean;
|
|
137
|
+
minimizable: boolean;
|
|
138
|
+
maximizable: boolean;
|
|
139
|
+
closable: boolean;
|
|
140
|
+
decorated: boolean;
|
|
141
|
+
shadow: boolean;
|
|
142
|
+
contentProtected: boolean;
|
|
143
|
+
windowLevel: WindowLevel;
|
|
144
|
+
skipTaskbar: boolean;
|
|
145
|
+
visibleOnAllWorkspaces: boolean;
|
|
146
|
+
opacity: number;
|
|
147
|
+
hasIcon: boolean;
|
|
148
|
+
taskbarProgressState: TaskbarProgressState;
|
|
149
|
+
taskbarProgress: number;
|
|
150
|
+
hasTaskbarOverlayIcon: boolean;
|
|
151
|
+
cursorVisible: boolean;
|
|
152
|
+
cursorGrab: CursorGrabMode;
|
|
153
|
+
cursorHitTest: boolean;
|
|
154
|
+
cursorPosition?: { x: number; y: number };
|
|
155
|
+
representedFile: boolean;
|
|
156
|
+
documentEdited: boolean;
|
|
157
|
+
nativeTabbing: boolean;
|
|
158
|
+
nativeTabs: {
|
|
159
|
+
count: number;
|
|
160
|
+
selectedIndex?: number;
|
|
161
|
+
tabBarVisible: boolean;
|
|
162
|
+
overviewVisible: boolean;
|
|
163
|
+
truncated: boolean;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface ClipboardTextEntry {
|
|
168
|
+
type: "text";
|
|
169
|
+
text: string;
|
|
170
|
+
/** Application-owned JSON or string metadata. */
|
|
171
|
+
metadata?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface ClipboardImageEntry {
|
|
175
|
+
type: "image";
|
|
176
|
+
mimeType:
|
|
177
|
+
| "image/png"
|
|
178
|
+
| "image/jpeg"
|
|
179
|
+
| "image/webp"
|
|
180
|
+
| "image/gif"
|
|
181
|
+
| "image/svg+xml"
|
|
182
|
+
| "image/bmp"
|
|
183
|
+
| "image/tiff"
|
|
184
|
+
| "image/x-icon"
|
|
185
|
+
| "image/x-portable-anymap";
|
|
186
|
+
data: Uint8Array;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface ClipboardFilesEntry {
|
|
190
|
+
type: "files";
|
|
191
|
+
paths: readonly string[];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface ClipboardDataEntry {
|
|
195
|
+
type: "data";
|
|
196
|
+
mimeType: string;
|
|
197
|
+
data: Uint8Array;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface ClipboardBookmarkEntry {
|
|
201
|
+
type: "bookmark";
|
|
202
|
+
title: string;
|
|
203
|
+
url: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export type ClipboardEntry =
|
|
207
|
+
| ClipboardTextEntry
|
|
208
|
+
| ClipboardImageEntry
|
|
209
|
+
| ClipboardFilesEntry
|
|
210
|
+
| ClipboardDataEntry
|
|
211
|
+
| ClipboardBookmarkEntry;
|
|
212
|
+
|
|
213
|
+
export interface ClipboardItem {
|
|
214
|
+
/** Representations are committed atomically by one native clipboard write. */
|
|
215
|
+
entries: readonly ClipboardEntry[];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface NotificationAction {
|
|
219
|
+
id: string;
|
|
220
|
+
label: string;
|
|
221
|
+
type?: "button" | "text-input";
|
|
222
|
+
placeholder?: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface NotificationAttachment {
|
|
226
|
+
id: string;
|
|
227
|
+
path: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface NotificationOptions {
|
|
231
|
+
/** Stable identity used to replace or later dismiss this notification. */
|
|
232
|
+
tag: string;
|
|
233
|
+
title: string;
|
|
234
|
+
body?: string;
|
|
235
|
+
subtitle?: string;
|
|
236
|
+
actions?: readonly NotificationAction[];
|
|
237
|
+
/** `default`, `silent`, or a platform-recognized sound name. */
|
|
238
|
+
sound?: string;
|
|
239
|
+
icon?: string;
|
|
240
|
+
attachments?: readonly NotificationAttachment[];
|
|
241
|
+
deliverAt?: Date;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface NotificationResponse {
|
|
245
|
+
tag: string;
|
|
246
|
+
/** Omitted when the notification body, rather than an action button, was activated. */
|
|
247
|
+
actionId?: string;
|
|
248
|
+
/** Inline text returned by a `text-input` action. */
|
|
249
|
+
reply?: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export type NotificationPermissionStatus =
|
|
253
|
+
| "not-determined"
|
|
254
|
+
| "granted"
|
|
255
|
+
| "denied"
|
|
256
|
+
| "unsupported";
|
|
257
|
+
|
|
258
|
+
export type MenuRole =
|
|
259
|
+
| "cut"
|
|
260
|
+
| "copy"
|
|
261
|
+
| "paste"
|
|
262
|
+
| "select-all"
|
|
263
|
+
| "undo"
|
|
264
|
+
| "redo"
|
|
265
|
+
| "about"
|
|
266
|
+
| "hide-application"
|
|
267
|
+
| "hide-other-applications"
|
|
268
|
+
| "show-all-applications"
|
|
269
|
+
| "quit"
|
|
270
|
+
| "close-window"
|
|
271
|
+
| "minimize-window"
|
|
272
|
+
| "zoom-window"
|
|
273
|
+
| "toggle-fullscreen"
|
|
274
|
+
| "bring-all-to-front"
|
|
275
|
+
| "show-help";
|
|
276
|
+
export type MenuItemMark = "none" | "check" | "radio";
|
|
277
|
+
|
|
278
|
+
export interface MenuActionItem {
|
|
279
|
+
type?: "action";
|
|
280
|
+
label: string;
|
|
281
|
+
enabled?: boolean;
|
|
282
|
+
checked?: boolean;
|
|
283
|
+
mark?: MenuItemMark;
|
|
284
|
+
icon?: ImageSource;
|
|
285
|
+
role?: MenuRole;
|
|
286
|
+
click?: () => void;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface MenuRoleItem {
|
|
290
|
+
type: "role";
|
|
291
|
+
label: string;
|
|
292
|
+
role: MenuRole;
|
|
293
|
+
enabled?: boolean;
|
|
294
|
+
checked?: boolean;
|
|
295
|
+
mark?: MenuItemMark;
|
|
296
|
+
icon?: ImageSource;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface MenuSeparatorItem {
|
|
300
|
+
type: "separator";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface MenuSubmenuItem {
|
|
304
|
+
type: "submenu";
|
|
305
|
+
label: string;
|
|
306
|
+
enabled?: boolean;
|
|
307
|
+
items: readonly MenuItem[];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export interface MenuServicesItem {
|
|
311
|
+
/** macOS Services. Other platforms omit this operating-system-owned submenu. */
|
|
312
|
+
type: "services";
|
|
313
|
+
label?: string;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface MenuSystemItem {
|
|
317
|
+
type: "system-menu";
|
|
318
|
+
label: string;
|
|
319
|
+
menu: "services" | "window" | "help";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export type MenuItem =
|
|
323
|
+
| MenuActionItem
|
|
324
|
+
| MenuRoleItem
|
|
325
|
+
| MenuSeparatorItem
|
|
326
|
+
| MenuSubmenuItem
|
|
327
|
+
| MenuServicesItem
|
|
328
|
+
| MenuSystemItem;
|
|
329
|
+
|
|
330
|
+
export interface MenuDefinition {
|
|
331
|
+
label: string;
|
|
332
|
+
enabled?: boolean;
|
|
333
|
+
items: readonly MenuItem[];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export type GlobalShortcutListener = () => void;
|
|
337
|
+
export type PowerEventType =
|
|
338
|
+
| "suspend"
|
|
339
|
+
| "resume"
|
|
340
|
+
| "lock-screen"
|
|
341
|
+
| "unlock-screen"
|
|
342
|
+
| "shutdown-requested"
|
|
343
|
+
| "power-source-changed"
|
|
344
|
+
| "thermal-state-changed"
|
|
345
|
+
| "low-power-mode-changed"
|
|
346
|
+
| "cpu-speed-limit-changed";
|
|
347
|
+
|
|
348
|
+
export type PowerEvent =
|
|
349
|
+
| { type: "suspend" | "resume" | "lock-screen" | "unlock-screen" | "shutdown-requested" }
|
|
350
|
+
| { type: "power-source-changed"; source: "ac" | "battery" | "unknown" }
|
|
351
|
+
| {
|
|
352
|
+
type: "thermal-state-changed";
|
|
353
|
+
state: "unknown" | "nominal" | "fair" | "serious" | "critical";
|
|
354
|
+
}
|
|
355
|
+
| { type: "low-power-mode-changed"; enabled: boolean }
|
|
356
|
+
| { type: "cpu-speed-limit-changed"; percent: number };
|
|
357
|
+
|
|
358
|
+
export type PowerSource = "ac" | "battery" | "unknown";
|
|
359
|
+
export type BatteryStatus =
|
|
360
|
+
| "charging"
|
|
361
|
+
| "discharging"
|
|
362
|
+
| "full"
|
|
363
|
+
| "not-charging"
|
|
364
|
+
| "unknown";
|
|
365
|
+
export type ThermalState = "unknown" | "nominal" | "fair" | "serious" | "critical";
|
|
366
|
+
export type SessionState = "active" | "inactive" | "locked" | "unknown";
|
|
367
|
+
export type IdleState = "active" | "idle" | "locked" | "unknown";
|
|
368
|
+
export type PowerAssertionKind =
|
|
369
|
+
| "prevent-application-suspension"
|
|
370
|
+
| "prevent-display-sleep";
|
|
371
|
+
|
|
372
|
+
export interface PowerState {
|
|
373
|
+
source: PowerSource;
|
|
374
|
+
battery?: { chargePercent?: number; status: BatteryStatus };
|
|
375
|
+
thermalState: ThermalState;
|
|
376
|
+
lowPowerMode?: boolean;
|
|
377
|
+
cpuSpeedLimitPercent?: number;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export interface SystemColor {
|
|
381
|
+
red: number;
|
|
382
|
+
green: number;
|
|
383
|
+
blue: number;
|
|
384
|
+
alpha: number;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export interface SystemPreferencesSnapshot {
|
|
388
|
+
colorScheme: "light" | "dark" | "unknown";
|
|
389
|
+
reduceMotion?: boolean;
|
|
390
|
+
reduceTransparency?: boolean;
|
|
391
|
+
increaseContrast?: boolean;
|
|
392
|
+
differentiateWithoutColor?: boolean;
|
|
393
|
+
invertColors?: boolean;
|
|
394
|
+
forcedColors?: boolean;
|
|
395
|
+
screenReader?: boolean;
|
|
396
|
+
switchControl?: boolean;
|
|
397
|
+
colors: Partial<
|
|
398
|
+
Record<
|
|
399
|
+
| "accent"
|
|
400
|
+
| "highlight"
|
|
401
|
+
| "highlightText"
|
|
402
|
+
| "windowBackground"
|
|
403
|
+
| "windowText"
|
|
404
|
+
| "controlBackground"
|
|
405
|
+
| "controlText"
|
|
406
|
+
| "link",
|
|
407
|
+
SystemColor
|
|
408
|
+
>
|
|
409
|
+
>;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export type PermissionKind = "camera" | "microphone" | "screen-recording" | "accessibility";
|
|
413
|
+
export type PermissionStatus =
|
|
414
|
+
| "not-determined"
|
|
415
|
+
| "granted"
|
|
416
|
+
| "denied"
|
|
417
|
+
| "restricted"
|
|
418
|
+
| "unknown";
|
|
419
|
+
|
|
420
|
+
/** RAII native sleep assertion. Releasing or dropping this object removes the OS assertion. */
|
|
421
|
+
export class PowerAssertion {
|
|
422
|
+
readonly #native: binding.NativePowerAssertion;
|
|
423
|
+
|
|
424
|
+
constructor(kind: PowerAssertionKind, reason: string) {
|
|
425
|
+
this.#native = new binding.NativePowerAssertion(kind, reason);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
get kind(): PowerAssertionKind {
|
|
429
|
+
return this.#native.kind as PowerAssertionKind;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
get reason(): string {
|
|
433
|
+
return this.#native.reason;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
get active(): boolean {
|
|
437
|
+
return this.#native.active;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
release(): boolean {
|
|
441
|
+
return this.#native.release();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
type AppContext = {
|
|
446
|
+
appId: number;
|
|
447
|
+
hosted: boolean;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
type ContextResolver = () => AppContext;
|
|
451
|
+
type WindowResolver = (window?: Window) => { context: AppContext; window: Window };
|
|
452
|
+
|
|
453
|
+
let resolveContext: ContextResolver | undefined;
|
|
454
|
+
let resolveWindow: WindowResolver | undefined;
|
|
455
|
+
|
|
456
|
+
export function configureSystemContext(
|
|
457
|
+
context: ContextResolver,
|
|
458
|
+
window: WindowResolver,
|
|
459
|
+
): void {
|
|
460
|
+
resolveContext = context;
|
|
461
|
+
resolveWindow = window;
|
|
462
|
+
configureTrayContext(context);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function context(): AppContext {
|
|
466
|
+
if (!resolveContext) throw new Error("QuickGUI's native system context is not configured");
|
|
467
|
+
return resolveContext();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function windowContext(window?: Window): { context: AppContext; window: Window } {
|
|
471
|
+
if (!resolveWindow) throw new Error("QuickGUI's native window context is not configured");
|
|
472
|
+
return resolveWindow(window);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function nativeDisplays(): binding.NativeDisplay[] {
|
|
476
|
+
const current = context();
|
|
477
|
+
return current.hosted
|
|
478
|
+
? binding.getHostedDisplays(current.appId)
|
|
479
|
+
: binding.getDisplays(current.appId);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function normalizeDisplay(display: binding.NativeDisplay): Display {
|
|
483
|
+
const normalized: Display = {
|
|
484
|
+
id: display.id,
|
|
485
|
+
name: display.name,
|
|
486
|
+
bounds: { ...display.bounds },
|
|
487
|
+
workArea: { ...display.workArea },
|
|
488
|
+
scaleFactor: display.scaleFactor,
|
|
489
|
+
primary: display.primary,
|
|
490
|
+
};
|
|
491
|
+
if (display.uuid !== undefined) normalized.uuid = display.uuid;
|
|
492
|
+
if (display.refreshRate !== undefined) normalized.refreshRate = display.refreshRate;
|
|
493
|
+
return normalized;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function nativeKeyboardLayout(): KeyboardLayout {
|
|
497
|
+
const current = context();
|
|
498
|
+
const layout = current.hosted
|
|
499
|
+
? binding.getHostedKeyboardLayout(current.appId)
|
|
500
|
+
: binding.getKeyboardLayout(current.appId);
|
|
501
|
+
return { id: layout.id, name: layout.name };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function normalizeWindowState(state: binding.NativeWindowState): WindowState {
|
|
505
|
+
const normalized: WindowState = {
|
|
506
|
+
kind: state.kind as WindowKind,
|
|
507
|
+
bounds: { x: state.x, y: state.y, width: state.width, height: state.height },
|
|
508
|
+
viewportSize: { width: state.viewportWidth, height: state.viewportHeight },
|
|
509
|
+
scaleFactor: state.scaleFactor,
|
|
510
|
+
appearance: state.appearance === "dark" ? "dark" : "light",
|
|
511
|
+
backgroundAppearance: state.backgroundAppearance as WindowBackgroundAppearance,
|
|
512
|
+
focused: state.focused,
|
|
513
|
+
focusable: state.focusable,
|
|
514
|
+
visible: state.visible,
|
|
515
|
+
minimized: state.minimized,
|
|
516
|
+
maximized: state.maximized,
|
|
517
|
+
fullscreen: state.fullscreen,
|
|
518
|
+
occluded: state.occluded,
|
|
519
|
+
movable: state.movable,
|
|
520
|
+
resizable: state.resizable,
|
|
521
|
+
minimizable: state.minimizable,
|
|
522
|
+
maximizable: state.maximizable,
|
|
523
|
+
closable: state.closable,
|
|
524
|
+
decorated: state.decorated,
|
|
525
|
+
shadow: state.shadow,
|
|
526
|
+
contentProtected: state.contentProtected,
|
|
527
|
+
windowLevel: state.windowLevel as WindowLevel,
|
|
528
|
+
skipTaskbar: state.skipTaskbar,
|
|
529
|
+
visibleOnAllWorkspaces: state.visibleOnAllWorkspaces,
|
|
530
|
+
opacity: state.opacity,
|
|
531
|
+
hasIcon: state.hasIcon,
|
|
532
|
+
taskbarProgressState: state.taskbarProgressState as TaskbarProgressState,
|
|
533
|
+
taskbarProgress: state.taskbarProgress,
|
|
534
|
+
hasTaskbarOverlayIcon: state.hasTaskbarOverlayIcon,
|
|
535
|
+
cursorVisible: state.cursorVisible,
|
|
536
|
+
cursorGrab: state.cursorGrab as CursorGrabMode,
|
|
537
|
+
cursorHitTest: state.cursorHitTest,
|
|
538
|
+
representedFile: state.representedFile,
|
|
539
|
+
documentEdited: state.documentEdited,
|
|
540
|
+
nativeTabbing: state.nativeTabbing,
|
|
541
|
+
nativeTabs: {
|
|
542
|
+
count: state.nativeTabCount,
|
|
543
|
+
tabBarVisible: state.nativeTabBarVisible,
|
|
544
|
+
overviewVisible: state.nativeTabOverviewVisible,
|
|
545
|
+
truncated: state.nativeTabsTruncated,
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
if (state.displayId !== undefined) normalized.displayId = state.displayId;
|
|
549
|
+
if (state.minimumWidth !== undefined && state.minimumHeight !== undefined) {
|
|
550
|
+
normalized.minimumSize = { width: state.minimumWidth, height: state.minimumHeight };
|
|
551
|
+
}
|
|
552
|
+
if (state.maximumWidth !== undefined && state.maximumHeight !== undefined) {
|
|
553
|
+
normalized.maximumSize = { width: state.maximumWidth, height: state.maximumHeight };
|
|
554
|
+
}
|
|
555
|
+
if (state.cursorX !== undefined && state.cursorY !== undefined) {
|
|
556
|
+
normalized.cursorPosition = { x: state.cursorX, y: state.cursorY };
|
|
557
|
+
}
|
|
558
|
+
if (state.nativeSelectedTab !== undefined) {
|
|
559
|
+
normalized.nativeTabs.selectedIndex = state.nativeSelectedTab;
|
|
560
|
+
}
|
|
561
|
+
return normalized;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function getNativeWindowState(window: Window): WindowState {
|
|
565
|
+
const { context: current, window: resolved } = windowContext(window);
|
|
566
|
+
const state = current.hosted
|
|
567
|
+
? binding.getHostedWindowState(current.appId, resolved.nativeId)
|
|
568
|
+
: binding.getWindowState(current.appId, resolved.nativeId);
|
|
569
|
+
return normalizeWindowState(state);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export function performNativeWindowAction(
|
|
573
|
+
window: Window,
|
|
574
|
+
action: string,
|
|
575
|
+
value?: string,
|
|
576
|
+
): void {
|
|
577
|
+
const { context: current, window: resolved } = windowContext(window);
|
|
578
|
+
if (current.hosted) {
|
|
579
|
+
binding.performHostedWindowAction(current.appId, resolved.nativeId, action, value);
|
|
580
|
+
} else {
|
|
581
|
+
binding.performWindowAction(current.appId, resolved.nativeId, action, value);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function performNativeWindowImageAction(
|
|
586
|
+
window: Window,
|
|
587
|
+
action: "set-icon" | "clear-icon" | "set-taskbar-overlay-icon",
|
|
588
|
+
image?: ImageSource,
|
|
589
|
+
description?: string,
|
|
590
|
+
): void {
|
|
591
|
+
const { context: current, window: resolved } = windowContext(window);
|
|
592
|
+
const native = image === undefined ? undefined : nativeImageSource(image);
|
|
593
|
+
if (current.hosted) {
|
|
594
|
+
binding.performHostedWindowImageAction(
|
|
595
|
+
current.appId,
|
|
596
|
+
resolved.nativeId,
|
|
597
|
+
action,
|
|
598
|
+
native,
|
|
599
|
+
description,
|
|
600
|
+
);
|
|
601
|
+
} else {
|
|
602
|
+
binding.performWindowImageAction(
|
|
603
|
+
current.appId,
|
|
604
|
+
resolved.nativeId,
|
|
605
|
+
action,
|
|
606
|
+
native,
|
|
607
|
+
description,
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export function nativeImageSource(source: ImageSource): binding.NativeImageSource {
|
|
613
|
+
if (typeof source === "string") return { path: resolvePath(source) };
|
|
614
|
+
const native: binding.NativeImageSource = {
|
|
615
|
+
data: Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength),
|
|
616
|
+
};
|
|
617
|
+
if (source.width !== undefined) native.width = source.width;
|
|
618
|
+
if (source.height !== undefined) native.height = source.height;
|
|
619
|
+
return native;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function nativeClipboardItem(item: ClipboardItem): binding.NativeClipboardItem {
|
|
623
|
+
return {
|
|
624
|
+
entries: item.entries.map((entry): binding.NativeClipboardEntry => {
|
|
625
|
+
if (entry.type === "text") {
|
|
626
|
+
const native: binding.NativeClipboardEntry = { kind: "text", text: entry.text };
|
|
627
|
+
if (entry.metadata !== undefined) native.metadata = entry.metadata;
|
|
628
|
+
return native;
|
|
629
|
+
}
|
|
630
|
+
if (entry.type === "image") {
|
|
631
|
+
return {
|
|
632
|
+
kind: "image",
|
|
633
|
+
format: entry.mimeType,
|
|
634
|
+
data: Buffer.from(entry.data.buffer, entry.data.byteOffset, entry.data.byteLength),
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (entry.type === "files") return { kind: "files", paths: [...entry.paths] };
|
|
638
|
+
if (entry.type === "data") {
|
|
639
|
+
return {
|
|
640
|
+
kind: "data",
|
|
641
|
+
format: entry.mimeType,
|
|
642
|
+
data: Buffer.from(entry.data.buffer, entry.data.byteOffset, entry.data.byteLength),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
return { kind: "bookmark", text: entry.title, url: entry.url };
|
|
646
|
+
}),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function clipboardItem(item: binding.NativeClipboardItem): ClipboardItem {
|
|
651
|
+
return {
|
|
652
|
+
entries: item.entries.map((entry): ClipboardEntry => {
|
|
653
|
+
if (entry.kind === "text" && entry.text !== undefined) {
|
|
654
|
+
const text: ClipboardTextEntry = { type: "text", text: entry.text };
|
|
655
|
+
if (entry.metadata !== undefined) text.metadata = entry.metadata;
|
|
656
|
+
return text;
|
|
657
|
+
}
|
|
658
|
+
if (entry.kind === "image" && entry.format && entry.data) {
|
|
659
|
+
return {
|
|
660
|
+
type: "image",
|
|
661
|
+
mimeType: entry.format as ClipboardImageEntry["mimeType"],
|
|
662
|
+
data: Uint8Array.from(entry.data),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
if (entry.kind === "files" && entry.paths) {
|
|
666
|
+
return { type: "files", paths: entry.paths };
|
|
667
|
+
}
|
|
668
|
+
if (entry.kind === "data" && entry.format && entry.data) {
|
|
669
|
+
return { type: "data", mimeType: entry.format, data: Uint8Array.from(entry.data) };
|
|
670
|
+
}
|
|
671
|
+
if (entry.kind === "bookmark" && entry.text !== undefined && entry.url !== undefined) {
|
|
672
|
+
return { type: "bookmark", title: entry.text, url: entry.url };
|
|
673
|
+
}
|
|
674
|
+
throw new Error(`the native clipboard returned an invalid ${entry.kind} entry`);
|
|
675
|
+
}),
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const screenListeners = new Set<(displays: readonly Display[]) => void>();
|
|
680
|
+
const appearanceListeners = new Set<(appearance: AppearanceMode, window: Window) => void>();
|
|
681
|
+
const keyboardListeners = new Set<(layout: KeyboardLayout) => void>();
|
|
682
|
+
const powerListeners = new Map<PowerEventType, Set<(event: PowerEvent) => void>>();
|
|
683
|
+
const notificationListeners = new Set<(response: NotificationResponse) => void>();
|
|
684
|
+
const systemPreferencesListeners = new Set<(preferences: SystemPreferencesSnapshot) => void>();
|
|
685
|
+
let menuCallbacks = new Map<number, () => void>();
|
|
686
|
+
let applicationMenuActionIds = new Set<number>();
|
|
687
|
+
let nextMenuAction = 1;
|
|
688
|
+
const windowStateListeners = new Map<number, Set<(state: WindowState) => void>>();
|
|
689
|
+
const pendingShellRequests = new Map<
|
|
690
|
+
number,
|
|
691
|
+
{ resolve: () => void; reject: (error: Error) => void }
|
|
692
|
+
>();
|
|
693
|
+
let nextShellRequest = 1;
|
|
694
|
+
const pendingNotificationPermissionRequests = new Map<
|
|
695
|
+
number,
|
|
696
|
+
{ resolve: (status: NotificationPermissionStatus) => void; reject: (error: Error) => void }
|
|
697
|
+
>();
|
|
698
|
+
let nextNotificationPermissionRequest = 1;
|
|
699
|
+
const pendingFileIconRequests = new Map<
|
|
700
|
+
number,
|
|
701
|
+
{ resolve: (icon: NativeImage) => void; reject: (error: Error) => void }
|
|
702
|
+
>();
|
|
703
|
+
const pendingUserTaskRequests = new Map<
|
|
704
|
+
number,
|
|
705
|
+
{ resolve: () => void; reject: (error: Error) => void }
|
|
706
|
+
>();
|
|
707
|
+
let nextFileIconRequest = 1;
|
|
708
|
+
let nextUserTaskRequest = 1;
|
|
709
|
+
let disposeDockMenu: (() => void) | undefined;
|
|
710
|
+
const globalShortcutRegistrations = new Map<
|
|
711
|
+
number,
|
|
712
|
+
{ accelerator: string; listener: GlobalShortcutListener }
|
|
713
|
+
>();
|
|
714
|
+
const globalShortcutIds = new Map<string, number>();
|
|
715
|
+
const pendingGlobalShortcutRequests = new Map<
|
|
716
|
+
number,
|
|
717
|
+
{ resolve: () => void; reject: (error: Error) => void }
|
|
718
|
+
>();
|
|
719
|
+
let nextGlobalShortcutRequest = 1;
|
|
720
|
+
let nextGlobalShortcutRegistration = 1;
|
|
721
|
+
|
|
722
|
+
export const Clipboard = Object.freeze({
|
|
723
|
+
async read(): Promise<ClipboardItem | undefined> {
|
|
724
|
+
const current = context();
|
|
725
|
+
const item = current.hosted
|
|
726
|
+
? binding.readHostedClipboard(current.appId)
|
|
727
|
+
: binding.readClipboard(current.appId);
|
|
728
|
+
return item ? clipboardItem(item) : undefined;
|
|
729
|
+
},
|
|
730
|
+
|
|
731
|
+
async write(item: ClipboardItem): Promise<void> {
|
|
732
|
+
const current = context();
|
|
733
|
+
const native = nativeClipboardItem(item);
|
|
734
|
+
if (current.hosted) binding.writeHostedClipboard(current.appId, native);
|
|
735
|
+
else binding.writeClipboard(current.appId, native);
|
|
736
|
+
},
|
|
737
|
+
|
|
738
|
+
async readText(): Promise<string> {
|
|
739
|
+
const item = await this.read();
|
|
740
|
+
if (!item) return "";
|
|
741
|
+
const text = item.entries
|
|
742
|
+
.filter((entry): entry is ClipboardTextEntry => entry.type === "text")
|
|
743
|
+
.map((entry) => entry.text)
|
|
744
|
+
.join("");
|
|
745
|
+
if (text) return text;
|
|
746
|
+
return item.entries
|
|
747
|
+
.filter((entry): entry is ClipboardFilesEntry => entry.type === "files")
|
|
748
|
+
.flatMap((entry) => entry.paths)
|
|
749
|
+
.join("\n");
|
|
750
|
+
},
|
|
751
|
+
|
|
752
|
+
async writeText(text: string): Promise<void> {
|
|
753
|
+
await this.write({ entries: [{ type: "text", text }] });
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
async clear(): Promise<void> {
|
|
757
|
+
await this.write({ entries: [] });
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
function allocateShellRequest(): number {
|
|
762
|
+
for (let attempt = 0; attempt <= pendingShellRequests.size; attempt += 1) {
|
|
763
|
+
const request = nextShellRequest;
|
|
764
|
+
nextShellRequest = request >= 0xffff_ffff ? 1 : request + 1;
|
|
765
|
+
if (!pendingShellRequests.has(request)) return request;
|
|
766
|
+
}
|
|
767
|
+
throw new Error("the native shell request id space is exhausted");
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function shellRequest(action: string, value: string): Promise<void> {
|
|
771
|
+
try {
|
|
772
|
+
const current = context();
|
|
773
|
+
const request = allocateShellRequest();
|
|
774
|
+
return new Promise<void>((resolve, reject) => {
|
|
775
|
+
pendingShellRequests.set(request, {
|
|
776
|
+
resolve,
|
|
777
|
+
reject: (error) => reject(error),
|
|
778
|
+
});
|
|
779
|
+
try {
|
|
780
|
+
if (current.hosted) {
|
|
781
|
+
binding.performHostedShellAction(current.appId, request, action, value);
|
|
782
|
+
} else {
|
|
783
|
+
binding.performShellAction(current.appId, request, action, value);
|
|
784
|
+
}
|
|
785
|
+
} catch (error) {
|
|
786
|
+
pendingShellRequests.delete(request);
|
|
787
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
} catch (error) {
|
|
791
|
+
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function externalUrl(value: string | URL): string {
|
|
796
|
+
const url = value instanceof URL ? value : new URL(value);
|
|
797
|
+
if (url.protocol === "file:") {
|
|
798
|
+
throw new TypeError("Shell.openExternal does not accept file URLs; use Shell.openPath");
|
|
799
|
+
}
|
|
800
|
+
return url.href;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** Cross-platform operations delegated to the user's registered system applications. */
|
|
804
|
+
export const Shell = Object.freeze({
|
|
805
|
+
openExternal(url: string | URL): Promise<void> {
|
|
806
|
+
try {
|
|
807
|
+
return shellRequest("open-external", externalUrl(url));
|
|
808
|
+
} catch (error) {
|
|
809
|
+
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
|
|
813
|
+
openPath(path: string): Promise<void> {
|
|
814
|
+
return shellRequest("open-path", resolvePath(path));
|
|
815
|
+
},
|
|
816
|
+
|
|
817
|
+
showItemInFolder(path: string): Promise<void> {
|
|
818
|
+
return shellRequest("reveal-path", resolvePath(path));
|
|
819
|
+
},
|
|
820
|
+
|
|
821
|
+
trashItem(path: string): Promise<void> {
|
|
822
|
+
return shellRequest("trash-path", resolvePath(path));
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
/** Operating-system notifications delivered outside QuickGUI windows. */
|
|
827
|
+
export const Notifications = Object.freeze({
|
|
828
|
+
isSupported(): boolean {
|
|
829
|
+
return process.platform === "darwin" || process.platform === "win32" || process.platform === "linux";
|
|
830
|
+
},
|
|
831
|
+
|
|
832
|
+
async show(options: NotificationOptions): Promise<void> {
|
|
833
|
+
const current = context();
|
|
834
|
+
const native: binding.NativeNotificationOptions = {
|
|
835
|
+
tag: options.tag,
|
|
836
|
+
title: options.title,
|
|
837
|
+
body: options.body ?? "",
|
|
838
|
+
actions: (options.actions ?? []).map((action) => {
|
|
839
|
+
const nativeAction: binding.NativeNotificationAction = {
|
|
840
|
+
id: action.id,
|
|
841
|
+
label: action.label,
|
|
842
|
+
};
|
|
843
|
+
if (action.type !== undefined) nativeAction.kind = action.type;
|
|
844
|
+
if (action.placeholder !== undefined) nativeAction.placeholder = action.placeholder;
|
|
845
|
+
return nativeAction;
|
|
846
|
+
}),
|
|
847
|
+
};
|
|
848
|
+
if (options.subtitle !== undefined) native.subtitle = options.subtitle;
|
|
849
|
+
if (options.sound !== undefined) native.sound = options.sound;
|
|
850
|
+
if (options.icon !== undefined) native.iconPath = resolvePath(options.icon);
|
|
851
|
+
if (options.attachments !== undefined) {
|
|
852
|
+
native.attachments = options.attachments.map((attachment) => ({
|
|
853
|
+
id: attachment.id,
|
|
854
|
+
path: resolvePath(attachment.path),
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
857
|
+
if (options.deliverAt !== undefined) native.deliveryAtMs = options.deliverAt.getTime();
|
|
858
|
+
if (current.hosted) binding.showHostedNotification(current.appId, native);
|
|
859
|
+
else binding.showNotification(current.appId, native);
|
|
860
|
+
},
|
|
861
|
+
|
|
862
|
+
async dismiss(tag: string): Promise<void> {
|
|
863
|
+
const current = context();
|
|
864
|
+
if (current.hosted) binding.dismissHostedNotification(current.appId, tag);
|
|
865
|
+
else binding.dismissNotification(current.appId, tag);
|
|
866
|
+
},
|
|
867
|
+
|
|
868
|
+
onResponse(listener: (response: NotificationResponse) => void): () => void {
|
|
869
|
+
notificationListeners.add(listener);
|
|
870
|
+
return () => notificationListeners.delete(listener);
|
|
871
|
+
},
|
|
872
|
+
|
|
873
|
+
getPermissionStatus(): Promise<NotificationPermissionStatus> {
|
|
874
|
+
return notificationPermissionRequest(false);
|
|
875
|
+
},
|
|
876
|
+
|
|
877
|
+
requestPermission(): Promise<NotificationPermissionStatus> {
|
|
878
|
+
return notificationPermissionRequest(true);
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
function notificationPermissionRequest(prompt: boolean): Promise<NotificationPermissionStatus> {
|
|
883
|
+
try {
|
|
884
|
+
const current = context();
|
|
885
|
+
const request = allocateBoundedId(
|
|
886
|
+
nextNotificationPermissionRequest,
|
|
887
|
+
pendingNotificationPermissionRequests,
|
|
888
|
+
);
|
|
889
|
+
nextNotificationPermissionRequest = request >= 0xffff_ffff ? 1 : request + 1;
|
|
890
|
+
return new Promise((resolve, reject) => {
|
|
891
|
+
pendingNotificationPermissionRequests.set(request, { resolve, reject });
|
|
892
|
+
try {
|
|
893
|
+
if (current.hosted) {
|
|
894
|
+
binding.performHostedNotificationPermissionRequest(
|
|
895
|
+
current.appId,
|
|
896
|
+
request,
|
|
897
|
+
prompt,
|
|
898
|
+
);
|
|
899
|
+
} else {
|
|
900
|
+
binding.performNotificationPermissionRequest(current.appId, request, prompt);
|
|
901
|
+
}
|
|
902
|
+
} catch (error) {
|
|
903
|
+
pendingNotificationPermissionRequests.delete(request);
|
|
904
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
} catch (error) {
|
|
908
|
+
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function allocateMenuAction(callbacks: Map<number, () => void>): number {
|
|
913
|
+
for (let attempt = 0; attempt <= callbacks.size + menuCallbacks.size; attempt += 1) {
|
|
914
|
+
const id = nextMenuAction;
|
|
915
|
+
nextMenuAction = id >= 0xffff_ffff ? 1 : id + 1;
|
|
916
|
+
if (!callbacks.has(id) && !menuCallbacks.has(id)) return id;
|
|
917
|
+
}
|
|
918
|
+
throw new Error("the native menu action id space is exhausted");
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
export function serializeNativeMenu(definitions: readonly MenuDefinition[]): {
|
|
922
|
+
json: string;
|
|
923
|
+
actionIds: readonly number[];
|
|
924
|
+
install: () => () => void;
|
|
925
|
+
} {
|
|
926
|
+
const callbacks = new Map<number, () => void>();
|
|
927
|
+
const native = definitions.map((menu) => ({
|
|
928
|
+
label: menu.label,
|
|
929
|
+
enabled: menu.enabled ?? true,
|
|
930
|
+
items: nativeMenuItems(menu.items, callbacks),
|
|
931
|
+
}));
|
|
932
|
+
return {
|
|
933
|
+
json: JSON.stringify(native),
|
|
934
|
+
actionIds: [...callbacks.keys()],
|
|
935
|
+
install: () => {
|
|
936
|
+
for (const [id, callback] of callbacks) menuCallbacks.set(id, callback);
|
|
937
|
+
return () => {
|
|
938
|
+
for (const id of callbacks.keys()) menuCallbacks.delete(id);
|
|
939
|
+
};
|
|
940
|
+
},
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function nativeMenuItems(
|
|
945
|
+
items: readonly MenuItem[],
|
|
946
|
+
callbacks: Map<number, () => void>,
|
|
947
|
+
): unknown[] {
|
|
948
|
+
return items.map((item) => {
|
|
949
|
+
if (item.type === "separator") return { type: "separator" };
|
|
950
|
+
if (item.type === "services") {
|
|
951
|
+
return { type: "services", label: item.label ?? "Services" };
|
|
952
|
+
}
|
|
953
|
+
if (item.type === "system-menu") {
|
|
954
|
+
return { type: "system-menu", label: item.label, menu: item.menu };
|
|
955
|
+
}
|
|
956
|
+
if (item.type === "submenu") {
|
|
957
|
+
return {
|
|
958
|
+
type: "submenu",
|
|
959
|
+
label: item.label,
|
|
960
|
+
enabled: item.enabled ?? true,
|
|
961
|
+
items: nativeMenuItems(item.items, callbacks),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
if (item.type === "role") {
|
|
965
|
+
return {
|
|
966
|
+
type: "role",
|
|
967
|
+
label: item.label,
|
|
968
|
+
enabled: item.enabled ?? true,
|
|
969
|
+
checked: item.checked ?? false,
|
|
970
|
+
...(item.mark ? { mark: item.mark } : {}),
|
|
971
|
+
role: item.role,
|
|
972
|
+
...(item.icon ? { icon: jsonImageSource(item.icon) } : {}),
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
const id = allocateMenuAction(callbacks);
|
|
976
|
+
callbacks.set(id, item.click ?? (() => {}));
|
|
977
|
+
return {
|
|
978
|
+
type: "action",
|
|
979
|
+
id,
|
|
980
|
+
label: item.label,
|
|
981
|
+
enabled: item.enabled ?? true,
|
|
982
|
+
checked: item.checked ?? false,
|
|
983
|
+
...(item.mark ? { mark: item.mark } : {}),
|
|
984
|
+
...(item.role ? { role: item.role } : {}),
|
|
985
|
+
...(item.icon ? { icon: jsonImageSource(item.icon) } : {}),
|
|
986
|
+
};
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function jsonImageSource(source: ImageSource): unknown {
|
|
991
|
+
if (typeof source === "string") return { path: resolvePath(source) };
|
|
992
|
+
return {
|
|
993
|
+
dataBase64: Buffer.from(
|
|
994
|
+
source.data.buffer,
|
|
995
|
+
source.data.byteOffset,
|
|
996
|
+
source.data.byteLength,
|
|
997
|
+
).toString("base64"),
|
|
998
|
+
...(source.width !== undefined ? { width: source.width } : {}),
|
|
999
|
+
...(source.height !== undefined ? { height: source.height } : {}),
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/** Declarative platform-native application menus. */
|
|
1004
|
+
export const Menu = Object.freeze({
|
|
1005
|
+
setApplicationMenu(definitions: readonly MenuDefinition[] | null): void {
|
|
1006
|
+
const current = context();
|
|
1007
|
+
const serialized = serializeNativeMenu(definitions ?? []);
|
|
1008
|
+
if (current.hosted) binding.setHostedApplicationMenu(current.appId, serialized.json);
|
|
1009
|
+
else binding.setApplicationMenu(current.appId, serialized.json);
|
|
1010
|
+
for (const id of applicationMenuActionIds) menuCallbacks.delete(id);
|
|
1011
|
+
serialized.install();
|
|
1012
|
+
applicationMenuActionIds = new Set(serialized.actionIds);
|
|
1013
|
+
// Application menu actions live until the next replacement; keep the disposer represented
|
|
1014
|
+
// by their exact ids so window and Dock menu callbacks remain independently owned.
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
function allocateBoundedId(next: number, used: ReadonlyMap<number, unknown>): number {
|
|
1019
|
+
let candidate = next;
|
|
1020
|
+
for (let attempt = 0; attempt <= used.size; attempt += 1) {
|
|
1021
|
+
if (!used.has(candidate)) return candidate;
|
|
1022
|
+
candidate = candidate >= 0xffff_ffff ? 1 : candidate + 1;
|
|
1023
|
+
}
|
|
1024
|
+
throw new Error("the native system id space is exhausted");
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function globalShortcutOperation(
|
|
1028
|
+
action: "register" | "unregister" | "unregister-all",
|
|
1029
|
+
registration?: number,
|
|
1030
|
+
accelerator?: string,
|
|
1031
|
+
): Promise<void> {
|
|
1032
|
+
try {
|
|
1033
|
+
const current = context();
|
|
1034
|
+
const request = allocateBoundedId(nextGlobalShortcutRequest, pendingGlobalShortcutRequests);
|
|
1035
|
+
nextGlobalShortcutRequest = request >= 0xffff_ffff ? 1 : request + 1;
|
|
1036
|
+
return new Promise<void>((resolve, reject) => {
|
|
1037
|
+
pendingGlobalShortcutRequests.set(request, { resolve, reject });
|
|
1038
|
+
try {
|
|
1039
|
+
if (current.hosted) {
|
|
1040
|
+
binding.performHostedGlobalShortcutAction(
|
|
1041
|
+
current.appId,
|
|
1042
|
+
request,
|
|
1043
|
+
action,
|
|
1044
|
+
registration,
|
|
1045
|
+
accelerator,
|
|
1046
|
+
);
|
|
1047
|
+
} else {
|
|
1048
|
+
binding.performGlobalShortcutAction(
|
|
1049
|
+
current.appId,
|
|
1050
|
+
request,
|
|
1051
|
+
action,
|
|
1052
|
+
registration,
|
|
1053
|
+
accelerator,
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
pendingGlobalShortcutRequests.delete(request);
|
|
1058
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/** System-wide keyboard shortcuts. Linux support requires an X11 session. */
|
|
1067
|
+
export const GlobalShortcut = Object.freeze({
|
|
1068
|
+
isSupported(): boolean {
|
|
1069
|
+
return process.platform === "darwin" || process.platform === "win32" || process.platform === "linux";
|
|
1070
|
+
},
|
|
1071
|
+
|
|
1072
|
+
async register(
|
|
1073
|
+
accelerator: string,
|
|
1074
|
+
listener: GlobalShortcutListener,
|
|
1075
|
+
): Promise<() => Promise<void>> {
|
|
1076
|
+
if (globalShortcutIds.has(accelerator)) {
|
|
1077
|
+
throw new Error(`the global shortcut \`${accelerator}\` is already registered`);
|
|
1078
|
+
}
|
|
1079
|
+
const registration = allocateBoundedId(
|
|
1080
|
+
nextGlobalShortcutRegistration,
|
|
1081
|
+
globalShortcutRegistrations,
|
|
1082
|
+
);
|
|
1083
|
+
nextGlobalShortcutRegistration = registration >= 0xffff_ffff ? 1 : registration + 1;
|
|
1084
|
+
globalShortcutIds.set(accelerator, registration);
|
|
1085
|
+
globalShortcutRegistrations.set(registration, { accelerator, listener });
|
|
1086
|
+
try {
|
|
1087
|
+
await globalShortcutOperation("register", registration, accelerator);
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
globalShortcutIds.delete(accelerator);
|
|
1090
|
+
globalShortcutRegistrations.delete(registration);
|
|
1091
|
+
throw error;
|
|
1092
|
+
}
|
|
1093
|
+
let disposed = false;
|
|
1094
|
+
return async () => {
|
|
1095
|
+
if (disposed) return;
|
|
1096
|
+
disposed = true;
|
|
1097
|
+
if (globalShortcutRegistrations.get(registration)?.accelerator !== accelerator) return;
|
|
1098
|
+
await globalShortcutOperation("unregister", registration);
|
|
1099
|
+
globalShortcutRegistrations.delete(registration);
|
|
1100
|
+
globalShortcutIds.delete(accelerator);
|
|
1101
|
+
};
|
|
1102
|
+
},
|
|
1103
|
+
|
|
1104
|
+
async unregister(accelerator: string): Promise<void> {
|
|
1105
|
+
const registration = globalShortcutIds.get(accelerator);
|
|
1106
|
+
if (registration === undefined) return;
|
|
1107
|
+
await globalShortcutOperation("unregister", registration);
|
|
1108
|
+
globalShortcutRegistrations.delete(registration);
|
|
1109
|
+
globalShortcutIds.delete(accelerator);
|
|
1110
|
+
},
|
|
1111
|
+
|
|
1112
|
+
async unregisterAll(): Promise<void> {
|
|
1113
|
+
await globalShortcutOperation("unregister-all");
|
|
1114
|
+
globalShortcutRegistrations.clear();
|
|
1115
|
+
globalShortcutIds.clear();
|
|
1116
|
+
},
|
|
1117
|
+
|
|
1118
|
+
isRegistered(accelerator: string): boolean {
|
|
1119
|
+
return globalShortcutIds.has(accelerator);
|
|
1120
|
+
},
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
export const Screen = Object.freeze({
|
|
1124
|
+
getAllDisplays(): Display[] {
|
|
1125
|
+
return nativeDisplays().map(normalizeDisplay);
|
|
1126
|
+
},
|
|
1127
|
+
|
|
1128
|
+
getPrimaryDisplay(): Display | undefined {
|
|
1129
|
+
return this.getAllDisplays().find((display) => display.primary);
|
|
1130
|
+
},
|
|
1131
|
+
|
|
1132
|
+
getDisplayNearestPoint(point: { x: number; y: number }): Display | undefined {
|
|
1133
|
+
let nearest: Display | undefined;
|
|
1134
|
+
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
1135
|
+
for (const display of this.getAllDisplays()) {
|
|
1136
|
+
const right = display.bounds.x + display.bounds.width;
|
|
1137
|
+
const bottom = display.bounds.y + display.bounds.height;
|
|
1138
|
+
const dx = point.x < display.bounds.x ? display.bounds.x - point.x : Math.max(0, point.x - right);
|
|
1139
|
+
const dy = point.y < display.bounds.y ? display.bounds.y - point.y : Math.max(0, point.y - bottom);
|
|
1140
|
+
const distance = dx * dx + dy * dy;
|
|
1141
|
+
if (distance < nearestDistance) {
|
|
1142
|
+
nearest = display;
|
|
1143
|
+
nearestDistance = distance;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return nearest;
|
|
1147
|
+
},
|
|
1148
|
+
|
|
1149
|
+
getCursorScreenPoint(): { x: number; y: number } {
|
|
1150
|
+
const current = context();
|
|
1151
|
+
const point = current.hosted
|
|
1152
|
+
? binding.getHostedCursorScreenPosition(current.appId)
|
|
1153
|
+
: binding.getCursorScreenPosition(current.appId);
|
|
1154
|
+
return { x: point.x, y: point.y };
|
|
1155
|
+
},
|
|
1156
|
+
|
|
1157
|
+
onChange(listener: (displays: readonly Display[]) => void): () => void {
|
|
1158
|
+
screenListeners.add(listener);
|
|
1159
|
+
return () => screenListeners.delete(listener);
|
|
1160
|
+
},
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
/** Compile-time native integration availability for the current target. */
|
|
1164
|
+
export const Desktop = Object.freeze({
|
|
1165
|
+
getSupport(): DesktopIntegrationSupport {
|
|
1166
|
+
const current = context();
|
|
1167
|
+
const support = current.hosted
|
|
1168
|
+
? binding.getHostedDesktopIntegrationSupport(current.appId)
|
|
1169
|
+
: binding.getDesktopIntegrationSupport(current.appId);
|
|
1170
|
+
return { ...support };
|
|
1171
|
+
},
|
|
1172
|
+
|
|
1173
|
+
setDockBadge(value?: string): void {
|
|
1174
|
+
const current = context();
|
|
1175
|
+
if (current.hosted) binding.setHostedDockBadge(current.appId, value);
|
|
1176
|
+
else binding.setDockBadge(current.appId, value);
|
|
1177
|
+
},
|
|
1178
|
+
|
|
1179
|
+
setDockIcon(icon?: ImageSource): void {
|
|
1180
|
+
const current = context();
|
|
1181
|
+
const native = icon === undefined ? undefined : nativeImageSource(icon);
|
|
1182
|
+
if (current.hosted) binding.setHostedDockIcon(current.appId, native);
|
|
1183
|
+
else binding.setDockIcon(current.appId, native);
|
|
1184
|
+
},
|
|
1185
|
+
|
|
1186
|
+
setDockMenu(menu?: MenuDefinition): void {
|
|
1187
|
+
const current = context();
|
|
1188
|
+
const serialized = menu === undefined ? undefined : serializeNativeMenu([menu]);
|
|
1189
|
+
if (current.hosted) binding.setHostedDockMenu(current.appId, serialized?.json);
|
|
1190
|
+
else binding.setDockMenu(current.appId, serialized?.json);
|
|
1191
|
+
disposeDockMenu?.();
|
|
1192
|
+
disposeDockMenu = serialized?.install();
|
|
1193
|
+
},
|
|
1194
|
+
|
|
1195
|
+
addRecentDocument(path: string): void {
|
|
1196
|
+
const current = context();
|
|
1197
|
+
const native = resolvePath(path);
|
|
1198
|
+
if (current.hosted) binding.addHostedRecentDocument(current.appId, native);
|
|
1199
|
+
else binding.addRecentDocument(current.appId, native);
|
|
1200
|
+
},
|
|
1201
|
+
|
|
1202
|
+
clearRecentDocuments(): void {
|
|
1203
|
+
const current = context();
|
|
1204
|
+
if (current.hosted) binding.clearHostedRecentDocuments(current.appId);
|
|
1205
|
+
else binding.clearRecentDocuments(current.appId);
|
|
1206
|
+
},
|
|
1207
|
+
|
|
1208
|
+
showAboutPanel(options: AboutPanelOptions = {}): void {
|
|
1209
|
+
const current = context();
|
|
1210
|
+
const native: binding.NativeAboutPanelOptions = {};
|
|
1211
|
+
if (options.applicationName !== undefined) native.applicationName = options.applicationName;
|
|
1212
|
+
if (options.applicationVersion !== undefined) {
|
|
1213
|
+
native.applicationVersion = options.applicationVersion;
|
|
1214
|
+
}
|
|
1215
|
+
if (options.version !== undefined) native.version = options.version;
|
|
1216
|
+
if (options.copyright !== undefined) native.copyright = options.copyright;
|
|
1217
|
+
if (options.credits !== undefined) native.credits = options.credits;
|
|
1218
|
+
if (options.icon !== undefined) native.icon = nativeImageSource(options.icon);
|
|
1219
|
+
if (current.hosted) binding.showHostedAboutPanel(current.appId, native);
|
|
1220
|
+
else binding.showAboutPanel(current.appId, native);
|
|
1221
|
+
},
|
|
1222
|
+
|
|
1223
|
+
getFileIcon(path: string, size: "small" | "normal" | "large" = "normal"): Promise<NativeImage> {
|
|
1224
|
+
try {
|
|
1225
|
+
const current = context();
|
|
1226
|
+
const request = allocateBoundedId(nextFileIconRequest, pendingFileIconRequests);
|
|
1227
|
+
nextFileIconRequest = request >= 0xffff_ffff ? 1 : request + 1;
|
|
1228
|
+
return new Promise((resolve, reject) => {
|
|
1229
|
+
pendingFileIconRequests.set(request, { resolve, reject });
|
|
1230
|
+
try {
|
|
1231
|
+
if (current.hosted) {
|
|
1232
|
+
binding.requestHostedFileIcon(current.appId, request, resolvePath(path), size);
|
|
1233
|
+
} else {
|
|
1234
|
+
binding.requestFileIcon(current.appId, request, resolvePath(path), size);
|
|
1235
|
+
}
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
pendingFileIconRequests.delete(request);
|
|
1238
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
1243
|
+
}
|
|
1244
|
+
},
|
|
1245
|
+
|
|
1246
|
+
setUserTasks(tasks: readonly UserTask[]): Promise<void> {
|
|
1247
|
+
try {
|
|
1248
|
+
const current = context();
|
|
1249
|
+
const request = allocateBoundedId(nextUserTaskRequest, pendingUserTaskRequests);
|
|
1250
|
+
nextUserTaskRequest = request >= 0xffff_ffff ? 1 : request + 1;
|
|
1251
|
+
const native = tasks.map((task): binding.NativeUserTask => {
|
|
1252
|
+
const value: binding.NativeUserTask = {
|
|
1253
|
+
title: task.title,
|
|
1254
|
+
arguments: task.arguments,
|
|
1255
|
+
};
|
|
1256
|
+
if (task.program !== undefined) value.program = resolvePath(task.program);
|
|
1257
|
+
if (task.description !== undefined) value.description = task.description;
|
|
1258
|
+
if (task.workingDirectory !== undefined) {
|
|
1259
|
+
value.workingDirectory = resolvePath(task.workingDirectory);
|
|
1260
|
+
}
|
|
1261
|
+
if (task.icon !== undefined) {
|
|
1262
|
+
value.iconPath = resolvePath(task.icon.path);
|
|
1263
|
+
value.iconIndex = task.icon.index;
|
|
1264
|
+
}
|
|
1265
|
+
return value;
|
|
1266
|
+
});
|
|
1267
|
+
return new Promise((resolve, reject) => {
|
|
1268
|
+
pendingUserTaskRequests.set(request, { resolve, reject });
|
|
1269
|
+
try {
|
|
1270
|
+
if (current.hosted) binding.setHostedUserTasks(current.appId, request, native);
|
|
1271
|
+
else binding.setUserTasks(current.appId, request, native);
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
pendingUserTaskRequests.delete(request);
|
|
1274
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
export const Appearance = Object.freeze({
|
|
1284
|
+
getCurrent(window?: Window): AppearanceMode {
|
|
1285
|
+
return getNativeWindowState(windowContext(window).window).appearance;
|
|
1286
|
+
},
|
|
1287
|
+
|
|
1288
|
+
onChange(listener: (appearance: AppearanceMode, window: Window) => void): () => void {
|
|
1289
|
+
appearanceListeners.add(listener);
|
|
1290
|
+
return () => appearanceListeners.delete(listener);
|
|
1291
|
+
},
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
export const Keyboard = Object.freeze({
|
|
1295
|
+
getLayout(): KeyboardLayout {
|
|
1296
|
+
return nativeKeyboardLayout();
|
|
1297
|
+
},
|
|
1298
|
+
|
|
1299
|
+
onLayoutChange(listener: (layout: KeyboardLayout) => void): () => void {
|
|
1300
|
+
keyboardListeners.add(listener);
|
|
1301
|
+
return () => keyboardListeners.delete(listener);
|
|
1302
|
+
},
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
/** Native operating-system sleep and user-session transitions. */
|
|
1306
|
+
export const PowerMonitor = Object.freeze({
|
|
1307
|
+
getState(): PowerState {
|
|
1308
|
+
const state = binding.getPowerState();
|
|
1309
|
+
const result: PowerState = {
|
|
1310
|
+
source: state.source as PowerSource,
|
|
1311
|
+
thermalState: state.thermalState as ThermalState,
|
|
1312
|
+
};
|
|
1313
|
+
if (state.battery !== undefined) {
|
|
1314
|
+
const battery: NonNullable<PowerState["battery"]> = {
|
|
1315
|
+
status: state.battery.status as BatteryStatus,
|
|
1316
|
+
};
|
|
1317
|
+
if (state.battery.chargePercent !== undefined) {
|
|
1318
|
+
battery.chargePercent = state.battery.chargePercent;
|
|
1319
|
+
}
|
|
1320
|
+
result.battery = battery;
|
|
1321
|
+
}
|
|
1322
|
+
if (state.lowPowerMode !== undefined) result.lowPowerMode = state.lowPowerMode;
|
|
1323
|
+
if (state.cpuSpeedLimitPercent !== undefined) {
|
|
1324
|
+
result.cpuSpeedLimitPercent = state.cpuSpeedLimitPercent;
|
|
1325
|
+
}
|
|
1326
|
+
return result;
|
|
1327
|
+
},
|
|
1328
|
+
|
|
1329
|
+
isOnBatteryPower(): boolean {
|
|
1330
|
+
return this.getState().source === "battery";
|
|
1331
|
+
},
|
|
1332
|
+
|
|
1333
|
+
getCurrentThermalState(): ThermalState {
|
|
1334
|
+
return this.getState().thermalState;
|
|
1335
|
+
},
|
|
1336
|
+
|
|
1337
|
+
getSystemIdleTime(): number {
|
|
1338
|
+
return binding.getSystemIdleTime();
|
|
1339
|
+
},
|
|
1340
|
+
|
|
1341
|
+
getSystemIdleState(thresholdSeconds: number): IdleState {
|
|
1342
|
+
return binding.getSystemIdleState(thresholdSeconds) as IdleState;
|
|
1343
|
+
},
|
|
1344
|
+
|
|
1345
|
+
getSessionState(): SessionState {
|
|
1346
|
+
return binding.getSessionState() as SessionState;
|
|
1347
|
+
},
|
|
1348
|
+
|
|
1349
|
+
on<T extends PowerEventType>(
|
|
1350
|
+
event: T,
|
|
1351
|
+
listener: (event: Extract<PowerEvent, { type: T }>) => void,
|
|
1352
|
+
): () => void {
|
|
1353
|
+
const listeners = powerListeners.get(event) ?? new Set();
|
|
1354
|
+
const wrapped = listener as (event: PowerEvent) => void;
|
|
1355
|
+
listeners.add(wrapped);
|
|
1356
|
+
powerListeners.set(event, listeners);
|
|
1357
|
+
return () => {
|
|
1358
|
+
listeners.delete(wrapped);
|
|
1359
|
+
if (listeners.size === 0) powerListeners.delete(event);
|
|
1360
|
+
};
|
|
1361
|
+
},
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
function nativeSystemPreferences(): binding.NativeSystemPreferences {
|
|
1365
|
+
const current = context();
|
|
1366
|
+
return current.hosted
|
|
1367
|
+
? binding.getHostedSystemPreferences(current.appId)
|
|
1368
|
+
: binding.getSystemPreferences(current.appId);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function normalizeSystemPreferences(
|
|
1372
|
+
preferences: binding.NativeSystemPreferences,
|
|
1373
|
+
): SystemPreferencesSnapshot {
|
|
1374
|
+
const result: SystemPreferencesSnapshot = {
|
|
1375
|
+
colorScheme: preferences.colorScheme as SystemPreferencesSnapshot["colorScheme"],
|
|
1376
|
+
colors: {},
|
|
1377
|
+
};
|
|
1378
|
+
const optionalBooleans = [
|
|
1379
|
+
"reduceMotion",
|
|
1380
|
+
"reduceTransparency",
|
|
1381
|
+
"increaseContrast",
|
|
1382
|
+
"differentiateWithoutColor",
|
|
1383
|
+
"invertColors",
|
|
1384
|
+
"forcedColors",
|
|
1385
|
+
"screenReader",
|
|
1386
|
+
"switchControl",
|
|
1387
|
+
] as const;
|
|
1388
|
+
for (const key of optionalBooleans) {
|
|
1389
|
+
const value = preferences[key];
|
|
1390
|
+
if (value !== undefined) result[key] = value;
|
|
1391
|
+
}
|
|
1392
|
+
const colors = [
|
|
1393
|
+
["accent", "accentColor"],
|
|
1394
|
+
["highlight", "highlightColor"],
|
|
1395
|
+
["highlightText", "highlightTextColor"],
|
|
1396
|
+
["windowBackground", "windowBackgroundColor"],
|
|
1397
|
+
["windowText", "windowTextColor"],
|
|
1398
|
+
["controlBackground", "controlBackgroundColor"],
|
|
1399
|
+
["controlText", "controlTextColor"],
|
|
1400
|
+
["link", "linkColor"],
|
|
1401
|
+
] as const;
|
|
1402
|
+
for (const [key, nativeKey] of colors) {
|
|
1403
|
+
const value = preferences[nativeKey];
|
|
1404
|
+
if (value !== undefined) result.colors[key] = { ...value };
|
|
1405
|
+
}
|
|
1406
|
+
return result;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/** Core-retained appearance and accessibility settings. */
|
|
1410
|
+
export const SystemPreferences = Object.freeze({
|
|
1411
|
+
getCurrent(): SystemPreferencesSnapshot {
|
|
1412
|
+
return normalizeSystemPreferences(nativeSystemPreferences());
|
|
1413
|
+
},
|
|
1414
|
+
|
|
1415
|
+
onChange(listener: (preferences: SystemPreferencesSnapshot) => void): () => void {
|
|
1416
|
+
systemPreferencesListeners.add(listener);
|
|
1417
|
+
return () => systemPreferencesListeners.delete(listener);
|
|
1418
|
+
},
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
/** Explicit native privacy authorization checks and prompts. */
|
|
1422
|
+
export const Permissions = Object.freeze({
|
|
1423
|
+
status(kind: PermissionKind): PermissionStatus {
|
|
1424
|
+
return binding.getPermissionStatus(kind) as PermissionStatus;
|
|
1425
|
+
},
|
|
1426
|
+
|
|
1427
|
+
async request(kind: PermissionKind): Promise<PermissionStatus> {
|
|
1428
|
+
return (await binding.requestPermission(kind)) as PermissionStatus;
|
|
1429
|
+
},
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
export function onNativeWindowStateChange(
|
|
1433
|
+
window: Window,
|
|
1434
|
+
listener: (state: WindowState) => void,
|
|
1435
|
+
): () => void {
|
|
1436
|
+
const listeners = windowStateListeners.get(window.nativeId) ?? new Set();
|
|
1437
|
+
listeners.add(listener);
|
|
1438
|
+
windowStateListeners.set(window.nativeId, listeners);
|
|
1439
|
+
return () => {
|
|
1440
|
+
listeners.delete(listener);
|
|
1441
|
+
if (listeners.size === 0) windowStateListeners.delete(window.nativeId);
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
export function removeNativeWindowStateListeners(window: Window): void {
|
|
1446
|
+
windowStateListeners.delete(window.nativeId);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
export function rejectPendingSystemRequests(error: Error): void {
|
|
1450
|
+
for (const [request, pending] of pendingShellRequests) {
|
|
1451
|
+
pendingShellRequests.delete(request);
|
|
1452
|
+
pending.reject(error);
|
|
1453
|
+
}
|
|
1454
|
+
for (const [request, pending] of pendingNotificationPermissionRequests) {
|
|
1455
|
+
pendingNotificationPermissionRequests.delete(request);
|
|
1456
|
+
pending.reject(error);
|
|
1457
|
+
}
|
|
1458
|
+
for (const [request, pending] of pendingFileIconRequests) {
|
|
1459
|
+
pendingFileIconRequests.delete(request);
|
|
1460
|
+
pending.reject(error);
|
|
1461
|
+
}
|
|
1462
|
+
for (const [request, pending] of pendingUserTaskRequests) {
|
|
1463
|
+
pendingUserTaskRequests.delete(request);
|
|
1464
|
+
pending.reject(error);
|
|
1465
|
+
}
|
|
1466
|
+
for (const [request, pending] of pendingGlobalShortcutRequests) {
|
|
1467
|
+
pendingGlobalShortcutRequests.delete(request);
|
|
1468
|
+
pending.reject(error);
|
|
1469
|
+
}
|
|
1470
|
+
globalShortcutRegistrations.clear();
|
|
1471
|
+
globalShortcutIds.clear();
|
|
1472
|
+
rejectPendingTrayRequests(error);
|
|
1473
|
+
screenListeners.clear();
|
|
1474
|
+
appearanceListeners.clear();
|
|
1475
|
+
keyboardListeners.clear();
|
|
1476
|
+
powerListeners.clear();
|
|
1477
|
+
notificationListeners.clear();
|
|
1478
|
+
systemPreferencesListeners.clear();
|
|
1479
|
+
menuCallbacks.clear();
|
|
1480
|
+
applicationMenuActionIds.clear();
|
|
1481
|
+
disposeDockMenu?.();
|
|
1482
|
+
disposeDockMenu = undefined;
|
|
1483
|
+
windowStateListeners.clear();
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
export function dispatchSystemEvent(
|
|
1487
|
+
event: binding.NativeEvent,
|
|
1488
|
+
findWindow: (id: number) => Window | undefined,
|
|
1489
|
+
): boolean {
|
|
1490
|
+
if (dispatchTrayEvent(event)) return true;
|
|
1491
|
+
if (event.kind === "shell") {
|
|
1492
|
+
const pending = pendingShellRequests.get(event.target);
|
|
1493
|
+
if (!pending) return true;
|
|
1494
|
+
pendingShellRequests.delete(event.target);
|
|
1495
|
+
if (event.error !== undefined) pending.reject(new Error(event.error));
|
|
1496
|
+
else pending.resolve();
|
|
1497
|
+
return true;
|
|
1498
|
+
}
|
|
1499
|
+
if (event.kind === "global-shortcut-operation") {
|
|
1500
|
+
const pending = pendingGlobalShortcutRequests.get(event.target);
|
|
1501
|
+
if (!pending) return true;
|
|
1502
|
+
pendingGlobalShortcutRequests.delete(event.target);
|
|
1503
|
+
if (event.error !== undefined) pending.reject(new Error(event.error));
|
|
1504
|
+
else pending.resolve();
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
if (event.kind === "global-shortcut") {
|
|
1508
|
+
globalShortcutRegistrations.get(event.target)?.listener();
|
|
1509
|
+
return true;
|
|
1510
|
+
}
|
|
1511
|
+
if (event.kind === "power-event") {
|
|
1512
|
+
const value = parsePowerEvent(event.value);
|
|
1513
|
+
if (!value) return true;
|
|
1514
|
+
for (const listener of powerListeners.get(value.type) ?? []) listener(value);
|
|
1515
|
+
return true;
|
|
1516
|
+
}
|
|
1517
|
+
if (event.kind === "system-preferences-change") {
|
|
1518
|
+
const preferences = SystemPreferences.getCurrent();
|
|
1519
|
+
for (const listener of systemPreferencesListeners) listener(preferences);
|
|
1520
|
+
return true;
|
|
1521
|
+
}
|
|
1522
|
+
if (event.kind === "notification-response") {
|
|
1523
|
+
try {
|
|
1524
|
+
const value = JSON.parse(event.value ?? "{}") as {
|
|
1525
|
+
tag?: unknown;
|
|
1526
|
+
actionId?: unknown;
|
|
1527
|
+
reply?: unknown;
|
|
1528
|
+
};
|
|
1529
|
+
if (typeof value.tag !== "string") return true;
|
|
1530
|
+
const response: NotificationResponse = { tag: value.tag };
|
|
1531
|
+
if (typeof value.actionId === "string") response.actionId = value.actionId;
|
|
1532
|
+
if (typeof value.reply === "string") response.reply = value.reply;
|
|
1533
|
+
for (const listener of notificationListeners) listener(response);
|
|
1534
|
+
} catch {
|
|
1535
|
+
// The app-level dispatcher follows the same fail-closed parsing contract.
|
|
1536
|
+
}
|
|
1537
|
+
return notificationListeners.size > 0;
|
|
1538
|
+
}
|
|
1539
|
+
if (event.kind === "notification-permission") {
|
|
1540
|
+
const pending = pendingNotificationPermissionRequests.get(event.target);
|
|
1541
|
+
if (!pending) return true;
|
|
1542
|
+
pendingNotificationPermissionRequests.delete(event.target);
|
|
1543
|
+
if (event.error !== undefined) pending.reject(new Error(event.error));
|
|
1544
|
+
else if (
|
|
1545
|
+
event.value === "not-determined" ||
|
|
1546
|
+
event.value === "granted" ||
|
|
1547
|
+
event.value === "denied" ||
|
|
1548
|
+
event.value === "unsupported"
|
|
1549
|
+
) {
|
|
1550
|
+
pending.resolve(event.value);
|
|
1551
|
+
} else {
|
|
1552
|
+
pending.reject(new Error("the native notification permission response was invalid"));
|
|
1553
|
+
}
|
|
1554
|
+
return true;
|
|
1555
|
+
}
|
|
1556
|
+
if (event.kind === "file-icon") {
|
|
1557
|
+
const pending = pendingFileIconRequests.get(event.target);
|
|
1558
|
+
if (!pending) return true;
|
|
1559
|
+
pendingFileIconRequests.delete(event.target);
|
|
1560
|
+
if (event.error !== undefined) pending.reject(new Error(event.error));
|
|
1561
|
+
else if (event.data !== undefined && event.width !== undefined && event.height !== undefined) {
|
|
1562
|
+
pending.resolve({
|
|
1563
|
+
data: Uint8Array.from(event.data),
|
|
1564
|
+
width: event.width,
|
|
1565
|
+
height: event.height,
|
|
1566
|
+
});
|
|
1567
|
+
} else {
|
|
1568
|
+
pending.reject(new Error("the native file-icon response was invalid"));
|
|
1569
|
+
}
|
|
1570
|
+
return true;
|
|
1571
|
+
}
|
|
1572
|
+
if (event.kind === "user-tasks") {
|
|
1573
|
+
const pending = pendingUserTaskRequests.get(event.target);
|
|
1574
|
+
if (!pending) return true;
|
|
1575
|
+
pendingUserTaskRequests.delete(event.target);
|
|
1576
|
+
if (event.error !== undefined) pending.reject(new Error(event.error));
|
|
1577
|
+
else pending.resolve();
|
|
1578
|
+
return true;
|
|
1579
|
+
}
|
|
1580
|
+
if (event.kind === "menu-action") {
|
|
1581
|
+
menuCallbacks.get(event.target)?.();
|
|
1582
|
+
return true;
|
|
1583
|
+
}
|
|
1584
|
+
if (event.kind === "screen-change") {
|
|
1585
|
+
const displays = Screen.getAllDisplays();
|
|
1586
|
+
for (const listener of screenListeners) listener(displays);
|
|
1587
|
+
return true;
|
|
1588
|
+
}
|
|
1589
|
+
if (event.kind === "keyboard-layout-change") {
|
|
1590
|
+
const layout = Keyboard.getLayout();
|
|
1591
|
+
for (const listener of keyboardListeners) listener(layout);
|
|
1592
|
+
return true;
|
|
1593
|
+
}
|
|
1594
|
+
if (event.kind === "appearance-change") {
|
|
1595
|
+
const window = findWindow(event.window);
|
|
1596
|
+
if (!window) return true;
|
|
1597
|
+
const appearance = event.value === "dark" ? "dark" : "light";
|
|
1598
|
+
for (const listener of appearanceListeners) listener(appearance, window);
|
|
1599
|
+
return true;
|
|
1600
|
+
}
|
|
1601
|
+
if (event.kind === "window-state-change") {
|
|
1602
|
+
const window = findWindow(event.window);
|
|
1603
|
+
if (!window) return true;
|
|
1604
|
+
const listeners = windowStateListeners.get(event.window);
|
|
1605
|
+
if (!listeners?.size) return true;
|
|
1606
|
+
const state = getNativeWindowState(window);
|
|
1607
|
+
for (const listener of listeners) listener(state);
|
|
1608
|
+
return true;
|
|
1609
|
+
}
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function parsePowerEvent(value: string | undefined): PowerEvent | undefined {
|
|
1614
|
+
try {
|
|
1615
|
+
const event = JSON.parse(value ?? "{}") as Record<string, unknown>;
|
|
1616
|
+
switch (event.type) {
|
|
1617
|
+
case "suspend":
|
|
1618
|
+
case "resume":
|
|
1619
|
+
case "lock-screen":
|
|
1620
|
+
case "unlock-screen":
|
|
1621
|
+
case "shutdown-requested":
|
|
1622
|
+
return { type: event.type };
|
|
1623
|
+
case "power-source-changed":
|
|
1624
|
+
if (event.source === "ac" || event.source === "battery" || event.source === "unknown") {
|
|
1625
|
+
return { type: event.type, source: event.source };
|
|
1626
|
+
}
|
|
1627
|
+
return undefined;
|
|
1628
|
+
case "thermal-state-changed":
|
|
1629
|
+
if (
|
|
1630
|
+
event.state === "unknown" ||
|
|
1631
|
+
event.state === "nominal" ||
|
|
1632
|
+
event.state === "fair" ||
|
|
1633
|
+
event.state === "serious" ||
|
|
1634
|
+
event.state === "critical"
|
|
1635
|
+
) {
|
|
1636
|
+
return { type: event.type, state: event.state };
|
|
1637
|
+
}
|
|
1638
|
+
return undefined;
|
|
1639
|
+
case "low-power-mode-changed":
|
|
1640
|
+
return typeof event.enabled === "boolean"
|
|
1641
|
+
? { type: event.type, enabled: event.enabled }
|
|
1642
|
+
: undefined;
|
|
1643
|
+
case "cpu-speed-limit-changed":
|
|
1644
|
+
return typeof event.percent === "number" && Number.isFinite(event.percent)
|
|
1645
|
+
? { type: event.type, percent: event.percent }
|
|
1646
|
+
: undefined;
|
|
1647
|
+
default:
|
|
1648
|
+
return undefined;
|
|
1649
|
+
}
|
|
1650
|
+
} catch {
|
|
1651
|
+
return undefined;
|
|
1652
|
+
}
|
|
1653
|
+
}
|