electrobun 1.18.1 → 1.18.4-beta.5
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 +2 -0
- package/dist/api/browser/global.d.ts +5 -0
- package/dist/api/browser/index.ts +64 -22
- package/dist/api/bun/ElectrobunConfig.ts +45 -1
- package/dist/api/bun/core/BrowserView.ts +95 -54
- package/dist/api/bun/core/BrowserWindow.ts +19 -29
- package/dist/api/bun/core/BuildConfig.ts +31 -4
- package/dist/api/bun/core/GpuWindow.ts +20 -7
- package/dist/api/bun/core/Socket.ts +13 -196
- package/dist/api/bun/core/Tray.ts +33 -32
- package/dist/api/bun/core/Utils.ts +2 -4
- package/dist/api/bun/core/WGPUView.ts +43 -13
- package/dist/api/bun/index.ts +35 -0
- package/dist/api/bun/preload/.generated/compiled.ts +1 -1
- package/dist/api/bun/preload/globals.d.ts +7 -0
- package/dist/api/bun/preload/index.ts +18 -8
- package/dist/api/bun/preload/webviewTag.ts +32 -3
- package/dist/api/bun/proc/native.ts +914 -745
- package/dist/api/bun/webGPU.ts +1 -1
- package/dist/main.js +26 -22
- package/dist/preload-full.js +913 -0
- package/dist/preload-sandboxed.js +111 -0
- package/dist/zig-sdk/electrobun.zig +1993 -0
- package/package.json +2 -3
- package/src/cli/index.ts +410 -139
- package/dist/api/bun/core/windowIds.ts +0 -5
|
@@ -3,13 +3,13 @@ import electrobunEventEmitter from "../events/eventEmitter";
|
|
|
3
3
|
import { BrowserView } from "./BrowserView";
|
|
4
4
|
import { type Pointer } from "bun:ffi";
|
|
5
5
|
import { BuildConfig } from "./BuildConfig";
|
|
6
|
-
import { quit } from "./Utils";
|
|
7
6
|
import { type RPCWithTransport } from "../../shared/rpc.js";
|
|
8
|
-
import { getNextWindowId } from "./windowIds";
|
|
9
|
-
import { GpuWindowMap } from "./GpuWindow";
|
|
10
7
|
import { WGPUView } from "./WGPUView";
|
|
11
8
|
|
|
12
|
-
const buildConfig =
|
|
9
|
+
const buildConfig = BuildConfig.getSync();
|
|
10
|
+
ffi.request.setExitOnLastWindowClosed({
|
|
11
|
+
enabled: buildConfig.runtime?.exitOnLastWindowClosed ?? true,
|
|
12
|
+
});
|
|
13
13
|
|
|
14
14
|
export type WindowOptionsType<T = undefined> = {
|
|
15
15
|
trafficLightOffset?: {
|
|
@@ -73,7 +73,7 @@ export const BrowserWindowMap: {
|
|
|
73
73
|
[id: number]: BrowserWindow<RPCWithTransport>;
|
|
74
74
|
} = {};
|
|
75
75
|
|
|
76
|
-
// Clean up
|
|
76
|
+
// Clean up JS wrapper state when a window closes. Native child cleanup is core-owned.
|
|
77
77
|
electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
|
|
78
78
|
const windowId = event.data.id;
|
|
79
79
|
delete BrowserWindowMap[windowId];
|
|
@@ -89,35 +89,16 @@ electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
|
|
|
89
89
|
const wgpuViews = WGPUView.getAll().filter(v => v.windowId === windowId);
|
|
90
90
|
for (const view of wgpuViews) {
|
|
91
91
|
try {
|
|
92
|
-
|
|
93
|
-
if (view.ptr === null) {
|
|
94
|
-
// Already cleaned up, skip
|
|
95
|
-
} else {
|
|
96
|
-
// Programmatic close path - remove the view
|
|
97
|
-
view.remove();
|
|
98
|
-
}
|
|
92
|
+
view.remove();
|
|
99
93
|
} catch (e) {
|
|
100
94
|
console.error(`Error cleaning up WGPU view ${view.id}:`, e);
|
|
101
|
-
// If remove() failed, at least mark it as cleaned up
|
|
102
|
-
view.ptr = null as any;
|
|
103
95
|
}
|
|
104
96
|
}
|
|
105
97
|
|
|
106
|
-
const exitOnLastWindowClosed =
|
|
107
|
-
buildConfig.runtime?.exitOnLastWindowClosed ?? true;
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
exitOnLastWindowClosed &&
|
|
111
|
-
Object.keys(BrowserWindowMap).length === 0 &&
|
|
112
|
-
Object.keys(GpuWindowMap).length === 0
|
|
113
|
-
) {
|
|
114
|
-
quit();
|
|
115
|
-
}
|
|
116
98
|
});
|
|
117
99
|
|
|
118
100
|
export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
119
|
-
id
|
|
120
|
-
ptr!: Pointer;
|
|
101
|
+
id = 0;
|
|
121
102
|
title: string = "Electrobun";
|
|
122
103
|
state: "creating" | "created" = "creating";
|
|
123
104
|
url: string | null = null;
|
|
@@ -146,6 +127,10 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
146
127
|
// todo (yoav): make this an array of ids or something
|
|
147
128
|
webviewId!: number;
|
|
148
129
|
|
|
130
|
+
get ptr(): Pointer | null {
|
|
131
|
+
return ffi.request.getWindowPointer({ winId: this.id }) as Pointer | null;
|
|
132
|
+
}
|
|
133
|
+
|
|
149
134
|
constructor(options: Partial<WindowOptionsType<T>> = defaultOptions) {
|
|
150
135
|
this.title = options.title || "New Window";
|
|
151
136
|
this.frame = options.frame
|
|
@@ -177,8 +162,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
177
162
|
hidden,
|
|
178
163
|
activate,
|
|
179
164
|
}: Partial<WindowOptionsType<T>>) {
|
|
180
|
-
|
|
181
|
-
id: this.id,
|
|
165
|
+
const windowId = ffi.request.createWindow({
|
|
182
166
|
title: this.title,
|
|
183
167
|
url: this.url || "",
|
|
184
168
|
frame: {
|
|
@@ -221,7 +205,13 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
221
205
|
hidden: hidden ?? false,
|
|
222
206
|
activate: activate ?? true,
|
|
223
207
|
trafficLightOffset: this.trafficLightOffset,
|
|
224
|
-
})
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (!windowId) {
|
|
211
|
+
throw "Failed to create window";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.id = windowId as number;
|
|
225
215
|
|
|
226
216
|
BrowserWindowMap[this.id] = this;
|
|
227
217
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
|
|
1
3
|
export type BuildConfigType = {
|
|
2
4
|
defaultRenderer: "native" | "cef";
|
|
3
5
|
availableRenderers: ("native" | "cef")[];
|
|
@@ -11,6 +13,13 @@ export type BuildConfigType = {
|
|
|
11
13
|
|
|
12
14
|
let buildConfig: BuildConfigType | null = null;
|
|
13
15
|
|
|
16
|
+
function fallbackBuildConfig(): BuildConfigType {
|
|
17
|
+
return {
|
|
18
|
+
defaultRenderer: "native",
|
|
19
|
+
availableRenderers: ["native"],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
const BuildConfig = {
|
|
15
24
|
/**
|
|
16
25
|
* Get the build configuration. Loads from build.json on first call, then returns cached value.
|
|
@@ -26,10 +35,28 @@ const BuildConfig = {
|
|
|
26
35
|
return buildConfig!;
|
|
27
36
|
} catch (error) {
|
|
28
37
|
// Fallback for dev mode or missing file
|
|
29
|
-
buildConfig =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
buildConfig = fallbackBuildConfig();
|
|
39
|
+
return buildConfig;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the build configuration synchronously.
|
|
45
|
+
* Useful for modules that cannot use top-level await.
|
|
46
|
+
*/
|
|
47
|
+
getSync: (): BuildConfigType => {
|
|
48
|
+
if (buildConfig) {
|
|
49
|
+
return buildConfig;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const resourcesDir = "Resources";
|
|
54
|
+
buildConfig = JSON.parse(
|
|
55
|
+
readFileSync(`../${resourcesDir}/build.json`, "utf8"),
|
|
56
|
+
) as BuildConfigType;
|
|
57
|
+
return buildConfig;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
buildConfig = fallbackBuildConfig();
|
|
33
60
|
return buildConfig;
|
|
34
61
|
}
|
|
35
62
|
},
|
|
@@ -2,7 +2,7 @@ import { ffi } from "../proc/native";
|
|
|
2
2
|
import electrobunEventEmitter from "../events/eventEmitter";
|
|
3
3
|
import { type Pointer } from "bun:ffi";
|
|
4
4
|
import { WGPUView } from "./WGPUView";
|
|
5
|
-
import {
|
|
5
|
+
import { BuildConfig } from "./BuildConfig";
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
export type GpuWindowOptionsType = {
|
|
@@ -35,11 +35,16 @@ const defaultOptions: GpuWindowOptionsType = {
|
|
|
35
35
|
transparent: false,
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
const buildConfig = BuildConfig.getSync();
|
|
39
|
+
ffi.request.setExitOnLastWindowClosed({
|
|
40
|
+
enabled: buildConfig.runtime?.exitOnLastWindowClosed ?? true,
|
|
41
|
+
});
|
|
42
|
+
|
|
38
43
|
export const GpuWindowMap: {
|
|
39
44
|
[id: number]: GpuWindow;
|
|
40
45
|
} = {};
|
|
41
46
|
|
|
42
|
-
// Clean up
|
|
47
|
+
// Clean up JS wrapper state when a window closes. Native child cleanup is core-owned.
|
|
43
48
|
electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
|
|
44
49
|
const windowId = event.data.id;
|
|
45
50
|
delete GpuWindowMap[windowId];
|
|
@@ -54,8 +59,7 @@ electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
|
|
|
54
59
|
});
|
|
55
60
|
|
|
56
61
|
export class GpuWindow {
|
|
57
|
-
id
|
|
58
|
-
ptr!: Pointer;
|
|
62
|
+
id = 0;
|
|
59
63
|
title: string = "Electrobun";
|
|
60
64
|
state: "creating" | "created" = "creating";
|
|
61
65
|
transparent: boolean = false;
|
|
@@ -73,6 +77,10 @@ export class GpuWindow {
|
|
|
73
77
|
};
|
|
74
78
|
wgpuViewId!: number;
|
|
75
79
|
|
|
80
|
+
get ptr(): Pointer | null {
|
|
81
|
+
return ffi.request.getWindowPointer({ winId: this.id }) as Pointer | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
76
84
|
constructor(options: Partial<GpuWindowOptionsType> = defaultOptions) {
|
|
77
85
|
this.title = options.title || "New Window";
|
|
78
86
|
this.frame = options.frame
|
|
@@ -93,8 +101,7 @@ export class GpuWindow {
|
|
|
93
101
|
transparent,
|
|
94
102
|
activate,
|
|
95
103
|
}: Partial<GpuWindowOptionsType>) {
|
|
96
|
-
|
|
97
|
-
id: this.id,
|
|
104
|
+
const windowId = ffi.request.createWindow({
|
|
98
105
|
title: this.title,
|
|
99
106
|
url: "",
|
|
100
107
|
frame: {
|
|
@@ -136,7 +143,13 @@ export class GpuWindow {
|
|
|
136
143
|
transparent: transparent ?? false,
|
|
137
144
|
activate: activate ?? true,
|
|
138
145
|
trafficLightOffset: this.trafficLightOffset,
|
|
139
|
-
})
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!windowId) {
|
|
149
|
+
throw "Failed to create window";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.id = windowId as number;
|
|
140
153
|
|
|
141
154
|
GpuWindowMap[this.id] = this;
|
|
142
155
|
|
|
@@ -1,205 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { BrowserView } from "./BrowserView";
|
|
3
|
-
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
4
|
-
|
|
5
|
-
function base64ToUint8Array(base64: string) {
|
|
6
|
-
{
|
|
7
|
-
return new Uint8Array(
|
|
8
|
-
atob(base64)
|
|
9
|
-
.split("")
|
|
10
|
-
.map((char) => char.charCodeAt(0)),
|
|
11
|
-
);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Encrypt function
|
|
16
|
-
function encrypt(secretKey: Uint8Array, text: string) {
|
|
17
|
-
const iv = new Uint8Array(randomBytes(12)); // IV for AES-GCM
|
|
18
|
-
const cipher = createCipheriv("aes-256-gcm", secretKey, iv);
|
|
19
|
-
const encrypted = Buffer.concat([
|
|
20
|
-
new Uint8Array(cipher.update(text, "utf8")),
|
|
21
|
-
new Uint8Array(cipher.final()),
|
|
22
|
-
]).toString("base64");
|
|
23
|
-
const tag = cipher.getAuthTag().toString("base64");
|
|
24
|
-
return { encrypted, iv: Buffer.from(iv).toString("base64"), tag };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Decrypt function
|
|
28
|
-
function decrypt(
|
|
29
|
-
secretKey: Uint8Array,
|
|
30
|
-
encryptedData: Uint8Array,
|
|
31
|
-
iv: Uint8Array,
|
|
32
|
-
tag: Uint8Array,
|
|
33
|
-
) {
|
|
34
|
-
const decipher = createDecipheriv("aes-256-gcm", secretKey, iv);
|
|
35
|
-
decipher.setAuthTag(tag);
|
|
36
|
-
const decrypted = Buffer.concat([
|
|
37
|
-
new Uint8Array(decipher.update(encryptedData)),
|
|
38
|
-
new Uint8Array(decipher.final()),
|
|
39
|
-
]);
|
|
40
|
-
return decrypted.toString("utf8");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const socketMap: {
|
|
44
|
-
[webviewId: string]: {
|
|
45
|
-
socket: null | ServerWebSocket<unknown>;
|
|
46
|
-
queue: string[];
|
|
47
|
-
};
|
|
48
|
-
} = {};
|
|
1
|
+
import { ffi } from "../proc/native";
|
|
49
2
|
|
|
50
3
|
export const removeSocketForWebview = (webviewId: number) => {
|
|
51
|
-
|
|
52
|
-
if (!rpc) return;
|
|
53
|
-
|
|
54
|
-
rpc.socket = null;
|
|
55
|
-
delete socketMap[webviewId];
|
|
4
|
+
ffi.request.clearWebviewHostTransport({ id: webviewId });
|
|
56
5
|
};
|
|
57
6
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const endPort = 65535;
|
|
61
|
-
const payloadLimit = 1024 * 1024 * 500; // 500MB
|
|
62
|
-
let port = startPort;
|
|
63
|
-
let server = null;
|
|
64
|
-
|
|
65
|
-
while (port <= endPort) {
|
|
66
|
-
try {
|
|
67
|
-
server = Bun.serve<{ webviewId: number }>({
|
|
68
|
-
port,
|
|
69
|
-
fetch(req: Request, server: Server<{ webviewId: number }>) {
|
|
70
|
-
const url = new URL(req.url);
|
|
71
|
-
// const token = new URL(req.url).searchParams.get("token");
|
|
72
|
-
// if (token !== AUTH_TOKEN)
|
|
73
|
-
// return new Response("Unauthorized", { status: 401 });
|
|
74
|
-
// console.log("fetch!!", url.pathname);
|
|
75
|
-
if (url.pathname === "/socket") {
|
|
76
|
-
const webviewIdString = url.searchParams.get("webviewId");
|
|
77
|
-
if (!webviewIdString) {
|
|
78
|
-
return new Response("Missing webviewId", { status: 400 });
|
|
79
|
-
}
|
|
80
|
-
const webviewId = parseInt(webviewIdString, 10);
|
|
81
|
-
const success = server.upgrade(req, { data: { webviewId } });
|
|
82
|
-
return success
|
|
83
|
-
? undefined
|
|
84
|
-
: new Response("Upgrade failed", { status: 500 });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
console.log("unhandled RPC Server request", req.url);
|
|
88
|
-
},
|
|
89
|
-
websocket: {
|
|
90
|
-
idleTimeout: 960,
|
|
91
|
-
// 500MB max payload should be plenty
|
|
92
|
-
maxPayloadLength: payloadLimit,
|
|
93
|
-
// Anything beyond the backpressure limit will be dropped
|
|
94
|
-
backpressureLimit: payloadLimit * 2,
|
|
95
|
-
open(ws: ServerWebSocket<{ webviewId: number }>) {
|
|
96
|
-
if (!ws?.data) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
const { webviewId } = ws.data;
|
|
100
|
-
|
|
101
|
-
if (!socketMap[webviewId]) {
|
|
102
|
-
socketMap[webviewId] = { socket: ws, queue: [] };
|
|
103
|
-
} else {
|
|
104
|
-
socketMap[webviewId].socket = ws;
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
close(ws: ServerWebSocket<{ webviewId: number }>, _code: number, _reason: string) {
|
|
108
|
-
if (!ws?.data) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
const { webviewId } = ws.data;
|
|
112
|
-
// console.log("Closed:", webviewId, code, reason);
|
|
113
|
-
if (socketMap[webviewId]) {
|
|
114
|
-
socketMap[webviewId].socket = null;
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
message(ws: ServerWebSocket<{ webviewId: number }>, message: string | Buffer) {
|
|
119
|
-
if (!ws?.data) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const { webviewId } = ws.data;
|
|
123
|
-
const browserView = BrowserView.getById(webviewId);
|
|
124
|
-
if (!browserView) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (browserView.rpcHandler) {
|
|
129
|
-
if (typeof message === "string") {
|
|
130
|
-
try {
|
|
131
|
-
const encryptedPacket = JSON.parse(message);
|
|
132
|
-
const decrypted = decrypt(
|
|
133
|
-
browserView.secretKey,
|
|
134
|
-
base64ToUint8Array(encryptedPacket.encryptedData),
|
|
135
|
-
base64ToUint8Array(encryptedPacket.iv),
|
|
136
|
-
base64ToUint8Array(encryptedPacket.tag),
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
// Note: At this point the secretKey for the webview id would
|
|
140
|
-
// have had to match the encrypted packet data, so we can trust
|
|
141
|
-
// that this message can be passed to this browserview's rpc
|
|
142
|
-
// methods.
|
|
143
|
-
browserView.rpcHandler(JSON.parse(decrypted));
|
|
144
|
-
} catch (error) {
|
|
145
|
-
console.log("Error handling message:", error);
|
|
146
|
-
}
|
|
147
|
-
} else if (message instanceof ArrayBuffer) {
|
|
148
|
-
console.log("TODO: Received ArrayBuffer message:", message);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
break;
|
|
156
|
-
} catch (error: any) {
|
|
157
|
-
if (error.code === "EADDRINUSE") {
|
|
158
|
-
console.log(`Port ${port} in use, trying next port...`);
|
|
159
|
-
port++;
|
|
160
|
-
} else {
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return { rpcServer: server, rpcPort: port };
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
export const { rpcServer, rpcPort } = startRPCServer();
|
|
170
|
-
|
|
171
|
-
// Will return true if message was sent over websocket
|
|
172
|
-
// false if it was not (caller should fallback to postMessage/evaluateJS rpc)
|
|
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.
|
|
173
9
|
export const sendMessageToWebviewViaSocket = (
|
|
174
10
|
webviewId: number,
|
|
175
|
-
message:
|
|
11
|
+
message: unknown,
|
|
176
12
|
): boolean => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const encrypted = encrypt(browserView.secretKey, unencryptedString);
|
|
186
|
-
|
|
187
|
-
const encryptedPacket = {
|
|
188
|
-
encryptedData: encrypted.encrypted,
|
|
189
|
-
iv: encrypted.iv,
|
|
190
|
-
tag: encrypted.tag,
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const encryptedPacketString = JSON.stringify(encryptedPacket);
|
|
194
|
-
|
|
195
|
-
rpc.socket.send(encryptedPacketString);
|
|
196
|
-
return true;
|
|
197
|
-
} catch (error) {
|
|
198
|
-
console.error("Error sending message to webview via socket:", error);
|
|
199
|
-
}
|
|
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;
|
|
200
21
|
}
|
|
201
|
-
|
|
202
|
-
return false;
|
|
203
22
|
};
|
|
204
|
-
|
|
205
|
-
console.log("Server started at", rpcServer?.url.origin);
|
|
@@ -2,14 +2,12 @@ import { ffi, type MenuItemConfig, type Rectangle } from "../proc/native";
|
|
|
2
2
|
import electrobunEventEmitter from "../events/eventEmitter";
|
|
3
3
|
import { VIEWS_FOLDER } from "./Paths";
|
|
4
4
|
import { join } from "path";
|
|
5
|
-
import { type Pointer } from "bun:ffi";
|
|
6
5
|
|
|
7
6
|
type NonDividerMenuItem = Exclude<
|
|
8
7
|
MenuItemConfig,
|
|
9
8
|
{ type: "divider" | "separator" }
|
|
10
9
|
>;
|
|
11
10
|
|
|
12
|
-
let nextTrayId = 1;
|
|
13
11
|
const TrayMap: { [id: number]: Tray } = {};
|
|
14
12
|
|
|
15
13
|
export type TrayOptions = {
|
|
@@ -21,9 +19,8 @@ export type TrayOptions = {
|
|
|
21
19
|
};
|
|
22
20
|
|
|
23
21
|
export class Tray {
|
|
24
|
-
id
|
|
25
|
-
|
|
26
|
-
visible = true;
|
|
22
|
+
id = 0;
|
|
23
|
+
visible = false;
|
|
27
24
|
title = "";
|
|
28
25
|
image = "";
|
|
29
26
|
template = true;
|
|
@@ -44,36 +41,35 @@ export class Tray {
|
|
|
44
41
|
this.width = width;
|
|
45
42
|
this.height = height;
|
|
46
43
|
|
|
47
|
-
this.createNativeTray()
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
if (this.createNativeTray()) {
|
|
45
|
+
TrayMap[this.id] = this;
|
|
46
|
+
}
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
private createNativeTray() {
|
|
49
|
+
private createNativeTray(): boolean {
|
|
53
50
|
try {
|
|
54
|
-
|
|
55
|
-
id: this.id,
|
|
51
|
+
const trayId = ffi.request.createTray({
|
|
56
52
|
title: this.title,
|
|
57
53
|
image: this.resolveImagePath(this.image),
|
|
58
54
|
template: this.template,
|
|
59
55
|
width: this.width,
|
|
60
56
|
height: this.height,
|
|
61
|
-
}) as
|
|
57
|
+
}) as number;
|
|
58
|
+
|
|
59
|
+
if (!trayId) {
|
|
60
|
+
throw new Error("Tray creation returned an invalid id");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.id = trayId;
|
|
62
64
|
this.visible = true;
|
|
65
|
+
return true;
|
|
63
66
|
} catch (error) {
|
|
64
67
|
console.warn("Tray creation failed:", error);
|
|
65
68
|
console.warn(
|
|
66
69
|
"System tray functionality may not be available on this platform",
|
|
67
70
|
);
|
|
68
|
-
this.ptr = null;
|
|
69
71
|
this.visible = false;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (this.ptr && this.menu) {
|
|
73
|
-
ffi.request.setTrayMenu({
|
|
74
|
-
id: this.id,
|
|
75
|
-
menuConfig: JSON.stringify(menuConfigWithDefaults(this.menu)),
|
|
76
|
-
});
|
|
72
|
+
return false;
|
|
77
73
|
}
|
|
78
74
|
}
|
|
79
75
|
|
|
@@ -88,13 +84,13 @@ export class Tray {
|
|
|
88
84
|
|
|
89
85
|
setTitle(title: string) {
|
|
90
86
|
this.title = title;
|
|
91
|
-
if (!this.
|
|
87
|
+
if (!this.id) return;
|
|
92
88
|
ffi.request.setTrayTitle({ id: this.id, title });
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
setImage(imgPath: string) {
|
|
96
92
|
this.image = imgPath;
|
|
97
|
-
if (!this.
|
|
93
|
+
if (!this.id) return;
|
|
98
94
|
ffi.request.setTrayImage({
|
|
99
95
|
id: this.id,
|
|
100
96
|
image: this.resolveImagePath(imgPath),
|
|
@@ -103,7 +99,7 @@ export class Tray {
|
|
|
103
99
|
|
|
104
100
|
setMenu(menu: Array<MenuItemConfig>) {
|
|
105
101
|
this.menu = menu;
|
|
106
|
-
if (!this.
|
|
102
|
+
if (!this.id) return;
|
|
107
103
|
const menuWithDefaults = menuConfigWithDefaults(menu);
|
|
108
104
|
ffi.request.setTrayMenu({
|
|
109
105
|
id: this.id,
|
|
@@ -122,15 +118,19 @@ export class Tray {
|
|
|
122
118
|
}
|
|
123
119
|
|
|
124
120
|
if (!visible) {
|
|
125
|
-
if (this.
|
|
126
|
-
ffi.request.removeTray({ id: this.id });
|
|
127
|
-
this.ptr = null;
|
|
128
|
-
}
|
|
121
|
+
if (this.id) ffi.request.hideTray({ id: this.id });
|
|
129
122
|
this.visible = false;
|
|
130
123
|
return;
|
|
131
124
|
}
|
|
132
125
|
|
|
133
|
-
this.
|
|
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
134
|
}
|
|
135
135
|
|
|
136
136
|
getBounds(): Rectangle {
|
|
@@ -139,12 +139,13 @@ export class Tray {
|
|
|
139
139
|
|
|
140
140
|
remove() {
|
|
141
141
|
console.log("Tray.remove() called for id:", this.id);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
const trayId = this.id;
|
|
143
|
+
if (trayId) {
|
|
144
|
+
ffi.request.removeTray({ id: trayId });
|
|
145
145
|
}
|
|
146
146
|
this.visible = false;
|
|
147
|
-
delete TrayMap[
|
|
147
|
+
delete TrayMap[trayId];
|
|
148
|
+
this.id = 0;
|
|
148
149
|
console.log("Tray removed from TrayMap");
|
|
149
150
|
}
|
|
150
151
|
|
|
@@ -137,9 +137,7 @@ export const quit = () => {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
if (native) {
|
|
140
|
-
|
|
141
|
-
native.symbols.waitForShutdownComplete(5000);
|
|
142
|
-
native.symbols.forceExit(0);
|
|
140
|
+
ffi.request.quitGracefully({ code: 0, timeoutMs: 5000 });
|
|
143
141
|
} else {
|
|
144
142
|
process.exit(0);
|
|
145
143
|
}
|
|
@@ -150,7 +148,7 @@ const _originalProcessExit = process.exit;
|
|
|
150
148
|
process.exit = ((code?: number) => {
|
|
151
149
|
if (native) {
|
|
152
150
|
if (isQuitting) {
|
|
153
|
-
|
|
151
|
+
ffi.request.quitGracefully({ code: code ?? 0, timeoutMs: 0 });
|
|
154
152
|
return;
|
|
155
153
|
}
|
|
156
154
|
quit();
|