sparkbun 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/sparkbun.cjs +18 -0
- package/dist-linux-arm64/bsdiff +0 -0
- package/dist-linux-arm64/bspatch +0 -0
- package/dist-linux-arm64/libElectrobunCore.so +0 -0
- package/dist-linux-arm64/libNativeWrapper.so +0 -0
- package/dist-linux-arm64/libasar.so +0 -0
- package/dist-linux-x64/bsdiff +0 -0
- package/dist-linux-x64/bspatch +0 -0
- package/dist-linux-x64/libElectrobunCore.so +0 -0
- package/dist-linux-x64/libNativeWrapper.so +0 -0
- package/dist-linux-x64/libasar.so +0 -0
- package/dist-macos-arm64/bsdiff +0 -0
- package/dist-macos-arm64/bspatch +0 -0
- package/dist-macos-arm64/libElectrobunCore.dylib +0 -0
- package/dist-macos-arm64/libNativeWrapper.dylib +0 -0
- package/dist-macos-arm64/libasar.dylib +0 -0
- package/dist-macos-arm64/libwebgpu_dawn.dylib +0 -0
- package/dist-macos-arm64/preload-full.js +885 -0
- package/dist-macos-arm64/preload-sandboxed.js +111 -0
- package/dist-macos-arm64/process_helper +0 -0
- package/dist-win-x64/ElectrobunCore.dll +0 -0
- package/dist-win-x64/WebView2Loader.dll +0 -0
- package/dist-win-x64/bsdiff.exe +0 -0
- package/dist-win-x64/bspatch.exe +0 -0
- package/dist-win-x64/libNativeWrapper.dll +0 -0
- package/dist-win-x64/zig-asar/arm64/libasar.dll +0 -0
- package/dist-win-x64/zig-asar/x64/libasar.dll +0 -0
- package/package.json +47 -0
- package/scripts/build-and-upload-artifacts.js +207 -0
- package/scripts/gen-webgpu-ffi.mjs +162 -0
- package/scripts/install-windows-deps.ps1 +80 -0
- package/scripts/package-release.js +237 -0
- package/scripts/push-version.js +84 -0
- package/scripts/update-bun-version.ts +122 -0
- package/scripts/update-cef-version.ts +145 -0
- package/src/browser/builtinrpcSchema.ts +19 -0
- package/src/browser/global.d.ts +36 -0
- package/src/browser/index.ts +234 -0
- package/src/browser/webviewtag.ts +88 -0
- package/src/browser/wgputag.ts +48 -0
- package/src/bun/SparkBunConfig.ts +497 -0
- package/src/bun/__tests__/ffi-contract.test.ts +105 -0
- package/src/bun/core/ApplicationMenu.ts +70 -0
- package/src/bun/core/BrowserView.ts +416 -0
- package/src/bun/core/BrowserWindow.ts +396 -0
- package/src/bun/core/BuildConfig.ts +71 -0
- package/src/bun/core/ContextMenu.ts +75 -0
- package/src/bun/core/GpuWindow.ts +289 -0
- package/src/bun/core/Paths.ts +5 -0
- package/src/bun/core/Socket.ts +22 -0
- package/src/bun/core/Tray.ts +197 -0
- package/src/bun/core/Updater.ts +1131 -0
- package/src/bun/core/Utils.ts +487 -0
- package/src/bun/core/WGPUView.ts +167 -0
- package/src/bun/core/menuRoles.ts +181 -0
- package/src/bun/events/ApplicationEvents.ts +22 -0
- package/src/bun/events/event.ts +27 -0
- package/src/bun/events/eventEmitter.ts +45 -0
- package/src/bun/events/trayEvents.ts +11 -0
- package/src/bun/events/webviewEvents.ts +39 -0
- package/src/bun/events/windowEvents.ts +23 -0
- package/src/bun/index.ts +120 -0
- package/src/bun/preload/.generated/compiled.ts +2 -0
- package/src/bun/preload/build.ts +65 -0
- package/src/bun/preload/dragRegions.ts +41 -0
- package/src/bun/preload/encryption.ts +86 -0
- package/src/bun/preload/events.ts +171 -0
- package/src/bun/preload/globals.d.ts +45 -0
- package/src/bun/preload/index-sandboxed.ts +28 -0
- package/src/bun/preload/index.ts +77 -0
- package/src/bun/preload/internalRpc.ts +80 -0
- package/src/bun/preload/overlaySync.ts +107 -0
- package/src/bun/preload/webviewTag.ts +451 -0
- package/src/bun/preload/wgpuTag.ts +246 -0
- package/src/bun/proc/linux.md +43 -0
- package/src/bun/proc/native.ts +3253 -0
- package/src/bun/webGPU.ts +346 -0
- package/src/bun/webgpuAdapter.ts +3011 -0
- package/src/cli/bun.lockb +0 -0
- package/src/cli/index.ts +4653 -0
- package/src/cli/package-lock.json +81 -0
- package/src/cli/package.json +11 -0
- package/src/cli/templates/embedded.ts +2 -0
- package/src/core/build.zig +16 -0
- package/src/core/main.zig +3378 -0
- package/src/extractor/build.zig +22 -0
- package/src/installer/installer-template.ts +216 -0
- package/src/launcher/main.ts +221 -0
- package/src/native/build/libNativeWrapper.so +0 -0
- package/src/native/linux/build/nativeWrapper.o +0 -0
- package/src/native/linux/cef_loader.cpp +110 -0
- package/src/native/linux/cef_loader.h +28 -0
- package/src/native/linux/cef_process_helper_linux.cpp +160 -0
- package/src/native/linux/nativeWrapper.cpp +11768 -0
- package/src/native/macos/cef_process_helper_mac.cc +160 -0
- package/src/native/macos/nativeWrapper.mm +9172 -0
- package/src/native/shared/accelerator_parser.h +72 -0
- package/src/native/shared/app_paths.h +110 -0
- package/src/native/shared/asar.h +35 -0
- package/src/native/shared/cache_migration.h +244 -0
- package/src/native/shared/callbacks.h +57 -0
- package/src/native/shared/cef_response_filter.h +189 -0
- package/src/native/shared/chromium_flags.h +181 -0
- package/src/native/shared/config.h +66 -0
- package/src/native/shared/download_event.h +197 -0
- package/src/native/shared/ffi_helpers.h +139 -0
- package/src/native/shared/glob_match.h +59 -0
- package/src/native/shared/json_menu_parser.h +223 -0
- package/src/native/shared/mime_types.h +101 -0
- package/src/native/shared/navigation_rules.h +98 -0
- package/src/native/shared/partition_context.h +137 -0
- package/src/native/shared/pending_resize_queue.h +45 -0
- package/src/native/shared/permissions.h +118 -0
- package/src/native/shared/permissions_cef.h +74 -0
- package/src/native/shared/preload_script.h +71 -0
- package/src/native/shared/shutdown_guard.h +134 -0
- package/src/native/shared/thread_safe_map.h +138 -0
- package/src/native/shared/webview_storage.h +91 -0
- package/src/native/win/cef_process_helper_win.cpp +143 -0
- package/src/native/win/dcomp_compositor.h +352 -0
- package/src/native/win/nativeWrapper.cpp +12434 -0
- package/src/npmbin/index.js +34 -0
- package/src/shared/bun-version.ts +3 -0
- package/src/shared/cef-version.ts +5 -0
- package/src/shared/naming.test.ts +327 -0
- package/src/shared/naming.ts +188 -0
- package/src/shared/platform.ts +48 -0
- package/src/shared/rpc.ts +541 -0
- package/src/shared/sparkbun-version.ts +2 -0
- package/src/types/three.d.ts +1 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { ffi } from "../proc/native";
|
|
2
|
+
import sparkBunEventEmitter from "../events/eventEmitter";
|
|
3
|
+
import { type Pointer } from "bun:ffi";
|
|
4
|
+
import { WGPUView } from "./WGPUView";
|
|
5
|
+
import { BuildConfig } from "./BuildConfig";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export type GpuWindowOptionsType = {
|
|
9
|
+
trafficLightOffset?: {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
};
|
|
13
|
+
activate?: boolean;
|
|
14
|
+
title: string;
|
|
15
|
+
frame: {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
};
|
|
21
|
+
styleMask?: {};
|
|
22
|
+
titleBarStyle: "hidden" | "hiddenInset" | "default";
|
|
23
|
+
transparent: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const defaultOptions: GpuWindowOptionsType = {
|
|
27
|
+
title: "SparkBun",
|
|
28
|
+
frame: {
|
|
29
|
+
x: 0,
|
|
30
|
+
y: 0,
|
|
31
|
+
width: 800,
|
|
32
|
+
height: 600,
|
|
33
|
+
},
|
|
34
|
+
titleBarStyle: "default",
|
|
35
|
+
transparent: false,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const buildConfig = BuildConfig.getSync();
|
|
39
|
+
ffi.request.setExitOnLastWindowClosed({
|
|
40
|
+
enabled: buildConfig.runtime?.exitOnLastWindowClosed ?? true,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const GpuWindowMap: {
|
|
44
|
+
[id: number]: GpuWindow;
|
|
45
|
+
} = {};
|
|
46
|
+
|
|
47
|
+
// Clean up JS wrapper state when a window closes. Native child cleanup is core-owned.
|
|
48
|
+
sparkBunEventEmitter.on("close", (event: { data: { id: number } }) => {
|
|
49
|
+
const windowId = event.data.id;
|
|
50
|
+
delete GpuWindowMap[windowId];
|
|
51
|
+
|
|
52
|
+
// Clean up all WGPU views associated with this window
|
|
53
|
+
for (const view of WGPUView.getAll()) {
|
|
54
|
+
if (view.windowId === windowId) {
|
|
55
|
+
view.remove();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export class GpuWindow {
|
|
62
|
+
id = 0;
|
|
63
|
+
title: string = "SparkBun";
|
|
64
|
+
state: "creating" | "created" = "creating";
|
|
65
|
+
transparent: boolean = false;
|
|
66
|
+
trafficLightOffset: { x: number; y: number } = { x: 0, y: 0 };
|
|
67
|
+
frame: {
|
|
68
|
+
x: number;
|
|
69
|
+
y: number;
|
|
70
|
+
width: number;
|
|
71
|
+
height: number;
|
|
72
|
+
} = {
|
|
73
|
+
x: 0,
|
|
74
|
+
y: 0,
|
|
75
|
+
width: 800,
|
|
76
|
+
height: 600,
|
|
77
|
+
};
|
|
78
|
+
wgpuViewId!: number;
|
|
79
|
+
|
|
80
|
+
get ptr(): Pointer | null {
|
|
81
|
+
return ffi.request.getWindowPointer({ winId: this.id }) as Pointer | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
constructor(options: Partial<GpuWindowOptionsType> = defaultOptions) {
|
|
85
|
+
this.title = options.title || "New Window";
|
|
86
|
+
this.frame = options.frame
|
|
87
|
+
? { ...defaultOptions.frame, ...options.frame }
|
|
88
|
+
: { ...defaultOptions.frame };
|
|
89
|
+
this.transparent = options.transparent ?? false;
|
|
90
|
+
this.trafficLightOffset = {
|
|
91
|
+
x: options.trafficLightOffset?.x ?? 0,
|
|
92
|
+
y: options.trafficLightOffset?.y ?? 0,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
this.init(options);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
init({
|
|
99
|
+
styleMask,
|
|
100
|
+
titleBarStyle,
|
|
101
|
+
transparent,
|
|
102
|
+
activate,
|
|
103
|
+
}: Partial<GpuWindowOptionsType>) {
|
|
104
|
+
const windowId = ffi.request.createWindow({
|
|
105
|
+
title: this.title,
|
|
106
|
+
url: "",
|
|
107
|
+
frame: {
|
|
108
|
+
width: this.frame.width,
|
|
109
|
+
height: this.frame.height,
|
|
110
|
+
x: this.frame.x,
|
|
111
|
+
y: this.frame.y,
|
|
112
|
+
},
|
|
113
|
+
styleMask: {
|
|
114
|
+
Borderless: false,
|
|
115
|
+
Titled: true,
|
|
116
|
+
Closable: true,
|
|
117
|
+
Miniaturizable: true,
|
|
118
|
+
Resizable: true,
|
|
119
|
+
UnifiedTitleAndToolbar: false,
|
|
120
|
+
FullScreen: false,
|
|
121
|
+
FullSizeContentView: false,
|
|
122
|
+
UtilityWindow: false,
|
|
123
|
+
DocModalWindow: false,
|
|
124
|
+
NonactivatingPanel: false,
|
|
125
|
+
HUDWindow: false,
|
|
126
|
+
...(styleMask || {}),
|
|
127
|
+
// hiddenInset: transparent titlebar with inset native controls
|
|
128
|
+
...(titleBarStyle === "hiddenInset"
|
|
129
|
+
? {
|
|
130
|
+
Titled: true,
|
|
131
|
+
FullSizeContentView: true,
|
|
132
|
+
}
|
|
133
|
+
: {}),
|
|
134
|
+
// hidden: no titlebar, no native controls (for fully custom chrome)
|
|
135
|
+
...(titleBarStyle === "hidden"
|
|
136
|
+
? {
|
|
137
|
+
Titled: false,
|
|
138
|
+
FullSizeContentView: true,
|
|
139
|
+
}
|
|
140
|
+
: {}),
|
|
141
|
+
},
|
|
142
|
+
titleBarStyle: titleBarStyle || "default",
|
|
143
|
+
transparent: transparent ?? false,
|
|
144
|
+
activate: activate ?? true,
|
|
145
|
+
trafficLightOffset: this.trafficLightOffset,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!windowId) {
|
|
149
|
+
throw "Failed to create window";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.id = windowId as number;
|
|
153
|
+
|
|
154
|
+
GpuWindowMap[this.id] = this;
|
|
155
|
+
|
|
156
|
+
const wgpuView = new WGPUView({
|
|
157
|
+
frame: {
|
|
158
|
+
x: 0,
|
|
159
|
+
y: 0,
|
|
160
|
+
width: this.frame.width,
|
|
161
|
+
height: this.frame.height,
|
|
162
|
+
},
|
|
163
|
+
windowId: this.id,
|
|
164
|
+
autoResize: true,
|
|
165
|
+
startTransparent: false,
|
|
166
|
+
startPassthrough: false,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
this.wgpuViewId = wgpuView.id;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get wgpuView() {
|
|
173
|
+
return WGPUView.getById(this.wgpuViewId) as WGPUView;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static getById(id: number) {
|
|
177
|
+
return GpuWindowMap[id];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setTitle(title: string) {
|
|
181
|
+
this.title = title;
|
|
182
|
+
return ffi.request.setTitle({ winId: this.id, title });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
close() {
|
|
186
|
+
return ffi.request.closeWindow({ winId: this.id });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
activate() {
|
|
190
|
+
return ffi.request.activateWindow({ winId: this.id });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
focus() {
|
|
194
|
+
console.log(
|
|
195
|
+
"[sparkbun] GpuWindow.focus() is deprecated. Use window.activate() instead.",
|
|
196
|
+
);
|
|
197
|
+
return this.activate();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
show() {
|
|
201
|
+
return ffi.request.showWindow({ winId: this.id, activate: true });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
showInactive() {
|
|
205
|
+
return ffi.request.showWindow({ winId: this.id, activate: false });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
minimize() {
|
|
209
|
+
return ffi.request.minimizeWindow({ winId: this.id });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
unminimize() {
|
|
213
|
+
return ffi.request.restoreWindow({ winId: this.id });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
isMinimized(): boolean {
|
|
217
|
+
return ffi.request.isWindowMinimized({ winId: this.id });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
maximize() {
|
|
221
|
+
return ffi.request.maximizeWindow({ winId: this.id });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
unmaximize() {
|
|
225
|
+
return ffi.request.unmaximizeWindow({ winId: this.id });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
isMaximized(): boolean {
|
|
229
|
+
return ffi.request.isWindowMaximized({ winId: this.id });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
setFullScreen(fullScreen: boolean) {
|
|
233
|
+
return ffi.request.setWindowFullScreen({ winId: this.id, fullScreen });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
isFullScreen(): boolean {
|
|
237
|
+
return ffi.request.isWindowFullScreen({ winId: this.id });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
setAlwaysOnTop(alwaysOnTop: boolean) {
|
|
241
|
+
return ffi.request.setWindowAlwaysOnTop({ winId: this.id, alwaysOnTop });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
isAlwaysOnTop(): boolean {
|
|
245
|
+
return ffi.request.isWindowAlwaysOnTop({ winId: this.id });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
setPosition(x: number, y: number) {
|
|
249
|
+
this.frame.x = x;
|
|
250
|
+
this.frame.y = y;
|
|
251
|
+
return ffi.request.setWindowPosition({ winId: this.id, x, y });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
setWindowButtonPosition(x: number, y: number) {
|
|
255
|
+
return ffi.request.setWindowButtonPosition({ winId: this.id, x, y });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
setSize(width: number, height: number) {
|
|
259
|
+
this.frame.width = width;
|
|
260
|
+
this.frame.height = height;
|
|
261
|
+
return ffi.request.setWindowSize({ winId: this.id, width, height });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setFrame(x: number, y: number, width: number, height: number) {
|
|
265
|
+
this.frame = { x, y, width, height };
|
|
266
|
+
return ffi.request.setWindowFrame({ winId: this.id, x, y, width, height });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getFrame(): { x: number; y: number; width: number; height: number } {
|
|
270
|
+
const frame = ffi.request.getWindowFrame({ winId: this.id });
|
|
271
|
+
this.frame = frame;
|
|
272
|
+
return frame;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
getPosition(): { x: number; y: number } {
|
|
276
|
+
const frame = this.getFrame();
|
|
277
|
+
return { x: frame.x, y: frame.y };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
getSize(): { width: number; height: number } {
|
|
281
|
+
const frame = this.getFrame();
|
|
282
|
+
return { width: frame.width, height: frame.height };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
on(name: string, handler: (event: unknown) => void) {
|
|
286
|
+
const specificName = `${name}-${this.id}`;
|
|
287
|
+
sparkBunEventEmitter.on(specificName, handler);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ffi } from "../proc/native";
|
|
2
|
+
|
|
3
|
+
export const removeSocketForWebview = (webviewId: number) => {
|
|
4
|
+
ffi.request.clearWebviewHostTransport({ id: webviewId });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// Will return true if message was sent over the core-owned websocket transport.
|
|
8
|
+
// False means the caller should fall back to the native bridge / evaluateJS path.
|
|
9
|
+
export const sendMessageToWebviewViaSocket = (
|
|
10
|
+
webviewId: number,
|
|
11
|
+
message: unknown,
|
|
12
|
+
): boolean => {
|
|
13
|
+
try {
|
|
14
|
+
return ffi.request.sendHostMessageToWebviewViaTransport({
|
|
15
|
+
id: webviewId,
|
|
16
|
+
messageJson: JSON.stringify(message),
|
|
17
|
+
}) as boolean;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error("Error sending message to webview via host transport:", error);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { ffi, type MenuItemConfig, type Rectangle } from "../proc/native";
|
|
2
|
+
import sparkBunEventEmitter from "../events/eventEmitter";
|
|
3
|
+
import { VIEWS_FOLDER } from "./Paths";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
type NonDividerMenuItem = Exclude<
|
|
7
|
+
MenuItemConfig,
|
|
8
|
+
{ type: "divider" | "separator" }
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
const TrayMap: { [id: number]: Tray } = {};
|
|
12
|
+
|
|
13
|
+
export type TrayOptions = {
|
|
14
|
+
title?: string;
|
|
15
|
+
image?: string;
|
|
16
|
+
template?: boolean;
|
|
17
|
+
width?: number;
|
|
18
|
+
height?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class Tray {
|
|
22
|
+
id = 0;
|
|
23
|
+
visible = false;
|
|
24
|
+
title = "";
|
|
25
|
+
image = "";
|
|
26
|
+
template = true;
|
|
27
|
+
width = 16;
|
|
28
|
+
height = 16;
|
|
29
|
+
menu: Array<MenuItemConfig> | null = null;
|
|
30
|
+
|
|
31
|
+
constructor({
|
|
32
|
+
title = "",
|
|
33
|
+
image = "",
|
|
34
|
+
template = true,
|
|
35
|
+
width = 16,
|
|
36
|
+
height = 16,
|
|
37
|
+
}: TrayOptions = {}) {
|
|
38
|
+
this.title = title;
|
|
39
|
+
this.image = image;
|
|
40
|
+
this.template = template;
|
|
41
|
+
this.width = width;
|
|
42
|
+
this.height = height;
|
|
43
|
+
|
|
44
|
+
if (this.createNativeTray()) {
|
|
45
|
+
TrayMap[this.id] = this;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private createNativeTray(): boolean {
|
|
50
|
+
try {
|
|
51
|
+
const trayId = ffi.request.createTray({
|
|
52
|
+
title: this.title,
|
|
53
|
+
image: this.resolveImagePath(this.image),
|
|
54
|
+
template: this.template,
|
|
55
|
+
width: this.width,
|
|
56
|
+
height: this.height,
|
|
57
|
+
}) as number;
|
|
58
|
+
|
|
59
|
+
if (!trayId) {
|
|
60
|
+
throw new Error("Tray creation returned an invalid id");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.id = trayId;
|
|
64
|
+
this.visible = true;
|
|
65
|
+
return true;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn("Tray creation failed:", error);
|
|
68
|
+
console.warn(
|
|
69
|
+
"System tray functionality may not be available on this platform",
|
|
70
|
+
);
|
|
71
|
+
this.visible = false;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resolveImagePath(imgPath: string) {
|
|
77
|
+
if (imgPath.startsWith("views://")) {
|
|
78
|
+
return join(VIEWS_FOLDER, imgPath.replace("views://", ""));
|
|
79
|
+
} else {
|
|
80
|
+
// can specify any file path here
|
|
81
|
+
return imgPath;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setTitle(title: string) {
|
|
86
|
+
this.title = title;
|
|
87
|
+
if (!this.id) return;
|
|
88
|
+
ffi.request.setTrayTitle({ id: this.id, title });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setImage(imgPath: string) {
|
|
92
|
+
this.image = imgPath;
|
|
93
|
+
if (!this.id) return;
|
|
94
|
+
ffi.request.setTrayImage({
|
|
95
|
+
id: this.id,
|
|
96
|
+
image: this.resolveImagePath(imgPath),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setMenu(menu: Array<MenuItemConfig>) {
|
|
101
|
+
this.menu = menu;
|
|
102
|
+
if (!this.id) return;
|
|
103
|
+
const menuWithDefaults = menuConfigWithDefaults(menu);
|
|
104
|
+
ffi.request.setTrayMenu({
|
|
105
|
+
id: this.id,
|
|
106
|
+
menuConfig: JSON.stringify(menuWithDefaults),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
on(name: "tray-clicked", handler: (event: unknown) => void) {
|
|
111
|
+
const specificName = `${name}-${this.id}`;
|
|
112
|
+
sparkBunEventEmitter.on(specificName, handler);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
setVisible(visible: boolean) {
|
|
116
|
+
if (visible === this.visible) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!visible) {
|
|
121
|
+
if (this.id) ffi.request.hideTray({ id: this.id });
|
|
122
|
+
this.visible = false;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!this.id) {
|
|
127
|
+
if (this.createNativeTray()) {
|
|
128
|
+
TrayMap[this.id] = this;
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.visible = ffi.request.showTray({ id: this.id }) as boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getBounds(): Rectangle {
|
|
137
|
+
return ffi.request.getTrayBounds({ id: this.id });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
remove() {
|
|
141
|
+
console.log("Tray.remove() called for id:", this.id);
|
|
142
|
+
const trayId = this.id;
|
|
143
|
+
if (trayId) {
|
|
144
|
+
ffi.request.removeTray({ id: trayId });
|
|
145
|
+
}
|
|
146
|
+
this.visible = false;
|
|
147
|
+
delete TrayMap[trayId];
|
|
148
|
+
this.id = 0;
|
|
149
|
+
console.log("Tray removed from TrayMap");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static getById(id: number) {
|
|
153
|
+
return TrayMap[id];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static getAll() {
|
|
157
|
+
return Object.values(TrayMap);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static removeById(id: number) {
|
|
161
|
+
const tray = TrayMap[id];
|
|
162
|
+
if (tray) {
|
|
163
|
+
tray.remove();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const menuConfigWithDefaults = (
|
|
169
|
+
menu: Array<MenuItemConfig>,
|
|
170
|
+
): Array<MenuItemConfig> => {
|
|
171
|
+
return menu.map((item) => {
|
|
172
|
+
if (item.type === "divider" || item.type === "separator") {
|
|
173
|
+
return { type: "divider" } as const;
|
|
174
|
+
} else {
|
|
175
|
+
const menuItem = item as NonDividerMenuItem;
|
|
176
|
+
// Use shared serialization method
|
|
177
|
+
const actionWithDataId = ffi.internal.serializeMenuAction(
|
|
178
|
+
menuItem.action || "",
|
|
179
|
+
menuItem.data,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
label: menuItem.label || "",
|
|
184
|
+
type: menuItem.type || "normal",
|
|
185
|
+
action: actionWithDataId,
|
|
186
|
+
// default enabled to true unless explicitly set to false
|
|
187
|
+
enabled: menuItem.enabled === false ? false : true,
|
|
188
|
+
checked: Boolean(menuItem.checked),
|
|
189
|
+
hidden: Boolean(menuItem.hidden),
|
|
190
|
+
tooltip: menuItem.tooltip || undefined,
|
|
191
|
+
...(menuItem.submenu
|
|
192
|
+
? { submenu: menuConfigWithDefaults(menuItem.submenu) }
|
|
193
|
+
: {}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
};
|