electrobun 1.18.1 → 1.18.4-beta.10
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 +37 -14
- package/dash.config.ts +28 -0
- package/dist/api/browser/global.d.ts +6 -0
- package/dist/api/browser/index.ts +162 -44
- package/dist/api/{bun → config}/ElectrobunConfig.ts +109 -40
- package/dist/api/config/validate.test.ts +31 -0
- package/dist/api/config/validate.ts +19 -0
- package/dist/api/preload/.generated/compiled.ts +8 -0
- package/dist/api/{bun/preload → preload}/build.ts +6 -6
- package/dist/api/{bun/preload → preload}/encryption.ts +11 -5
- package/dist/api/{bun/preload → preload}/globals.d.ts +8 -0
- package/dist/api/{bun/preload → preload}/index.ts +18 -8
- package/dist/api/{bun/preload → preload}/webviewTag.ts +32 -3
- package/dist/api/{bun/preload → preload}/wgpuTag.ts +33 -2
- package/dist/api/{bun → sdks/bun}/__tests__/ffi-contract.test.ts +6 -9
- package/dist/api/{bun → sdks/bun}/core/BrowserView.ts +96 -55
- package/dist/api/{bun → sdks/bun}/core/BrowserWindow.ts +20 -30
- package/dist/api/{bun → sdks/bun}/core/BuildConfig.ts +32 -5
- package/dist/api/{bun → sdks/bun}/core/GpuWindow.ts +20 -7
- package/dist/api/sdks/bun/core/Socket.ts +22 -0
- package/dist/api/{bun → sdks/bun}/core/Tray.ts +33 -32
- package/dist/api/{bun → sdks/bun}/core/Updater.ts +10 -10
- package/dist/api/{bun → sdks/bun}/core/Utils.ts +3 -5
- package/dist/api/{bun → sdks/bun}/core/WGPUView.ts +43 -13
- package/dist/api/{bun → sdks/bun}/index.ts +39 -10
- package/dist/api/{bun → sdks/bun}/proc/native.ts +942 -746
- package/dist/api/{bun → sdks/bun}/webGPU.ts +1 -1
- package/dist/api/{bun → sdks/bun}/webgpuAdapter.ts +40 -33
- package/dist/api/shared/build-dependencies.test.ts +48 -0
- package/dist/api/shared/build-dependencies.ts +46 -0
- package/dist/api/shared/go-version.ts +3 -0
- package/dist/api/shared/odin-version.ts +6 -0
- package/dist/api/shared/rust-version.ts +3 -0
- package/dist/go-sdk/callbacks.go +260 -0
- package/dist/go-sdk/electrobun.go +2021 -0
- package/dist/main.js +35 -28
- package/dist/odin-sdk/electrobun/electrobun.odin +2337 -0
- package/dist/preload-full.js +948 -0
- package/dist/preload-sandboxed.js +111 -0
- package/dist/rust-sdk/electrobun.rs +2396 -0
- package/dist/zig-sdk/electrobun.zig +2005 -0
- package/package.json +5 -29
- package/src/cli/index.ts +868 -654
- package/bin/electrobun.cjs +0 -169
- package/dist/api/bun/core/Socket.ts +0 -205
- package/dist/api/bun/core/windowIds.ts +0 -5
- package/dist/api/bun/preload/.generated/compiled.ts +0 -8
- package/dist/api/shared/bun-version.ts +0 -3
- /package/dist/api/{bun/preload → preload}/dragRegions.ts +0 -0
- /package/dist/api/{bun/preload → preload}/events.ts +0 -0
- /package/dist/api/{bun/preload → preload}/index-sandboxed.ts +0 -0
- /package/dist/api/{bun/preload → preload}/internalRpc.ts +0 -0
- /package/dist/api/{bun/preload → preload}/overlaySync.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/core/ApplicationMenu.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/core/ContextMenu.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/core/Paths.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/core/menuRoles.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/events/ApplicationEvents.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/events/event.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/events/eventEmitter.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/events/trayEvents.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/events/webviewEvents.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/events/windowEvents.ts +0 -0
- /package/dist/api/{bun → sdks/bun}/proc/linux.md +0 -0
|
@@ -1,16 +1,13 @@
|
|
|
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,
|
|
6
5
|
type ElectrobunRPCConfig,
|
|
7
6
|
type RPCWithTransport,
|
|
8
7
|
defineElectrobunRPC,
|
|
9
|
-
} from "
|
|
10
|
-
import { Updater } from "./Updater";
|
|
8
|
+
} from "../../../shared/rpc.js";
|
|
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({
|
|
@@ -191,23 +177,23 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
191
177
|
this.rpc!.setTransport(this.createTransport());
|
|
192
178
|
}
|
|
193
179
|
|
|
194
|
-
|
|
180
|
+
sendHostMessageToWebviewViaExecute(jsonMessage: unknown) {
|
|
195
181
|
const stringifiedMessage =
|
|
196
182
|
typeof jsonMessage === "string"
|
|
197
183
|
? jsonMessage
|
|
198
184
|
: JSON.stringify(jsonMessage);
|
|
199
185
|
// todo (yoav): make this a shared const with the browser api
|
|
200
|
-
const wrappedMessage = `window.__electrobun.
|
|
186
|
+
const wrappedMessage = `window.__electrobun.receiveMessageFromHost(${stringifiedMessage})`;
|
|
201
187
|
this.executeJavascript(wrappedMessage);
|
|
202
188
|
}
|
|
203
189
|
|
|
204
|
-
|
|
190
|
+
sendInternalHostMessageViaExecute(jsonMessage: unknown) {
|
|
205
191
|
const stringifiedMessage =
|
|
206
192
|
typeof jsonMessage === "string"
|
|
207
193
|
? jsonMessage
|
|
208
194
|
: JSON.stringify(jsonMessage);
|
|
209
195
|
// todo (yoav): make this a shared const with the browser api
|
|
210
|
-
const wrappedMessage = `window.__electrobun.
|
|
196
|
+
const wrappedMessage = `window.__electrobun.receiveInternalMessageFromHost(${stringifiedMessage})`;
|
|
211
197
|
this.executeJavascript(wrappedMessage);
|
|
212
198
|
}
|
|
213
199
|
|
|
@@ -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.
|
|
@@ -326,9 +312,9 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
326
312
|
if (!sentOverSocket) {
|
|
327
313
|
try {
|
|
328
314
|
const messageString = JSON.stringify(message);
|
|
329
|
-
that.
|
|
315
|
+
that.sendHostMessageToWebviewViaExecute(messageString);
|
|
330
316
|
} catch (error) {
|
|
331
|
-
console.error("
|
|
317
|
+
console.error("host: failed to serialize message to webview", error);
|
|
332
318
|
}
|
|
333
319
|
}
|
|
334
320
|
},
|
|
@@ -342,10 +328,9 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
342
328
|
};
|
|
343
329
|
|
|
344
330
|
remove() {
|
|
345
|
-
if (
|
|
331
|
+
if (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,16 +341,72 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
356
341
|
unregisterHandler() {},
|
|
357
342
|
});
|
|
358
343
|
this.rpcHandler = undefined;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
344
|
+
try {
|
|
345
|
+
ffi.request.webviewRemove({ id: this.id });
|
|
346
|
+
} catch (error) {
|
|
347
|
+
console.error(`Error removing webview ${this.id}:`, error);
|
|
348
|
+
}
|
|
363
349
|
}
|
|
364
350
|
|
|
365
351
|
static getById(id: number) {
|
|
366
352
|
return BrowserViewMap[id];
|
|
367
353
|
}
|
|
368
354
|
|
|
355
|
+
// Core can create webviews before Bun has constructed a JS wrapper for them.
|
|
356
|
+
// Use this in native/runtime paths that need to ensure a wrapper exists.
|
|
357
|
+
static ensureWrapped<T extends RPCWithTransport = RPCWithTransport>(
|
|
358
|
+
id: number,
|
|
359
|
+
options: Partial<BrowserViewOptions<T>> = {},
|
|
360
|
+
) {
|
|
361
|
+
return (
|
|
362
|
+
(BrowserViewMap[id] as BrowserView<T> | undefined) ??
|
|
363
|
+
BrowserView.adoptExisting(id, options)
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
static adoptExisting<T extends RPCWithTransport = RPCWithTransport>(
|
|
368
|
+
id: number,
|
|
369
|
+
options: Partial<BrowserViewOptions<T>> = {},
|
|
370
|
+
) {
|
|
371
|
+
const existing = BrowserViewMap[id] as BrowserView<T> | undefined;
|
|
372
|
+
if (existing) {
|
|
373
|
+
return existing;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const ptr = ffi.request.getWebviewPointer({ id }) as Pointer | null;
|
|
377
|
+
if (!ptr) {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const view = Object.create(BrowserView.prototype) as BrowserView<T>;
|
|
382
|
+
view.id = id;
|
|
383
|
+
view.hostWebviewId = options.hostWebviewId;
|
|
384
|
+
view.windowId = options.windowId ?? 0;
|
|
385
|
+
view.renderer = options.renderer ?? defaultOptions.renderer ?? "native";
|
|
386
|
+
view.url = options.url ?? defaultOptions.url ?? null;
|
|
387
|
+
view.html = options.html ?? defaultOptions.html ?? null;
|
|
388
|
+
view.preload = options.preload ?? defaultOptions.preload ?? null;
|
|
389
|
+
view.viewsRoot = options.viewsRoot ?? defaultOptions.viewsRoot ?? null;
|
|
390
|
+
view.partition = options.partition ?? null;
|
|
391
|
+
view.frame = {
|
|
392
|
+
x: options.frame?.x ?? defaultOptions.frame!.x,
|
|
393
|
+
y: options.frame?.y ?? defaultOptions.frame!.y,
|
|
394
|
+
width: options.frame?.width ?? defaultOptions.frame!.width,
|
|
395
|
+
height: options.frame?.height ?? defaultOptions.frame!.height,
|
|
396
|
+
};
|
|
397
|
+
view.secretKey = new Uint8Array(0);
|
|
398
|
+
view.rpc = options.rpc;
|
|
399
|
+
view.rpcHandler = undefined;
|
|
400
|
+
view.autoResize = options.autoResize === false ? false : true;
|
|
401
|
+
view.navigationRules = options.navigationRules ?? null;
|
|
402
|
+
view.sandbox = options.sandbox ?? false;
|
|
403
|
+
view.startTransparent = options.startTransparent ?? false;
|
|
404
|
+
view.startPassthrough = options.startPassthrough ?? false;
|
|
405
|
+
view.isRemoved = false;
|
|
406
|
+
BrowserViewMap[id] = view as BrowserView<any>;
|
|
407
|
+
return view;
|
|
408
|
+
}
|
|
409
|
+
|
|
369
410
|
static getAll() {
|
|
370
411
|
return Object.values(BrowserViewMap);
|
|
371
412
|
}
|
|
@@ -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 {
|
|
7
|
-
import { type RPCWithTransport } from "../../shared/rpc.js";
|
|
8
|
-
import { getNextWindowId } from "./windowIds";
|
|
9
|
-
import { GpuWindowMap } from "./GpuWindow";
|
|
6
|
+
import { type RPCWithTransport } from "../../../shared/rpc.js";
|
|
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,8 +1,10 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
|
|
1
3
|
export type BuildConfigType = {
|
|
4
|
+
mainProcess?: "cottontail" | "zig" | "rust" | "go" | "odin";
|
|
2
5
|
defaultRenderer: "native" | "cef";
|
|
3
6
|
availableRenderers: ("native" | "cef")[];
|
|
4
7
|
cefVersion?: string;
|
|
5
|
-
bunVersion?: string;
|
|
6
8
|
runtime?: {
|
|
7
9
|
exitOnLastWindowClosed?: boolean;
|
|
8
10
|
[key: string]: unknown;
|
|
@@ -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
|
|
|
@@ -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
|
+
};
|