@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/index.ts ADDED
@@ -0,0 +1,1603 @@
1
+ import * as binding from "./binding.js";
2
+ import { Buffer } from "node:buffer";
3
+ import {
4
+ type AlertDialogOptions,
5
+ type OpenDialogOptions,
6
+ type OpenDialogResult,
7
+ type SaveDialogOptions,
8
+ type SaveDialogResult,
9
+ normalizeAlertDialogOptions,
10
+ normalizeOpenDialogOptions,
11
+ normalizeSaveDialogOptions,
12
+ } from "./dialog.ts";
13
+ import {
14
+ MutationBatch,
15
+ NativeNodeTag,
16
+ PropertyCode,
17
+ PROTOCOL_VERSION,
18
+ ROOT_NODE_ID,
19
+ type NativePropertyValue,
20
+ } from "./protocol.ts";
21
+ import {
22
+ NativeNode,
23
+ QuickGuiEvent,
24
+ cleanupNativeNodes,
25
+ createNativeElement,
26
+ createNativeSentinel,
27
+ createNativeText,
28
+ getNativeFirstChild,
29
+ getNativeNextSibling,
30
+ getNativeParent,
31
+ insertNativeNode,
32
+ isNativeText,
33
+ parseColor,
34
+ removeNativeNode,
35
+ replaceNativeText,
36
+ setNativeEventListener,
37
+ setNativeProperty,
38
+ type ColorValue,
39
+ type NativeElementName,
40
+ type NativeEventListener,
41
+ type NativeEventType,
42
+ } from "./native-tree.ts";
43
+ import {
44
+ configureSystemContext,
45
+ dispatchSystemEvent,
46
+ getNativeWindowState,
47
+ nativeImageSource,
48
+ onNativeWindowStateChange,
49
+ performNativeWindowAction,
50
+ performNativeWindowImageAction,
51
+ rejectPendingSystemRequests,
52
+ removeNativeWindowStateListeners,
53
+ serializeNativeMenu,
54
+ } from "./system.ts";
55
+ import {
56
+ type SecondInstanceEvent,
57
+ urlsFromArguments,
58
+ } from "./single-instance.ts";
59
+
60
+ export { NativeNodeTag, PropertyCode } from "./protocol.ts";
61
+ export {
62
+ NativeNode,
63
+ QuickGuiEvent,
64
+ cleanupNativeNodes,
65
+ createNativeElement,
66
+ createNativeSentinel,
67
+ createNativeText,
68
+ getNativeFirstChild,
69
+ getNativeNextSibling,
70
+ getNativeParent,
71
+ insertNativeNode,
72
+ isNativeText,
73
+ parseColor,
74
+ removeNativeNode,
75
+ replaceNativeText,
76
+ setNativeEventListener,
77
+ setNativeProperty,
78
+ } from "./native-tree.ts";
79
+ export type {
80
+ ColorValue,
81
+ NativeElementName,
82
+ NativeEventListener,
83
+ NativeEventType,
84
+ } from "./native-tree.ts";
85
+ export type {
86
+ AlertDialogButton,
87
+ AlertDialogButtonRole,
88
+ AlertDialogLevel,
89
+ AlertDialogOptions,
90
+ FileDialogFilter,
91
+ OpenDialogOptions,
92
+ OpenDialogProperty,
93
+ OpenDialogResult,
94
+ SaveDialogOptions,
95
+ SaveDialogResult,
96
+ } from "./dialog.ts";
97
+ export {
98
+ Appearance,
99
+ Clipboard,
100
+ Desktop,
101
+ GlobalShortcut,
102
+ Keyboard,
103
+ Menu,
104
+ Notifications,
105
+ Permissions,
106
+ PowerMonitor,
107
+ PowerAssertion,
108
+ Screen,
109
+ SystemPreferences,
110
+ Shell,
111
+ Tray,
112
+ TrayIcon,
113
+ } from "./system.ts";
114
+ export { AutoStart, SecureStorage, Updater } from "./integrations.ts";
115
+ export { DeepLink } from "./single-instance.ts";
116
+ export type { SecondInstanceEvent } from "./single-instance.ts";
117
+ export type {
118
+ AutoStartMode,
119
+ AutoStartOptions,
120
+ AvailableUpdate,
121
+ InstalledUpdate,
122
+ ProtocolRegistrationOptions,
123
+ UpdateClientOptions,
124
+ UpdateInstallOptions,
125
+ } from "./integrations.ts";
126
+ export type {
127
+ AppearanceMode,
128
+ AppearancePreference,
129
+ ClipboardEntry,
130
+ ClipboardBookmarkEntry,
131
+ ClipboardDataEntry,
132
+ ClipboardFilesEntry,
133
+ ClipboardImageEntry,
134
+ ClipboardItem,
135
+ ClipboardTextEntry,
136
+ Display,
137
+ DesktopIntegrationSupport,
138
+ AboutPanelOptions,
139
+ UserTask,
140
+ NativeImage,
141
+ GlobalShortcutListener,
142
+ KeyboardLayout,
143
+ MenuActionItem,
144
+ MenuDefinition,
145
+ MenuItem,
146
+ MenuRole,
147
+ MenuItemMark,
148
+ MenuRoleItem,
149
+ MenuSystemItem,
150
+ MenuSeparatorItem,
151
+ MenuServicesItem,
152
+ MenuSubmenuItem,
153
+ NotificationAction,
154
+ NotificationOptions,
155
+ NotificationAttachment,
156
+ NotificationPermissionStatus,
157
+ NotificationResponse,
158
+ PermissionKind,
159
+ PermissionStatus,
160
+ PowerEvent,
161
+ PowerEventType,
162
+ PowerAssertionKind,
163
+ PowerSource,
164
+ PowerState,
165
+ BatteryStatus,
166
+ ThermalState,
167
+ SessionState,
168
+ IdleState,
169
+ SystemColor,
170
+ SystemPreferencesSnapshot,
171
+ Rectangle,
172
+ TrayEvent,
173
+ TrayEventType,
174
+ TrayIconOptions,
175
+ TrayIconSource,
176
+ TrayMenuActionItem,
177
+ TrayMenuItem,
178
+ TrayMenuSeparatorItem,
179
+ TrayMenuSubmenuItem,
180
+ WindowState,
181
+ WindowBackgroundAppearance,
182
+ WindowKind,
183
+ WindowLevel,
184
+ CursorGrabMode,
185
+ TaskbarProgressState,
186
+ ImageSource,
187
+ } from "./system.ts";
188
+ import type {
189
+ AppearancePreference,
190
+ CursorGrabMode,
191
+ ImageSource,
192
+ KeyboardLayout,
193
+ MenuDefinition,
194
+ NotificationResponse,
195
+ TaskbarProgressState,
196
+ WindowBackgroundAppearance,
197
+ WindowKind,
198
+ WindowLevel,
199
+ WindowState,
200
+ } from "./system.ts";
201
+
202
+ export type WindowCloseListener = (window: Window) => void;
203
+ export type WindowRenderer = (window: Window) => () => void;
204
+ export type PopoverPlacement =
205
+ | "top-start"
206
+ | "top"
207
+ | "top-end"
208
+ | "bottom-start"
209
+ | "bottom"
210
+ | "bottom-end"
211
+ | "left-start"
212
+ | "left"
213
+ | "left-end"
214
+ | "right-start"
215
+ | "right"
216
+ | "right-end";
217
+
218
+ export type PerformanceProfile = "low-power" | "balanced" | "high-performance";
219
+ export type InitialWindowState = "normal" | "maximized" | "fullscreen";
220
+ export interface Size {
221
+ width: number;
222
+ height: number;
223
+ }
224
+ export interface Point {
225
+ x: number;
226
+ y: number;
227
+ }
228
+ export interface WindowBounds extends Point, Size {
229
+ state?: InitialWindowState;
230
+ }
231
+
232
+ export interface WindowOptions {
233
+ renderer: WindowRenderer;
234
+ title?: string;
235
+ width?: number;
236
+ height?: number;
237
+ position?: Point;
238
+ initialState?: InitialWindowState;
239
+ displayId?: string;
240
+ /** Pass `null` to remove the core's default minimum window size. */
241
+ minimumSize?: Size | null;
242
+ minimumWidth?: number;
243
+ minimumHeight?: number;
244
+ maximumSize?: Size;
245
+ maximumWidth?: number;
246
+ maximumHeight?: number;
247
+ representedFile?: string;
248
+ documentEdited?: boolean;
249
+ tabbingIdentifier?: string;
250
+ background?: ColorValue;
251
+ backgroundAppearance?: WindowBackgroundAppearance;
252
+ performanceProfile?: PerformanceProfile;
253
+ appearance?: AppearancePreference;
254
+ titleBarStyle?: "default" | "hidden" | "hiddenInset";
255
+ kind?: WindowKind;
256
+ focus?: boolean;
257
+ focusable?: boolean;
258
+ visible?: boolean;
259
+ movable?: boolean;
260
+ resizable?: boolean;
261
+ minimizable?: boolean;
262
+ maximizable?: boolean;
263
+ closable?: boolean;
264
+ decorated?: boolean;
265
+ shadow?: boolean;
266
+ contentProtected?: boolean;
267
+ windowLevel?: WindowLevel | "automatic";
268
+ skipTaskbar?: boolean;
269
+ visibleOnAllWorkspaces?: boolean;
270
+ opacity?: number;
271
+ icon?: ImageSource;
272
+ taskbarProgress?: { state: TaskbarProgressState; progress: number };
273
+ taskbarOverlay?: { icon: ImageSource; description: string };
274
+ cursorVisible?: boolean;
275
+ cursorGrab?: CursorGrabMode;
276
+ cursorHitTest?: boolean;
277
+ cursorPosition?: Point;
278
+ menu?: readonly MenuDefinition[];
279
+ lineScrollPixels?: number;
280
+ keySequenceTimeoutMs?: number;
281
+ reduceMotion?: boolean;
282
+ trafficLightPosition?: { x: number; y: number };
283
+ transparent?: boolean;
284
+ blur?: boolean;
285
+ /** Open this window as a system popover anchored to the mounted node. */
286
+ anchor?: NativeNode;
287
+ placement?: PopoverPlacement;
288
+ gap?: number;
289
+ offset?: { x: number; y: number };
290
+ viewportMargin?: number;
291
+ dismissOnEscape?: boolean;
292
+ dismissOnPointerOutside?: boolean;
293
+ grab?: boolean;
294
+ acceptsKeyFocus?: boolean;
295
+ }
296
+
297
+ export interface RunOptions {
298
+ /** Pump interval used only when running a source file outside the QuickGUI CLI host. */
299
+ sliceMs?: number;
300
+ }
301
+
302
+ export type QuitMode = "default" | "last-window-closed" | "explicit";
303
+
304
+ export interface AppPathOverrides {
305
+ resourceDir?: string;
306
+ configDir?: string;
307
+ dataDir?: string;
308
+ localDataDir?: string;
309
+ cacheDir?: string;
310
+ logDir?: string;
311
+ runtimeDir?: string;
312
+ tempDir?: string;
313
+ }
314
+
315
+ export interface AppOptions {
316
+ name?: string;
317
+ version?: string;
318
+ identifier?: string;
319
+ paths?: AppPathOverrides;
320
+ quitMode?: QuitMode;
321
+ /** OpenType font files registered by the Rust core before the first window is created. */
322
+ fonts?: readonly Uint8Array[];
323
+ }
324
+
325
+ export interface RelaunchOptions {
326
+ executable?: string;
327
+ /** Omit to preserve current arguments; pass `null` to relaunch without arguments. */
328
+ arguments?: readonly string[] | null;
329
+ workingDirectory?: string;
330
+ }
331
+
332
+ export interface AppInfo {
333
+ name: string;
334
+ version: string;
335
+ identifier: string;
336
+ }
337
+
338
+ export interface AppPaths {
339
+ executable: string;
340
+ executableDir: string;
341
+ resourceDir: string;
342
+ homeDir?: string;
343
+ configDir?: string;
344
+ dataDir?: string;
345
+ localDataDir?: string;
346
+ cacheDir?: string;
347
+ logDir?: string;
348
+ runtimeDir?: string;
349
+ tempDir: string;
350
+ audioDir?: string;
351
+ desktopDir?: string;
352
+ documentDir?: string;
353
+ downloadDir?: string;
354
+ pictureDir?: string;
355
+ videoDir?: string;
356
+ }
357
+
358
+ export interface SystemInfo {
359
+ operatingSystem:
360
+ | "macos"
361
+ | "windows"
362
+ | "linux"
363
+ | "freebsd"
364
+ | "dragonfly"
365
+ | "netbsd"
366
+ | "openbsd"
367
+ | "android"
368
+ | "ios"
369
+ | "wasm"
370
+ | "other";
371
+ family: "unix" | "windows" | "wasm" | "other";
372
+ name: string;
373
+ version?: string;
374
+ edition?: string;
375
+ codename?: string;
376
+ architecture: string;
377
+ bitness: "32" | "64" | "unknown";
378
+ hostname?: string;
379
+ locale?: string;
380
+ preferredLanguages: readonly string[];
381
+ languagesTruncated: boolean;
382
+ }
383
+
384
+ export interface AppEventMap {
385
+ ready: undefined;
386
+ quit: { exitCode: number };
387
+ openUrls: readonly string[];
388
+ reopen: { hasVisibleWindows: boolean };
389
+ systemWake: undefined;
390
+ keyboardLayoutChange: KeyboardLayout;
391
+ notificationResponse: NotificationResponse;
392
+ secondInstance: SecondInstanceEvent;
393
+ }
394
+
395
+ type DialogEventKind = "alert-dialog" | "open-dialog" | "save-dialog";
396
+
397
+ type PendingDialog = {
398
+ kind: DialogEventKind;
399
+ window: Window | undefined;
400
+ complete: (event: binding.NativeEvent) => void;
401
+ reject: (reason: Error) => void;
402
+ };
403
+
404
+ let activeApp: App | undefined;
405
+ let currentWindow: Window | undefined;
406
+ const hostedRuntime = process.env.QUICKGUI_APP_WORKER === "1";
407
+
408
+ function nativeAppOptions(options: AppOptions): binding.NativeAppOptions {
409
+ const native: binding.NativeAppOptions = {};
410
+ if (options.name !== undefined) native.name = options.name;
411
+ if (options.version !== undefined) native.version = options.version;
412
+ if (options.identifier !== undefined) native.identifier = options.identifier;
413
+ if (options.quitMode !== undefined) native.quitMode = options.quitMode;
414
+ if (options.fonts !== undefined) {
415
+ native.fontData = options.fonts.map((font) => Buffer.from(font));
416
+ }
417
+ const paths = options.paths;
418
+ if (paths?.resourceDir !== undefined) native.resourceDir = paths.resourceDir;
419
+ if (paths?.configDir !== undefined) native.configDir = paths.configDir;
420
+ if (paths?.dataDir !== undefined) native.dataDir = paths.dataDir;
421
+ if (paths?.localDataDir !== undefined) native.localDataDir = paths.localDataDir;
422
+ if (paths?.cacheDir !== undefined) native.cacheDir = paths.cacheDir;
423
+ if (paths?.logDir !== undefined) native.logDir = paths.logDir;
424
+ if (paths?.runtimeDir !== undefined) native.runtimeDir = paths.runtimeDir;
425
+ if (paths?.tempDir !== undefined) native.tempDir = paths.tempDir;
426
+ return native;
427
+ }
428
+
429
+ const embeddedAppOptions = (
430
+ globalThis as typeof globalThis & { __QUICKGUI_APP_OPTIONS__?: AppOptions }
431
+ ).__QUICKGUI_APP_OPTIONS__;
432
+
433
+ function withCurrentWindow<T>(window: Window, callback: () => T): T {
434
+ const previous = currentWindow;
435
+ currentWindow = window;
436
+ try {
437
+ return callback();
438
+ } finally {
439
+ currentWindow = previous;
440
+ }
441
+ }
442
+
443
+ configureSystemContext(
444
+ () => {
445
+ const app = activeApp;
446
+ if (!app) throw new Error("create a QuickGUI App before using a native system API");
447
+ app._assertReady();
448
+ return { appId: app.nativeId, hosted: hostedRuntime };
449
+ },
450
+ (window) => {
451
+ const app = window?.app ?? activeApp;
452
+ if (!app) throw new Error("create a QuickGUI App before using a native window API");
453
+ const resolved = window ?? app.windows.values().next().value;
454
+ if (!resolved || resolved.closed || app.windows.get(resolved.nativeId) !== resolved) {
455
+ throw new Error("a native window API requires an open QuickGUI Window");
456
+ }
457
+ app._assertReady();
458
+ return {
459
+ context: { appId: app.nativeId, hosted: hostedRuntime },
460
+ window: resolved,
461
+ };
462
+ },
463
+ );
464
+
465
+ const nativeProtocolVersion = binding.protocolVersion();
466
+ if (nativeProtocolVersion !== PROTOCOL_VERSION) {
467
+ throw new Error(
468
+ `QuickGUI native protocol mismatch: JavaScript uses ${PROTOCOL_VERSION}, binding uses ${nativeProtocolVersion}. Reinstall or rebuild @quickgui/native.`,
469
+ );
470
+ }
471
+
472
+ class App {
473
+ readonly nativeId: number;
474
+ readonly windows = new Map<number, Window>();
475
+ #running = false;
476
+ #destroyed = false;
477
+ readonly #readyPromise: Promise<void>;
478
+ #nextDialogRequest = 1;
479
+ readonly #pendingDialogs = new Map<number, PendingDialog>();
480
+ #singleInstanceIdentifier: string | undefined;
481
+ readonly #appEventListeners = new Map<
482
+ keyof AppEventMap,
483
+ Set<(payload: unknown) => unknown>
484
+ >();
485
+
486
+ constructor() {
487
+ if (activeApp) {
488
+ throw new Error("a QuickGUI App is already active in this JavaScript isolate");
489
+ }
490
+ const initialOptions = embeddedAppOptions
491
+ ? nativeAppOptions(embeddedAppOptions)
492
+ : undefined;
493
+ this.nativeId = hostedRuntime
494
+ ? binding.createHostedApp(initialOptions)
495
+ : binding.createApp(initialOptions);
496
+ activeApp = this;
497
+ this.#readyPromise = Promise.resolve().then(() => {
498
+ this.#assertAlive();
499
+ if (hostedRuntime) binding.prepareHostedApp(this.nativeId);
500
+ else binding.prepareApp(this.nativeId);
501
+ if (!this.isReady()) {
502
+ throw new Error("the native QuickGUI application did not become ready");
503
+ }
504
+ this.#emitAppEvent("ready", undefined);
505
+ });
506
+ }
507
+
508
+ isReady(): boolean {
509
+ this.#assertAlive();
510
+ return hostedRuntime
511
+ ? binding.isHostedAppReady(this.nativeId)
512
+ : binding.isAppReady(this.nativeId);
513
+ }
514
+
515
+ whenReady(): Promise<void> {
516
+ return this.#readyPromise;
517
+ }
518
+
519
+ /** Patch core-owned identity, paths, and quit policy before the first readiness turn. */
520
+ configure(options: AppOptions): void {
521
+ this.#assertAlive();
522
+ const native = nativeAppOptions(options);
523
+ if (hostedRuntime) binding.configureHostedApp(this.nativeId, native);
524
+ else binding.configureApp(this.nativeId, native);
525
+ }
526
+
527
+ getInfo(): AppInfo | undefined {
528
+ this._assertReady();
529
+ const info = hostedRuntime
530
+ ? binding.getHostedAppInfo(this.nativeId)
531
+ : binding.getAppInfo(this.nativeId);
532
+ return info ? { ...info } : undefined;
533
+ }
534
+
535
+ getPaths(): AppPaths | undefined {
536
+ this._assertReady();
537
+ const paths = hostedRuntime
538
+ ? binding.getHostedAppPaths(this.nativeId)
539
+ : binding.getAppPaths(this.nativeId);
540
+ if (!paths) return undefined;
541
+ const result: AppPaths = {
542
+ executable: paths.executable,
543
+ executableDir: paths.executableDir,
544
+ resourceDir: paths.resourceDir,
545
+ tempDir: paths.tempDir,
546
+ };
547
+ if (paths.homeDir !== undefined) result.homeDir = paths.homeDir;
548
+ if (paths.configDir !== undefined) result.configDir = paths.configDir;
549
+ if (paths.dataDir !== undefined) result.dataDir = paths.dataDir;
550
+ if (paths.localDataDir !== undefined) result.localDataDir = paths.localDataDir;
551
+ if (paths.cacheDir !== undefined) result.cacheDir = paths.cacheDir;
552
+ if (paths.logDir !== undefined) result.logDir = paths.logDir;
553
+ if (paths.runtimeDir !== undefined) result.runtimeDir = paths.runtimeDir;
554
+ if (paths.audioDir !== undefined) result.audioDir = paths.audioDir;
555
+ if (paths.desktopDir !== undefined) result.desktopDir = paths.desktopDir;
556
+ if (paths.documentDir !== undefined) result.documentDir = paths.documentDir;
557
+ if (paths.downloadDir !== undefined) result.downloadDir = paths.downloadDir;
558
+ if (paths.pictureDir !== undefined) result.pictureDir = paths.pictureDir;
559
+ if (paths.videoDir !== undefined) result.videoDir = paths.videoDir;
560
+ return result;
561
+ }
562
+
563
+ getSystemInfo(): SystemInfo {
564
+ this._assertReady();
565
+ const info = hostedRuntime
566
+ ? binding.getHostedSystemInfo(this.nativeId)
567
+ : binding.getSystemInfo(this.nativeId);
568
+ const result: SystemInfo = {
569
+ operatingSystem: info.operatingSystem as SystemInfo["operatingSystem"],
570
+ family: info.family as SystemInfo["family"],
571
+ name: info.name,
572
+ architecture: info.architecture,
573
+ bitness: info.bitness as SystemInfo["bitness"],
574
+ preferredLanguages: [...info.preferredLanguages],
575
+ languagesTruncated: info.languagesTruncated,
576
+ };
577
+ if (info.version !== undefined) result.version = info.version;
578
+ if (info.edition !== undefined) result.edition = info.edition;
579
+ if (info.codename !== undefined) result.codename = info.codename;
580
+ if (info.hostname !== undefined) result.hostname = info.hostname;
581
+ if (info.locale !== undefined) result.locale = info.locale;
582
+ return result;
583
+ }
584
+
585
+ getWindows(): readonly Window[] {
586
+ this._assertReady();
587
+ const registry = hostedRuntime
588
+ ? binding.getHostedWindowRegistry(this.nativeId)
589
+ : binding.getWindowRegistry(this.nativeId);
590
+ return registry.windows.flatMap((id) => {
591
+ const window = this.windows.get(id);
592
+ return window ? [window] : [];
593
+ });
594
+ }
595
+
596
+ getActiveWindow(): Window | undefined {
597
+ this._assertReady();
598
+ const registry = hostedRuntime
599
+ ? binding.getHostedWindowRegistry(this.nativeId)
600
+ : binding.getWindowRegistry(this.nativeId);
601
+ return registry.activeWindow === undefined
602
+ ? undefined
603
+ : this.windows.get(registry.activeWindow);
604
+ }
605
+
606
+ flush(): void {
607
+ this.#assertAlive();
608
+ for (const window of this.windows.values()) window.flush();
609
+ }
610
+
611
+ pump(sliceMs = 16): number {
612
+ this._assertReady();
613
+ this.flush();
614
+ return this.#finishPump(binding.pumpApp(this.nativeId, sliceMs));
615
+ }
616
+
617
+ #finishPump(exitCode: number): number {
618
+ this.dispatchEvents();
619
+ this.flush();
620
+ return exitCode;
621
+ }
622
+
623
+ dispatchEvents(): void {
624
+ this.#dispatchNativeEvents(binding.takeEvents(this.nativeId));
625
+ }
626
+
627
+ #dispatchNativeEvents(events: binding.NativeEvent[]): void {
628
+ for (const event of events) {
629
+ if (
630
+ event.kind === "alert-dialog" ||
631
+ event.kind === "open-dialog" ||
632
+ event.kind === "save-dialog"
633
+ ) {
634
+ this.#dispatchDialog(event);
635
+ continue;
636
+ }
637
+ const systemEvent = dispatchSystemEvent(event, (id) => this.windows.get(id));
638
+ const appEvent = this.#dispatchAppEvent(event);
639
+ if (systemEvent || appEvent) continue;
640
+ const window = this.windows.get(event.window);
641
+ if (!window) continue;
642
+ if (event.kind === "close") {
643
+ this._didCloseWindow(window);
644
+ } else {
645
+ window._dispatchEvent(event.kind as NativeEventType, event.target, event.value);
646
+ }
647
+ }
648
+ }
649
+
650
+ on<K extends keyof AppEventMap>(
651
+ type: K,
652
+ listener: (payload: AppEventMap[K]) => void,
653
+ ): () => void {
654
+ this.#assertAlive();
655
+ const listeners = this.#appEventListeners.get(type) ?? new Set();
656
+ const wrapped = (payload: unknown) => listener(payload as AppEventMap[K]);
657
+ listeners.add(wrapped);
658
+ this.#appEventListeners.set(type, listeners);
659
+ return () => {
660
+ listeners.delete(wrapped);
661
+ if (listeners.size === 0) this.#appEventListeners.delete(type);
662
+ };
663
+ }
664
+
665
+ async run(options: RunOptions = {}): Promise<number> {
666
+ if (this.#running) throw new Error("this QuickGUI app is already running");
667
+ this.#running = true;
668
+ let exitCode: number | undefined;
669
+ try {
670
+ await this.whenReady();
671
+ if (hostedRuntime) {
672
+ for (;;) {
673
+ const update = await binding.waitForHostedEvents(this.nativeId);
674
+ this.#dispatchNativeEvents(update.events);
675
+ this.flush();
676
+ if (update.exitCode !== undefined && update.exitCode !== null) {
677
+ exitCode = update.exitCode;
678
+ break;
679
+ }
680
+ }
681
+ } else {
682
+ const sliceMs = Math.max(0, Math.min(options.sliceMs ?? 16, 1_000));
683
+ for (;;) {
684
+ const nextExitCode = this.pump(sliceMs);
685
+ if (nextExitCode >= 0) {
686
+ exitCode = nextExitCode;
687
+ break;
688
+ }
689
+ await Bun.sleep(0);
690
+ }
691
+ }
692
+ if (exitCode === undefined) throw new Error("the QuickGUI app exited without a status code");
693
+ await this.#emitAppEventAndWait("quit", { exitCode });
694
+ return exitCode;
695
+ } finally {
696
+ this.#running = false;
697
+ this.releaseSingleInstanceLock();
698
+ }
699
+ }
700
+
701
+ async requestSingleInstanceLock(identifier: string): Promise<boolean> {
702
+ this.#assertAlive();
703
+ if (this.#singleInstanceIdentifier) {
704
+ if (this.#singleInstanceIdentifier !== identifier) {
705
+ throw new Error("this QuickGUI app already owns a different single-instance lock");
706
+ }
707
+ return true;
708
+ }
709
+ this._assertReady();
710
+ const acquired = hostedRuntime
711
+ ? binding.requestHostedSingleInstanceLock(this.nativeId, identifier)
712
+ : binding.requestSingleInstanceLock(this.nativeId, identifier);
713
+ if (acquired) this.#singleInstanceIdentifier = identifier;
714
+ return acquired;
715
+ }
716
+
717
+ releaseSingleInstanceLock(): void {
718
+ if (!this.#singleInstanceIdentifier || this.#destroyed) return;
719
+ if (hostedRuntime) binding.releaseHostedSingleInstanceLock(this.nativeId);
720
+ else binding.releaseSingleInstanceLock(this.nativeId);
721
+ this.#singleInstanceIdentifier = undefined;
722
+ }
723
+
724
+ /** Request an orderly native shutdown. Returns false after shutdown already began. */
725
+ quit(): boolean {
726
+ this.#assertAlive();
727
+ this._assertReady();
728
+ return hostedRuntime
729
+ ? binding.exitHostedApp(this.nativeId)
730
+ : binding.exitApp(this.nativeId);
731
+ }
732
+
733
+ /** Schedule a replacement process after ordinary child-first native teardown. */
734
+ relaunch(options: RelaunchOptions = {}): boolean {
735
+ this.#assertAlive();
736
+ this._assertReady();
737
+ const native: binding.NativeRelaunchOptions = {};
738
+ if (options.executable !== undefined) native.executable = options.executable;
739
+ if (options.arguments === null) native.clearArguments = true;
740
+ else if (options.arguments !== undefined) native.arguments = [...options.arguments];
741
+ if (options.workingDirectory !== undefined) {
742
+ native.workingDirectory = options.workingDirectory;
743
+ }
744
+ return hostedRuntime
745
+ ? binding.relaunchHostedApp(this.nativeId, native)
746
+ : binding.relaunchApp(this.nativeId, native);
747
+ }
748
+
749
+ destroy(): void {
750
+ if (this.#destroyed) return;
751
+ const error = new Error("the QuickGUI app was destroyed");
752
+ this.#rejectDialogs(undefined, error);
753
+ rejectPendingSystemRequests(error);
754
+ this.releaseSingleInstanceLock();
755
+ if (hostedRuntime) binding.destroyHostedApp(this.nativeId);
756
+ else binding.destroyApp(this.nativeId);
757
+ this.#destroyed = true;
758
+ this.#appEventListeners.clear();
759
+ if (activeApp === this) activeApp = undefined;
760
+ for (const window of this.windows.values()) window._didDestroy();
761
+ this.windows.clear();
762
+ }
763
+
764
+ #dispatchAppEvent(event: binding.NativeEvent): boolean {
765
+ let type: keyof AppEventMap;
766
+ let payload: AppEventMap[keyof AppEventMap];
767
+ if (event.kind === "open-urls") {
768
+ type = "openUrls";
769
+ try {
770
+ const parsed: unknown = JSON.parse(event.value ?? "[]");
771
+ payload = Array.isArray(parsed) && parsed.every((url) => typeof url === "string") ? parsed : [];
772
+ } catch {
773
+ payload = [];
774
+ }
775
+ } else if (event.kind === "reopen") {
776
+ type = "reopen";
777
+ payload = { hasVisibleWindows: event.value === "true" };
778
+ } else if (event.kind === "system-wake") {
779
+ type = "systemWake";
780
+ payload = undefined;
781
+ } else if (event.kind === "keyboard-layout-change") {
782
+ type = "keyboardLayoutChange";
783
+ const layout = hostedRuntime
784
+ ? binding.getHostedKeyboardLayout(this.nativeId)
785
+ : binding.getKeyboardLayout(this.nativeId);
786
+ payload = { id: layout.id, name: layout.name };
787
+ } else if (event.kind === "notification-response") {
788
+ type = "notificationResponse";
789
+ try {
790
+ const parsed = JSON.parse(event.value ?? "{}") as {
791
+ tag?: unknown;
792
+ actionId?: unknown;
793
+ reply?: unknown;
794
+ };
795
+ if (typeof parsed.tag !== "string") return true;
796
+ const response: NotificationResponse = { tag: parsed.tag };
797
+ if (typeof parsed.actionId === "string") response.actionId = parsed.actionId;
798
+ if (typeof parsed.reply === "string") response.reply = parsed.reply;
799
+ payload = response;
800
+ } catch {
801
+ return true;
802
+ }
803
+ } else if (event.kind === "second-instance") {
804
+ type = "secondInstance";
805
+ try {
806
+ const parsed = JSON.parse(event.value ?? "{}") as {
807
+ argv?: unknown;
808
+ cwd?: unknown;
809
+ };
810
+ if (
811
+ !Array.isArray(parsed.argv) ||
812
+ !parsed.argv.every((argument) => typeof argument === "string") ||
813
+ typeof parsed.cwd !== "string"
814
+ ) {
815
+ return true;
816
+ }
817
+ payload = { argv: parsed.argv, cwd: parsed.cwd };
818
+ const urls = urlsFromArguments(parsed.argv);
819
+ if (urls.length > 0) this.#emitAppEvent("openUrls", urls);
820
+ } catch {
821
+ return true;
822
+ }
823
+ } else {
824
+ return false;
825
+ }
826
+ this.#emitAppEvent(type, payload);
827
+ return true;
828
+ }
829
+
830
+ #emitAppEvent<K extends keyof AppEventMap>(type: K, payload: AppEventMap[K]): void {
831
+ for (const listener of this.#appEventListeners.get(type) ?? []) listener(payload);
832
+ }
833
+
834
+ async #emitAppEventAndWait<K extends keyof AppEventMap>(
835
+ type: K,
836
+ payload: AppEventMap[K],
837
+ ): Promise<void> {
838
+ await Promise.all(
839
+ [...(this.#appEventListeners.get(type) ?? [])].map((listener) => listener(payload)),
840
+ );
841
+ }
842
+
843
+ _registerWindow(window: Window): void {
844
+ this.#assertAlive();
845
+ this.windows.set(window.nativeId, window);
846
+ }
847
+
848
+ _assertReady(): void {
849
+ this.#assertAlive();
850
+ if (!this.isReady()) {
851
+ throw new Error("await app.whenReady() before using the native QuickGUI application");
852
+ }
853
+ }
854
+
855
+ _closeWindow(window: Window): void {
856
+ if (this.#destroyed || window.closed) return;
857
+ const closed = hostedRuntime
858
+ ? binding.closeHostedWindow(this.nativeId, window.nativeId)
859
+ : binding.closeWindow(this.nativeId, window.nativeId);
860
+ if (closed) this._didCloseWindow(window);
861
+ }
862
+
863
+ _didCloseWindow(window: Window): void {
864
+ if (this.windows.get(window.nativeId) !== window) return;
865
+ this.windows.delete(window.nativeId);
866
+ this.#rejectDialogs(window, new Error("the native dialog's owner window closed"));
867
+ window._didClose();
868
+ }
869
+
870
+ _showAlertDialog(window: Window | undefined, options: AlertDialogOptions): Promise<number> {
871
+ try {
872
+ const nativeOptions = normalizeAlertDialogOptions(options);
873
+ return this.#requestDialog(
874
+ window,
875
+ "alert-dialog",
876
+ (request) => {
877
+ if (hostedRuntime) {
878
+ binding.showHostedAlertDialog(this.nativeId, window?.nativeId, request, nativeOptions);
879
+ } else {
880
+ binding.showAlertDialog(this.nativeId, window?.nativeId, request, nativeOptions);
881
+ }
882
+ },
883
+ (event) => {
884
+ const response = Number(event.value);
885
+ if (!Number.isSafeInteger(response) || response < 0) {
886
+ throw new Error("the native dialog returned an invalid button index");
887
+ }
888
+ return response;
889
+ },
890
+ );
891
+ } catch (error) {
892
+ return Promise.reject(asError(error));
893
+ }
894
+ }
895
+
896
+ _showOpenDialog(
897
+ window: Window | undefined,
898
+ options: OpenDialogOptions,
899
+ ): Promise<OpenDialogResult> {
900
+ try {
901
+ const nativeOptions = normalizeOpenDialogOptions(options);
902
+ return this.#requestDialog(
903
+ window,
904
+ "open-dialog",
905
+ (request) => {
906
+ if (hostedRuntime) {
907
+ binding.showHostedOpenDialog(this.nativeId, window?.nativeId, request, nativeOptions);
908
+ } else {
909
+ binding.showOpenDialog(this.nativeId, window?.nativeId, request, nativeOptions);
910
+ }
911
+ },
912
+ (event) => ({
913
+ canceled: event.paths === undefined,
914
+ filePaths: event.paths ?? [],
915
+ }),
916
+ );
917
+ } catch (error) {
918
+ return Promise.reject(asError(error));
919
+ }
920
+ }
921
+
922
+ _showSaveDialog(
923
+ window: Window | undefined,
924
+ options: SaveDialogOptions,
925
+ ): Promise<SaveDialogResult> {
926
+ try {
927
+ const nativeOptions = normalizeSaveDialogOptions(options);
928
+ return this.#requestDialog(
929
+ window,
930
+ "save-dialog",
931
+ (request) => {
932
+ if (hostedRuntime) {
933
+ binding.showHostedSaveDialog(this.nativeId, window?.nativeId, request, nativeOptions);
934
+ } else {
935
+ binding.showSaveDialog(this.nativeId, window?.nativeId, request, nativeOptions);
936
+ }
937
+ },
938
+ (event) =>
939
+ event.value === undefined
940
+ ? { canceled: true }
941
+ : { canceled: false, filePath: event.value },
942
+ );
943
+ } catch (error) {
944
+ return Promise.reject(asError(error));
945
+ }
946
+ }
947
+
948
+ #requestDialog<T>(
949
+ window: Window | undefined,
950
+ kind: DialogEventKind,
951
+ invoke: (request: number) => void,
952
+ result: (event: binding.NativeEvent) => T,
953
+ ): Promise<T> {
954
+ this.#assertAlive();
955
+ if (window && (window.closed || this.windows.get(window.nativeId) !== window)) {
956
+ return Promise.reject(new Error("the native dialog parent must be an open window"));
957
+ }
958
+ const request = this.#allocateDialogRequest();
959
+ return new Promise<T>((resolve, reject) => {
960
+ this.#pendingDialogs.set(request, {
961
+ kind,
962
+ window,
963
+ complete: (event) => resolve(result(event)),
964
+ reject,
965
+ });
966
+ try {
967
+ invoke(request);
968
+ } catch (error) {
969
+ this.#pendingDialogs.delete(request);
970
+ reject(asError(error));
971
+ }
972
+ });
973
+ }
974
+
975
+ #dispatchDialog(event: binding.NativeEvent): void {
976
+ const pending = this.#pendingDialogs.get(event.target);
977
+ if (!pending) return;
978
+ this.#pendingDialogs.delete(event.target);
979
+ if ((pending.window?.nativeId ?? 0) !== event.window) {
980
+ pending.reject(new Error("the native dialog response had the wrong owner window"));
981
+ return;
982
+ }
983
+ if (pending.kind !== event.kind) {
984
+ pending.reject(new Error("the native dialog response had the wrong response type"));
985
+ return;
986
+ }
987
+ if (event.error !== undefined) {
988
+ pending.reject(new Error(event.error));
989
+ return;
990
+ }
991
+ try {
992
+ pending.complete(event);
993
+ } catch (error) {
994
+ pending.reject(asError(error));
995
+ }
996
+ }
997
+
998
+ #allocateDialogRequest(): number {
999
+ for (let attempt = 0; attempt <= this.#pendingDialogs.size; attempt += 1) {
1000
+ const request = this.#nextDialogRequest;
1001
+ this.#nextDialogRequest = request >= 0xffff_ffff ? 1 : request + 1;
1002
+ if (!this.#pendingDialogs.has(request)) return request;
1003
+ }
1004
+ throw new Error("the native dialog request id space is exhausted");
1005
+ }
1006
+
1007
+ #rejectDialogs(window: Window | undefined, error: Error): void {
1008
+ for (const [request, pending] of this.#pendingDialogs) {
1009
+ if (window && pending.window !== window) continue;
1010
+ this.#pendingDialogs.delete(request);
1011
+ pending.reject(error);
1012
+ }
1013
+ }
1014
+
1015
+ #assertAlive(): void {
1016
+ if (this.#destroyed) throw new Error("this QuickGUI app has been destroyed");
1017
+ }
1018
+ }
1019
+
1020
+ export class Window {
1021
+ readonly root: NativeNode;
1022
+ readonly nativeId: number;
1023
+ readonly app: App;
1024
+ readonly nodes = new Map<number, NativeNode>();
1025
+ #batch = new MutationBatch();
1026
+ #flushScheduled = false;
1027
+ #nativeReady = false;
1028
+ #closed = false;
1029
+ readonly #closeListeners = new Set<WindowCloseListener>();
1030
+ readonly #mountDisposers = new Set<() => void>();
1031
+
1032
+ /** Return the Window whose renderer or native event callback is currently executing. */
1033
+ static getCurrentWindow(): Window {
1034
+ if (!currentWindow) {
1035
+ throw new Error(
1036
+ "Window.getCurrentWindow() must be called while rendering or handling a window event",
1037
+ );
1038
+ }
1039
+ return currentWindow;
1040
+ }
1041
+
1042
+ constructor(options: WindowOptions) {
1043
+ const app = activeApp;
1044
+ if (!app) throw new Error("the QuickGUI app is unavailable");
1045
+ if (!app.isReady()) {
1046
+ throw new Error("await app.whenReady() before creating a QuickGUI Window");
1047
+ }
1048
+ const nativeOptions: binding.NativeWindowOptions = {};
1049
+ const serializedMenu = options.menu ? serializeNativeMenu(options.menu) : undefined;
1050
+ if (options.title !== undefined) nativeOptions.title = options.title;
1051
+ if (options.width !== undefined) nativeOptions.width = options.width;
1052
+ if (options.height !== undefined) nativeOptions.height = options.height;
1053
+ if (
1054
+ options.minimumSize !== undefined &&
1055
+ (options.minimumWidth !== undefined || options.minimumHeight !== undefined)
1056
+ ) {
1057
+ throw new TypeError("minimumSize cannot be combined with minimumWidth or minimumHeight");
1058
+ }
1059
+ if (options.minimumSize === null) {
1060
+ nativeOptions.minimumSizeEnabled = false;
1061
+ } else if (options.minimumSize !== undefined) {
1062
+ nativeOptions.minimumWidth = options.minimumSize.width;
1063
+ nativeOptions.minimumHeight = options.minimumSize.height;
1064
+ } else {
1065
+ if (options.minimumWidth !== undefined) nativeOptions.minimumWidth = options.minimumWidth;
1066
+ if (options.minimumHeight !== undefined) nativeOptions.minimumHeight = options.minimumHeight;
1067
+ }
1068
+ if (
1069
+ options.maximumSize !== undefined &&
1070
+ (options.maximumWidth !== undefined || options.maximumHeight !== undefined)
1071
+ ) {
1072
+ throw new TypeError("maximumSize cannot be combined with maximumWidth or maximumHeight");
1073
+ }
1074
+ if (options.maximumSize !== undefined) {
1075
+ nativeOptions.maximumWidth = options.maximumSize.width;
1076
+ nativeOptions.maximumHeight = options.maximumSize.height;
1077
+ } else {
1078
+ if (options.maximumWidth !== undefined) nativeOptions.maximumWidth = options.maximumWidth;
1079
+ if (options.maximumHeight !== undefined) nativeOptions.maximumHeight = options.maximumHeight;
1080
+ }
1081
+ if (options.position !== undefined) {
1082
+ nativeOptions.x = options.position.x;
1083
+ nativeOptions.y = options.position.y;
1084
+ }
1085
+ if (options.initialState !== undefined) nativeOptions.initialState = options.initialState;
1086
+ if (options.displayId !== undefined) nativeOptions.displayId = options.displayId;
1087
+ if (options.representedFile !== undefined) nativeOptions.representedFile = options.representedFile;
1088
+ if (options.documentEdited !== undefined) nativeOptions.documentEdited = options.documentEdited;
1089
+ if (options.tabbingIdentifier !== undefined) {
1090
+ nativeOptions.tabbingIdentifier = options.tabbingIdentifier;
1091
+ }
1092
+ if (options.background !== undefined) nativeOptions.background = parseColor(options.background);
1093
+ if (options.performanceProfile !== undefined) {
1094
+ nativeOptions.performanceProfile = options.performanceProfile;
1095
+ }
1096
+ if (options.appearance !== undefined) nativeOptions.appearance = options.appearance;
1097
+ if (options.titleBarStyle !== undefined) nativeOptions.titleBarStyle = options.titleBarStyle;
1098
+ if (options.kind !== undefined) nativeOptions.kind = options.kind;
1099
+ if (options.focus !== undefined) nativeOptions.focus = options.focus;
1100
+ if (options.focusable !== undefined) nativeOptions.focusable = options.focusable;
1101
+ if (options.visible !== undefined) nativeOptions.show = options.visible;
1102
+ if (options.movable !== undefined) nativeOptions.movable = options.movable;
1103
+ if (options.resizable !== undefined) nativeOptions.resizable = options.resizable;
1104
+ if (options.minimizable !== undefined) nativeOptions.minimizable = options.minimizable;
1105
+ if (options.maximizable !== undefined) nativeOptions.maximizable = options.maximizable;
1106
+ if (options.closable !== undefined) nativeOptions.closable = options.closable;
1107
+ if (options.decorated !== undefined) nativeOptions.decorated = options.decorated;
1108
+ if (options.shadow !== undefined) nativeOptions.shadow = options.shadow;
1109
+ if (options.contentProtected !== undefined) {
1110
+ nativeOptions.contentProtected = options.contentProtected;
1111
+ }
1112
+ if (options.windowLevel !== undefined) nativeOptions.windowLevel = options.windowLevel;
1113
+ if (options.skipTaskbar !== undefined) nativeOptions.skipTaskbar = options.skipTaskbar;
1114
+ if (options.visibleOnAllWorkspaces !== undefined) {
1115
+ nativeOptions.visibleOnAllWorkspaces = options.visibleOnAllWorkspaces;
1116
+ }
1117
+ if (options.opacity !== undefined) nativeOptions.opacity = options.opacity;
1118
+ if (options.icon !== undefined) nativeOptions.icon = nativeImageSource(options.icon);
1119
+ if (options.taskbarProgress !== undefined) {
1120
+ nativeOptions.taskbarProgressState = options.taskbarProgress.state;
1121
+ nativeOptions.taskbarProgress = options.taskbarProgress.progress;
1122
+ }
1123
+ if (options.taskbarOverlay !== undefined) {
1124
+ nativeOptions.taskbarOverlayIcon = nativeImageSource(options.taskbarOverlay.icon);
1125
+ nativeOptions.taskbarOverlayDescription = options.taskbarOverlay.description;
1126
+ }
1127
+ if (options.cursorVisible !== undefined) nativeOptions.cursorVisible = options.cursorVisible;
1128
+ if (options.cursorGrab !== undefined) nativeOptions.cursorGrab = options.cursorGrab;
1129
+ if (options.cursorHitTest !== undefined) nativeOptions.cursorHitTest = options.cursorHitTest;
1130
+ if (options.cursorPosition !== undefined) {
1131
+ nativeOptions.cursorX = options.cursorPosition.x;
1132
+ nativeOptions.cursorY = options.cursorPosition.y;
1133
+ }
1134
+ if (serializedMenu !== undefined) nativeOptions.menu = serializedMenu.json;
1135
+ if (options.lineScrollPixels !== undefined) {
1136
+ nativeOptions.lineScrollPixels = options.lineScrollPixels;
1137
+ }
1138
+ if (options.keySequenceTimeoutMs !== undefined) {
1139
+ nativeOptions.keySequenceTimeoutMs = options.keySequenceTimeoutMs;
1140
+ }
1141
+ if (options.reduceMotion !== undefined) nativeOptions.reduceMotion = options.reduceMotion;
1142
+ if (options.trafficLightPosition !== undefined) {
1143
+ nativeOptions.trafficLightX = options.trafficLightPosition.x;
1144
+ nativeOptions.trafficLightY = options.trafficLightPosition.y;
1145
+ }
1146
+ if (options.backgroundAppearance !== undefined) {
1147
+ nativeOptions.transparent = options.backgroundAppearance === "transparent";
1148
+ nativeOptions.blur = options.backgroundAppearance === "blurred";
1149
+ } else {
1150
+ if (options.transparent !== undefined) nativeOptions.transparent = options.transparent;
1151
+ if (options.blur !== undefined) nativeOptions.blur = options.blur;
1152
+ }
1153
+ if (options.placement !== undefined) nativeOptions.popoverPlacement = options.placement;
1154
+ if (options.gap !== undefined) nativeOptions.popoverGap = options.gap;
1155
+ if (options.offset !== undefined) {
1156
+ nativeOptions.popoverOffsetX = options.offset.x;
1157
+ nativeOptions.popoverOffsetY = options.offset.y;
1158
+ }
1159
+ if (options.viewportMargin !== undefined) {
1160
+ nativeOptions.popoverViewportMargin = options.viewportMargin;
1161
+ }
1162
+ if (options.dismissOnEscape !== undefined) {
1163
+ nativeOptions.popoverDismissOnEscape = options.dismissOnEscape;
1164
+ }
1165
+ if (options.dismissOnPointerOutside !== undefined) {
1166
+ nativeOptions.popoverDismissOnPointerOutside = options.dismissOnPointerOutside;
1167
+ }
1168
+ if (options.grab !== undefined) nativeOptions.popoverGrab = options.grab;
1169
+ if (options.acceptsKeyFocus !== undefined) {
1170
+ nativeOptions.popoverAcceptsKeyFocus = options.acceptsKeyFocus;
1171
+ }
1172
+ const parent = options.anchor?.host;
1173
+ if (
1174
+ options.anchor &&
1175
+ (!parent || parent.closed || !options.anchor.materialized)
1176
+ ) {
1177
+ throw new Error("a system popover requires a mounted node in an open parent Window");
1178
+ }
1179
+ parent?.flush();
1180
+ this.app = app;
1181
+ this.root = new NativeNode(NativeNodeTag.View, "", ROOT_NODE_ID);
1182
+ this.root.host = this;
1183
+ this.root.materialized = true;
1184
+ this.nodes.set(ROOT_NODE_ID, this.root);
1185
+ try {
1186
+ const dispose = withCurrentWindow(this, () => options.renderer(this));
1187
+ if (typeof dispose !== "function") {
1188
+ throw new TypeError("a QuickGUI Window renderer must return a dispose function");
1189
+ }
1190
+ this._trackMount(dispose);
1191
+ const initialBatch = this.#takePendingBatch();
1192
+ if (options.anchor) {
1193
+ this.nativeId = hostedRuntime
1194
+ ? binding.createHostedSystemPopover(
1195
+ app.nativeId,
1196
+ parent!.nativeId,
1197
+ options.anchor.id,
1198
+ nativeOptions,
1199
+ initialBatch,
1200
+ )
1201
+ : binding.createSystemPopover(
1202
+ app.nativeId,
1203
+ parent!.nativeId,
1204
+ options.anchor.id,
1205
+ nativeOptions,
1206
+ initialBatch,
1207
+ );
1208
+ } else {
1209
+ this.nativeId = hostedRuntime
1210
+ ? binding.createHostedWindow(app.nativeId, nativeOptions, initialBatch)
1211
+ : binding.createWindow(app.nativeId, nativeOptions, initialBatch);
1212
+ }
1213
+ this.#nativeReady = true;
1214
+ app._registerWindow(this);
1215
+ if (serializedMenu !== undefined) this._trackMount(serializedMenu.install());
1216
+ } catch (error) {
1217
+ this._didClose();
1218
+ throw error;
1219
+ }
1220
+ }
1221
+
1222
+ get closed(): boolean {
1223
+ return this.#closed;
1224
+ }
1225
+
1226
+ onClose(listener: WindowCloseListener): () => void {
1227
+ if (this.#closed) {
1228
+ listener(this);
1229
+ return () => {};
1230
+ }
1231
+ this.#closeListeners.add(listener);
1232
+ return () => this.#closeListeners.delete(listener);
1233
+ }
1234
+
1235
+ close(): void {
1236
+ this.app._closeWindow(this);
1237
+ }
1238
+
1239
+ getState(): WindowState {
1240
+ return getNativeWindowState(this);
1241
+ }
1242
+
1243
+ onStateChange(listener: (state: WindowState) => void): () => void {
1244
+ if (this.#closed) return () => {};
1245
+ return onNativeWindowStateChange(this, listener);
1246
+ }
1247
+
1248
+ setTitle(title: string): void {
1249
+ performNativeWindowAction(this, "set-title", title);
1250
+ }
1251
+
1252
+ setBounds(bounds: WindowBounds): void {
1253
+ performNativeWindowAction(this, "set-bounds", JSON.stringify(bounds));
1254
+ }
1255
+
1256
+ setPosition(position: Point): void {
1257
+ performNativeWindowAction(this, "move", JSON.stringify(position));
1258
+ }
1259
+
1260
+ setSize(size: Size): void {
1261
+ performNativeWindowAction(this, "resize", JSON.stringify(size));
1262
+ }
1263
+
1264
+ minimize(): void {
1265
+ performNativeWindowAction(this, "minimize");
1266
+ }
1267
+
1268
+ maximize(): void {
1269
+ performNativeWindowAction(this, "maximize");
1270
+ }
1271
+
1272
+ restore(): void {
1273
+ performNativeWindowAction(this, "restore");
1274
+ }
1275
+
1276
+ setFullscreen(fullscreen: boolean): void {
1277
+ performNativeWindowAction(this, "set-fullscreen", String(fullscreen));
1278
+ }
1279
+
1280
+ setResizable(resizable: boolean): void {
1281
+ performNativeWindowAction(this, "set-resizable", String(resizable));
1282
+ }
1283
+
1284
+ setMovable(movable: boolean): void {
1285
+ performNativeWindowAction(this, "set-movable", String(movable));
1286
+ }
1287
+
1288
+ setMinimumSize(size?: Size): void {
1289
+ performNativeWindowAction(
1290
+ this,
1291
+ "set-minimum-size",
1292
+ size === undefined ? undefined : JSON.stringify(size),
1293
+ );
1294
+ }
1295
+
1296
+ setMaximumSize(size?: Size): void {
1297
+ performNativeWindowAction(
1298
+ this,
1299
+ "set-maximum-size",
1300
+ size === undefined ? undefined : JSON.stringify(size),
1301
+ );
1302
+ }
1303
+
1304
+ setMinimizable(minimizable: boolean): void {
1305
+ performNativeWindowAction(this, "set-minimizable", String(minimizable));
1306
+ }
1307
+
1308
+ setMaximizable(maximizable: boolean): void {
1309
+ performNativeWindowAction(this, "set-maximizable", String(maximizable));
1310
+ }
1311
+
1312
+ setClosable(closable: boolean): void {
1313
+ performNativeWindowAction(this, "set-closable", String(closable));
1314
+ }
1315
+
1316
+ setDecorated(decorated: boolean): void {
1317
+ performNativeWindowAction(this, "set-decorated", String(decorated));
1318
+ }
1319
+
1320
+ setShadow(shadow: boolean): void {
1321
+ performNativeWindowAction(this, "set-shadow", String(shadow));
1322
+ }
1323
+
1324
+ setContentProtected(protected_: boolean): void {
1325
+ performNativeWindowAction(this, "set-content-protected", String(protected_));
1326
+ }
1327
+
1328
+ setWindowLevel(level: WindowLevel | "automatic"): void {
1329
+ performNativeWindowAction(this, "set-window-level", level);
1330
+ }
1331
+
1332
+ setFocusable(focusable: boolean): void {
1333
+ performNativeWindowAction(this, "set-focusable", String(focusable));
1334
+ }
1335
+
1336
+ setSkipTaskbar(skip: boolean): void {
1337
+ performNativeWindowAction(this, "set-skip-taskbar", String(skip));
1338
+ }
1339
+
1340
+ setVisibleOnAllWorkspaces(visible: boolean): void {
1341
+ performNativeWindowAction(this, "set-visible-on-all-workspaces", String(visible));
1342
+ }
1343
+
1344
+ setOpacity(opacity: number): void {
1345
+ performNativeWindowAction(this, "set-opacity", String(opacity));
1346
+ }
1347
+
1348
+ setIcon(icon: ImageSource): void {
1349
+ performNativeWindowImageAction(this, "set-icon", icon);
1350
+ }
1351
+
1352
+ clearIcon(): void {
1353
+ performNativeWindowImageAction(this, "clear-icon");
1354
+ }
1355
+
1356
+ setTaskbarProgress(state: TaskbarProgressState, progress: number): void {
1357
+ performNativeWindowAction(
1358
+ this,
1359
+ "set-taskbar-progress",
1360
+ JSON.stringify({ state, progress }),
1361
+ );
1362
+ }
1363
+
1364
+ setTaskbarOverlayIcon(icon: ImageSource, description: string): void {
1365
+ performNativeWindowImageAction(this, "set-taskbar-overlay-icon", icon, description);
1366
+ }
1367
+
1368
+ clearTaskbarOverlayIcon(): void {
1369
+ performNativeWindowAction(this, "clear-taskbar-overlay-icon");
1370
+ }
1371
+
1372
+ setCursorVisible(visible: boolean): void {
1373
+ performNativeWindowAction(this, "set-cursor-visible", String(visible));
1374
+ }
1375
+
1376
+ setCursorGrab(mode: CursorGrabMode): void {
1377
+ performNativeWindowAction(this, "set-cursor-grab", mode);
1378
+ }
1379
+
1380
+ setCursorHitTest(hitTest: boolean): void {
1381
+ performNativeWindowAction(this, "set-cursor-hit-test", String(hitTest));
1382
+ }
1383
+
1384
+ setCursorPosition(position: Point): void {
1385
+ performNativeWindowAction(this, "set-cursor-position", JSON.stringify(position));
1386
+ }
1387
+
1388
+ show(): void {
1389
+ performNativeWindowAction(this, "set-visible", "true");
1390
+ }
1391
+
1392
+ hide(): void {
1393
+ performNativeWindowAction(this, "set-visible", "false");
1394
+ }
1395
+
1396
+ focus(): void {
1397
+ performNativeWindowAction(this, "focus");
1398
+ }
1399
+
1400
+ requestAttention(): void {
1401
+ performNativeWindowAction(this, "request-attention");
1402
+ }
1403
+
1404
+ setRepresentedFile(path?: string): void {
1405
+ performNativeWindowAction(this, "set-represented-file", path ?? "");
1406
+ }
1407
+
1408
+ setDocumentEdited(edited: boolean): void {
1409
+ performNativeWindowAction(this, "set-document-edited", String(edited));
1410
+ }
1411
+
1412
+ setAppearance(appearance: AppearancePreference): void {
1413
+ performNativeWindowAction(this, "set-appearance", appearance);
1414
+ }
1415
+
1416
+ setBackgroundAppearance(appearance: WindowBackgroundAppearance): void {
1417
+ performNativeWindowAction(this, "set-background-appearance", appearance);
1418
+ }
1419
+
1420
+ _focusNode(node: NativeNode): boolean {
1421
+ if (this.#closed || node.host !== this) return false;
1422
+ this.flush();
1423
+ return hostedRuntime
1424
+ ? binding.focusHostedNode(this.app.nativeId, this.nativeId, node.id)
1425
+ : binding.focusNode(this.app.nativeId, this.nativeId, node.id);
1426
+ }
1427
+
1428
+ flush(): number | undefined {
1429
+ if (this.#closed || !this.#nativeReady) return undefined;
1430
+ this.#flushScheduled = false;
1431
+ if (this.#batch.empty) return undefined;
1432
+ const batch = this.#batch;
1433
+ this.#batch = new MutationBatch();
1434
+ const bytes = batch.finish();
1435
+ return hostedRuntime
1436
+ ? binding.applyHostedBatch(this.app.nativeId, this.nativeId, bytes)
1437
+ : binding.applyBatch(this.app.nativeId, this.nativeId, bytes);
1438
+ }
1439
+
1440
+ #takePendingBatch() {
1441
+ this.#flushScheduled = false;
1442
+ const batch = this.#batch;
1443
+ this.#batch = new MutationBatch();
1444
+ return batch.finish();
1445
+ }
1446
+
1447
+ _dispatchEvent(type: NativeEventType, targetId: number, value?: string): void {
1448
+ withCurrentWindow(this, () => {
1449
+ const target = this.nodes.get(targetId);
1450
+ if (!target) return;
1451
+ const quickGuiEvent = new QuickGuiEvent(type, target, value);
1452
+ if (type === "mouseenter" || type === "mouseleave") {
1453
+ target.listeners.get(type)?.(quickGuiEvent);
1454
+ return;
1455
+ }
1456
+ let current: NativeNode | undefined = target;
1457
+ while (current) {
1458
+ quickGuiEvent.currentTarget = current;
1459
+ current.listeners.get(type)?.(quickGuiEvent);
1460
+ if (quickGuiEvent.propagationStopped) break;
1461
+ current = current.parent;
1462
+ }
1463
+ });
1464
+ }
1465
+
1466
+ _didClose(): void {
1467
+ if (this.#closed) return;
1468
+ this.#closed = true;
1469
+ this.#flushScheduled = false;
1470
+ const disposers = [...this.#mountDisposers];
1471
+ this.#mountDisposers.clear();
1472
+ for (const dispose of disposers) dispose();
1473
+ this.#batch = new MutationBatch();
1474
+ removeNativeWindowStateListeners(this);
1475
+ for (const listener of this.#closeListeners) listener(this);
1476
+ this.#closeListeners.clear();
1477
+ this.nodes.clear();
1478
+ }
1479
+
1480
+ _didDestroy(): void {
1481
+ this._didClose();
1482
+ }
1483
+
1484
+ _trackMount(dispose: () => void): () => void {
1485
+ if (this.#closed) {
1486
+ dispose();
1487
+ return () => {};
1488
+ }
1489
+ this.#mountDisposers.add(dispose);
1490
+ return () => this.#mountDisposers.delete(dispose);
1491
+ }
1492
+
1493
+ _enqueueCreate(node: NativeNode): void {
1494
+ switch (node.tag) {
1495
+ case NativeNodeTag.Text:
1496
+ this.#batch.createText(node.id, node.text);
1497
+ break;
1498
+ case NativeNodeTag.Sentinel:
1499
+ this.#batch.createSentinel(node.id);
1500
+ break;
1501
+ default:
1502
+ this.#batch.createElement(node.id, node.tag);
1503
+ }
1504
+ }
1505
+
1506
+ _enqueueProperty(
1507
+ node: NativeNode,
1508
+ property: PropertyCode,
1509
+ value: NativePropertyValue,
1510
+ color: boolean,
1511
+ ): void {
1512
+ this.#batch.setProperty(node.id, property, value, color);
1513
+ this._scheduleFlush();
1514
+ }
1515
+
1516
+ _enqueueText(node: NativeNode): void {
1517
+ this.#batch.replaceText(node.id, node.text);
1518
+ this._scheduleFlush();
1519
+ }
1520
+
1521
+ _enqueueInsert(parent: NativeNode, child: NativeNode, before?: NativeNode): void {
1522
+ this.#batch.insert(parent.id, child.id, before?.id);
1523
+ this._scheduleFlush();
1524
+ }
1525
+
1526
+ _enqueueRemove(parent: NativeNode, child: NativeNode): void {
1527
+ this.#batch.remove(parent.id, child.id);
1528
+ this._scheduleFlush();
1529
+ }
1530
+
1531
+ _enqueueCleanup(parent: NativeNode, children: readonly NativeNode[]): void {
1532
+ this.#batch.cleanup(
1533
+ parent.id,
1534
+ children.map((child) => child.id),
1535
+ );
1536
+ this._scheduleFlush();
1537
+ }
1538
+
1539
+ _scheduleFlush(): void {
1540
+ if (this.#flushScheduled || this.#closed) return;
1541
+ this.#flushScheduled = true;
1542
+ queueMicrotask(() => {
1543
+ if (!this.#closed) this.flush();
1544
+ });
1545
+ }
1546
+ }
1547
+
1548
+ function showAlertDialog(options: AlertDialogOptions): Promise<number>;
1549
+ function showAlertDialog(window: Window, options: AlertDialogOptions): Promise<number>;
1550
+ function showAlertDialog(
1551
+ windowOrOptions: Window | AlertDialogOptions,
1552
+ maybeOptions?: AlertDialogOptions,
1553
+ ): Promise<number> {
1554
+ const hasWindow = windowOrOptions instanceof Window;
1555
+ const window = hasWindow ? windowOrOptions : undefined;
1556
+ const options = hasWindow ? maybeOptions : (windowOrOptions as AlertDialogOptions);
1557
+ if (!options) return Promise.reject(new TypeError("showAlertDialog requires options"));
1558
+ const app = window?.app ?? activeApp;
1559
+ if (!app) return Promise.reject(new Error("create a QuickGUI App before showing a dialog"));
1560
+ return app._showAlertDialog(window, options);
1561
+ }
1562
+
1563
+ function showOpenDialog(options?: OpenDialogOptions): Promise<OpenDialogResult>;
1564
+ function showOpenDialog(window: Window, options?: OpenDialogOptions): Promise<OpenDialogResult>;
1565
+ function showOpenDialog(
1566
+ windowOrOptions: Window | OpenDialogOptions = {},
1567
+ maybeOptions: OpenDialogOptions = {},
1568
+ ): Promise<OpenDialogResult> {
1569
+ const hasWindow = windowOrOptions instanceof Window;
1570
+ const window = hasWindow ? windowOrOptions : undefined;
1571
+ const options = hasWindow ? maybeOptions : (windowOrOptions as OpenDialogOptions);
1572
+ const app = window?.app ?? activeApp;
1573
+ if (!app) return Promise.reject(new Error("create a QuickGUI App before showing a dialog"));
1574
+ return app._showOpenDialog(window, options);
1575
+ }
1576
+
1577
+ function showSaveDialog(options?: SaveDialogOptions): Promise<SaveDialogResult>;
1578
+ function showSaveDialog(window: Window, options?: SaveDialogOptions): Promise<SaveDialogResult>;
1579
+ function showSaveDialog(
1580
+ windowOrOptions: Window | SaveDialogOptions = {},
1581
+ maybeOptions: SaveDialogOptions = {},
1582
+ ): Promise<SaveDialogResult> {
1583
+ const hasWindow = windowOrOptions instanceof Window;
1584
+ const window = hasWindow ? windowOrOptions : undefined;
1585
+ const options = hasWindow ? maybeOptions : (windowOrOptions as SaveDialogOptions);
1586
+ const app = window?.app ?? activeApp;
1587
+ if (!app) return Promise.reject(new Error("create a QuickGUI App before showing a dialog"));
1588
+ return app._showSaveDialog(window, options);
1589
+ }
1590
+
1591
+ /** Platform-native dialogs. Pass a Window first to attach the dialog; omit it for app-modal UI. */
1592
+ export const Dialog = Object.freeze({
1593
+ showAlertDialog,
1594
+ showOpenDialog,
1595
+ showSaveDialog,
1596
+ });
1597
+
1598
+ function asError(error: unknown): Error {
1599
+ return error instanceof Error ? error : new Error(String(error));
1600
+ }
1601
+
1602
+ export type Application = App;
1603
+ export const app = new App();