@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/dialog.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, resolve, sep } from "node:path";
|
|
3
|
+
import * as binding from "./binding.js";
|
|
4
|
+
|
|
5
|
+
export type AlertDialogLevel = "info" | "warning" | "critical";
|
|
6
|
+
export type AlertDialogButtonRole = "default" | "cancel" | "other";
|
|
7
|
+
|
|
8
|
+
export interface AlertDialogButton {
|
|
9
|
+
label: string;
|
|
10
|
+
role?: AlertDialogButtonRole;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AlertDialogOptions {
|
|
14
|
+
level?: AlertDialogLevel;
|
|
15
|
+
message: string;
|
|
16
|
+
detail?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Defaults to one system-styled OK button. macOS accepts up to 16 buttons; the portable
|
|
19
|
+
* Windows/Linux/BSD backend accepts up to three uniquely labelled buttons.
|
|
20
|
+
*/
|
|
21
|
+
buttons?: readonly (string | AlertDialogButton)[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type OpenDialogProperty =
|
|
25
|
+
| "openFile"
|
|
26
|
+
| "openDirectory"
|
|
27
|
+
| "multiSelections"
|
|
28
|
+
| "showHiddenFiles";
|
|
29
|
+
|
|
30
|
+
export interface FileDialogFilter {
|
|
31
|
+
name: string;
|
|
32
|
+
/** File extensions without a leading dot. Use `*` to match every file. */
|
|
33
|
+
extensions: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OpenDialogOptions {
|
|
37
|
+
title?: string;
|
|
38
|
+
/** Initial file or directory. */
|
|
39
|
+
defaultPath?: string;
|
|
40
|
+
filters?: readonly FileDialogFilter[];
|
|
41
|
+
/** Custom open-button text. Currently supported by the macOS backend. */
|
|
42
|
+
buttonLabel?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Defaults to `["openFile"]`. `showHiddenFiles` can currently be forced only on macOS;
|
|
45
|
+
* Windows/Linux otherwise follow the user's file-picker preference.
|
|
46
|
+
*/
|
|
47
|
+
properties?: readonly OpenDialogProperty[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface OpenDialogResult {
|
|
51
|
+
canceled: boolean;
|
|
52
|
+
filePaths: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SaveDialogOptions {
|
|
56
|
+
title?: string;
|
|
57
|
+
/** Initial directory or complete suggested file path. */
|
|
58
|
+
defaultPath?: string;
|
|
59
|
+
filters?: readonly FileDialogFilter[];
|
|
60
|
+
/** Custom save-button text. Currently supported by the macOS backend. */
|
|
61
|
+
buttonLabel?: string;
|
|
62
|
+
/** Currently supported by the macOS backend. */
|
|
63
|
+
showHiddenFiles?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SaveDialogResult {
|
|
67
|
+
canceled: boolean;
|
|
68
|
+
filePath?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function normalizeAlertDialogOptions(
|
|
72
|
+
options: AlertDialogOptions,
|
|
73
|
+
): binding.NativeDialogOptions {
|
|
74
|
+
const normalized: binding.NativeDialogOptions = {
|
|
75
|
+
message: options.message,
|
|
76
|
+
buttons: (options.buttons ?? [{ label: "OK", role: "default" }]).map((button) => {
|
|
77
|
+
if (typeof button === "string") return { label: button };
|
|
78
|
+
const normalizedButton: binding.NativeDialogButton = { label: button.label };
|
|
79
|
+
if (button.role !== undefined) normalizedButton.role = button.role;
|
|
80
|
+
return normalizedButton;
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
if (options.level !== undefined) normalized.level = options.level;
|
|
84
|
+
if (options.detail !== undefined) normalized.detail = options.detail;
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function normalizeOpenDialogOptions(
|
|
89
|
+
options: OpenDialogOptions,
|
|
90
|
+
): binding.NativeOpenDialogOptions {
|
|
91
|
+
const properties = new Set(options.properties ?? ["openFile"]);
|
|
92
|
+
const files = properties.has("openFile");
|
|
93
|
+
const directories = properties.has("openDirectory");
|
|
94
|
+
if (!files && !directories) {
|
|
95
|
+
throw new TypeError("showOpenDialog properties must include openFile or openDirectory");
|
|
96
|
+
}
|
|
97
|
+
if (files && directories && process.platform !== "darwin") {
|
|
98
|
+
throw new TypeError(
|
|
99
|
+
"showOpenDialog cannot combine openFile and openDirectory on this platform",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const normalized: binding.NativeOpenDialogOptions = {
|
|
103
|
+
files,
|
|
104
|
+
directories,
|
|
105
|
+
multiple: properties.has("multiSelections"),
|
|
106
|
+
filters: normalizeFileDialogFilters(options.filters),
|
|
107
|
+
showsHiddenFiles: properties.has("showHiddenFiles"),
|
|
108
|
+
};
|
|
109
|
+
if (options.title !== undefined) normalized.title = options.title;
|
|
110
|
+
if (options.buttonLabel !== undefined) normalized.prompt = options.buttonLabel;
|
|
111
|
+
if (options.defaultPath !== undefined) {
|
|
112
|
+
const defaultPath = resolve(options.defaultPath);
|
|
113
|
+
if (options.defaultPath.endsWith(sep) || isExistingDirectory(defaultPath)) {
|
|
114
|
+
normalized.directory = defaultPath;
|
|
115
|
+
} else {
|
|
116
|
+
normalized.directory = dirname(defaultPath);
|
|
117
|
+
normalized.suggestedName = basename(defaultPath);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return normalized;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function normalizeSaveDialogOptions(
|
|
124
|
+
options: SaveDialogOptions,
|
|
125
|
+
): binding.NativeSaveDialogOptions {
|
|
126
|
+
const normalized: binding.NativeSaveDialogOptions = {
|
|
127
|
+
directory: process.cwd(),
|
|
128
|
+
filters: normalizeFileDialogFilters(options.filters),
|
|
129
|
+
showsHiddenFiles: options.showHiddenFiles ?? false,
|
|
130
|
+
};
|
|
131
|
+
if (options.title !== undefined) normalized.title = options.title;
|
|
132
|
+
if (options.buttonLabel !== undefined) normalized.prompt = options.buttonLabel;
|
|
133
|
+
if (options.defaultPath !== undefined) {
|
|
134
|
+
const defaultPath = resolve(options.defaultPath);
|
|
135
|
+
if (options.defaultPath.endsWith(sep) || isExistingDirectory(defaultPath)) {
|
|
136
|
+
normalized.directory = defaultPath;
|
|
137
|
+
} else {
|
|
138
|
+
normalized.directory = dirname(defaultPath);
|
|
139
|
+
normalized.suggestedName = basename(defaultPath);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return normalized;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeFileDialogFilters(
|
|
146
|
+
filters: readonly FileDialogFilter[] | undefined,
|
|
147
|
+
): binding.NativeFileDialogFilter[] {
|
|
148
|
+
return (filters ?? []).map((filter) => {
|
|
149
|
+
if (filter.name.length === 0 || filter.extensions.length === 0) {
|
|
150
|
+
throw new TypeError("file dialog filters require a name and at least one extension");
|
|
151
|
+
}
|
|
152
|
+
const extensions = filter.extensions.map((extension) => {
|
|
153
|
+
if (
|
|
154
|
+
extension.length === 0 ||
|
|
155
|
+
extension.startsWith(".") ||
|
|
156
|
+
extension.includes("\0") ||
|
|
157
|
+
extension.includes("/") ||
|
|
158
|
+
extension.includes("\\")
|
|
159
|
+
) {
|
|
160
|
+
throw new TypeError(
|
|
161
|
+
"file dialog filter extensions must be nonempty and omit dots and path separators",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return extension;
|
|
165
|
+
});
|
|
166
|
+
return { name: filter.name, extensions };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isExistingDirectory(path: string): boolean {
|
|
171
|
+
try {
|
|
172
|
+
return statSync(path).isDirectory();
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
package/host.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as binding from "./binding.js";
|
|
2
|
+
|
|
3
|
+
const WORKER_ENV = "QUICKGUI_APP_WORKER";
|
|
4
|
+
const WORKER_READY = "quickgui:worker-ready";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Start the application Bun isolate, then permanently hand the process main thread to AppKit.
|
|
8
|
+
* Application code stays on Bun's supported Worker event loop; the native host sleeps until either
|
|
9
|
+
* Winit or the worker posts bounded work.
|
|
10
|
+
*/
|
|
11
|
+
export async function runApplicationWorker(entrypoint: string): Promise<number> {
|
|
12
|
+
if (!Bun.isMainThread) {
|
|
13
|
+
throw new Error("the QuickGUI native host must run on Bun's process main thread");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const worker = new Worker(entrypoint, {
|
|
17
|
+
name: "quickgui-app",
|
|
18
|
+
env: { ...process.env, [WORKER_ENV]: "1" },
|
|
19
|
+
});
|
|
20
|
+
await waitForWorkerReady(worker);
|
|
21
|
+
const onReady = developmentReadyCallback();
|
|
22
|
+
return binding.runAppHost(onReady);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Report an application-worker startup failure while the main thread is blocked in AppKit. */
|
|
26
|
+
export function reportWorkerFailure(error: unknown): void {
|
|
27
|
+
const message =
|
|
28
|
+
error instanceof Error ? (error.stack ?? error.message) : `QuickGUI worker failed: ${String(error)}`;
|
|
29
|
+
binding.abortAppHost(message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function waitForWorkerReady(worker: Worker): Promise<void> {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const ready = (event: MessageEvent): void => {
|
|
35
|
+
if (event.data !== WORKER_READY) return;
|
|
36
|
+
worker.removeEventListener("error", failed);
|
|
37
|
+
worker.removeEventListener("message", ready);
|
|
38
|
+
resolve();
|
|
39
|
+
};
|
|
40
|
+
const failed = (event: ErrorEvent): void => {
|
|
41
|
+
worker.removeEventListener("message", ready);
|
|
42
|
+
reject(new Error(event.message || "QuickGUI application worker failed to start"));
|
|
43
|
+
};
|
|
44
|
+
worker.addEventListener("message", ready);
|
|
45
|
+
worker.addEventListener("error", failed, { once: true });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function developmentReadyCallback(): (() => void) | undefined {
|
|
50
|
+
if (process.env.QUICKGUI_DEV !== "1" || typeof process.send !== "function") return undefined;
|
|
51
|
+
return () => {
|
|
52
|
+
try {
|
|
53
|
+
process.send?.({ type: "quickgui-ready" });
|
|
54
|
+
} catch {
|
|
55
|
+
// The CLI may have exited while the native window was being presented.
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|