electrobun 0.0.19-beta.13 → 0.0.19-beta.130
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/BUILD.md +90 -0
- package/README.md +1 -1
- package/bin/electrobun.cjs +2 -9
- package/debug.js +5 -0
- package/dist/api/browser/builtinrpcSchema.ts +19 -0
- package/dist/api/browser/index.ts +409 -0
- package/dist/api/browser/rpc/webview.ts +79 -0
- package/dist/api/browser/stylesAndElements.ts +3 -0
- package/dist/api/browser/webviewtag.ts +586 -0
- package/dist/api/bun/ElectrobunConfig.ts +171 -0
- package/dist/api/bun/core/ApplicationMenu.ts +66 -0
- package/dist/api/bun/core/BrowserView.ts +349 -0
- package/dist/api/bun/core/BrowserWindow.ts +195 -0
- package/dist/api/bun/core/ContextMenu.ts +67 -0
- package/dist/api/bun/core/Paths.ts +5 -0
- package/dist/api/bun/core/Socket.ts +181 -0
- package/dist/api/bun/core/Tray.ts +121 -0
- package/dist/api/bun/core/Updater.ts +681 -0
- package/dist/api/bun/core/Utils.ts +48 -0
- package/dist/api/bun/events/ApplicationEvents.ts +14 -0
- package/dist/api/bun/events/event.ts +29 -0
- package/dist/api/bun/events/eventEmitter.ts +45 -0
- package/dist/api/bun/events/trayEvents.ts +9 -0
- package/dist/api/bun/events/webviewEvents.ts +16 -0
- package/dist/api/bun/events/windowEvents.ts +12 -0
- package/dist/api/bun/index.ts +47 -0
- package/dist/api/bun/proc/linux.md +43 -0
- package/dist/api/bun/proc/native.ts +1322 -0
- package/dist/api/shared/platform.ts +48 -0
- package/dist/main.js +54 -0
- package/package.json +11 -6
- package/src/cli/index.ts +1353 -239
- package/templates/hello-world/README.md +57 -0
- package/templates/hello-world/bun.lock +225 -0
- package/templates/hello-world/electrobun.config.ts +28 -0
- package/templates/hello-world/package.json +16 -0
- package/templates/hello-world/src/bun/index.ts +15 -0
- package/templates/hello-world/src/mainview/index.css +124 -0
- package/templates/hello-world/src/mainview/index.html +46 -0
- package/templates/hello-world/src/mainview/index.ts +1 -0
- package/templates/interactive-playground/README.md +26 -0
- package/templates/interactive-playground/assets/tray-icon.png +0 -0
- package/templates/interactive-playground/electrobun.config.ts +36 -0
- package/templates/interactive-playground/package-lock.json +36 -0
- package/templates/interactive-playground/package.json +15 -0
- package/templates/interactive-playground/src/bun/demos/files.ts +70 -0
- package/templates/interactive-playground/src/bun/demos/menus.ts +139 -0
- package/templates/interactive-playground/src/bun/demos/rpc.ts +83 -0
- package/templates/interactive-playground/src/bun/demos/system.ts +72 -0
- package/templates/interactive-playground/src/bun/demos/updates.ts +105 -0
- package/templates/interactive-playground/src/bun/demos/windows.ts +90 -0
- package/templates/interactive-playground/src/bun/index.ts +124 -0
- package/templates/interactive-playground/src/bun/types/rpc.ts +109 -0
- package/templates/interactive-playground/src/mainview/components/EventLog.ts +107 -0
- package/templates/interactive-playground/src/mainview/components/Sidebar.ts +65 -0
- package/templates/interactive-playground/src/mainview/components/Toast.ts +57 -0
- package/templates/interactive-playground/src/mainview/demos/FileDemo.ts +211 -0
- package/templates/interactive-playground/src/mainview/demos/MenuDemo.ts +102 -0
- package/templates/interactive-playground/src/mainview/demos/RPCDemo.ts +229 -0
- package/templates/interactive-playground/src/mainview/demos/TrayDemo.ts +132 -0
- package/templates/interactive-playground/src/mainview/demos/WebViewDemo.ts +411 -0
- package/templates/interactive-playground/src/mainview/demos/WindowDemo.ts +207 -0
- package/templates/interactive-playground/src/mainview/index.css +538 -0
- package/templates/interactive-playground/src/mainview/index.html +103 -0
- package/templates/interactive-playground/src/mainview/index.ts +238 -0
- package/templates/multitab-browser/README.md +34 -0
- package/templates/multitab-browser/bun.lock +224 -0
- package/templates/multitab-browser/electrobun.config.ts +32 -0
- package/templates/multitab-browser/package-lock.json +20 -0
- package/templates/multitab-browser/package.json +12 -0
- package/templates/multitab-browser/src/bun/index.ts +144 -0
- package/templates/multitab-browser/src/bun/tabManager.ts +200 -0
- package/templates/multitab-browser/src/bun/types/rpc.ts +78 -0
- package/templates/multitab-browser/src/mainview/index.css +487 -0
- package/templates/multitab-browser/src/mainview/index.html +94 -0
- package/templates/multitab-browser/src/mainview/index.ts +630 -0
- package/templates/photo-booth/README.md +108 -0
- package/templates/photo-booth/bun.lock +239 -0
- package/templates/photo-booth/electrobun.config.ts +28 -0
- package/templates/photo-booth/package.json +16 -0
- package/templates/photo-booth/src/bun/index.ts +92 -0
- package/templates/photo-booth/src/mainview/index.css +465 -0
- package/templates/photo-booth/src/mainview/index.html +124 -0
- package/templates/photo-booth/src/mainview/index.ts +499 -0
- package/tests/bun.lock +14 -0
- package/tests/electrobun.config.ts +45 -0
- package/tests/package-lock.json +36 -0
- package/tests/package.json +13 -0
- package/tests/src/bun/index.ts +100 -0
- package/tests/src/bun/test-runner.ts +508 -0
- package/tests/src/mainview/index.html +110 -0
- package/tests/src/mainview/index.ts +458 -0
- package/tests/src/mainview/styles/main.css +451 -0
- package/tests/src/testviews/tray-test.html +57 -0
- package/tests/src/testviews/webview-mask.html +114 -0
- package/tests/src/testviews/webview-navigation.html +36 -0
- package/tests/src/testviews/window-create.html +17 -0
- package/tests/src/testviews/window-events.html +29 -0
- package/tests/src/testviews/window-focus.html +37 -0
- package/tests/src/webviewtag/index.ts +11 -0
|
@@ -0,0 +1,1322 @@
|
|
|
1
|
+
import { join, resolve } from "path";
|
|
2
|
+
import { type RPCSchema, type RPCTransport, createRPC } from "rpc-anywhere";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import electrobunEventEmitter from "../events/eventEmitter";
|
|
6
|
+
import { BrowserView } from "../core/BrowserView";
|
|
7
|
+
import { Updater } from "../core/Updater";
|
|
8
|
+
import { Tray } from "../core/Tray";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// todo: set up FFI, this is already in the webworker.
|
|
13
|
+
|
|
14
|
+
import { dirname } from "path";
|
|
15
|
+
import { dlopen, suffix, JSCallback, CString, ptr, FFIType, toArrayBuffer } from "bun:ffi";
|
|
16
|
+
import { BrowserWindow, BrowserWindowMap } from "../core/BrowserWindow";
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
export const native = (() => {
|
|
21
|
+
try {
|
|
22
|
+
return dlopen(`./libNativeWrapper.${suffix}`, {
|
|
23
|
+
// window
|
|
24
|
+
createWindowWithFrameAndStyleFromWorker: {
|
|
25
|
+
// Pass each parameter individually
|
|
26
|
+
args: [
|
|
27
|
+
FFIType.u32, // windowId
|
|
28
|
+
FFIType.f64, FFIType.f64, // x, y
|
|
29
|
+
FFIType.f64, FFIType.f64, // width, height
|
|
30
|
+
FFIType.u32, // styleMask
|
|
31
|
+
FFIType.cstring, // titleBarStyle
|
|
32
|
+
FFIType.function, // closeHandler
|
|
33
|
+
FFIType.function, // moveHandler
|
|
34
|
+
FFIType.function // resizeHandler
|
|
35
|
+
|
|
36
|
+
],
|
|
37
|
+
returns: FFIType.ptr
|
|
38
|
+
},
|
|
39
|
+
setNSWindowTitle: {
|
|
40
|
+
args: [
|
|
41
|
+
FFIType.ptr, // window ptr
|
|
42
|
+
FFIType.cstring, // title
|
|
43
|
+
],
|
|
44
|
+
returns: FFIType.void,
|
|
45
|
+
},
|
|
46
|
+
makeNSWindowKeyAndOrderFront: {
|
|
47
|
+
args: [
|
|
48
|
+
FFIType.ptr, // window ptr
|
|
49
|
+
],
|
|
50
|
+
returns: FFIType.void,
|
|
51
|
+
},
|
|
52
|
+
closeNSWindow: {
|
|
53
|
+
args: [
|
|
54
|
+
FFIType.ptr, // window ptr
|
|
55
|
+
],
|
|
56
|
+
returns: FFIType.void,
|
|
57
|
+
},
|
|
58
|
+
// webview
|
|
59
|
+
initWebview: {
|
|
60
|
+
args: [
|
|
61
|
+
FFIType.u32, // webviewId
|
|
62
|
+
FFIType.ptr, // windowPtr
|
|
63
|
+
FFIType.cstring, // renderer
|
|
64
|
+
FFIType.cstring, // url
|
|
65
|
+
FFIType.f64, FFIType.f64, // x, y
|
|
66
|
+
FFIType.f64, FFIType.f64, // width, height
|
|
67
|
+
FFIType.bool, // autoResize
|
|
68
|
+
FFIType.cstring, // partition
|
|
69
|
+
FFIType.function, // decideNavigation: *const fn (u32, [*:0]const u8) callconv(.C) bool,
|
|
70
|
+
FFIType.function, // webviewEventHandler: *const fn (u32, [*:0]const u8, [*:0]const u8) callconv(.C) void,
|
|
71
|
+
FFIType.function, // bunBridgePostmessageHandler: *const fn (u32, [*:0]const u8) callconv(.C) void,
|
|
72
|
+
FFIType.function, // internalBridgeHandler: *const fn (u32, [*:0]const u8) callconv(.C) void,
|
|
73
|
+
FFIType.cstring, // electrobunPreloadScript
|
|
74
|
+
FFIType.cstring, // customPreloadScript
|
|
75
|
+
],
|
|
76
|
+
returns: FFIType.ptr
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// webviewtag
|
|
80
|
+
webviewCanGoBack: {
|
|
81
|
+
args: [FFIType.ptr],
|
|
82
|
+
returns: FFIType.bool
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
webviewCanGoForward: {
|
|
86
|
+
args: [FFIType.ptr],
|
|
87
|
+
returns: FFIType.bool
|
|
88
|
+
},
|
|
89
|
+
// TODO: Curently CEF doesn't support this directly
|
|
90
|
+
// revisit after refactor
|
|
91
|
+
// callAsyncJavaScript: {
|
|
92
|
+
// args: [
|
|
93
|
+
// FFIType.
|
|
94
|
+
// ],
|
|
95
|
+
// returns: FFIType.void
|
|
96
|
+
// },
|
|
97
|
+
resizeWebview: {
|
|
98
|
+
args: [
|
|
99
|
+
FFIType.ptr, // webview handle
|
|
100
|
+
FFIType.f64, // x
|
|
101
|
+
FFIType.f64, // y
|
|
102
|
+
FFIType.f64, // width
|
|
103
|
+
FFIType.f64, // height
|
|
104
|
+
FFIType.cstring // maskJson
|
|
105
|
+
],
|
|
106
|
+
returns: FFIType.void
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
loadURLInWebView: {
|
|
110
|
+
args: [FFIType.ptr, FFIType.cstring],
|
|
111
|
+
returns: FFIType.void
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
updatePreloadScriptToWebView: {
|
|
115
|
+
args: [
|
|
116
|
+
FFIType.ptr, // webview handle
|
|
117
|
+
FFIType.cstring, // script identifier
|
|
118
|
+
FFIType.cstring, // script
|
|
119
|
+
FFIType.bool // allframes
|
|
120
|
+
],
|
|
121
|
+
returns: FFIType.void
|
|
122
|
+
},
|
|
123
|
+
webviewGoBack: {
|
|
124
|
+
args: [FFIType.ptr],
|
|
125
|
+
returns: FFIType.void
|
|
126
|
+
},
|
|
127
|
+
webviewGoForward: {
|
|
128
|
+
args: [FFIType.ptr],
|
|
129
|
+
returns: FFIType.void
|
|
130
|
+
},
|
|
131
|
+
webviewReload: {
|
|
132
|
+
args: [FFIType.ptr],
|
|
133
|
+
returns: FFIType.void
|
|
134
|
+
},
|
|
135
|
+
webviewRemove: {
|
|
136
|
+
args: [FFIType.ptr],
|
|
137
|
+
returns: FFIType.void
|
|
138
|
+
},
|
|
139
|
+
startWindowMove: {
|
|
140
|
+
args: [FFIType.ptr],
|
|
141
|
+
returns: FFIType.void
|
|
142
|
+
},
|
|
143
|
+
stopWindowMove: {
|
|
144
|
+
args: [],
|
|
145
|
+
returns: FFIType.void
|
|
146
|
+
},
|
|
147
|
+
webviewSetTransparent: {
|
|
148
|
+
// TODO XX: bools or ints?
|
|
149
|
+
args: [FFIType.ptr, FFIType.bool],
|
|
150
|
+
returns: FFIType.void
|
|
151
|
+
},
|
|
152
|
+
webviewSetPassthrough: {
|
|
153
|
+
args: [FFIType.ptr, FFIType.bool],
|
|
154
|
+
returns: FFIType.void
|
|
155
|
+
},
|
|
156
|
+
webviewSetHidden: {
|
|
157
|
+
args: [FFIType.ptr, FFIType.bool],
|
|
158
|
+
returns: FFIType.void
|
|
159
|
+
},
|
|
160
|
+
evaluateJavaScriptWithNoCompletion: {
|
|
161
|
+
args: [FFIType.ptr, FFIType.cstring],
|
|
162
|
+
returns: FFIType.void
|
|
163
|
+
},
|
|
164
|
+
// Tray
|
|
165
|
+
createTray: {
|
|
166
|
+
args: [
|
|
167
|
+
FFIType.u32, // id
|
|
168
|
+
FFIType.cstring, // title
|
|
169
|
+
FFIType.cstring, // pathToImage
|
|
170
|
+
FFIType.bool, // isTemplate
|
|
171
|
+
FFIType.u32, // width
|
|
172
|
+
FFIType.u32, //height
|
|
173
|
+
FFIType.function, // trayItemHandler
|
|
174
|
+
],
|
|
175
|
+
returns: FFIType.ptr
|
|
176
|
+
},
|
|
177
|
+
setTrayTitle: {
|
|
178
|
+
args: [FFIType.ptr, FFIType.cstring],
|
|
179
|
+
returns: FFIType.void
|
|
180
|
+
},
|
|
181
|
+
setTrayImage: {
|
|
182
|
+
args: [FFIType.ptr, FFIType.cstring],
|
|
183
|
+
returns: FFIType.void
|
|
184
|
+
},
|
|
185
|
+
setTrayMenu: {
|
|
186
|
+
args: [FFIType.ptr, FFIType.cstring],
|
|
187
|
+
returns: FFIType.void
|
|
188
|
+
},
|
|
189
|
+
removeTray: {
|
|
190
|
+
args: [FFIType.ptr],
|
|
191
|
+
returns: FFIType.void
|
|
192
|
+
},
|
|
193
|
+
setApplicationMenu: {
|
|
194
|
+
args: [FFIType.cstring, FFIType.function],
|
|
195
|
+
returns: FFIType.void
|
|
196
|
+
},
|
|
197
|
+
showContextMenu: {
|
|
198
|
+
args: [FFIType.cstring, FFIType.function],
|
|
199
|
+
returns: FFIType.void
|
|
200
|
+
},
|
|
201
|
+
moveToTrash: {
|
|
202
|
+
args: [FFIType.cstring],
|
|
203
|
+
returns: FFIType.bool
|
|
204
|
+
},
|
|
205
|
+
showItemInFolder: {
|
|
206
|
+
args: [FFIType.cstring],
|
|
207
|
+
returns: FFIType.void
|
|
208
|
+
},
|
|
209
|
+
openFileDialog: {
|
|
210
|
+
args: [
|
|
211
|
+
FFIType.cstring,
|
|
212
|
+
FFIType.cstring,
|
|
213
|
+
FFIType.int,
|
|
214
|
+
FFIType.int,
|
|
215
|
+
FFIType.int,
|
|
216
|
+
],
|
|
217
|
+
returns: FFIType.cstring
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
// MacOS specific native utils
|
|
221
|
+
getNSWindowStyleMask: {
|
|
222
|
+
args: [
|
|
223
|
+
FFIType.bool,
|
|
224
|
+
FFIType.bool,
|
|
225
|
+
FFIType.bool,
|
|
226
|
+
FFIType.bool,
|
|
227
|
+
FFIType.bool,
|
|
228
|
+
FFIType.bool,
|
|
229
|
+
FFIType.bool,
|
|
230
|
+
FFIType.bool,
|
|
231
|
+
FFIType.bool,
|
|
232
|
+
FFIType.bool,
|
|
233
|
+
FFIType.bool,
|
|
234
|
+
FFIType.bool,
|
|
235
|
+
],
|
|
236
|
+
returns: FFIType.u32
|
|
237
|
+
},
|
|
238
|
+
// JSCallback utils for native code to use
|
|
239
|
+
setJSUtils: {
|
|
240
|
+
args: [
|
|
241
|
+
FFIType.function, // get Mimetype from url/filename
|
|
242
|
+
FFIType.function, // get html property from webview
|
|
243
|
+
],
|
|
244
|
+
returns: FFIType.void
|
|
245
|
+
},
|
|
246
|
+
killApp: {
|
|
247
|
+
args: [],
|
|
248
|
+
returns: FFIType.void
|
|
249
|
+
},
|
|
250
|
+
testFFI2: {
|
|
251
|
+
args: [FFIType.function],
|
|
252
|
+
returns: FFIType.void
|
|
253
|
+
},
|
|
254
|
+
// FFIFn: {
|
|
255
|
+
// args: [],
|
|
256
|
+
// returns: FFIType.void
|
|
257
|
+
// },
|
|
258
|
+
});
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.log('FATAL Error opening native FFI:', err.message);
|
|
261
|
+
console.log('This may be due to:');
|
|
262
|
+
console.log(' - Missing libNativeWrapper.dll/so/dylib');
|
|
263
|
+
console.log(' - Architecture mismatch (ARM64 vs x64)');
|
|
264
|
+
console.log(' - Missing WebView2 or CEF dependencies');
|
|
265
|
+
if (suffix === 'so') {
|
|
266
|
+
console.log(' - Missing system libraries (try: ldd ./libNativeWrapper.so)');
|
|
267
|
+
}
|
|
268
|
+
console.log('Check that the build process completed successfully for your architecture.');
|
|
269
|
+
process.exit();
|
|
270
|
+
}
|
|
271
|
+
})();
|
|
272
|
+
|
|
273
|
+
const callbacks = [];
|
|
274
|
+
|
|
275
|
+
// NOTE: Bun seems to hit limits on args or arg types. eg: trying to send 12 bools results
|
|
276
|
+
// in only about 8 going through then params after that. I think it may be similar to
|
|
277
|
+
// a zig bug I ran into last year. So check number of args in a signature when alignment issues occur.
|
|
278
|
+
|
|
279
|
+
// TODO XX: maybe this should actually be inside BrowserWindow and BrowserView as static methods
|
|
280
|
+
export const ffi = {
|
|
281
|
+
request: {
|
|
282
|
+
createWindow: (params: {
|
|
283
|
+
id: number,
|
|
284
|
+
url: string | null,
|
|
285
|
+
title: string,
|
|
286
|
+
frame: {
|
|
287
|
+
width: number,
|
|
288
|
+
height: number,
|
|
289
|
+
x: number,
|
|
290
|
+
y: number,
|
|
291
|
+
},
|
|
292
|
+
styleMask: {
|
|
293
|
+
Borderless: boolean,
|
|
294
|
+
Titled: boolean,
|
|
295
|
+
Closable: boolean,
|
|
296
|
+
Miniaturizable: boolean,
|
|
297
|
+
Resizable: boolean,
|
|
298
|
+
UnifiedTitleAndToolbar: boolean,
|
|
299
|
+
FullScreen: boolean,
|
|
300
|
+
FullSizeContentView: boolean,
|
|
301
|
+
UtilityWindow: boolean,
|
|
302
|
+
DocModalWindow: boolean,
|
|
303
|
+
NonactivatingPanel: boolean,
|
|
304
|
+
HUDWindow: boolean,
|
|
305
|
+
},
|
|
306
|
+
titleBarStyle: string,
|
|
307
|
+
}): FFIType.ptr => {
|
|
308
|
+
const {id, url, title, frame: {x, y, width, height}, styleMask: {
|
|
309
|
+
Borderless,
|
|
310
|
+
Titled,
|
|
311
|
+
Closable,
|
|
312
|
+
Miniaturizable,
|
|
313
|
+
Resizable,
|
|
314
|
+
UnifiedTitleAndToolbar,
|
|
315
|
+
FullScreen,
|
|
316
|
+
FullSizeContentView,
|
|
317
|
+
UtilityWindow,
|
|
318
|
+
DocModalWindow,
|
|
319
|
+
NonactivatingPanel,
|
|
320
|
+
HUDWindow
|
|
321
|
+
},
|
|
322
|
+
titleBarStyle} = params
|
|
323
|
+
|
|
324
|
+
const styleMask = native.symbols.getNSWindowStyleMask(
|
|
325
|
+
Borderless,
|
|
326
|
+
Titled,
|
|
327
|
+
Closable,
|
|
328
|
+
Miniaturizable,
|
|
329
|
+
Resizable,
|
|
330
|
+
UnifiedTitleAndToolbar,
|
|
331
|
+
FullScreen,
|
|
332
|
+
FullSizeContentView,
|
|
333
|
+
UtilityWindow,
|
|
334
|
+
DocModalWindow,
|
|
335
|
+
NonactivatingPanel,
|
|
336
|
+
HUDWindow
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
const windowPtr = native.symbols.createWindowWithFrameAndStyleFromWorker(
|
|
340
|
+
id,
|
|
341
|
+
// frame
|
|
342
|
+
x, y, width, height,
|
|
343
|
+
styleMask,
|
|
344
|
+
// style
|
|
345
|
+
toCString(titleBarStyle),
|
|
346
|
+
// callbacks
|
|
347
|
+
windowCloseCallback,
|
|
348
|
+
windowMoveCallback,
|
|
349
|
+
windowResizeCallback,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
if (!windowPtr) {
|
|
354
|
+
throw "Failed to create window"
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
native.symbols.setNSWindowTitle(windowPtr, toCString(title));
|
|
358
|
+
// setTimeout(() => {
|
|
359
|
+
// console.log('calling makeNSWindowKeyAndOrderFront', windowPtr)
|
|
360
|
+
native.symbols.makeNSWindowKeyAndOrderFront(windowPtr);
|
|
361
|
+
// }, 1000)
|
|
362
|
+
|
|
363
|
+
return windowPtr;
|
|
364
|
+
},
|
|
365
|
+
setTitle: (params: {winId: number, title: string}) => {
|
|
366
|
+
const {winId, title} = params;
|
|
367
|
+
const windowPtr = BrowserWindow.getById(winId)?.ptr;
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
if (!windowPtr) {
|
|
371
|
+
throw `Can't add webview to window. window no longer exists`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
native.symbols.setNSWindowTitle(windowPtr, toCString(title));
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
closeWindow: (params: {winId: number}) => {
|
|
378
|
+
const {winId} = params;
|
|
379
|
+
const windowPtr = BrowserWindow.getById(winId)?.ptr;
|
|
380
|
+
|
|
381
|
+
if (!windowPtr) {
|
|
382
|
+
throw `Can't close window. Window no longer exists`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
native.symbols.closeNSWindow(windowPtr);
|
|
386
|
+
// Note: Cleanup of BrowserWindowMap happens in the windowCloseCallback
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
focusWindow: (params: {winId: number}) => {
|
|
390
|
+
const {winId} = params;
|
|
391
|
+
const windowPtr = BrowserWindow.getById(winId)?.ptr;
|
|
392
|
+
|
|
393
|
+
if (!windowPtr) {
|
|
394
|
+
throw `Can't focus window. Window no longer exists`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
native.symbols.makeNSWindowKeyAndOrderFront(windowPtr);
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
createWebview: (params: {
|
|
401
|
+
id: number;
|
|
402
|
+
windowId: number;
|
|
403
|
+
renderer: "cef" | "native";
|
|
404
|
+
rpcPort: number;
|
|
405
|
+
secretKey: string;
|
|
406
|
+
hostWebviewId: number | null;
|
|
407
|
+
pipePrefix: string;
|
|
408
|
+
url: string | null;
|
|
409
|
+
html: string | null;
|
|
410
|
+
partition: string | null;
|
|
411
|
+
preload: string | null;
|
|
412
|
+
frame: {
|
|
413
|
+
x: number;
|
|
414
|
+
y: number;
|
|
415
|
+
width: number;
|
|
416
|
+
height: number;
|
|
417
|
+
};
|
|
418
|
+
autoResize: boolean;
|
|
419
|
+
navigationRules: string | null;
|
|
420
|
+
}): FFIType.ptr => {
|
|
421
|
+
|
|
422
|
+
const { id,
|
|
423
|
+
windowId,
|
|
424
|
+
renderer,
|
|
425
|
+
rpcPort,
|
|
426
|
+
secretKey,
|
|
427
|
+
// hostWebviewId: number | null;
|
|
428
|
+
// pipePrefix: string;
|
|
429
|
+
url,
|
|
430
|
+
// html: string | null;
|
|
431
|
+
partition,
|
|
432
|
+
preload,
|
|
433
|
+
frame: {
|
|
434
|
+
x,
|
|
435
|
+
y,
|
|
436
|
+
width,
|
|
437
|
+
height,
|
|
438
|
+
},
|
|
439
|
+
autoResize} = params
|
|
440
|
+
|
|
441
|
+
const windowPtr = BrowserWindow.getById(windowId)?.ptr;
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
if (!windowPtr) {
|
|
445
|
+
throw `Can't add webview to window. window no longer exists`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const electrobunPreload = `
|
|
449
|
+
window.__electrobunWebviewId = ${id};
|
|
450
|
+
window.__electrobunWindowId = ${windowId};
|
|
451
|
+
window.__electrobunRpcSocketPort = ${rpcPort};
|
|
452
|
+
window.__electrobunInternalBridge = window.webkit?.messageHandlers?.internalBridge || window.internalBridge || window.chrome?.webview?.hostObjects?.internalBridge;
|
|
453
|
+
window.__electrobunBunBridge = window.webkit?.messageHandlers?.bunBridge || window.bunBridge || window.chrome?.webview?.hostObjects?.bunBridge;
|
|
454
|
+
(async () => {
|
|
455
|
+
|
|
456
|
+
function base64ToUint8Array(base64) {
|
|
457
|
+
return new Uint8Array(atob(base64).split('').map(char => char.charCodeAt(0)));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function uint8ArrayToBase64(uint8Array) {
|
|
461
|
+
let binary = '';
|
|
462
|
+
for (let i = 0; i < uint8Array.length; i++) {
|
|
463
|
+
binary += String.fromCharCode(uint8Array[i]);
|
|
464
|
+
}
|
|
465
|
+
return btoa(binary);
|
|
466
|
+
}
|
|
467
|
+
const generateKeyFromText = async (rawKey) => {
|
|
468
|
+
return await window.crypto.subtle.importKey(
|
|
469
|
+
'raw', // Key format
|
|
470
|
+
rawKey, // Key data
|
|
471
|
+
{ name: 'AES-GCM' }, // Algorithm details
|
|
472
|
+
true, // Extractable (set to false for better security)
|
|
473
|
+
['encrypt', 'decrypt'] // Key usages
|
|
474
|
+
);
|
|
475
|
+
};
|
|
476
|
+
const secretKey = await generateKeyFromText(new Uint8Array([${secretKey}]));
|
|
477
|
+
|
|
478
|
+
const encryptString = async (plaintext) => {
|
|
479
|
+
const encoder = new TextEncoder();
|
|
480
|
+
const encodedText = encoder.encode(plaintext);
|
|
481
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12)); // Initialization vector (12 bytes)
|
|
482
|
+
const encryptedBuffer = await window.crypto.subtle.encrypt(
|
|
483
|
+
{
|
|
484
|
+
name: "AES-GCM",
|
|
485
|
+
iv: iv,
|
|
486
|
+
},
|
|
487
|
+
secretKey,
|
|
488
|
+
encodedText
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
// Split the tag (last 16 bytes) from the ciphertext
|
|
493
|
+
const encryptedData = new Uint8Array(encryptedBuffer.slice(0, -16));
|
|
494
|
+
const tag = new Uint8Array(encryptedBuffer.slice(-16));
|
|
495
|
+
|
|
496
|
+
return { encryptedData: uint8ArrayToBase64(encryptedData), iv: uint8ArrayToBase64(iv), tag: uint8ArrayToBase64(tag) };
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// All args passed in as base64 strings
|
|
500
|
+
const decryptString = async (encryptedData, iv, tag) => {
|
|
501
|
+
encryptedData = base64ToUint8Array(encryptedData);
|
|
502
|
+
iv = base64ToUint8Array(iv);
|
|
503
|
+
tag = base64ToUint8Array(tag);
|
|
504
|
+
// Combine encrypted data and tag to match the format expected by SubtleCrypto
|
|
505
|
+
const combinedData = new Uint8Array(encryptedData.length + tag.length);
|
|
506
|
+
combinedData.set(encryptedData);
|
|
507
|
+
combinedData.set(tag, encryptedData.length);
|
|
508
|
+
const decryptedBuffer = await window.crypto.subtle.decrypt(
|
|
509
|
+
{
|
|
510
|
+
name: "AES-GCM",
|
|
511
|
+
iv: iv,
|
|
512
|
+
},
|
|
513
|
+
secretKey,
|
|
514
|
+
combinedData // Pass the combined data (ciphertext + tag)
|
|
515
|
+
);
|
|
516
|
+
const decoder = new TextDecoder();
|
|
517
|
+
return decoder.decode(decryptedBuffer);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
window.__electrobun_encrypt = encryptString;
|
|
521
|
+
window.__electrobun_decrypt = decryptString;
|
|
522
|
+
})();
|
|
523
|
+
` + `
|
|
524
|
+
function emitWebviewEvent (eventName, detail) {
|
|
525
|
+
// Note: There appears to be some race bug with Bun FFI where sites can
|
|
526
|
+
// init (like views://myview/index.html) so fast while the Bun FFI to load a url is still executing
|
|
527
|
+
// or something where the JSCallback that this postMessage fires is not available or busy or
|
|
528
|
+
// its memory is allocated to something else or something and the handler receives garbage data in Bun.
|
|
529
|
+
setTimeout(() => {
|
|
530
|
+
console.log('emitWebviewEvent', eventName, detail)
|
|
531
|
+
window.__electrobunInternalBridge?.postMessage(JSON.stringify({id: 'webviewEvent', type: 'message', payload: {id: window.__electrobunWebviewId, eventName, detail}}));
|
|
532
|
+
});
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
window.addEventListener('load', function(event) {
|
|
536
|
+
// Check if the current window is the top-level window
|
|
537
|
+
if (window === window.top) {
|
|
538
|
+
emitWebviewEvent('dom-ready', document.location.href);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
window.addEventListener('popstate', function(event) {
|
|
543
|
+
emitWebviewEvent('did-navigate-in-page', window.location.href);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
window.addEventListener('hashchange', function(event) {
|
|
547
|
+
emitWebviewEvent('did-navigate-in-page', window.location.href);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
document.addEventListener('click', function(event) {
|
|
551
|
+
if ((event.metaKey || event.ctrlKey) && event.target.tagName === 'A') {
|
|
552
|
+
event.preventDefault();
|
|
553
|
+
event.stopPropagation();
|
|
554
|
+
|
|
555
|
+
// Get the href of the link
|
|
556
|
+
const url = event.target.href;
|
|
557
|
+
|
|
558
|
+
// Open the URL in a new window or tab
|
|
559
|
+
// Note: we already handle new windows in objc
|
|
560
|
+
window.open(url, '_blank');
|
|
561
|
+
}
|
|
562
|
+
}, true);
|
|
563
|
+
|
|
564
|
+
// prevent overscroll
|
|
565
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
566
|
+
var style = document.createElement('style');
|
|
567
|
+
style.type = 'text/css';
|
|
568
|
+
style.appendChild(document.createTextNode('html, body { overscroll-behavior: none; }'));
|
|
569
|
+
document.head.appendChild(style);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
`
|
|
573
|
+
const customPreload = preload;
|
|
574
|
+
|
|
575
|
+
const webviewPtr = native.symbols.initWebview(
|
|
576
|
+
id,
|
|
577
|
+
windowPtr,
|
|
578
|
+
toCString(renderer),
|
|
579
|
+
toCString(url || ''),
|
|
580
|
+
x, y,
|
|
581
|
+
width, height,
|
|
582
|
+
autoResize,
|
|
583
|
+
toCString(partition || 'persist:default'),
|
|
584
|
+
webviewDecideNavigation,
|
|
585
|
+
webviewEventJSCallback,
|
|
586
|
+
bunBridgePostmessageHandler,
|
|
587
|
+
internalBridgeHandler,
|
|
588
|
+
toCString(electrobunPreload),
|
|
589
|
+
toCString(customPreload || ''),
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
if (!webviewPtr) {
|
|
593
|
+
throw "Failed to create webview"
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return webviewPtr;
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
evaluateJavascriptWithNoCompletion: (params: {id: number; js: string}) => {
|
|
600
|
+
const {id, js} = params;
|
|
601
|
+
const webview = BrowserView.getById(id);
|
|
602
|
+
|
|
603
|
+
if (!webview?.ptr) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
native.symbols.evaluateJavaScriptWithNoCompletion(webview.ptr, toCString(js))
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
createTray: (params: {
|
|
611
|
+
id: number;
|
|
612
|
+
title: string;
|
|
613
|
+
image: string;
|
|
614
|
+
template: boolean;
|
|
615
|
+
width: number;
|
|
616
|
+
height: number;
|
|
617
|
+
}): FFIType.ptr => {
|
|
618
|
+
const {
|
|
619
|
+
id,
|
|
620
|
+
title,
|
|
621
|
+
image,
|
|
622
|
+
template,
|
|
623
|
+
width,
|
|
624
|
+
height
|
|
625
|
+
} = params;
|
|
626
|
+
|
|
627
|
+
const trayPtr = native.symbols.createTray(
|
|
628
|
+
id,
|
|
629
|
+
toCString(title),
|
|
630
|
+
toCString(image),
|
|
631
|
+
template,
|
|
632
|
+
width,
|
|
633
|
+
height,
|
|
634
|
+
trayItemHandler,
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
if (!trayPtr) {
|
|
638
|
+
throw 'Failed to create tray';
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return trayPtr;
|
|
642
|
+
},
|
|
643
|
+
setTrayTitle: (params: {
|
|
644
|
+
id: number,
|
|
645
|
+
title: string,
|
|
646
|
+
}): void => {
|
|
647
|
+
const {
|
|
648
|
+
id,
|
|
649
|
+
title
|
|
650
|
+
} = params;
|
|
651
|
+
|
|
652
|
+
const tray = Tray.getById(id);
|
|
653
|
+
|
|
654
|
+
native.symbols.setTrayTitle(
|
|
655
|
+
tray.ptr,
|
|
656
|
+
toCString(title)
|
|
657
|
+
);
|
|
658
|
+
},
|
|
659
|
+
setTrayImage: (params: {
|
|
660
|
+
id: number,
|
|
661
|
+
image: string,
|
|
662
|
+
}): void => {
|
|
663
|
+
const {
|
|
664
|
+
id,
|
|
665
|
+
image
|
|
666
|
+
} = params;
|
|
667
|
+
|
|
668
|
+
const tray = Tray.getById(id);
|
|
669
|
+
|
|
670
|
+
native.symbols.setTrayImage(
|
|
671
|
+
tray.ptr,
|
|
672
|
+
toCString(image)
|
|
673
|
+
);
|
|
674
|
+
},
|
|
675
|
+
setTrayMenu: (params: {
|
|
676
|
+
id: number,
|
|
677
|
+
// json string of config
|
|
678
|
+
menuConfig: string,
|
|
679
|
+
}): void => {
|
|
680
|
+
const {
|
|
681
|
+
id,
|
|
682
|
+
menuConfig
|
|
683
|
+
} = params;
|
|
684
|
+
|
|
685
|
+
const tray = Tray.getById(id);
|
|
686
|
+
console.log('native.symbols.setTrayMenu', tray.ptr, menuConfig)
|
|
687
|
+
native.symbols.setTrayMenu(
|
|
688
|
+
tray.ptr,
|
|
689
|
+
toCString(menuConfig)
|
|
690
|
+
);
|
|
691
|
+
},
|
|
692
|
+
|
|
693
|
+
removeTray: (params: { id: number }): void => {
|
|
694
|
+
const { id } = params;
|
|
695
|
+
console.log('removeTray called with id:', id);
|
|
696
|
+
const tray = Tray.getById(id);
|
|
697
|
+
console.log('Found tray:', tray);
|
|
698
|
+
|
|
699
|
+
if (!tray) {
|
|
700
|
+
throw `Can't remove tray. Tray no longer exists`;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
console.log('Calling native.symbols.removeTray with ptr:', tray.ptr);
|
|
704
|
+
native.symbols.removeTray(tray.ptr);
|
|
705
|
+
console.log('Native removeTray called successfully');
|
|
706
|
+
// The Tray class will handle removing from TrayMap
|
|
707
|
+
},
|
|
708
|
+
setApplicationMenu: (params: {menuConfig: string}): void => {
|
|
709
|
+
const {
|
|
710
|
+
menuConfig
|
|
711
|
+
} = params;
|
|
712
|
+
|
|
713
|
+
native.symbols.setApplicationMenu(
|
|
714
|
+
toCString(menuConfig),
|
|
715
|
+
applicationMenuHandler
|
|
716
|
+
);
|
|
717
|
+
},
|
|
718
|
+
showContextMenu: (params: {menuConfig: string}): void => {
|
|
719
|
+
const {
|
|
720
|
+
menuConfig
|
|
721
|
+
} = params;
|
|
722
|
+
|
|
723
|
+
native.symbols.showContextMenu(
|
|
724
|
+
toCString(menuConfig),
|
|
725
|
+
contextMenuHandler
|
|
726
|
+
);
|
|
727
|
+
},
|
|
728
|
+
moveToTrash: (params: {path: string}): boolean => {
|
|
729
|
+
const {
|
|
730
|
+
path
|
|
731
|
+
} = params;
|
|
732
|
+
|
|
733
|
+
return native.symbols.moveToTrash(toCString(path));
|
|
734
|
+
},
|
|
735
|
+
showItemInFolder: (params: {path: string}): void => {
|
|
736
|
+
const {
|
|
737
|
+
path
|
|
738
|
+
} = params;
|
|
739
|
+
|
|
740
|
+
native.symbols.showItemInFolder(toCString(path));
|
|
741
|
+
},
|
|
742
|
+
openFileDialog: (params: {startingFolder: string, allowedFileTypes: string, canChooseFiles: boolean, canChooseDirectory: boolean, allowsMultipleSelection: boolean}): string => {
|
|
743
|
+
const {
|
|
744
|
+
startingFolder,
|
|
745
|
+
allowedFileTypes,
|
|
746
|
+
canChooseFiles,
|
|
747
|
+
canChooseDirectory,
|
|
748
|
+
allowsMultipleSelection,
|
|
749
|
+
} = params;
|
|
750
|
+
const filePath = native.symbols.openFileDialog(toCString(startingFolder), toCString(allowedFileTypes), canChooseFiles, canChooseDirectory, allowsMultipleSelection);
|
|
751
|
+
|
|
752
|
+
return filePath.toString();
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
// ffifunc: (params: {}): void => {
|
|
756
|
+
// const {
|
|
757
|
+
|
|
758
|
+
// } = params;
|
|
759
|
+
|
|
760
|
+
// native.symbols.ffifunc(
|
|
761
|
+
|
|
762
|
+
// );
|
|
763
|
+
// },
|
|
764
|
+
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Worker management. Move to a different file
|
|
769
|
+
process.on('uncaughtException', (err) => {
|
|
770
|
+
console.error('Uncaught exception in worker:', err);
|
|
771
|
+
// Since the main js event loop is blocked by the native event loop
|
|
772
|
+
// we use FFI to dispatch a kill command to main
|
|
773
|
+
native.symbols.killApp();
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
777
|
+
console.error('Unhandled rejection in worker:', reason);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
process.on('SIGINT', () => {
|
|
781
|
+
console.log('[electrobun] Received SIGINT, calling killApp() for graceful shutdown...');
|
|
782
|
+
native.symbols.killApp();
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
process.on('SIGTERM', () => {
|
|
786
|
+
console.log('[electrobun] Received SIGTERM, calling killApp() for graceful shutdown...');
|
|
787
|
+
native.symbols.killApp();
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
// const testCallback = new JSCallback(
|
|
794
|
+
// (windowId, x, y) => {
|
|
795
|
+
// console.log(`TEST FFI Callback reffed GLOBALLY in js`);
|
|
796
|
+
// // Your window move handler implementation
|
|
797
|
+
// },
|
|
798
|
+
// {
|
|
799
|
+
// args: [],
|
|
800
|
+
// returns: "void",
|
|
801
|
+
// threadsafe: true,
|
|
802
|
+
|
|
803
|
+
// }
|
|
804
|
+
// );
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
const windowCloseCallback = new JSCallback(
|
|
808
|
+
(id) => {
|
|
809
|
+
const handler = electrobunEventEmitter.events.window.close;
|
|
810
|
+
const event = handler({
|
|
811
|
+
id,
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
let result;
|
|
815
|
+
// global event
|
|
816
|
+
result = electrobunEventEmitter.emitEvent(event);
|
|
817
|
+
|
|
818
|
+
result = electrobunEventEmitter.emitEvent(event, id);
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
args: ["u32"],
|
|
822
|
+
returns: "void",
|
|
823
|
+
threadsafe: true,
|
|
824
|
+
}
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
const windowMoveCallback = new JSCallback(
|
|
828
|
+
(id, x, y) => {
|
|
829
|
+
const handler = electrobunEventEmitter.events.window.move;
|
|
830
|
+
const event = handler({
|
|
831
|
+
id,
|
|
832
|
+
x,
|
|
833
|
+
y,
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
let result;
|
|
837
|
+
// global event
|
|
838
|
+
result = electrobunEventEmitter.emitEvent(event);
|
|
839
|
+
|
|
840
|
+
result = electrobunEventEmitter.emitEvent(event, id);
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
args: ["u32", "f64", "f64"],
|
|
844
|
+
returns: "void",
|
|
845
|
+
threadsafe: true,
|
|
846
|
+
}
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
const windowResizeCallback = new JSCallback(
|
|
850
|
+
(id, x, y, width, height) => {
|
|
851
|
+
const handler = electrobunEventEmitter.events.window.resize;
|
|
852
|
+
const event = handler({
|
|
853
|
+
id,
|
|
854
|
+
x,
|
|
855
|
+
y,
|
|
856
|
+
width,
|
|
857
|
+
height,
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
let result;
|
|
861
|
+
// global event
|
|
862
|
+
result = electrobunEventEmitter.emitEvent(event);
|
|
863
|
+
|
|
864
|
+
result = electrobunEventEmitter.emitEvent(event, id);
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
args: ["u32", "f64", "f64", "f64", "f64"],
|
|
868
|
+
returns: "void",
|
|
869
|
+
threadsafe: true,
|
|
870
|
+
}
|
|
871
|
+
);
|
|
872
|
+
|
|
873
|
+
const getMimeType = new JSCallback((filePath) => {
|
|
874
|
+
const _filePath = new CString(filePath).toString();
|
|
875
|
+
const mimeType = Bun.file(_filePath).type;// || "application/octet-stream";
|
|
876
|
+
|
|
877
|
+
// For this usecase we generally don't want the charset included in the mimetype
|
|
878
|
+
// otherwise it can break. eg: for html with text/javascript;charset=utf-8 browsers
|
|
879
|
+
// will tend to render the code/text instead of interpreting the html.
|
|
880
|
+
|
|
881
|
+
return toCString(mimeType.split(';')[0]);
|
|
882
|
+
}, {
|
|
883
|
+
args: [FFIType.cstring],
|
|
884
|
+
returns: FFIType.cstring,
|
|
885
|
+
// threadsafe: true
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
const getHTMLForWebviewSync = new JSCallback((webviewId) => {
|
|
889
|
+
const webview = BrowserView.getById(webviewId);
|
|
890
|
+
|
|
891
|
+
return toCString(webview?.html || '');
|
|
892
|
+
}, {
|
|
893
|
+
args: [FFIType.cstring],
|
|
894
|
+
returns: FFIType.cstring,
|
|
895
|
+
// threadsafe: true
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
native.symbols.setJSUtils(getMimeType, getHTMLForWebviewSync);
|
|
900
|
+
|
|
901
|
+
// TODO XX: revisit this as integrated into the will-navigate handler
|
|
902
|
+
const webviewDecideNavigation = new JSCallback((webviewId, url) => {
|
|
903
|
+
console.log('webviewDecideNavigation', webviewId, new CString(url))
|
|
904
|
+
return true;
|
|
905
|
+
}, {
|
|
906
|
+
args: [FFIType.u32, FFIType.cstring],
|
|
907
|
+
// NOTE: In Objc true is YES which is so dumb, but that doesn't work with Bun's FFIType.bool
|
|
908
|
+
// in JSCallbacks right now (it always infers false) so to make this cross platform we have to use
|
|
909
|
+
// FFIType.u32 and uint32_t and then just treat it as a boolean in code.
|
|
910
|
+
returns: FFIType.u32,
|
|
911
|
+
threadsafe: true
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
const webviewEventHandler = (id, eventName, detail) => {
|
|
916
|
+
const webview = BrowserView.getById(id);
|
|
917
|
+
if (webview.hostWebviewId) {
|
|
918
|
+
// This is a webviewtag so we should send the event into the parent as well
|
|
919
|
+
// TODO XX: escape event name and detail to remove `
|
|
920
|
+
const js = `document.querySelector('#electrobun-webview-${id}').emit(\`${eventName}\`, \`${detail}\`);`
|
|
921
|
+
|
|
922
|
+
native.symbols.evaluateJavaScriptWithNoCompletion(webview.ptr, toCString(js))
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const eventMap = {
|
|
926
|
+
"will-navigate": "willNavigate",
|
|
927
|
+
"did-navigate": "didNavigate",
|
|
928
|
+
"did-navigate-in-page": "didNavigateInPage",
|
|
929
|
+
"did-commit-navigation": "didCommitNavigation",
|
|
930
|
+
"dom-ready": "domReady",
|
|
931
|
+
"new-window-open": "newWindowOpen",
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
// todo: the events map should use the same hyphenated names instead of camelCase
|
|
935
|
+
const handler =
|
|
936
|
+
electrobunEventEmitter.events.webview[eventMap[eventName]];
|
|
937
|
+
|
|
938
|
+
if (!handler) {
|
|
939
|
+
|
|
940
|
+
return { success: false };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const event = handler({
|
|
944
|
+
id,
|
|
945
|
+
detail,
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
let result;
|
|
949
|
+
// global event
|
|
950
|
+
result = electrobunEventEmitter.emitEvent(event);
|
|
951
|
+
result = electrobunEventEmitter.emitEvent(event, id);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const webviewEventJSCallback = new JSCallback((id, _eventName, _detail) => {
|
|
955
|
+
const eventName = new CString(_eventName);
|
|
956
|
+
const detail = new CString(_detail);
|
|
957
|
+
|
|
958
|
+
webviewEventHandler(id, eventName, detail);
|
|
959
|
+
}, {
|
|
960
|
+
args: [FFIType.u32, FFIType.cstring],
|
|
961
|
+
returns: FFIType.void,
|
|
962
|
+
threadsafe: true
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
const bunBridgePostmessageHandler = new JSCallback((id, msg) => {
|
|
968
|
+
try {
|
|
969
|
+
const msgStr = new CString(msg);
|
|
970
|
+
|
|
971
|
+
if (!msgStr.length) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const msgJson = JSON.parse(msgStr);
|
|
975
|
+
|
|
976
|
+
const webview = BrowserView.getById(id);
|
|
977
|
+
|
|
978
|
+
webview.rpcHandler?.(msgJson).then(result => {
|
|
979
|
+
}).catch(err => console.log('error in rpchandler', err))
|
|
980
|
+
|
|
981
|
+
} catch (err) {
|
|
982
|
+
console.error('error sending message to bun: ', err)
|
|
983
|
+
console.error('msgString: ', new CString(msg));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
}, {
|
|
988
|
+
args: [FFIType.u32, FFIType.cstring],
|
|
989
|
+
returns: FFIType.void,
|
|
990
|
+
threadsafe: true
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// internalRPC (bun <-> browser internal stuff)
|
|
994
|
+
// BrowserView.rpc (user defined bun <-> browser rpc unique to each webview)
|
|
995
|
+
// nativeRPC (internal bun <-> native rpc)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
const internalBridgeHandler = new JSCallback((id, msg) => {
|
|
999
|
+
try {
|
|
1000
|
+
const batchMessage = new CString(msg);
|
|
1001
|
+
const jsonBatch = JSON.parse(batchMessage);
|
|
1002
|
+
|
|
1003
|
+
if (jsonBatch.id === 'webviewEvent'){
|
|
1004
|
+
// Note: Some WebviewEvents from inside the webview are routed through here
|
|
1005
|
+
// Others call the JSCallback directly from native code.
|
|
1006
|
+
const {payload} = jsonBatch;
|
|
1007
|
+
webviewEventHandler(payload.id, payload.eventName, payload.detail);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
jsonBatch.forEach((msgStr) => {
|
|
1013
|
+
// if (!msgStr.length) {
|
|
1014
|
+
// console.error('WEBVIEW EVENT SENT TO WEBVIEW TAG BRIDGE HANDLER?', )
|
|
1015
|
+
// return;
|
|
1016
|
+
// }
|
|
1017
|
+
const msgJson = JSON.parse(msgStr);
|
|
1018
|
+
|
|
1019
|
+
if (msgJson.type === 'message') {
|
|
1020
|
+
const handler = internalRpcHandlers.message[msgJson.id];
|
|
1021
|
+
handler(msgJson.payload);
|
|
1022
|
+
} else if(msgJson.type === 'request') {
|
|
1023
|
+
const hostWebview = BrowserView.getById(msgJson.hostWebviewId);
|
|
1024
|
+
// const targetWebview = BrowserView.getById(msgJson.params.params.hostWebviewId);
|
|
1025
|
+
const handler = internalRpcHandlers.request[msgJson.method];
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
const payload = handler(msgJson.params);
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
const resultObj = {
|
|
1032
|
+
type: 'response',
|
|
1033
|
+
id: msgJson.id,
|
|
1034
|
+
success: true,
|
|
1035
|
+
payload,
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (!hostWebview) {
|
|
1039
|
+
console.log('--->>> internal request in bun: NO HOST WEBVIEW FOUND');
|
|
1040
|
+
return
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
hostWebview.sendInternalMessageViaExecute(resultObj);
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
console.error('error in internalBridgeHandler: ', err)
|
|
1049
|
+
// console.log('msgStr: ', id, new CString(msg));
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
}, {
|
|
1054
|
+
args: [FFIType.u32, FFIType.cstring],
|
|
1055
|
+
returns: FFIType.void,
|
|
1056
|
+
threadsafe: true
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
const trayItemHandler = new JSCallback((id, action) => {
|
|
1060
|
+
const event = electrobunEventEmitter.events.tray.trayClicked({
|
|
1061
|
+
id,
|
|
1062
|
+
action: new CString(action),
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
let result;
|
|
1066
|
+
// global event
|
|
1067
|
+
result = electrobunEventEmitter.emitEvent(event);
|
|
1068
|
+
result = electrobunEventEmitter.emitEvent(event, id);
|
|
1069
|
+
}, {
|
|
1070
|
+
args: [FFIType.u32, FFIType.cstring],
|
|
1071
|
+
returns: FFIType.void,
|
|
1072
|
+
threadsafe: true,
|
|
1073
|
+
})
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
const applicationMenuHandler = new JSCallback((id, action) => {
|
|
1077
|
+
const event = electrobunEventEmitter.events.app.applicationMenuClicked({
|
|
1078
|
+
id,
|
|
1079
|
+
action: new CString(action),
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
// global event
|
|
1083
|
+
electrobunEventEmitter.emitEvent(event);
|
|
1084
|
+
}, {
|
|
1085
|
+
args: [FFIType.u32, FFIType.cstring],
|
|
1086
|
+
returns: FFIType.void,
|
|
1087
|
+
threadsafe: true
|
|
1088
|
+
})
|
|
1089
|
+
|
|
1090
|
+
const contextMenuHandler = new JSCallback((id, action) => {
|
|
1091
|
+
const event = electrobunEventEmitter.events.app.contextMenuClicked({
|
|
1092
|
+
action: new CString(action),
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
electrobunEventEmitter.emitEvent(event);
|
|
1096
|
+
}, {
|
|
1097
|
+
args: [FFIType.u32, FFIType.cstring],
|
|
1098
|
+
returns: FFIType.void,
|
|
1099
|
+
threadsafe: true
|
|
1100
|
+
})
|
|
1101
|
+
|
|
1102
|
+
// Note: When passed over FFI JS will GC the buffer/pointer. Make sure to use strdup() or something
|
|
1103
|
+
// on the c side to duplicate the string so objc/c++ gc can own it
|
|
1104
|
+
export function toCString(jsString: string, addNullTerminator: boolean = true): CString {
|
|
1105
|
+
let appendWith = '';
|
|
1106
|
+
|
|
1107
|
+
if (addNullTerminator && !jsString.endsWith('\0')) {
|
|
1108
|
+
appendWith = '\0';
|
|
1109
|
+
}
|
|
1110
|
+
const buff = Buffer.from(jsString + appendWith, 'utf8');
|
|
1111
|
+
|
|
1112
|
+
// @ts-ignore - This is valid in Bun
|
|
1113
|
+
return ptr(buff);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
export const internalRpcHandlers = {
|
|
1119
|
+
request: {
|
|
1120
|
+
// todo: this shouldn't be getting method, just params.
|
|
1121
|
+
webviewTagInit: (params:
|
|
1122
|
+
BrowserViewOptions & { windowId: number }
|
|
1123
|
+
) => {
|
|
1124
|
+
|
|
1125
|
+
const {
|
|
1126
|
+
hostWebviewId,
|
|
1127
|
+
windowId,
|
|
1128
|
+
renderer,
|
|
1129
|
+
html,
|
|
1130
|
+
preload,
|
|
1131
|
+
partition,
|
|
1132
|
+
frame,
|
|
1133
|
+
navigationRules,
|
|
1134
|
+
} = params;
|
|
1135
|
+
|
|
1136
|
+
const url = !params.url && !html ? "https://electrobun.dev" : params.url;
|
|
1137
|
+
|
|
1138
|
+
const webviewForTag = new BrowserView({
|
|
1139
|
+
url,
|
|
1140
|
+
html,
|
|
1141
|
+
preload,
|
|
1142
|
+
partition,
|
|
1143
|
+
frame,
|
|
1144
|
+
hostWebviewId,
|
|
1145
|
+
autoResize: false,
|
|
1146
|
+
windowId,
|
|
1147
|
+
renderer,//: "cef",
|
|
1148
|
+
navigationRules,
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
return webviewForTag.id;
|
|
1152
|
+
},
|
|
1153
|
+
webviewTagCanGoBack: (params) => {
|
|
1154
|
+
const {id} = params;
|
|
1155
|
+
const webviewPtr = BrowserView.getById(id)?.ptr;
|
|
1156
|
+
if (!webviewPtr) {
|
|
1157
|
+
console.error('no webview ptr')
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
return native.symbols.webviewCanGoBack(webviewPtr);
|
|
1162
|
+
},
|
|
1163
|
+
webviewTagCanGoForward: (params) => {
|
|
1164
|
+
const {id} = params;
|
|
1165
|
+
const webviewPtr = BrowserView.getById(id)?.ptr;
|
|
1166
|
+
if (!webviewPtr) {
|
|
1167
|
+
console.error('no webview ptr')
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return native.symbols.webviewCanGoForward(webviewPtr);
|
|
1172
|
+
},
|
|
1173
|
+
webviewTagCallAsyncJavaScript: (params) => {
|
|
1174
|
+
console.log('-----------+ request: ', 'webviewTagCallAsyncJavaScript', params)
|
|
1175
|
+
// Not implemented - users can use RPC for JavaScript execution if needed
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
message: {
|
|
1179
|
+
webviewTagResize: (params) => {
|
|
1180
|
+
const browserView = BrowserView.getById(params.id);
|
|
1181
|
+
const webviewPtr = browserView?.ptr;
|
|
1182
|
+
|
|
1183
|
+
if (!webviewPtr) {
|
|
1184
|
+
console.log('[Bun] ERROR: webviewTagResize - no webview ptr found for id:', params.id);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const {x, y, width, height} = params.frame;
|
|
1189
|
+
native.symbols.resizeWebview(webviewPtr, x, y, width, height, toCString(params.masks))
|
|
1190
|
+
},
|
|
1191
|
+
webviewTagUpdateSrc: (params) => {
|
|
1192
|
+
const webview = BrowserView.getById(params.id);
|
|
1193
|
+
if (!webview || !webview.ptr) {
|
|
1194
|
+
console.error(`webviewTagUpdateSrc: BrowserView not found or has no ptr for id ${params.id}`);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
native.symbols.loadURLInWebView(webview.ptr, toCString(params.url));
|
|
1198
|
+
},
|
|
1199
|
+
webviewTagUpdateHtml: (params) => {
|
|
1200
|
+
const webview = BrowserView.getById(params.id);
|
|
1201
|
+
if (!webview || !webview.ptr) {
|
|
1202
|
+
console.error(`webviewTagUpdateHtml: BrowserView not found or has no ptr for id ${params.id}`);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
webview.loadHTML(params.html);
|
|
1206
|
+
webview.html = params.html;
|
|
1207
|
+
},
|
|
1208
|
+
webviewTagUpdatePreload: (params) => {
|
|
1209
|
+
const webview = BrowserView.getById(params.id);
|
|
1210
|
+
if (!webview || !webview.ptr) {
|
|
1211
|
+
console.error(`webviewTagUpdatePreload: BrowserView not found or has no ptr for id ${params.id}`);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
native.symbols.updatePreloadScriptToWebView(webview.ptr, toCString('electrobun_custom_preload_script'), toCString(params.preload), true);
|
|
1215
|
+
},
|
|
1216
|
+
webviewTagGoBack: (params) => {
|
|
1217
|
+
const webview = BrowserView.getById(params.id);
|
|
1218
|
+
if (!webview || !webview.ptr) {
|
|
1219
|
+
console.error(`webviewTagGoBack: BrowserView not found or has no ptr for id ${params.id}`);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
native.symbols.webviewGoBack(webview.ptr);
|
|
1223
|
+
},
|
|
1224
|
+
webviewTagGoForward: (params) => {
|
|
1225
|
+
const webview = BrowserView.getById(params.id);
|
|
1226
|
+
if (!webview || !webview.ptr) {
|
|
1227
|
+
console.error(`webviewTagGoForward: BrowserView not found or has no ptr for id ${params.id}`);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
native.symbols.webviewGoForward(webview.ptr);
|
|
1231
|
+
},
|
|
1232
|
+
webviewTagReload: (params) => {
|
|
1233
|
+
const webview = BrowserView.getById(params.id);
|
|
1234
|
+
if (!webview || !webview.ptr) {
|
|
1235
|
+
console.error(`webviewTagReload: BrowserView not found or has no ptr for id ${params.id}`);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
native.symbols.webviewReload(webview.ptr);
|
|
1239
|
+
},
|
|
1240
|
+
webviewTagRemove: (params) => {
|
|
1241
|
+
const webview = BrowserView.getById(params.id);
|
|
1242
|
+
if (!webview || !webview.ptr) {
|
|
1243
|
+
console.error(`webviewTagRemove: BrowserView not found or has no ptr for id ${params.id}`);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
native.symbols.webviewRemove(webview.ptr);
|
|
1247
|
+
},
|
|
1248
|
+
startWindowMove: (params) => {
|
|
1249
|
+
const window = BrowserWindow.getById(params.id);
|
|
1250
|
+
native.symbols.startWindowMove(window.ptr);
|
|
1251
|
+
},
|
|
1252
|
+
stopWindowMove: (params) => {
|
|
1253
|
+
native.symbols.stopWindowMove();
|
|
1254
|
+
},
|
|
1255
|
+
webviewTagSetTransparent: (params) => {
|
|
1256
|
+
const webview = BrowserView.getById(params.id);
|
|
1257
|
+
if (!webview || !webview.ptr) {
|
|
1258
|
+
console.error(`webviewTagSetTransparent: BrowserView not found or has no ptr for id ${params.id}`);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
native.symbols.webviewSetTransparent(webview.ptr, params.transparent);
|
|
1262
|
+
},
|
|
1263
|
+
webviewTagSetPassthrough: (params) => {
|
|
1264
|
+
const webview = BrowserView.getById(params.id);
|
|
1265
|
+
if (!webview || !webview.ptr) {
|
|
1266
|
+
console.error(`webviewTagSetPassthrough: BrowserView not found or has no ptr for id ${params.id}`);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
native.symbols.webviewSetPassthrough(webview.ptr, params.enablePassthrough);
|
|
1270
|
+
},
|
|
1271
|
+
webviewTagSetHidden: (params) => {
|
|
1272
|
+
const webview = BrowserView.getById(params.id);
|
|
1273
|
+
if (!webview || !webview.ptr) {
|
|
1274
|
+
console.error(`webviewTagSetHidden: BrowserView not found or has no ptr for id ${params.id}`);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
native.symbols.webviewSetHidden(webview.ptr, params.hidden);
|
|
1278
|
+
},
|
|
1279
|
+
webviewEvent: (params) => {
|
|
1280
|
+
console.log('-----------------+webviewEvent', params)
|
|
1281
|
+
},
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
// todo: consider renaming to TrayMenuItemConfig
|
|
1286
|
+
export type MenuItemConfig =
|
|
1287
|
+
| { type: "divider" | "separator" }
|
|
1288
|
+
| {
|
|
1289
|
+
type: "normal";
|
|
1290
|
+
label: string;
|
|
1291
|
+
tooltip?: string;
|
|
1292
|
+
action?: string;
|
|
1293
|
+
submenu?: Array<MenuItemConfig>;
|
|
1294
|
+
enabled?: boolean;
|
|
1295
|
+
checked?: boolean;
|
|
1296
|
+
hidden?: boolean;
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
export type ApplicationMenuItemConfig =
|
|
1300
|
+
| { type: "divider" | "separator" }
|
|
1301
|
+
| {
|
|
1302
|
+
type?: "normal";
|
|
1303
|
+
label: string;
|
|
1304
|
+
tooltip?: string;
|
|
1305
|
+
action?: string;
|
|
1306
|
+
submenu?: Array<ApplicationMenuItemConfig>;
|
|
1307
|
+
enabled?: boolean;
|
|
1308
|
+
checked?: boolean;
|
|
1309
|
+
hidden?: boolean;
|
|
1310
|
+
accelerator?: string;
|
|
1311
|
+
}
|
|
1312
|
+
| {
|
|
1313
|
+
type?: "normal";
|
|
1314
|
+
label?: string;
|
|
1315
|
+
tooltip?: string;
|
|
1316
|
+
role?: string;
|
|
1317
|
+
submenu?: Array<ApplicationMenuItemConfig>;
|
|
1318
|
+
enabled?: boolean;
|
|
1319
|
+
checked?: boolean;
|
|
1320
|
+
hidden?: boolean;
|
|
1321
|
+
accelerator?: string;
|
|
1322
|
+
};
|