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,195 @@
|
|
|
1
|
+
import { ffi } from "../proc/native";
|
|
2
|
+
import electrobunEventEmitter from "../events/eventEmitter";
|
|
3
|
+
import { BrowserView } from "./BrowserView";
|
|
4
|
+
import { type RPC } from "rpc-anywhere";
|
|
5
|
+
import {FFIType} from 'bun:ffi'
|
|
6
|
+
|
|
7
|
+
let nextWindowId = 1;
|
|
8
|
+
|
|
9
|
+
// todo (yoav): if we default to builtInSchema, we don't want dev to have to define custom handlers
|
|
10
|
+
// for the built-in schema stuff.
|
|
11
|
+
type WindowOptionsType<T = undefined> = {
|
|
12
|
+
title: string;
|
|
13
|
+
frame: {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
19
|
+
url: string | null;
|
|
20
|
+
html: string | null;
|
|
21
|
+
preload: string | null;
|
|
22
|
+
renderer: 'native' | 'cef';
|
|
23
|
+
rpc?: T;
|
|
24
|
+
styleMask?: {};
|
|
25
|
+
// TODO: implement all of them
|
|
26
|
+
titleBarStyle: "hiddenInset" | "default";
|
|
27
|
+
navigationRules: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const defaultOptions: WindowOptionsType = {
|
|
31
|
+
title: "Electrobun",
|
|
32
|
+
frame: {
|
|
33
|
+
x: 0,
|
|
34
|
+
y: 0,
|
|
35
|
+
width: 800,
|
|
36
|
+
height: 600,
|
|
37
|
+
},
|
|
38
|
+
url: "https://electrobun.dev",
|
|
39
|
+
html: null,
|
|
40
|
+
preload: null,
|
|
41
|
+
renderer: 'native',
|
|
42
|
+
titleBarStyle: "default",
|
|
43
|
+
navigationRules: null,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const BrowserWindowMap = {};
|
|
47
|
+
|
|
48
|
+
// todo (yoav): do something where the type extends the default schema
|
|
49
|
+
// that way we can provide built-in requests/messages and devs can extend it
|
|
50
|
+
|
|
51
|
+
export class BrowserWindow<T> {
|
|
52
|
+
id: number = nextWindowId++;
|
|
53
|
+
ptr: FFIType.ptr;
|
|
54
|
+
title: string = "Electrobun";
|
|
55
|
+
state: "creating" | "created" = "creating";
|
|
56
|
+
url: string | null = null;
|
|
57
|
+
html: string | null = null;
|
|
58
|
+
preload: string | null = null;
|
|
59
|
+
renderer: 'native' | 'cef';
|
|
60
|
+
frame: {
|
|
61
|
+
x: number;
|
|
62
|
+
y: number;
|
|
63
|
+
width: number;
|
|
64
|
+
height: number;
|
|
65
|
+
} = {
|
|
66
|
+
x: 0,
|
|
67
|
+
y: 0,
|
|
68
|
+
width: 800,
|
|
69
|
+
height: 600,
|
|
70
|
+
};
|
|
71
|
+
// todo (yoav): make this an array of ids or something
|
|
72
|
+
webviewId: number;
|
|
73
|
+
|
|
74
|
+
constructor(options: Partial<WindowOptionsType<T>> = defaultOptions) {
|
|
75
|
+
this.title = options.title || "New Window";
|
|
76
|
+
this.frame = options.frame
|
|
77
|
+
? { ...defaultOptions.frame, ...options.frame }
|
|
78
|
+
: { ...defaultOptions.frame };
|
|
79
|
+
this.url = options.url || null;
|
|
80
|
+
this.html = options.html || null;
|
|
81
|
+
this.preload = options.preload || null;
|
|
82
|
+
this.renderer = options.renderer === 'cef' ? 'cef' : 'native';
|
|
83
|
+
this.navigationRules = options.navigationRules || null;
|
|
84
|
+
|
|
85
|
+
this.init(options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
init({
|
|
89
|
+
rpc,
|
|
90
|
+
styleMask,
|
|
91
|
+
titleBarStyle,
|
|
92
|
+
}: Partial<WindowOptionsType<T>>) {
|
|
93
|
+
|
|
94
|
+
this.ptr = ffi.request.createWindow({
|
|
95
|
+
id: this.id,
|
|
96
|
+
title: this.title,
|
|
97
|
+
url: this.url || "",
|
|
98
|
+
frame: {
|
|
99
|
+
width: this.frame.width,
|
|
100
|
+
height: this.frame.height,
|
|
101
|
+
x: this.frame.x,
|
|
102
|
+
y: this.frame.y,
|
|
103
|
+
},
|
|
104
|
+
styleMask: {
|
|
105
|
+
Borderless: false,
|
|
106
|
+
Titled: true,
|
|
107
|
+
Closable: true,
|
|
108
|
+
Miniaturizable: true,
|
|
109
|
+
Resizable: true,
|
|
110
|
+
UnifiedTitleAndToolbar: false,
|
|
111
|
+
FullScreen: false,
|
|
112
|
+
FullSizeContentView: false,
|
|
113
|
+
UtilityWindow: false,
|
|
114
|
+
DocModalWindow: false,
|
|
115
|
+
NonactivatingPanel: false,
|
|
116
|
+
HUDWindow: false,
|
|
117
|
+
...(styleMask || {}),
|
|
118
|
+
...(titleBarStyle === "hiddenInset"
|
|
119
|
+
? {
|
|
120
|
+
Titled: true,
|
|
121
|
+
FullSizeContentView: true,
|
|
122
|
+
}
|
|
123
|
+
: {}),
|
|
124
|
+
},
|
|
125
|
+
titleBarStyle: titleBarStyle || "default",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
BrowserWindowMap[this.id] = this;
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
// todo (yoav): user should be able to override this and pass in their
|
|
133
|
+
// own webview instance, or instances for attaching to the window.
|
|
134
|
+
const webview = new BrowserView({
|
|
135
|
+
// TODO: decide whether we want to keep sending url/html
|
|
136
|
+
// here, if we're manually calling loadURL/loadHTML below
|
|
137
|
+
// then we can remove it from the api here
|
|
138
|
+
url: this.url,
|
|
139
|
+
html: this.html,
|
|
140
|
+
preload: this.preload,
|
|
141
|
+
// frame: this.frame,
|
|
142
|
+
renderer: this.renderer,
|
|
143
|
+
frame: {
|
|
144
|
+
x: 0,
|
|
145
|
+
y: 0,
|
|
146
|
+
width: this.frame.width,
|
|
147
|
+
height: this.frame.height,
|
|
148
|
+
},
|
|
149
|
+
rpc,
|
|
150
|
+
// todo: we need to send the window here and attach it in one go
|
|
151
|
+
// then the view creation code in objc can toggle between offscreen
|
|
152
|
+
// or on screen views depending on if windowId is null
|
|
153
|
+
// does this mean browserView needs to track the windowId or handle it ephemerally?
|
|
154
|
+
windowId: this.id,
|
|
155
|
+
navigationRules: this.navigationRules,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
console.log('setting webviewId: ', webview.id)
|
|
159
|
+
|
|
160
|
+
this.webviewId = webview.id;
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get webview() {
|
|
166
|
+
console.log('getting webview for window: ', this.webviewId)
|
|
167
|
+
// todo (yoav): we don't want this to be undefined, so maybe we should just
|
|
168
|
+
// link directly to the browserview object instead of a getter
|
|
169
|
+
return BrowserView.getById(this.webviewId) as BrowserView<T>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static getById(id: number) {
|
|
173
|
+
return BrowserWindowMap[id];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setTitle(title: string) {
|
|
177
|
+
this.title = title;
|
|
178
|
+
return ffi.request.setTitle({ winId: this.id, title });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
close() {
|
|
182
|
+
return ffi.request.closeWindow({ winId: this.id });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
focus() {
|
|
186
|
+
return ffi.request.focusWindow({ winId: this.id });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// todo (yoav): move this to a class that also has off, append, prepend, etc.
|
|
190
|
+
// name should only allow browserWindow events
|
|
191
|
+
on(name, handler) {
|
|
192
|
+
const specificName = `${name}-${this.id}`;
|
|
193
|
+
electrobunEventEmitter.on(specificName, handler);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// TODO: have a context specific menu that excludes role
|
|
2
|
+
import { ffi, type ApplicationMenuItemConfig } from "../proc/native";
|
|
3
|
+
import electrobunEventEmitter from "../events/eventEmitter";
|
|
4
|
+
|
|
5
|
+
export const showContextMenu = (menu: Array<ApplicationMenuItemConfig>) => {
|
|
6
|
+
const menuWithDefaults = menuConfigWithDefaults(menu);
|
|
7
|
+
ffi.request.showContextMenu({
|
|
8
|
+
menuConfig: JSON.stringify(menuWithDefaults),
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const on = (name: "context-menu-clicked", handler) => {
|
|
13
|
+
const specificName = `${name}`;
|
|
14
|
+
electrobunEventEmitter.on(specificName, handler);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// todo: Consolidate Application menu, context menu, and tray menus can all have roles.
|
|
18
|
+
const roleLabelMap = {
|
|
19
|
+
quit: "Quit",
|
|
20
|
+
hide: "Hide",
|
|
21
|
+
hideOthers: "Hide Others",
|
|
22
|
+
showAll: "Show All",
|
|
23
|
+
undo: "Undo",
|
|
24
|
+
redo: "Redo",
|
|
25
|
+
cut: "Cut",
|
|
26
|
+
copy: "Copy",
|
|
27
|
+
paste: "Paste",
|
|
28
|
+
pasteAndMatchStyle: "Paste And Match Style",
|
|
29
|
+
delete: "Delete",
|
|
30
|
+
selectAll: "Select All",
|
|
31
|
+
startSpeaking: "Start Speaking",
|
|
32
|
+
stopSpeaking: "Stop Speaking",
|
|
33
|
+
enterFullScreen: "Enter FullScreen",
|
|
34
|
+
exitFullScreen: "Exit FullScreen",
|
|
35
|
+
toggleFullScreen: "Toggle Full Screen",
|
|
36
|
+
minimize: "Minimize",
|
|
37
|
+
zoom: "Zoom",
|
|
38
|
+
bringAllToFront: "Bring All To Front",
|
|
39
|
+
close: "Close",
|
|
40
|
+
cycleThroughWindows: "Cycle Through Windows",
|
|
41
|
+
showHelp: "Show Help",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const menuConfigWithDefaults = (
|
|
45
|
+
menu: Array<ApplicationMenuItemConfig>
|
|
46
|
+
): Array<ApplicationMenuItemConfig> => {
|
|
47
|
+
return menu.map((item) => {
|
|
48
|
+
if (item.type === "divider" || item.type === "separator") {
|
|
49
|
+
return { type: "divider" };
|
|
50
|
+
} else {
|
|
51
|
+
return {
|
|
52
|
+
label: item.label || roleLabelMap[item.role] || "",
|
|
53
|
+
type: item.type || "normal",
|
|
54
|
+
// application menus can either have an action or a role. not both.
|
|
55
|
+
...(item.role ? { role: item.role } : { action: item.action || "" }),
|
|
56
|
+
// default enabled to true unless explicitly set to false
|
|
57
|
+
enabled: item.enabled === false ? false : true,
|
|
58
|
+
checked: Boolean(item.checked),
|
|
59
|
+
hidden: Boolean(item.hidden),
|
|
60
|
+
tooltip: item.tooltip || undefined,
|
|
61
|
+
...(item.submenu
|
|
62
|
+
? { submenu: menuConfigWithDefaults(item.submenu) }
|
|
63
|
+
: {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { ServerWebSocket } from "bun";
|
|
2
|
+
import { BrowserView } from "./BrowserView";
|
|
3
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
4
|
+
|
|
5
|
+
function base64ToUint8Array(base64: string) {
|
|
6
|
+
{
|
|
7
|
+
return new Uint8Array(
|
|
8
|
+
atob(base64)
|
|
9
|
+
.split("")
|
|
10
|
+
.map((char) => char.charCodeAt(0))
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Encrypt function
|
|
16
|
+
function encrypt(secretKey: Uint8Array, text: string) {
|
|
17
|
+
const iv = new Uint8Array(randomBytes(12)); // IV for AES-GCM
|
|
18
|
+
const cipher = createCipheriv("aes-256-gcm", secretKey, iv);
|
|
19
|
+
const encrypted = Buffer.concat([
|
|
20
|
+
new Uint8Array(cipher.update(text, "utf8")),
|
|
21
|
+
new Uint8Array(cipher.final()),
|
|
22
|
+
]).toString("base64");
|
|
23
|
+
const tag = cipher.getAuthTag().toString("base64");
|
|
24
|
+
return { encrypted, iv: Buffer.from(iv).toString("base64"), tag };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Decrypt function
|
|
28
|
+
function decrypt(
|
|
29
|
+
secretKey: Uint8Array,
|
|
30
|
+
encryptedData: Uint8Array,
|
|
31
|
+
iv: Uint8Array,
|
|
32
|
+
tag: Uint8Array
|
|
33
|
+
) {
|
|
34
|
+
const decipher = createDecipheriv("aes-256-gcm", secretKey, iv);
|
|
35
|
+
decipher.setAuthTag(tag);
|
|
36
|
+
const decrypted = Buffer.concat([
|
|
37
|
+
new Uint8Array(decipher.update(encryptedData)),
|
|
38
|
+
new Uint8Array(decipher.final()),
|
|
39
|
+
]);
|
|
40
|
+
return decrypted.toString("utf8");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const socketMap: {
|
|
44
|
+
[webviewId: string]: {
|
|
45
|
+
socket: null | ServerWebSocket<unknown>;
|
|
46
|
+
queue: string[];
|
|
47
|
+
};
|
|
48
|
+
} = {};
|
|
49
|
+
|
|
50
|
+
const startRPCServer = () => {
|
|
51
|
+
const startPort = 50000;
|
|
52
|
+
const endPort = 65535;
|
|
53
|
+
const payloadLimit = 1024 * 1024 * 500; // 500MB
|
|
54
|
+
let port = startPort;
|
|
55
|
+
let server = null;
|
|
56
|
+
|
|
57
|
+
while (port <= endPort) {
|
|
58
|
+
try {
|
|
59
|
+
server = Bun.serve<{ webviewId: number }>({
|
|
60
|
+
port,
|
|
61
|
+
fetch(req, server) {
|
|
62
|
+
const url = new URL(req.url);
|
|
63
|
+
// const token = new URL(req.url).searchParams.get("token");
|
|
64
|
+
// if (token !== AUTH_TOKEN)
|
|
65
|
+
// return new Response("Unauthorized", { status: 401 });
|
|
66
|
+
// console.log("fetch!!", url.pathname);
|
|
67
|
+
if (url.pathname === "/socket") {
|
|
68
|
+
const webviewIdString = url.searchParams.get("webviewId");
|
|
69
|
+
if (!webviewIdString) {
|
|
70
|
+
return new Response("Missing webviewId", { status: 400 });
|
|
71
|
+
}
|
|
72
|
+
const webviewId = parseInt(webviewIdString, 10);
|
|
73
|
+
const success = server.upgrade(req, { data: { webviewId } });
|
|
74
|
+
return success
|
|
75
|
+
? undefined
|
|
76
|
+
: new Response("Upgrade failed", { status: 500 });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log("unhandled RPC Server request", req.url);
|
|
80
|
+
},
|
|
81
|
+
websocket: {
|
|
82
|
+
idleTimeout: 960,
|
|
83
|
+
// 500MB max payload should be plenty
|
|
84
|
+
maxPayloadLength: payloadLimit,
|
|
85
|
+
// Anything beyond the backpressure limit will be dropped
|
|
86
|
+
backpressureLimit: payloadLimit * 2,
|
|
87
|
+
open(ws) {
|
|
88
|
+
const { webviewId } = ws.data;
|
|
89
|
+
|
|
90
|
+
if (!socketMap[webviewId]) {
|
|
91
|
+
socketMap[webviewId] = { socket: ws, queue: [] };
|
|
92
|
+
} else {
|
|
93
|
+
socketMap[webviewId].socket = ws;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
close(ws, code, reason) {
|
|
97
|
+
const { webviewId } = ws.data;
|
|
98
|
+
console.log("Closed:", webviewId, code, reason);
|
|
99
|
+
socketMap[webviewId].socket = null;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
message(ws, message) {
|
|
103
|
+
const { webviewId } = ws.data;
|
|
104
|
+
const browserView = BrowserView.getById(webviewId);
|
|
105
|
+
|
|
106
|
+
if (browserView.rpcHandler) {
|
|
107
|
+
if (typeof message === "string") {
|
|
108
|
+
try {
|
|
109
|
+
const encryptedPacket = JSON.parse(message);
|
|
110
|
+
const decrypted = decrypt(
|
|
111
|
+
browserView.secretKey,
|
|
112
|
+
base64ToUint8Array(encryptedPacket.encryptedData),
|
|
113
|
+
base64ToUint8Array(encryptedPacket.iv),
|
|
114
|
+
base64ToUint8Array(encryptedPacket.tag)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Note: At this point the secretKey for the webview id would
|
|
118
|
+
// have had to match the encrypted packet data, so we can trust
|
|
119
|
+
// that this message can be passed to this browserview's rpc
|
|
120
|
+
// methods.
|
|
121
|
+
browserView.rpcHandler(JSON.parse(decrypted));
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.log("Error handling message:", error);
|
|
124
|
+
}
|
|
125
|
+
} else if (message instanceof ArrayBuffer) {
|
|
126
|
+
console.log("TODO: Received ArrayBuffer message:", message);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
break;
|
|
134
|
+
} catch (error: any) {
|
|
135
|
+
if (error.code === "EADDRINUSE") {
|
|
136
|
+
console.log(`Port ${port} in use, trying next port...`);
|
|
137
|
+
port++;
|
|
138
|
+
} else {
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { rpcServer: server, rpcPort: port };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const { rpcServer, rpcPort } = startRPCServer();
|
|
148
|
+
|
|
149
|
+
// Will return true if message was sent over websocket
|
|
150
|
+
// false if it was not (caller should fallback to postMessage/evaluateJS rpc)
|
|
151
|
+
export const sendMessageToWebviewViaSocket = (
|
|
152
|
+
webviewId: number,
|
|
153
|
+
message: any
|
|
154
|
+
): boolean => {
|
|
155
|
+
const rpc = socketMap[webviewId];
|
|
156
|
+
const browserView = BrowserView.getById(webviewId);
|
|
157
|
+
|
|
158
|
+
if (rpc?.socket?.readyState === WebSocket.OPEN) {
|
|
159
|
+
try {
|
|
160
|
+
const unencryptedString = JSON.stringify(message);
|
|
161
|
+
const encrypted = encrypt(browserView.secretKey, unencryptedString);
|
|
162
|
+
|
|
163
|
+
const encryptedPacket = {
|
|
164
|
+
encryptedData: encrypted.encrypted,
|
|
165
|
+
iv: encrypted.iv,
|
|
166
|
+
tag: encrypted.tag,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const encryptedPacketString = JSON.stringify(encryptedPacket);
|
|
170
|
+
|
|
171
|
+
rpc.socket.send(encryptedPacketString);
|
|
172
|
+
return true;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error("Error sending message to webview via socket:", error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return false;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
console.log("Server started at", rpcServer?.url.origin);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { ffi, type MenuItemConfig } from "../proc/native";
|
|
2
|
+
import electrobunEventEmitter from "../events/eventEmitter";
|
|
3
|
+
import { VIEWS_FOLDER } from "./Paths";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import {FFIType} from 'bun:ffi';
|
|
6
|
+
|
|
7
|
+
let nextTrayId = 1;
|
|
8
|
+
const TrayMap = {};
|
|
9
|
+
|
|
10
|
+
type ConstructorOptions = {
|
|
11
|
+
title?: string;
|
|
12
|
+
image?: string;
|
|
13
|
+
template?: boolean;
|
|
14
|
+
width?: number;
|
|
15
|
+
height?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class Tray {
|
|
19
|
+
id: number = nextTrayId++;
|
|
20
|
+
ptr: FFIType.ptr;
|
|
21
|
+
|
|
22
|
+
constructor({
|
|
23
|
+
title = "",
|
|
24
|
+
image = "",
|
|
25
|
+
template = true,
|
|
26
|
+
width = 16,
|
|
27
|
+
height = 16,
|
|
28
|
+
}: ConstructorOptions = {}) {
|
|
29
|
+
console.log("img", image);
|
|
30
|
+
console.log("img", this.resolveImagePath(image));
|
|
31
|
+
this.ptr = ffi.request.createTray({
|
|
32
|
+
id: this.id,
|
|
33
|
+
title,
|
|
34
|
+
image: this.resolveImagePath(image),
|
|
35
|
+
template,
|
|
36
|
+
width,
|
|
37
|
+
height,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
TrayMap[this.id] = this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
resolveImagePath(imgPath: string) {
|
|
44
|
+
if (imgPath.startsWith("views://")) {
|
|
45
|
+
return join(VIEWS_FOLDER, imgPath.replace("views://", ""));
|
|
46
|
+
} else {
|
|
47
|
+
// can specify any file path here
|
|
48
|
+
return imgPath;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setTitle(title: string) {
|
|
53
|
+
ffi.request.setTrayTitle({ id: this.id, title });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setImage(imgPath: string) {
|
|
57
|
+
ffi.request.setTrayImage({
|
|
58
|
+
id: this.id,
|
|
59
|
+
image: this.resolveImagePath(imgPath),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setMenu(menu: Array<MenuItemConfig>) {
|
|
64
|
+
const menuWithDefaults = menuConfigWithDefaults(menu);
|
|
65
|
+
ffi.request.setTrayMenu({
|
|
66
|
+
id: this.id,
|
|
67
|
+
menuConfig: JSON.stringify(menuWithDefaults),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
on(name: "tray-clicked", handler) {
|
|
72
|
+
const specificName = `${name}-${this.id}`;
|
|
73
|
+
electrobunEventEmitter.on(specificName, handler);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
remove() {
|
|
77
|
+
console.log('Tray.remove() called for id:', this.id);
|
|
78
|
+
ffi.request.removeTray({ id: this.id });
|
|
79
|
+
delete TrayMap[this.id];
|
|
80
|
+
console.log('Tray removed from TrayMap');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static getById(id: number) {
|
|
84
|
+
return TrayMap[id];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static getAll() {
|
|
88
|
+
return Object.values(TrayMap);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static removeById(id: number) {
|
|
92
|
+
const tray = TrayMap[id];
|
|
93
|
+
if (tray) {
|
|
94
|
+
tray.remove();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const menuConfigWithDefaults = (
|
|
100
|
+
menu: Array<MenuItemConfig>
|
|
101
|
+
): Array<MenuItemConfig> => {
|
|
102
|
+
return menu.map((item) => {
|
|
103
|
+
if (item.type === "divider" || item.type === "separator") {
|
|
104
|
+
return { type: "divider" };
|
|
105
|
+
} else {
|
|
106
|
+
return {
|
|
107
|
+
label: item.label || "",
|
|
108
|
+
type: item.type || "normal",
|
|
109
|
+
action: item.action || "",
|
|
110
|
+
// default enabled to true unless explicitly set to false
|
|
111
|
+
enabled: item.enabled === false ? false : true,
|
|
112
|
+
checked: Boolean(item.checked),
|
|
113
|
+
hidden: Boolean(item.hidden),
|
|
114
|
+
tooltip: item.tooltip || undefined,
|
|
115
|
+
...(item.submenu
|
|
116
|
+
? { submenu: menuConfigWithDefaults(item.submenu) }
|
|
117
|
+
: {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
};
|