electrobun 0.0.19-beta.8 → 0.0.19-beta.81

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