@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/integrations.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
import * as binding from "./binding.js";
|
|
4
|
+
|
|
5
|
+
export type AutoStartMode =
|
|
6
|
+
| "native"
|
|
7
|
+
| "macos-launch-agent"
|
|
8
|
+
| "macos-apple-script"
|
|
9
|
+
| "linux-systemd"
|
|
10
|
+
| "windows-system";
|
|
11
|
+
|
|
12
|
+
export interface AutoStartOptions {
|
|
13
|
+
/** Portable registration identifier: ASCII letters, digits, dots, underscores, and hyphens. */
|
|
14
|
+
appName: string;
|
|
15
|
+
/** Defaults to the current packaged executable. */
|
|
16
|
+
executable?: string;
|
|
17
|
+
arguments?: readonly string[];
|
|
18
|
+
mode?: AutoStartMode;
|
|
19
|
+
/** Required by some macOS autostart modes. */
|
|
20
|
+
bundleIdentifier?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ProtocolRegistrationOptions {
|
|
24
|
+
scheme: string;
|
|
25
|
+
appName: string;
|
|
26
|
+
/** Stable reverse-DNS identifier used for Linux desktop integration. */
|
|
27
|
+
appId: string;
|
|
28
|
+
/** Defaults to the current packaged executable. */
|
|
29
|
+
executable?: string;
|
|
30
|
+
arguments?: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UpdateClientOptions {
|
|
34
|
+
currentVersion: string;
|
|
35
|
+
/** Minisign public key, either raw or in Minisign's public-key file format. */
|
|
36
|
+
publicKey: string;
|
|
37
|
+
/** Defaults to QuickGUI's current `system-architecture` target. */
|
|
38
|
+
target?: string;
|
|
39
|
+
/** Hard download limit. The core caps this at 2 GiB. */
|
|
40
|
+
maximumDownloadBytes?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AvailableUpdate {
|
|
44
|
+
version: string;
|
|
45
|
+
currentVersion: string;
|
|
46
|
+
target: string;
|
|
47
|
+
url: string;
|
|
48
|
+
signature: string;
|
|
49
|
+
notes?: string;
|
|
50
|
+
publishedAt?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface UpdateInstallOptions {
|
|
54
|
+
targetExecutable?: string;
|
|
55
|
+
retainBackup?: boolean;
|
|
56
|
+
windowsMode?: "basic-ui" | "quiet" | "passive";
|
|
57
|
+
installerArguments?: readonly string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface InstalledUpdate {
|
|
61
|
+
version: string;
|
|
62
|
+
disposition: "applied" | "installer-launched";
|
|
63
|
+
installedPath: string;
|
|
64
|
+
backupPath?: string;
|
|
65
|
+
installerProcessId?: number;
|
|
66
|
+
requiresApplicationExit: boolean;
|
|
67
|
+
relaunchRecommended: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Native login-launch registration. Operations run outside the JavaScript event loop. */
|
|
71
|
+
export const AutoStart = Object.freeze({
|
|
72
|
+
isSupported(): boolean {
|
|
73
|
+
return binding.isAutoStartSupported();
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
enable(options: AutoStartOptions): Promise<void> {
|
|
77
|
+
return binding.enableAutoStart(nativeAutoStartOptions(options));
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
disable(options: AutoStartOptions): Promise<void> {
|
|
81
|
+
return binding.disableAutoStart(nativeAutoStartOptions(options));
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
isEnabled(options: AutoStartOptions): Promise<boolean> {
|
|
85
|
+
return binding.isAutoStartEnabled(nativeAutoStartOptions(options));
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/** Native URL-scheme registration. macOS schemes are declared in the signed app bundle. */
|
|
90
|
+
export const Protocol = Object.freeze({
|
|
91
|
+
supportsDynamicRegistration(): boolean {
|
|
92
|
+
return binding.supportsDynamicProtocolRegistration();
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async register(options: ProtocolRegistrationOptions): Promise<void> {
|
|
96
|
+
await binding.registerProtocol(nativeProtocolOptions(options));
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
unregister(options: ProtocolRegistrationOptions): Promise<boolean> {
|
|
100
|
+
return binding.unregisterProtocol(nativeProtocolOptions(options));
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
isRegistered(options: ProtocolRegistrationOptions): Promise<boolean> {
|
|
104
|
+
return binding.isProtocolRegistered(nativeProtocolOptions(options));
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/** Keychain Services, Windows Credential Manager, or Secret Service. */
|
|
109
|
+
export const SecureStorage = Object.freeze({
|
|
110
|
+
isSupported(): boolean {
|
|
111
|
+
return binding.isSecureStorageSupported();
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async set(service: string, account: string, secret: Uint8Array): Promise<void> {
|
|
115
|
+
await binding.setSecureStorage(service, account, Buffer.from(secret));
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async get(service: string, account: string): Promise<Uint8Array | undefined> {
|
|
119
|
+
const secret = await binding.getSecureStorage(service, account);
|
|
120
|
+
return secret === undefined ? undefined : new Uint8Array(secret);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async setText(service: string, account: string, value: string): Promise<void> {
|
|
124
|
+
await binding.setSecureStorage(service, account, Buffer.from(value, "utf8"));
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
async getText(service: string, account: string): Promise<string | undefined> {
|
|
128
|
+
const secret = await binding.getSecureStorage(service, account);
|
|
129
|
+
return secret?.toString("utf8");
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
delete(service: string, account: string): Promise<boolean> {
|
|
133
|
+
return binding.deleteSecureStorage(service, account);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/** Signed update discovery, staging, mandatory re-verification, and native installation. */
|
|
138
|
+
export const Updater = Object.freeze({
|
|
139
|
+
defaultTarget(): string {
|
|
140
|
+
return binding.defaultUpdateTarget();
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
async check(
|
|
144
|
+
endpoint: string,
|
|
145
|
+
options: UpdateClientOptions,
|
|
146
|
+
): Promise<AvailableUpdate | undefined> {
|
|
147
|
+
const update = await binding.checkForUpdate(endpoint, nativeUpdateOptions(options));
|
|
148
|
+
return update === undefined ? undefined : normalizeAvailableUpdate(update);
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
downloadAndStage(
|
|
152
|
+
update: AvailableUpdate,
|
|
153
|
+
destinationDirectory: string,
|
|
154
|
+
options: UpdateClientOptions,
|
|
155
|
+
): Promise<string> {
|
|
156
|
+
return binding.stageUpdate(
|
|
157
|
+
nativeAvailableUpdate(update),
|
|
158
|
+
destinationDirectory,
|
|
159
|
+
nativeUpdateOptions(options),
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
verify(
|
|
164
|
+
path: string,
|
|
165
|
+
signature: string,
|
|
166
|
+
options: UpdateClientOptions,
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
return binding.verifyUpdate(path, signature, nativeUpdateOptions(options));
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
async install(
|
|
172
|
+
update: AvailableUpdate,
|
|
173
|
+
artifact: string,
|
|
174
|
+
installOptions: UpdateInstallOptions,
|
|
175
|
+
options: UpdateClientOptions,
|
|
176
|
+
): Promise<InstalledUpdate> {
|
|
177
|
+
const native: binding.NativeUpdateInstallOptions = {};
|
|
178
|
+
if (installOptions.targetExecutable !== undefined) {
|
|
179
|
+
native.targetExecutable = resolvePath(installOptions.targetExecutable);
|
|
180
|
+
}
|
|
181
|
+
if (installOptions.retainBackup !== undefined) {
|
|
182
|
+
native.retainBackup = installOptions.retainBackup;
|
|
183
|
+
}
|
|
184
|
+
if (installOptions.windowsMode !== undefined) native.windowsMode = installOptions.windowsMode;
|
|
185
|
+
if (installOptions.installerArguments !== undefined) {
|
|
186
|
+
native.installerArguments = [...installOptions.installerArguments];
|
|
187
|
+
}
|
|
188
|
+
const installed = await binding.installUpdate(
|
|
189
|
+
nativeAvailableUpdate(update),
|
|
190
|
+
resolvePath(artifact),
|
|
191
|
+
native,
|
|
192
|
+
nativeUpdateOptions(options),
|
|
193
|
+
);
|
|
194
|
+
return { ...installed } as InstalledUpdate;
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
function nativeAutoStartOptions(options: AutoStartOptions): binding.NativeAutoStartOptions {
|
|
199
|
+
const native: binding.NativeAutoStartOptions = { appName: options.appName };
|
|
200
|
+
if (options.executable !== undefined) native.executable = options.executable;
|
|
201
|
+
if (options.arguments !== undefined) native.arguments = [...options.arguments];
|
|
202
|
+
if (options.mode !== undefined) native.mode = options.mode;
|
|
203
|
+
if (options.bundleIdentifier !== undefined) {
|
|
204
|
+
native.bundleIdentifier = options.bundleIdentifier;
|
|
205
|
+
}
|
|
206
|
+
return native;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function nativeProtocolOptions(
|
|
210
|
+
options: ProtocolRegistrationOptions,
|
|
211
|
+
): binding.NativeProtocolRegistrationOptions {
|
|
212
|
+
const native: binding.NativeProtocolRegistrationOptions = {
|
|
213
|
+
scheme: options.scheme,
|
|
214
|
+
appName: options.appName,
|
|
215
|
+
appId: options.appId,
|
|
216
|
+
};
|
|
217
|
+
if (options.executable !== undefined) native.executable = options.executable;
|
|
218
|
+
if (options.arguments !== undefined) native.arguments = [...options.arguments];
|
|
219
|
+
return native;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function nativeUpdateOptions(options: UpdateClientOptions): binding.NativeUpdateClientOptions {
|
|
223
|
+
const native: binding.NativeUpdateClientOptions = {
|
|
224
|
+
currentVersion: options.currentVersion,
|
|
225
|
+
publicKey: options.publicKey,
|
|
226
|
+
};
|
|
227
|
+
if (options.target !== undefined) native.target = options.target;
|
|
228
|
+
if (options.maximumDownloadBytes !== undefined) {
|
|
229
|
+
native.maximumDownloadBytes = options.maximumDownloadBytes;
|
|
230
|
+
}
|
|
231
|
+
return native;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function nativeAvailableUpdate(update: AvailableUpdate): binding.NativeAvailableUpdate {
|
|
235
|
+
const native: binding.NativeAvailableUpdate = {
|
|
236
|
+
version: update.version,
|
|
237
|
+
currentVersion: update.currentVersion,
|
|
238
|
+
target: update.target,
|
|
239
|
+
url: update.url,
|
|
240
|
+
signature: update.signature,
|
|
241
|
+
};
|
|
242
|
+
if (update.notes !== undefined) native.notes = update.notes;
|
|
243
|
+
if (update.publishedAt !== undefined) native.publishedAt = update.publishedAt;
|
|
244
|
+
return native;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function normalizeAvailableUpdate(update: binding.NativeAvailableUpdate): AvailableUpdate {
|
|
248
|
+
const normalized: AvailableUpdate = {
|
|
249
|
+
version: update.version,
|
|
250
|
+
currentVersion: update.currentVersion,
|
|
251
|
+
target: update.target,
|
|
252
|
+
url: update.url,
|
|
253
|
+
signature: update.signature,
|
|
254
|
+
};
|
|
255
|
+
if (update.notes !== undefined) normalized.notes = update.notes;
|
|
256
|
+
if (update.publishedAt !== undefined) normalized.publishedAt = update.publishedAt;
|
|
257
|
+
return normalized;
|
|
258
|
+
}
|
package/native-tree.ts
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NativeNodeTag,
|
|
3
|
+
PropertyCode,
|
|
4
|
+
type NativePropertyValue,
|
|
5
|
+
} from "./protocol.ts";
|
|
6
|
+
|
|
7
|
+
export type ColorValue = number | string;
|
|
8
|
+
export type NativeElementName =
|
|
9
|
+
| "view"
|
|
10
|
+
| "div"
|
|
11
|
+
| "text"
|
|
12
|
+
| "button"
|
|
13
|
+
| "input"
|
|
14
|
+
| "textarea"
|
|
15
|
+
| "markdown"
|
|
16
|
+
| "virtual-list"
|
|
17
|
+
| "terminal"
|
|
18
|
+
| "svg";
|
|
19
|
+
export type NativeEventType =
|
|
20
|
+
| "click"
|
|
21
|
+
| "mouseenter"
|
|
22
|
+
| "mouseleave"
|
|
23
|
+
| "input"
|
|
24
|
+
| "submit"
|
|
25
|
+
| "dismiss"
|
|
26
|
+
| "terminal"
|
|
27
|
+
| "pointer";
|
|
28
|
+
export type NativeEventListener = (event: QuickGuiEvent) => void;
|
|
29
|
+
|
|
30
|
+
export interface NativeNodeHost {
|
|
31
|
+
readonly nativeId: number;
|
|
32
|
+
readonly app: { readonly nativeId: number };
|
|
33
|
+
readonly nodes: Map<number, NativeNode>;
|
|
34
|
+
readonly closed: boolean;
|
|
35
|
+
flush(): number | undefined;
|
|
36
|
+
_trackMount(dispose: () => void): () => void;
|
|
37
|
+
_focusNode(node: NativeNode): boolean;
|
|
38
|
+
_enqueueCreate(node: NativeNode): void;
|
|
39
|
+
_enqueueProperty(
|
|
40
|
+
node: NativeNode,
|
|
41
|
+
property: PropertyCode,
|
|
42
|
+
value: NativePropertyValue,
|
|
43
|
+
color: boolean,
|
|
44
|
+
): void;
|
|
45
|
+
_enqueueText(node: NativeNode): void;
|
|
46
|
+
_enqueueInsert(
|
|
47
|
+
parent: NativeNode,
|
|
48
|
+
child: NativeNode,
|
|
49
|
+
before?: NativeNode,
|
|
50
|
+
): void;
|
|
51
|
+
_enqueueRemove(parent: NativeNode, child: NativeNode): void;
|
|
52
|
+
_enqueueCleanup(parent: NativeNode, children: readonly NativeNode[]): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let nextNodeId = 1;
|
|
56
|
+
|
|
57
|
+
export class NativeNode {
|
|
58
|
+
readonly id: number;
|
|
59
|
+
readonly tag: NativeNodeTag;
|
|
60
|
+
text: string;
|
|
61
|
+
parent: NativeNode | undefined;
|
|
62
|
+
readonly children: NativeNode[] = [];
|
|
63
|
+
readonly properties = new Map<PropertyCode, NativePropertyValue>();
|
|
64
|
+
readonly colorProperties = new Set<PropertyCode>();
|
|
65
|
+
readonly listeners = new Map<NativeEventType, NativeEventListener>();
|
|
66
|
+
host: NativeNodeHost | undefined;
|
|
67
|
+
materialized = false;
|
|
68
|
+
|
|
69
|
+
constructor(tag: NativeNodeTag, text = "", id = allocateNodeId()) {
|
|
70
|
+
this.id = id;
|
|
71
|
+
this.tag = tag;
|
|
72
|
+
this.text = text;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Focus this mounted node, matching the web `HTMLElement.focus()` shape. */
|
|
76
|
+
focus(): boolean {
|
|
77
|
+
return this.host?._focusNode(this) ?? false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class QuickGuiEvent {
|
|
82
|
+
readonly type: NativeEventType;
|
|
83
|
+
readonly target: NativeNode;
|
|
84
|
+
currentTarget: NativeNode;
|
|
85
|
+
defaultPrevented = false;
|
|
86
|
+
propagationStopped = false;
|
|
87
|
+
readonly value: string | undefined;
|
|
88
|
+
|
|
89
|
+
constructor(type: NativeEventType, target: NativeNode, value?: string) {
|
|
90
|
+
this.type = type;
|
|
91
|
+
this.target = target;
|
|
92
|
+
this.currentTarget = target;
|
|
93
|
+
this.value = value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
preventDefault(): void {
|
|
97
|
+
this.defaultPrevented = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
stopPropagation(): void {
|
|
101
|
+
this.propagationStopped = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createNativeElement(name: NativeElementName): NativeNode {
|
|
106
|
+
const tag =
|
|
107
|
+
name === "button"
|
|
108
|
+
? NativeNodeTag.Button
|
|
109
|
+
: name === "input" || name === "textarea"
|
|
110
|
+
? NativeNodeTag.Input
|
|
111
|
+
: name === "markdown"
|
|
112
|
+
? NativeNodeTag.Markdown
|
|
113
|
+
: name === "virtual-list"
|
|
114
|
+
? NativeNodeTag.VirtualList
|
|
115
|
+
: name === "terminal"
|
|
116
|
+
? NativeNodeTag.Terminal
|
|
117
|
+
: name === "svg"
|
|
118
|
+
? NativeNodeTag.Svg
|
|
119
|
+
: NativeNodeTag.View;
|
|
120
|
+
const node = new NativeNode(tag);
|
|
121
|
+
if (name === "textarea")
|
|
122
|
+
setNativeProperty(node, PropertyCode.Multiline, true);
|
|
123
|
+
return node;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function createNativeText(value: string): NativeNode {
|
|
127
|
+
return new NativeNode(NativeNodeTag.Text, value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createNativeSentinel(): NativeNode {
|
|
131
|
+
return new NativeNode(NativeNodeTag.Sentinel);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function replaceNativeText(node: NativeNode, value: string): void {
|
|
135
|
+
if (node.tag !== NativeNodeTag.Text)
|
|
136
|
+
throw new TypeError("replaceText expects a text node");
|
|
137
|
+
if (node.text === value) return;
|
|
138
|
+
node.text = value;
|
|
139
|
+
if (node.materialized) node.host?._enqueueText(node);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function setNativeProperty(
|
|
143
|
+
node: NativeNode,
|
|
144
|
+
property: PropertyCode,
|
|
145
|
+
value: NativePropertyValue,
|
|
146
|
+
options: { color?: boolean } = {},
|
|
147
|
+
): void {
|
|
148
|
+
const normalized = value ?? null;
|
|
149
|
+
if (normalized === null) {
|
|
150
|
+
if (!node.properties.delete(property)) return;
|
|
151
|
+
node.colorProperties.delete(property);
|
|
152
|
+
} else {
|
|
153
|
+
const previous = node.properties.get(property);
|
|
154
|
+
if (
|
|
155
|
+
Object.is(previous, normalized) &&
|
|
156
|
+
node.colorProperties.has(property) === !!options.color
|
|
157
|
+
) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
node.properties.set(property, normalized);
|
|
161
|
+
if (options.color) node.colorProperties.add(property);
|
|
162
|
+
else node.colorProperties.delete(property);
|
|
163
|
+
}
|
|
164
|
+
if (node.materialized) {
|
|
165
|
+
node.host?._enqueueProperty(node, property, normalized, !!options.color);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function setNativeEventListener(
|
|
170
|
+
node: NativeNode,
|
|
171
|
+
type: NativeEventType,
|
|
172
|
+
listener: NativeEventListener | undefined,
|
|
173
|
+
): void {
|
|
174
|
+
if (listener) node.listeners.set(type, listener);
|
|
175
|
+
else node.listeners.delete(type);
|
|
176
|
+
if (type === "click") {
|
|
177
|
+
setNativeProperty(
|
|
178
|
+
node,
|
|
179
|
+
PropertyCode.ClickListener,
|
|
180
|
+
node.listeners.has("click"),
|
|
181
|
+
);
|
|
182
|
+
} else if (type === "input") {
|
|
183
|
+
setNativeProperty(
|
|
184
|
+
node,
|
|
185
|
+
PropertyCode.InputListener,
|
|
186
|
+
node.listeners.has("input"),
|
|
187
|
+
);
|
|
188
|
+
} else if (type === "submit") {
|
|
189
|
+
setNativeProperty(
|
|
190
|
+
node,
|
|
191
|
+
PropertyCode.SubmitListener,
|
|
192
|
+
node.listeners.has("submit"),
|
|
193
|
+
);
|
|
194
|
+
} else if (type === "dismiss") {
|
|
195
|
+
setNativeProperty(
|
|
196
|
+
node,
|
|
197
|
+
PropertyCode.DismissListener,
|
|
198
|
+
node.listeners.has("dismiss"),
|
|
199
|
+
);
|
|
200
|
+
} else if (type === "terminal") {
|
|
201
|
+
setNativeProperty(
|
|
202
|
+
node,
|
|
203
|
+
PropertyCode.TerminalStatusListener,
|
|
204
|
+
node.listeners.has("terminal"),
|
|
205
|
+
);
|
|
206
|
+
} else if (type === "pointer") {
|
|
207
|
+
setNativeProperty(
|
|
208
|
+
node,
|
|
209
|
+
PropertyCode.PointerListener,
|
|
210
|
+
node.listeners.has("pointer"),
|
|
211
|
+
);
|
|
212
|
+
} else {
|
|
213
|
+
const listensForHover =
|
|
214
|
+
node.listeners.has("mouseenter") || node.listeners.has("mouseleave");
|
|
215
|
+
setNativeProperty(node, PropertyCode.HoverListener, listensForHover);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function insertNativeNode(
|
|
220
|
+
parent: NativeNode,
|
|
221
|
+
node: NativeNode,
|
|
222
|
+
anchor?: NativeNode,
|
|
223
|
+
): void {
|
|
224
|
+
if (anchor && anchor.parent !== parent)
|
|
225
|
+
throw new Error("anchor is not a child of parent");
|
|
226
|
+
if (node === parent) throw new Error("a native node cannot contain itself");
|
|
227
|
+
if (anchor === node && node.parent === parent) return;
|
|
228
|
+
|
|
229
|
+
if (node.parent) {
|
|
230
|
+
const previousIndex = node.parent.children.indexOf(node);
|
|
231
|
+
if (previousIndex >= 0) node.parent.children.splice(previousIndex, 1);
|
|
232
|
+
}
|
|
233
|
+
const index = anchor
|
|
234
|
+
? parent.children.indexOf(anchor)
|
|
235
|
+
: parent.children.length;
|
|
236
|
+
parent.children.splice(index, 0, node);
|
|
237
|
+
node.parent = parent;
|
|
238
|
+
|
|
239
|
+
if (parent.host) {
|
|
240
|
+
materialize(node, parent.host);
|
|
241
|
+
parent.host._enqueueInsert(parent, node, anchor);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function removeNativeNode(parent: NativeNode, node: NativeNode): void {
|
|
246
|
+
if (node.parent !== parent) return;
|
|
247
|
+
const index = parent.children.indexOf(node);
|
|
248
|
+
if (index >= 0) parent.children.splice(index, 1);
|
|
249
|
+
node.parent = undefined;
|
|
250
|
+
if (node.materialized && parent.host)
|
|
251
|
+
parent.host._enqueueRemove(parent, node);
|
|
252
|
+
dematerialize(node);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function cleanupNativeNodes(
|
|
256
|
+
parent: NativeNode,
|
|
257
|
+
nodes: readonly NativeNode[],
|
|
258
|
+
): void {
|
|
259
|
+
const attached = nodes.filter((node) => node.parent === parent);
|
|
260
|
+
if (attached.length === 0) return;
|
|
261
|
+
for (const node of attached) {
|
|
262
|
+
const index = parent.children.indexOf(node);
|
|
263
|
+
if (index >= 0) parent.children.splice(index, 1);
|
|
264
|
+
node.parent = undefined;
|
|
265
|
+
}
|
|
266
|
+
if (parent.host) parent.host._enqueueCleanup(parent, attached);
|
|
267
|
+
for (const node of attached) dematerialize(node);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function getNativeParent(node: NativeNode): NativeNode | undefined {
|
|
271
|
+
return node.parent;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function getNativeFirstChild(node: NativeNode): NativeNode | undefined {
|
|
275
|
+
return node.children[0];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function getNativeNextSibling(node: NativeNode): NativeNode | undefined {
|
|
279
|
+
if (!node.parent) return undefined;
|
|
280
|
+
const index = node.parent.children.indexOf(node);
|
|
281
|
+
return index < 0 ? undefined : node.parent.children[index + 1];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function isNativeText(node: NativeNode): boolean {
|
|
285
|
+
return node.tag === NativeNodeTag.Text;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function parseColor(value: ColorValue): number {
|
|
289
|
+
if (typeof value === "number") return value >>> 0;
|
|
290
|
+
const color = value.trim().toLowerCase();
|
|
291
|
+
if (color === "transparent") return 0;
|
|
292
|
+
if (color === "black") return packColor(0, 0, 0, 255);
|
|
293
|
+
if (color === "white") return packColor(255, 255, 255, 255);
|
|
294
|
+
if (color.startsWith("#")) {
|
|
295
|
+
const hex = color.slice(1);
|
|
296
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
297
|
+
const [r = "0", g = "0", b = "0", a = "f"] = hex;
|
|
298
|
+
return packColor(
|
|
299
|
+
Number.parseInt(r + r, 16),
|
|
300
|
+
Number.parseInt(g + g, 16),
|
|
301
|
+
Number.parseInt(b + b, 16),
|
|
302
|
+
Number.parseInt(a + a, 16),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (hex.length === 6 || hex.length === 8) {
|
|
306
|
+
return packColor(
|
|
307
|
+
Number.parseInt(hex.slice(0, 2), 16),
|
|
308
|
+
Number.parseInt(hex.slice(2, 4), 16),
|
|
309
|
+
Number.parseInt(hex.slice(4, 6), 16),
|
|
310
|
+
hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) : 255,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const rgb = color.match(/^rgba?\(([^)]+)\)$/);
|
|
315
|
+
if (rgb) {
|
|
316
|
+
const parts = rgb[1]?.split(",").map((part) => part.trim()) ?? [];
|
|
317
|
+
if (parts.length === 3 || parts.length === 4) {
|
|
318
|
+
return packColor(
|
|
319
|
+
Number(parts[0]),
|
|
320
|
+
Number(parts[1]),
|
|
321
|
+
Number(parts[2]),
|
|
322
|
+
parts[3] === undefined ? 255 : Math.round(Number(parts[3]) * 255),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
throw new TypeError(`unsupported QuickGUI color \`${value}\``);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function packColor(r: number, g: number, b: number, a: number): number {
|
|
330
|
+
const component = (value: number) =>
|
|
331
|
+
Math.max(0, Math.min(255, Math.round(value)));
|
|
332
|
+
return (
|
|
333
|
+
(component(r) |
|
|
334
|
+
(component(g) << 8) |
|
|
335
|
+
(component(b) << 16) |
|
|
336
|
+
(component(a) << 24)) >>>
|
|
337
|
+
0
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function allocateNodeId(): number {
|
|
342
|
+
if (nextNodeId >= 0xffff_ffff)
|
|
343
|
+
throw new Error("QuickGUI native node id space exhausted");
|
|
344
|
+
return nextNodeId++;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function materialize(node: NativeNode, host: NativeNodeHost): void {
|
|
348
|
+
if (node.materialized) {
|
|
349
|
+
if (node.host !== host)
|
|
350
|
+
throw new Error("a native node cannot move between QuickGUI windows");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
node.host = host;
|
|
354
|
+
node.materialized = true;
|
|
355
|
+
host.nodes.set(node.id, node);
|
|
356
|
+
host._enqueueCreate(node);
|
|
357
|
+
for (const [property, value] of node.properties) {
|
|
358
|
+
host._enqueueProperty(
|
|
359
|
+
node,
|
|
360
|
+
property,
|
|
361
|
+
value,
|
|
362
|
+
node.colorProperties.has(property),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
for (const child of node.children) {
|
|
366
|
+
materialize(child, host);
|
|
367
|
+
host._enqueueInsert(node, child);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function dematerialize(node: NativeNode): void {
|
|
372
|
+
const host = node.host;
|
|
373
|
+
if (host) host.nodes.delete(node.id);
|
|
374
|
+
node.materialized = false;
|
|
375
|
+
node.host = undefined;
|
|
376
|
+
for (const child of node.children) dematerialize(child);
|
|
377
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quickgui/native",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Low-overhead Bun and Node-API host for QuickGUI",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/egoist/quickgui.git",
|
|
8
|
+
"directory": "packages/native"
|
|
9
|
+
},
|
|
10
|
+
"bugs": "https://github.com/egoist/quickgui/issues",
|
|
11
|
+
"homepage": "https://github.com/egoist/quickgui#readme",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./index.ts",
|
|
15
|
+
"./host": "./host.ts",
|
|
16
|
+
"./protocol": "./protocol.ts"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.ts",
|
|
20
|
+
"dialog.ts",
|
|
21
|
+
"integrations.ts",
|
|
22
|
+
"native-tree.ts",
|
|
23
|
+
"single-instance.ts",
|
|
24
|
+
"system.ts",
|
|
25
|
+
"tray.ts",
|
|
26
|
+
"host.ts",
|
|
27
|
+
"protocol.ts",
|
|
28
|
+
"scripts",
|
|
29
|
+
"binding.js",
|
|
30
|
+
"binding.d.ts",
|
|
31
|
+
"*.node",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "bun ./scripts/build.ts",
|
|
36
|
+
"build:debug": "bun ./scripts/build.ts --debug",
|
|
37
|
+
"test": "bun test"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@napi-rs/cli": "3.8.6"
|
|
41
|
+
},
|
|
42
|
+
"napi": {
|
|
43
|
+
"binaryName": "quickgui-native",
|
|
44
|
+
"targets": [
|
|
45
|
+
"aarch64-apple-darwin",
|
|
46
|
+
"x86_64-apple-darwin",
|
|
47
|
+
"aarch64-unknown-linux-gnu",
|
|
48
|
+
"x86_64-unknown-linux-gnu",
|
|
49
|
+
"aarch64-pc-windows-msvc",
|
|
50
|
+
"x86_64-pc-windows-msvc"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"bun": ">=1.3.0"
|
|
55
|
+
},
|
|
56
|
+
"os": [
|
|
57
|
+
"darwin"
|
|
58
|
+
],
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"license": "MIT OR Apache-2.0"
|
|
63
|
+
}
|