electrobun 1.18.1 → 1.18.4-beta.3
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 +1 -0
- package/dist/api/bun/ElectrobunConfig.ts +20 -0
- package/dist/api/bun/core/BrowserView.ts +29 -47
- package/dist/api/bun/core/BrowserWindow.ts +14 -7
- package/dist/api/bun/core/BuildConfig.ts +31 -4
- package/dist/api/bun/core/GpuWindow.ts +13 -6
- package/dist/api/bun/core/Tray.ts +33 -32
- package/dist/api/bun/proc/native.ts +682 -655
- package/dist/main.js +26 -22
- package/dist/preload-full.js +885 -0
- package/dist/preload-sandboxed.js +111 -0
- package/dist/zig-sdk/electrobun.zig +1979 -0
- package/package.json +2 -3
- package/src/cli/index.ts +313 -135
- package/dist/api/bun/core/windowIds.ts +0 -5
package/README.md
CHANGED
|
@@ -66,6 +66,7 @@ Don't miss our:
|
|
|
66
66
|
- [Marginalia](https://github.com/lars-hoeijmans/Marginalia) - a simple note taking app
|
|
67
67
|
- [MarkBun](https://github.com/xiaochong/markbun) - fast, beautiful, Typora-like markdown desktop editor
|
|
68
68
|
- [md-browse](https://github.com/needle-tools/md-browse) - a markdown-first browser that converts web pages to clean markdown
|
|
69
|
+
- [moop](https://github.com/zrubinrattet/moop/) - desktop app for batch image optimization for the web
|
|
69
70
|
- [Patchline](https://github.com/adwaithks/Patchline) - lightweight desktop Git client for reading patches and line diffs, then staging and committing changes
|
|
70
71
|
- [peekachu](https://github.com/needle-tools/peekachu) - password manager for AIs; store secrets in your OS keychain and scrub output so AI assistants never see actual values
|
|
71
72
|
- [PiBun](https://github.com/khairold/pibun) - desktop GUI for the Pi coding agent with chat, terminal, git integration, and plugin system
|
|
@@ -112,6 +112,14 @@ export interface ElectrobunConfig {
|
|
|
112
112
|
* Build configuration options
|
|
113
113
|
*/
|
|
114
114
|
build?: {
|
|
115
|
+
/**
|
|
116
|
+
* Main process implementation to build and package.
|
|
117
|
+
* - "bun": bundle and run the Bun main process entrypoint
|
|
118
|
+
* - "zig": compile and run the Zig main process entrypoint
|
|
119
|
+
* @default "bun"
|
|
120
|
+
*/
|
|
121
|
+
mainProcess?: "bun" | "zig";
|
|
122
|
+
|
|
115
123
|
/**
|
|
116
124
|
* Bun process build configuration.
|
|
117
125
|
* Accepts all Bun.build() options (plugins, sourcemap, minify, define, etc.)
|
|
@@ -125,6 +133,18 @@ export interface ElectrobunConfig {
|
|
|
125
133
|
entrypoint?: string;
|
|
126
134
|
} & BunBuildOptions;
|
|
127
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Zig main process build configuration.
|
|
138
|
+
* Used when `build.mainProcess` is set to `"zig"`.
|
|
139
|
+
*/
|
|
140
|
+
zig?: {
|
|
141
|
+
/**
|
|
142
|
+
* Entry point for the main Zig process
|
|
143
|
+
* @default "src/zig/main.zig"
|
|
144
|
+
*/
|
|
145
|
+
entrypoint?: string;
|
|
146
|
+
};
|
|
147
|
+
|
|
128
148
|
/**
|
|
129
149
|
* Browser view build configurations.
|
|
130
150
|
* Each view accepts all Bun.build() options (plugins, sourcemap, minify, define, etc.)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as fs from "fs";
|
|
1
|
+
import { ffi } from "../proc/native";
|
|
3
2
|
import electrobunEventEmitter from "../events/eventEmitter";
|
|
4
3
|
import {
|
|
5
4
|
type ElectrobunRPCSchema,
|
|
@@ -7,10 +6,8 @@ import {
|
|
|
7
6
|
type RPCWithTransport,
|
|
8
7
|
defineElectrobunRPC,
|
|
9
8
|
} from "../../shared/rpc.js";
|
|
10
|
-
import { Updater } from "./Updater";
|
|
11
9
|
import { BuildConfig } from "./BuildConfig";
|
|
12
10
|
import {
|
|
13
|
-
rpcPort,
|
|
14
11
|
sendMessageToWebviewViaSocket,
|
|
15
12
|
removeSocketForWebview,
|
|
16
13
|
} from "./Socket";
|
|
@@ -20,7 +17,6 @@ import { type Pointer } from "bun:ffi";
|
|
|
20
17
|
const BrowserViewMap: {
|
|
21
18
|
[id: number]: BrowserView<any>;
|
|
22
19
|
} = {};
|
|
23
|
-
let nextWebviewId = 1;
|
|
24
20
|
|
|
25
21
|
export type BrowserViewOptions<T = undefined> = {
|
|
26
22
|
url: string | null;
|
|
@@ -38,7 +34,6 @@ export type BrowserViewOptions<T = undefined> = {
|
|
|
38
34
|
rpc: T;
|
|
39
35
|
hostWebviewId: number;
|
|
40
36
|
autoResize: boolean;
|
|
41
|
-
|
|
42
37
|
windowId: number;
|
|
43
38
|
navigationRules: string | null;
|
|
44
39
|
// Sandbox mode: when true, disables RPC and only allows event emission
|
|
@@ -52,8 +47,7 @@ export type BrowserViewOptions<T = undefined> = {
|
|
|
52
47
|
// renderer:
|
|
53
48
|
};
|
|
54
49
|
|
|
55
|
-
const
|
|
56
|
-
const buildConfig = await BuildConfig.get();
|
|
50
|
+
const buildConfig = BuildConfig.getSync();
|
|
57
51
|
|
|
58
52
|
const defaultOptions: Partial<BrowserViewOptions> = {
|
|
59
53
|
url: null,
|
|
@@ -68,13 +62,8 @@ const defaultOptions: Partial<BrowserViewOptions> = {
|
|
|
68
62
|
height: 600,
|
|
69
63
|
},
|
|
70
64
|
};
|
|
71
|
-
// Note: we use the build's hash to separate from different apps and different builds
|
|
72
|
-
// but we also want a randomId to separate different instances of the same app
|
|
73
|
-
const randomId = Math.random().toString(36).substring(7);
|
|
74
|
-
|
|
75
65
|
export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
76
|
-
id
|
|
77
|
-
ptr: Pointer | null = null;
|
|
66
|
+
id = 0;
|
|
78
67
|
hostWebviewId?: number;
|
|
79
68
|
windowId!: number;
|
|
80
69
|
renderer!: "cef" | "native";
|
|
@@ -95,9 +84,6 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
95
84
|
width: 800,
|
|
96
85
|
height: 600,
|
|
97
86
|
};
|
|
98
|
-
pipePrefix!: string;
|
|
99
|
-
inStream!: fs.WriteStream;
|
|
100
|
-
outStream!: ReadableStream<Uint8Array>;
|
|
101
87
|
secretKey!: Uint8Array;
|
|
102
88
|
rpc?: T;
|
|
103
89
|
rpcHandler?: (msg: unknown) => void;
|
|
@@ -108,6 +94,13 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
108
94
|
startPassthrough: boolean = false;
|
|
109
95
|
isRemoved: boolean = false;
|
|
110
96
|
|
|
97
|
+
get ptr(): Pointer | null {
|
|
98
|
+
if (this.isRemoved) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return ffi.request.getWebviewPointer({ id: this.id }) as Pointer | null;
|
|
102
|
+
}
|
|
103
|
+
|
|
111
104
|
constructor(options: Partial<BrowserViewOptions<T>> = defaultOptions) {
|
|
112
105
|
// const rpc = options.rpc;
|
|
113
106
|
|
|
@@ -124,9 +117,6 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
124
117
|
this.rpc = options.rpc;
|
|
125
118
|
this.secretKey = new Uint8Array(randomBytes(32));
|
|
126
119
|
this.partition = options.partition || null;
|
|
127
|
-
// todo (yoav): since collisions can crash the app add a function that checks if the
|
|
128
|
-
// file exists first
|
|
129
|
-
this.pipePrefix = `/private/tmp/electrobun_ipc_pipe_${hash}_${randomId}_${this.id}`;
|
|
130
120
|
this.hostWebviewId = options.hostWebviewId;
|
|
131
121
|
this.windowId = options.windowId ?? 0;
|
|
132
122
|
this.autoResize = options.autoResize === false ? false : true;
|
|
@@ -136,8 +126,8 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
136
126
|
this.startTransparent = options.startTransparent ?? false;
|
|
137
127
|
this.startPassthrough = options.startPassthrough ?? false;
|
|
138
128
|
|
|
129
|
+
this.id = this.init() as number;
|
|
139
130
|
BrowserViewMap[this.id] = this;
|
|
140
|
-
this.ptr = this.init() as Pointer;
|
|
141
131
|
|
|
142
132
|
// If HTML content was provided, load it after webview creation.
|
|
143
133
|
if (this.html) {
|
|
@@ -148,21 +138,17 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
148
138
|
}
|
|
149
139
|
|
|
150
140
|
init() {
|
|
151
|
-
this.
|
|
141
|
+
this.initializeRpcTransport();
|
|
152
142
|
|
|
153
143
|
return ffi.request.createWebview({
|
|
154
|
-
id: this.id,
|
|
155
144
|
windowId: this.windowId,
|
|
145
|
+
hostWebviewId: this.hostWebviewId ?? null,
|
|
156
146
|
renderer: this.renderer,
|
|
157
|
-
rpcPort: rpcPort,
|
|
158
147
|
// todo: consider sending secretKey as base64
|
|
159
148
|
secretKey: this.secretKey.toString(),
|
|
160
|
-
hostWebviewId: this.hostWebviewId || null,
|
|
161
|
-
pipePrefix: this.pipePrefix,
|
|
162
149
|
partition: this.partition,
|
|
163
150
|
// Only pass URL if no HTML content is provided to avoid conflicts
|
|
164
151
|
url: this.html ? null : this.url,
|
|
165
|
-
html: this.html,
|
|
166
152
|
preload: this.preload,
|
|
167
153
|
viewsRoot: this.viewsRoot,
|
|
168
154
|
frame: {
|
|
@@ -180,7 +166,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
180
166
|
});
|
|
181
167
|
}
|
|
182
168
|
|
|
183
|
-
|
|
169
|
+
initializeRpcTransport() {
|
|
184
170
|
if (!this.rpc) {
|
|
185
171
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
186
172
|
this.rpc = BrowserView.defineRPC({
|
|
@@ -224,7 +210,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
224
210
|
|
|
225
211
|
loadURL(url: string) {
|
|
226
212
|
this.url = url;
|
|
227
|
-
|
|
213
|
+
ffi.request.loadURLInWebView({ id: this.id, url: this.url });
|
|
228
214
|
}
|
|
229
215
|
|
|
230
216
|
loadHTML(html: string) {
|
|
@@ -232,18 +218,18 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
232
218
|
|
|
233
219
|
if (this.renderer === "cef") {
|
|
234
220
|
// For CEF, store HTML content in native map and use scheme handler
|
|
235
|
-
|
|
221
|
+
ffi.request.setWebviewHTMLContent({ id: this.id, html });
|
|
236
222
|
this.loadURL("views://internal/index.html");
|
|
237
223
|
} else {
|
|
238
224
|
// For WKWebView, load HTML content directly
|
|
239
|
-
|
|
225
|
+
ffi.request.loadHTMLInWebView({ id: this.id, html });
|
|
240
226
|
}
|
|
241
227
|
}
|
|
242
228
|
|
|
243
229
|
setNavigationRules(rules: string[]) {
|
|
244
230
|
this.navigationRules = JSON.stringify(rules);
|
|
245
231
|
const rulesJson = JSON.stringify(rules);
|
|
246
|
-
|
|
232
|
+
ffi.request.setWebviewNavigationRules({ id: this.id, rulesJson });
|
|
247
233
|
}
|
|
248
234
|
|
|
249
235
|
findInPage(
|
|
@@ -252,28 +238,28 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
252
238
|
) {
|
|
253
239
|
const forward = options?.forward ?? true;
|
|
254
240
|
const matchCase = options?.matchCase ?? false;
|
|
255
|
-
|
|
256
|
-
this.
|
|
257
|
-
|
|
241
|
+
ffi.request.webviewFindInPage({
|
|
242
|
+
id: this.id,
|
|
243
|
+
searchText,
|
|
258
244
|
forward,
|
|
259
245
|
matchCase,
|
|
260
|
-
);
|
|
246
|
+
});
|
|
261
247
|
}
|
|
262
248
|
|
|
263
249
|
stopFindInPage() {
|
|
264
|
-
|
|
250
|
+
ffi.request.webviewStopFind({ id: this.id });
|
|
265
251
|
}
|
|
266
252
|
|
|
267
253
|
openDevTools() {
|
|
268
|
-
|
|
254
|
+
ffi.request.webviewOpenDevTools({ id: this.id });
|
|
269
255
|
}
|
|
270
256
|
|
|
271
257
|
closeDevTools() {
|
|
272
|
-
|
|
258
|
+
ffi.request.webviewCloseDevTools({ id: this.id });
|
|
273
259
|
}
|
|
274
260
|
|
|
275
261
|
toggleDevTools() {
|
|
276
|
-
|
|
262
|
+
ffi.request.webviewToggleDevTools({ id: this.id });
|
|
277
263
|
}
|
|
278
264
|
|
|
279
265
|
/**
|
|
@@ -281,7 +267,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
281
267
|
* @param zoomLevel - The zoom level (1.0 = 100%, 1.5 = 150%, etc.)
|
|
282
268
|
*/
|
|
283
269
|
setPageZoom(zoomLevel: number) {
|
|
284
|
-
|
|
270
|
+
ffi.request.webviewSetPageZoom({ id: this.id, zoomLevel });
|
|
285
271
|
}
|
|
286
272
|
|
|
287
273
|
/**
|
|
@@ -289,7 +275,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
289
275
|
* @returns The current zoom level (1.0 = 100%)
|
|
290
276
|
*/
|
|
291
277
|
getPageZoom(): number {
|
|
292
|
-
return
|
|
278
|
+
return ffi.request.webviewGetPageZoom({ id: this.id }) as number;
|
|
293
279
|
}
|
|
294
280
|
|
|
295
281
|
// todo (yoav): move this to a class that also has off, append, prepend, etc.
|
|
@@ -345,7 +331,6 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
345
331
|
if (!this.ptr || this.isRemoved) {
|
|
346
332
|
return;
|
|
347
333
|
}
|
|
348
|
-
const ptr = this.ptr;
|
|
349
334
|
this.isRemoved = true;
|
|
350
335
|
// Drop JS-side references first so late callbacks cannot target a stale view.
|
|
351
336
|
delete BrowserViewMap[this.id];
|
|
@@ -356,10 +341,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
356
341
|
unregisterHandler() {},
|
|
357
342
|
});
|
|
358
343
|
this.rpcHandler = undefined;
|
|
359
|
-
|
|
360
|
-
this.rpcHandler = undefined;
|
|
361
|
-
this.ptr = null;
|
|
362
|
-
native!.symbols.webviewRemove(ptr);
|
|
344
|
+
ffi.request.webviewRemove({ id: this.id });
|
|
363
345
|
}
|
|
364
346
|
|
|
365
347
|
static getById(id: number) {
|
|
@@ -5,11 +5,10 @@ import { type Pointer } from "bun:ffi";
|
|
|
5
5
|
import { BuildConfig } from "./BuildConfig";
|
|
6
6
|
import { quit } from "./Utils";
|
|
7
7
|
import { type RPCWithTransport } from "../../shared/rpc.js";
|
|
8
|
-
import { getNextWindowId } from "./windowIds";
|
|
9
8
|
import { GpuWindowMap } from "./GpuWindow";
|
|
10
9
|
import { WGPUView } from "./WGPUView";
|
|
11
10
|
|
|
12
|
-
const buildConfig =
|
|
11
|
+
const buildConfig = BuildConfig.getSync();
|
|
13
12
|
|
|
14
13
|
export type WindowOptionsType<T = undefined> = {
|
|
15
14
|
trafficLightOffset?: {
|
|
@@ -116,8 +115,7 @@ electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
|
|
|
116
115
|
});
|
|
117
116
|
|
|
118
117
|
export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
119
|
-
id
|
|
120
|
-
ptr!: Pointer;
|
|
118
|
+
id = 0;
|
|
121
119
|
title: string = "Electrobun";
|
|
122
120
|
state: "creating" | "created" = "creating";
|
|
123
121
|
url: string | null = null;
|
|
@@ -146,6 +144,10 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
146
144
|
// todo (yoav): make this an array of ids or something
|
|
147
145
|
webviewId!: number;
|
|
148
146
|
|
|
147
|
+
get ptr(): Pointer | null {
|
|
148
|
+
return ffi.request.getWindowPointer({ winId: this.id }) as Pointer | null;
|
|
149
|
+
}
|
|
150
|
+
|
|
149
151
|
constructor(options: Partial<WindowOptionsType<T>> = defaultOptions) {
|
|
150
152
|
this.title = options.title || "New Window";
|
|
151
153
|
this.frame = options.frame
|
|
@@ -177,8 +179,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
177
179
|
hidden,
|
|
178
180
|
activate,
|
|
179
181
|
}: Partial<WindowOptionsType<T>>) {
|
|
180
|
-
|
|
181
|
-
id: this.id,
|
|
182
|
+
const windowId = ffi.request.createWindow({
|
|
182
183
|
title: this.title,
|
|
183
184
|
url: this.url || "",
|
|
184
185
|
frame: {
|
|
@@ -221,7 +222,13 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
221
222
|
hidden: hidden ?? false,
|
|
222
223
|
activate: activate ?? true,
|
|
223
224
|
trafficLightOffset: this.trafficLightOffset,
|
|
224
|
-
})
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (!windowId) {
|
|
228
|
+
throw "Failed to create window";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.id = windowId as number;
|
|
225
232
|
|
|
226
233
|
BrowserWindowMap[this.id] = this;
|
|
227
234
|
|
|
@@ -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,6 @@ 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 { getNextWindowId } from "./windowIds";
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
export type GpuWindowOptionsType = {
|
|
@@ -54,8 +53,7 @@ electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
|
|
|
54
53
|
});
|
|
55
54
|
|
|
56
55
|
export class GpuWindow {
|
|
57
|
-
id
|
|
58
|
-
ptr!: Pointer;
|
|
56
|
+
id = 0;
|
|
59
57
|
title: string = "Electrobun";
|
|
60
58
|
state: "creating" | "created" = "creating";
|
|
61
59
|
transparent: boolean = false;
|
|
@@ -73,6 +71,10 @@ export class GpuWindow {
|
|
|
73
71
|
};
|
|
74
72
|
wgpuViewId!: number;
|
|
75
73
|
|
|
74
|
+
get ptr(): Pointer | null {
|
|
75
|
+
return ffi.request.getWindowPointer({ winId: this.id }) as Pointer | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
76
78
|
constructor(options: Partial<GpuWindowOptionsType> = defaultOptions) {
|
|
77
79
|
this.title = options.title || "New Window";
|
|
78
80
|
this.frame = options.frame
|
|
@@ -93,8 +95,7 @@ export class GpuWindow {
|
|
|
93
95
|
transparent,
|
|
94
96
|
activate,
|
|
95
97
|
}: Partial<GpuWindowOptionsType>) {
|
|
96
|
-
|
|
97
|
-
id: this.id,
|
|
98
|
+
const windowId = ffi.request.createWindow({
|
|
98
99
|
title: this.title,
|
|
99
100
|
url: "",
|
|
100
101
|
frame: {
|
|
@@ -136,7 +137,13 @@ export class GpuWindow {
|
|
|
136
137
|
transparent: transparent ?? false,
|
|
137
138
|
activate: activate ?? true,
|
|
138
139
|
trafficLightOffset: this.trafficLightOffset,
|
|
139
|
-
})
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!windowId) {
|
|
143
|
+
throw "Failed to create window";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.id = windowId as number;
|
|
140
147
|
|
|
141
148
|
GpuWindowMap[this.id] = this;
|
|
142
149
|
|
|
@@ -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
|
|