electrobun 1.18.1 → 1.18.4-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/api/browser/global.d.ts +5 -0
- package/dist/api/browser/index.ts +64 -22
- package/dist/api/bun/ElectrobunConfig.ts +45 -1
- package/dist/api/bun/core/BrowserView.ts +95 -54
- package/dist/api/bun/core/BrowserWindow.ts +19 -29
- package/dist/api/bun/core/BuildConfig.ts +31 -4
- package/dist/api/bun/core/GpuWindow.ts +20 -7
- package/dist/api/bun/core/Socket.ts +13 -196
- package/dist/api/bun/core/Tray.ts +33 -32
- package/dist/api/bun/core/Utils.ts +2 -4
- package/dist/api/bun/core/WGPUView.ts +43 -13
- package/dist/api/bun/index.ts +35 -0
- package/dist/api/bun/preload/.generated/compiled.ts +1 -1
- package/dist/api/bun/preload/globals.d.ts +7 -0
- package/dist/api/bun/preload/index.ts +18 -8
- package/dist/api/bun/preload/webviewTag.ts +32 -3
- package/dist/api/bun/proc/native.ts +914 -745
- package/dist/api/bun/webGPU.ts +1 -1
- package/dist/main.js +26 -22
- package/dist/preload-full.js +913 -0
- package/dist/preload-sandboxed.js +111 -0
- package/dist/zig-sdk/electrobun.zig +1993 -0
- package/package.json +2 -3
- package/src/cli/index.ts +410 -139
- package/dist/api/bun/core/windowIds.ts +0 -5
package/README.md
CHANGED
|
@@ -63,9 +63,11 @@ Don't miss our:
|
|
|
63
63
|
- [GOG Achievements GUI](https://github.com/timendum/gog-achievements-gui) - desktop app for managing GOG achievements
|
|
64
64
|
- [groov](https://github.com/laurenzcodes/groov) - desktop audio deck monitor
|
|
65
65
|
- [Guerilla Glass](https://github.com/okikeSolutions/guerillaglass) - open-source cross-platform creator studio for fast Record -> Edit -> Deliver workflows
|
|
66
|
+
- [Invoke](https://getinvoke.com) - macOS UI automation & shortcut platform
|
|
66
67
|
- [Marginalia](https://github.com/lars-hoeijmans/Marginalia) - a simple note taking app
|
|
67
68
|
- [MarkBun](https://github.com/xiaochong/markbun) - fast, beautiful, Typora-like markdown desktop editor
|
|
68
69
|
- [md-browse](https://github.com/needle-tools/md-browse) - a markdown-first browser that converts web pages to clean markdown
|
|
70
|
+
- [moop](https://github.com/zrubinrattet/moop/) - desktop app for batch image optimization for the web
|
|
69
71
|
- [Patchline](https://github.com/adwaithks/Patchline) - lightweight desktop Git client for reading patches and line diffs, then staging and committing changes
|
|
70
72
|
- [peekachu](https://github.com/needle-tools/peekachu) - password manager for AIs; store secrets in your OS keychain and scrub output so AI assistants never see actual values
|
|
71
73
|
- [PiBun](https://github.com/khairold/pibun) - desktop GUI for the Pi coding agent with chat, terminal, git integration, and plugin system
|
|
@@ -7,6 +7,8 @@ interface ElectrobunEncryptResult {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
interface ElectrobunBridge {
|
|
10
|
+
receiveMessageFromHost: (msg: unknown) => void;
|
|
11
|
+
receiveInternalMessageFromHost: (msg: unknown) => void;
|
|
10
12
|
receiveMessageFromBun: (msg: unknown) => void;
|
|
11
13
|
receiveInternalMessageFromBun: (msg: unknown) => void;
|
|
12
14
|
}
|
|
@@ -20,10 +22,13 @@ declare global {
|
|
|
20
22
|
__electrobunWebviewId: number;
|
|
21
23
|
__electrobunWindowId: number;
|
|
22
24
|
__electrobunRpcSocketPort: number;
|
|
25
|
+
__electrobunHostSocketPort?: number;
|
|
23
26
|
__electrobun?: ElectrobunBridge;
|
|
27
|
+
__electrobunPendingHostMessages?: unknown[];
|
|
24
28
|
__electrobun_encrypt: (msg: string) => Promise<ElectrobunEncryptResult>;
|
|
25
29
|
__electrobun_decrypt: (encryptedData: string, iv: string, tag: string) => Promise<string>;
|
|
26
30
|
__electrobunInternalBridge?: MessageHandler;
|
|
31
|
+
__electrobunHostBridge?: MessageHandler;
|
|
27
32
|
__electrobunBunBridge?: MessageHandler;
|
|
28
33
|
}
|
|
29
34
|
}
|
|
@@ -15,13 +15,23 @@ import { type WgpuTagElement, type WgpuEventTypes } from "./wgputag";
|
|
|
15
15
|
import "./global.d.ts";
|
|
16
16
|
|
|
17
17
|
const WEBVIEW_ID = window.__electrobunWebviewId;
|
|
18
|
-
const
|
|
18
|
+
const HOST_SOCKET_PORT =
|
|
19
|
+
window.__electrobunHostSocketPort ?? window.__electrobunRpcSocketPort;
|
|
19
20
|
|
|
20
21
|
class Electroview<T extends RPCWithTransport> {
|
|
21
|
-
|
|
22
|
+
hostSocket?: WebSocket;
|
|
23
|
+
hostSocketCanSend = false;
|
|
22
24
|
// user's custom rpc browser <-> bun
|
|
23
25
|
rpc?: T;
|
|
24
26
|
rpcHandler?: (msg: unknown) => void;
|
|
27
|
+
carrots = {
|
|
28
|
+
invoke: <R = unknown>(
|
|
29
|
+
carrotId: string,
|
|
30
|
+
method: string,
|
|
31
|
+
params?: unknown,
|
|
32
|
+
options?: { windowId?: string },
|
|
33
|
+
) => this.invokeCarrot<R>(carrotId, method, params, options),
|
|
34
|
+
};
|
|
25
35
|
|
|
26
36
|
constructor(config: { rpc: T }) {
|
|
27
37
|
this.rpc = config.rpc;
|
|
@@ -29,34 +39,42 @@ class Electroview<T extends RPCWithTransport> {
|
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
init() {
|
|
32
|
-
this.
|
|
42
|
+
this.initSocketToHost();
|
|
33
43
|
|
|
34
|
-
// Set up handler for user RPC messages from
|
|
35
|
-
|
|
36
|
-
window.__electrobun!.
|
|
37
|
-
|
|
44
|
+
// Set up handler for user RPC messages from the host runtime.
|
|
45
|
+
const hostMessageHandler = this.receiveMessageFromHost.bind(this);
|
|
46
|
+
window.__electrobun!.receiveMessageFromHost = hostMessageHandler;
|
|
47
|
+
window.__electrobun!.receiveMessageFromBun = hostMessageHandler;
|
|
38
48
|
|
|
39
49
|
if (this.rpc) {
|
|
40
50
|
this.rpc.setTransport(this.createTransport());
|
|
41
51
|
}
|
|
52
|
+
|
|
53
|
+
const pendingMessages = window.__electrobunPendingHostMessages;
|
|
54
|
+
if (pendingMessages?.length) {
|
|
55
|
+
window.__electrobunPendingHostMessages = [];
|
|
56
|
+
for (const message of pendingMessages) {
|
|
57
|
+
hostMessageHandler(message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
42
60
|
}
|
|
43
61
|
|
|
44
|
-
|
|
62
|
+
initSocketToHost() {
|
|
45
63
|
// Skip native socket when running in a remote browser (no port/webview ID)
|
|
46
|
-
if (!
|
|
64
|
+
if (!HOST_SOCKET_PORT || !WEBVIEW_ID) {
|
|
47
65
|
return;
|
|
48
66
|
}
|
|
49
67
|
|
|
50
|
-
// Note: Using ws:// for
|
|
68
|
+
// Note: Using ws:// for loopback is intentional - all RPC messages are
|
|
51
69
|
// encrypted with per-webview AES-GCM keys, making TLS redundant
|
|
52
70
|
const socket = new WebSocket(
|
|
53
|
-
`ws://
|
|
71
|
+
`ws://127.0.0.1:${HOST_SOCKET_PORT}/socket?webviewId=${WEBVIEW_ID}`,
|
|
54
72
|
);
|
|
55
73
|
|
|
56
|
-
this.
|
|
74
|
+
this.hostSocket = socket;
|
|
57
75
|
|
|
58
76
|
socket.addEventListener("open", () => {
|
|
59
|
-
|
|
77
|
+
this.hostSocketCanSend = true;
|
|
60
78
|
});
|
|
61
79
|
|
|
62
80
|
socket.addEventListener("message", async (event) => {
|
|
@@ -71,6 +89,7 @@ class Electroview<T extends RPCWithTransport> {
|
|
|
71
89
|
encryptedPacket.tag,
|
|
72
90
|
);
|
|
73
91
|
|
|
92
|
+
this.hostSocketCanSend = true;
|
|
74
93
|
this.rpcHandler?.(JSON.parse(decrypted));
|
|
75
94
|
} catch (err) {
|
|
76
95
|
console.error("Error parsing bun message:", err);
|
|
@@ -83,10 +102,12 @@ class Electroview<T extends RPCWithTransport> {
|
|
|
83
102
|
});
|
|
84
103
|
|
|
85
104
|
socket.addEventListener("error", (event) => {
|
|
105
|
+
this.hostSocketCanSend = false;
|
|
86
106
|
console.error("Socket error:", event);
|
|
87
107
|
});
|
|
88
108
|
|
|
89
109
|
socket.addEventListener("close", (_event) => {
|
|
110
|
+
this.hostSocketCanSend = false;
|
|
90
111
|
// console.log("Socket closed:", event);
|
|
91
112
|
});
|
|
92
113
|
}
|
|
@@ -97,9 +118,9 @@ class Electroview<T extends RPCWithTransport> {
|
|
|
97
118
|
send(message: unknown) {
|
|
98
119
|
try {
|
|
99
120
|
const messageString = JSON.stringify(message);
|
|
100
|
-
that.
|
|
121
|
+
that.sendMessageToHost(messageString);
|
|
101
122
|
} catch (error) {
|
|
102
|
-
console.error("
|
|
123
|
+
console.error("host: failed to serialize message to webview", error);
|
|
103
124
|
}
|
|
104
125
|
},
|
|
105
126
|
registerHandler(handler: (msg: unknown) => void) {
|
|
@@ -108,8 +129,11 @@ class Electroview<T extends RPCWithTransport> {
|
|
|
108
129
|
};
|
|
109
130
|
}
|
|
110
131
|
|
|
111
|
-
async
|
|
112
|
-
if (
|
|
132
|
+
async sendMessageToHost(msg: string) {
|
|
133
|
+
if (
|
|
134
|
+
this.hostSocketCanSend &&
|
|
135
|
+
this.hostSocket?.readyState === WebSocket.OPEN
|
|
136
|
+
) {
|
|
113
137
|
try {
|
|
114
138
|
const { encryptedData, iv, tag } =
|
|
115
139
|
await window.__electrobun_encrypt(msg);
|
|
@@ -120,24 +144,42 @@ class Electroview<T extends RPCWithTransport> {
|
|
|
120
144
|
tag: tag,
|
|
121
145
|
};
|
|
122
146
|
const encryptedPacketString = JSON.stringify(encryptedPacket);
|
|
123
|
-
this.
|
|
147
|
+
this.hostSocket.send(encryptedPacketString);
|
|
124
148
|
return;
|
|
125
149
|
} catch (error) {
|
|
126
|
-
console.error("Error sending message to
|
|
150
|
+
console.error("Error sending message to host via socket:", error);
|
|
127
151
|
}
|
|
128
152
|
}
|
|
129
153
|
|
|
130
154
|
// if socket's are unavailable, fallback to postMessage
|
|
131
|
-
window.
|
|
155
|
+
window.__electrobunHostBridge?.postMessage(msg);
|
|
132
156
|
}
|
|
133
157
|
|
|
134
|
-
|
|
135
|
-
// NOTE: in the webview messages are passed by executing
|
|
158
|
+
receiveMessageFromHost(msg: unknown) {
|
|
159
|
+
// NOTE: in the webview messages are passed by executing window.__electrobun.receiveMessageFromHost(object)
|
|
136
160
|
// so they're already parsed into an object here
|
|
137
161
|
if (this.rpcHandler) {
|
|
138
162
|
this.rpcHandler(msg);
|
|
139
163
|
}
|
|
140
164
|
}
|
|
165
|
+
|
|
166
|
+
async invokeCarrot<R = unknown>(
|
|
167
|
+
carrotId: string,
|
|
168
|
+
method: string,
|
|
169
|
+
params?: unknown,
|
|
170
|
+
options?: { windowId?: string },
|
|
171
|
+
): Promise<R> {
|
|
172
|
+
const requestProxy = (this.rpc as any)?.request;
|
|
173
|
+
if (!requestProxy || typeof requestProxy.invokeCarrot !== "function") {
|
|
174
|
+
throw new Error("Renderer carrot invocation is not available in this Electrobun host.");
|
|
175
|
+
}
|
|
176
|
+
return requestProxy.invokeCarrot({
|
|
177
|
+
carrotId,
|
|
178
|
+
method,
|
|
179
|
+
params,
|
|
180
|
+
windowId: options?.windowId,
|
|
181
|
+
}) as Promise<R>;
|
|
182
|
+
}
|
|
141
183
|
static defineRPC<Schema extends ElectrobunRPCSchema>(
|
|
142
184
|
config: ElectrobunRPCConfig<Schema, "webview">,
|
|
143
185
|
) {
|
|
@@ -13,6 +13,28 @@ type BunBuildOptions = Omit<
|
|
|
13
13
|
"entrypoints" | "outdir" | "target"
|
|
14
14
|
>;
|
|
15
15
|
|
|
16
|
+
type CarrotFileActivatorConfig = {
|
|
17
|
+
baseName?: string;
|
|
18
|
+
nodeType?: "file" | "dir" | "any";
|
|
19
|
+
slate: {
|
|
20
|
+
type: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
icon?: string;
|
|
23
|
+
config?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type CarrotContributionsConfig = {
|
|
28
|
+
fileActivators?: CarrotFileActivatorConfig[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type CarrotUIDefinition = {
|
|
32
|
+
name?: string;
|
|
33
|
+
entrypoint?: string;
|
|
34
|
+
path?: string;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
};
|
|
37
|
+
|
|
16
38
|
export interface ElectrobunConfig {
|
|
17
39
|
/**
|
|
18
40
|
* Application metadata configuration
|
|
@@ -112,6 +134,14 @@ export interface ElectrobunConfig {
|
|
|
112
134
|
* Build configuration options
|
|
113
135
|
*/
|
|
114
136
|
build?: {
|
|
137
|
+
/**
|
|
138
|
+
* Main process implementation to build and package.
|
|
139
|
+
* - "bun": bundle and run the Bun main process entrypoint
|
|
140
|
+
* - "zig": compile and run the Zig main process entrypoint
|
|
141
|
+
* @default "bun"
|
|
142
|
+
*/
|
|
143
|
+
mainProcess?: "bun" | "zig";
|
|
144
|
+
|
|
115
145
|
/**
|
|
116
146
|
* Bun process build configuration.
|
|
117
147
|
* Accepts all Bun.build() options (plugins, sourcemap, minify, define, etc.)
|
|
@@ -125,6 +155,18 @@ export interface ElectrobunConfig {
|
|
|
125
155
|
entrypoint?: string;
|
|
126
156
|
} & BunBuildOptions;
|
|
127
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Zig main process build configuration.
|
|
160
|
+
* Used when `build.mainProcess` is set to `"zig"`.
|
|
161
|
+
*/
|
|
162
|
+
zig?: {
|
|
163
|
+
/**
|
|
164
|
+
* Entry point for the main Zig process
|
|
165
|
+
* @default "src/zig/main.zig"
|
|
166
|
+
*/
|
|
167
|
+
entrypoint?: string;
|
|
168
|
+
};
|
|
169
|
+
|
|
128
170
|
/**
|
|
129
171
|
* Browser view build configurations.
|
|
130
172
|
* Each view accepts all Bun.build() options (plugins, sourcemap, minify, define, etc.)
|
|
@@ -244,7 +286,9 @@ export interface ElectrobunConfig {
|
|
|
244
286
|
carrotOnly?: boolean;
|
|
245
287
|
permissions?: Record<string, unknown>;
|
|
246
288
|
dependencies?: Record<string, string>;
|
|
247
|
-
remoteUIs?: Record<string,
|
|
289
|
+
remoteUIs?: Record<string, CarrotUIDefinition>;
|
|
290
|
+
slateUIs?: Record<string, CarrotUIDefinition>;
|
|
291
|
+
contributions?: CarrotContributionsConfig;
|
|
248
292
|
};
|
|
249
293
|
|
|
250
294
|
/**
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as fs from "fs";
|
|
1
|
+
import { ffi } from "../proc/native";
|
|
3
2
|
import electrobunEventEmitter from "../events/eventEmitter";
|
|
4
3
|
import {
|
|
5
4
|
type ElectrobunRPCSchema,
|
|
@@ -7,10 +6,8 @@ import {
|
|
|
7
6
|
type RPCWithTransport,
|
|
8
7
|
defineElectrobunRPC,
|
|
9
8
|
} from "../../shared/rpc.js";
|
|
10
|
-
import { Updater } from "./Updater";
|
|
11
9
|
import { BuildConfig } from "./BuildConfig";
|
|
12
10
|
import {
|
|
13
|
-
rpcPort,
|
|
14
11
|
sendMessageToWebviewViaSocket,
|
|
15
12
|
removeSocketForWebview,
|
|
16
13
|
} from "./Socket";
|
|
@@ -20,7 +17,6 @@ import { type Pointer } from "bun:ffi";
|
|
|
20
17
|
const BrowserViewMap: {
|
|
21
18
|
[id: number]: BrowserView<any>;
|
|
22
19
|
} = {};
|
|
23
|
-
let nextWebviewId = 1;
|
|
24
20
|
|
|
25
21
|
export type BrowserViewOptions<T = undefined> = {
|
|
26
22
|
url: string | null;
|
|
@@ -38,7 +34,6 @@ export type BrowserViewOptions<T = undefined> = {
|
|
|
38
34
|
rpc: T;
|
|
39
35
|
hostWebviewId: number;
|
|
40
36
|
autoResize: boolean;
|
|
41
|
-
|
|
42
37
|
windowId: number;
|
|
43
38
|
navigationRules: string | null;
|
|
44
39
|
// Sandbox mode: when true, disables RPC and only allows event emission
|
|
@@ -52,8 +47,7 @@ export type BrowserViewOptions<T = undefined> = {
|
|
|
52
47
|
// renderer:
|
|
53
48
|
};
|
|
54
49
|
|
|
55
|
-
const
|
|
56
|
-
const buildConfig = await BuildConfig.get();
|
|
50
|
+
const buildConfig = BuildConfig.getSync();
|
|
57
51
|
|
|
58
52
|
const defaultOptions: Partial<BrowserViewOptions> = {
|
|
59
53
|
url: null,
|
|
@@ -68,13 +62,8 @@ const defaultOptions: Partial<BrowserViewOptions> = {
|
|
|
68
62
|
height: 600,
|
|
69
63
|
},
|
|
70
64
|
};
|
|
71
|
-
// Note: we use the build's hash to separate from different apps and different builds
|
|
72
|
-
// but we also want a randomId to separate different instances of the same app
|
|
73
|
-
const randomId = Math.random().toString(36).substring(7);
|
|
74
|
-
|
|
75
65
|
export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
76
|
-
id
|
|
77
|
-
ptr: Pointer | null = null;
|
|
66
|
+
id = 0;
|
|
78
67
|
hostWebviewId?: number;
|
|
79
68
|
windowId!: number;
|
|
80
69
|
renderer!: "cef" | "native";
|
|
@@ -95,9 +84,6 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
95
84
|
width: 800,
|
|
96
85
|
height: 600,
|
|
97
86
|
};
|
|
98
|
-
pipePrefix!: string;
|
|
99
|
-
inStream!: fs.WriteStream;
|
|
100
|
-
outStream!: ReadableStream<Uint8Array>;
|
|
101
87
|
secretKey!: Uint8Array;
|
|
102
88
|
rpc?: T;
|
|
103
89
|
rpcHandler?: (msg: unknown) => void;
|
|
@@ -108,6 +94,13 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
108
94
|
startPassthrough: boolean = false;
|
|
109
95
|
isRemoved: boolean = false;
|
|
110
96
|
|
|
97
|
+
get ptr(): Pointer | null {
|
|
98
|
+
if (this.isRemoved) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return ffi.request.getWebviewPointer({ id: this.id }) as Pointer | null;
|
|
102
|
+
}
|
|
103
|
+
|
|
111
104
|
constructor(options: Partial<BrowserViewOptions<T>> = defaultOptions) {
|
|
112
105
|
// const rpc = options.rpc;
|
|
113
106
|
|
|
@@ -124,9 +117,6 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
124
117
|
this.rpc = options.rpc;
|
|
125
118
|
this.secretKey = new Uint8Array(randomBytes(32));
|
|
126
119
|
this.partition = options.partition || null;
|
|
127
|
-
// todo (yoav): since collisions can crash the app add a function that checks if the
|
|
128
|
-
// file exists first
|
|
129
|
-
this.pipePrefix = `/private/tmp/electrobun_ipc_pipe_${hash}_${randomId}_${this.id}`;
|
|
130
120
|
this.hostWebviewId = options.hostWebviewId;
|
|
131
121
|
this.windowId = options.windowId ?? 0;
|
|
132
122
|
this.autoResize = options.autoResize === false ? false : true;
|
|
@@ -136,8 +126,8 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
136
126
|
this.startTransparent = options.startTransparent ?? false;
|
|
137
127
|
this.startPassthrough = options.startPassthrough ?? false;
|
|
138
128
|
|
|
129
|
+
this.id = this.init() as number;
|
|
139
130
|
BrowserViewMap[this.id] = this;
|
|
140
|
-
this.ptr = this.init() as Pointer;
|
|
141
131
|
|
|
142
132
|
// If HTML content was provided, load it after webview creation.
|
|
143
133
|
if (this.html) {
|
|
@@ -148,21 +138,17 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
148
138
|
}
|
|
149
139
|
|
|
150
140
|
init() {
|
|
151
|
-
this.
|
|
141
|
+
this.initializeRpcTransport();
|
|
152
142
|
|
|
153
143
|
return ffi.request.createWebview({
|
|
154
|
-
id: this.id,
|
|
155
144
|
windowId: this.windowId,
|
|
145
|
+
hostWebviewId: this.hostWebviewId ?? null,
|
|
156
146
|
renderer: this.renderer,
|
|
157
|
-
rpcPort: rpcPort,
|
|
158
147
|
// todo: consider sending secretKey as base64
|
|
159
148
|
secretKey: this.secretKey.toString(),
|
|
160
|
-
hostWebviewId: this.hostWebviewId || null,
|
|
161
|
-
pipePrefix: this.pipePrefix,
|
|
162
149
|
partition: this.partition,
|
|
163
150
|
// Only pass URL if no HTML content is provided to avoid conflicts
|
|
164
151
|
url: this.html ? null : this.url,
|
|
165
|
-
html: this.html,
|
|
166
152
|
preload: this.preload,
|
|
167
153
|
viewsRoot: this.viewsRoot,
|
|
168
154
|
frame: {
|
|
@@ -180,7 +166,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
180
166
|
});
|
|
181
167
|
}
|
|
182
168
|
|
|
183
|
-
|
|
169
|
+
initializeRpcTransport() {
|
|
184
170
|
if (!this.rpc) {
|
|
185
171
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
186
172
|
this.rpc = BrowserView.defineRPC({
|
|
@@ -191,23 +177,23 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
191
177
|
this.rpc!.setTransport(this.createTransport());
|
|
192
178
|
}
|
|
193
179
|
|
|
194
|
-
|
|
180
|
+
sendHostMessageToWebviewViaExecute(jsonMessage: unknown) {
|
|
195
181
|
const stringifiedMessage =
|
|
196
182
|
typeof jsonMessage === "string"
|
|
197
183
|
? jsonMessage
|
|
198
184
|
: JSON.stringify(jsonMessage);
|
|
199
185
|
// todo (yoav): make this a shared const with the browser api
|
|
200
|
-
const wrappedMessage = `window.__electrobun.
|
|
186
|
+
const wrappedMessage = `window.__electrobun.receiveMessageFromHost(${stringifiedMessage})`;
|
|
201
187
|
this.executeJavascript(wrappedMessage);
|
|
202
188
|
}
|
|
203
189
|
|
|
204
|
-
|
|
190
|
+
sendInternalHostMessageViaExecute(jsonMessage: unknown) {
|
|
205
191
|
const stringifiedMessage =
|
|
206
192
|
typeof jsonMessage === "string"
|
|
207
193
|
? jsonMessage
|
|
208
194
|
: JSON.stringify(jsonMessage);
|
|
209
195
|
// todo (yoav): make this a shared const with the browser api
|
|
210
|
-
const wrappedMessage = `window.__electrobun.
|
|
196
|
+
const wrappedMessage = `window.__electrobun.receiveInternalMessageFromHost(${stringifiedMessage})`;
|
|
211
197
|
this.executeJavascript(wrappedMessage);
|
|
212
198
|
}
|
|
213
199
|
|
|
@@ -224,7 +210,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
224
210
|
|
|
225
211
|
loadURL(url: string) {
|
|
226
212
|
this.url = url;
|
|
227
|
-
|
|
213
|
+
ffi.request.loadURLInWebView({ id: this.id, url: this.url });
|
|
228
214
|
}
|
|
229
215
|
|
|
230
216
|
loadHTML(html: string) {
|
|
@@ -232,18 +218,18 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
232
218
|
|
|
233
219
|
if (this.renderer === "cef") {
|
|
234
220
|
// For CEF, store HTML content in native map and use scheme handler
|
|
235
|
-
|
|
221
|
+
ffi.request.setWebviewHTMLContent({ id: this.id, html });
|
|
236
222
|
this.loadURL("views://internal/index.html");
|
|
237
223
|
} else {
|
|
238
224
|
// For WKWebView, load HTML content directly
|
|
239
|
-
|
|
225
|
+
ffi.request.loadHTMLInWebView({ id: this.id, html });
|
|
240
226
|
}
|
|
241
227
|
}
|
|
242
228
|
|
|
243
229
|
setNavigationRules(rules: string[]) {
|
|
244
230
|
this.navigationRules = JSON.stringify(rules);
|
|
245
231
|
const rulesJson = JSON.stringify(rules);
|
|
246
|
-
|
|
232
|
+
ffi.request.setWebviewNavigationRules({ id: this.id, rulesJson });
|
|
247
233
|
}
|
|
248
234
|
|
|
249
235
|
findInPage(
|
|
@@ -252,28 +238,28 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
252
238
|
) {
|
|
253
239
|
const forward = options?.forward ?? true;
|
|
254
240
|
const matchCase = options?.matchCase ?? false;
|
|
255
|
-
|
|
256
|
-
this.
|
|
257
|
-
|
|
241
|
+
ffi.request.webviewFindInPage({
|
|
242
|
+
id: this.id,
|
|
243
|
+
searchText,
|
|
258
244
|
forward,
|
|
259
245
|
matchCase,
|
|
260
|
-
);
|
|
246
|
+
});
|
|
261
247
|
}
|
|
262
248
|
|
|
263
249
|
stopFindInPage() {
|
|
264
|
-
|
|
250
|
+
ffi.request.webviewStopFind({ id: this.id });
|
|
265
251
|
}
|
|
266
252
|
|
|
267
253
|
openDevTools() {
|
|
268
|
-
|
|
254
|
+
ffi.request.webviewOpenDevTools({ id: this.id });
|
|
269
255
|
}
|
|
270
256
|
|
|
271
257
|
closeDevTools() {
|
|
272
|
-
|
|
258
|
+
ffi.request.webviewCloseDevTools({ id: this.id });
|
|
273
259
|
}
|
|
274
260
|
|
|
275
261
|
toggleDevTools() {
|
|
276
|
-
|
|
262
|
+
ffi.request.webviewToggleDevTools({ id: this.id });
|
|
277
263
|
}
|
|
278
264
|
|
|
279
265
|
/**
|
|
@@ -281,7 +267,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
281
267
|
* @param zoomLevel - The zoom level (1.0 = 100%, 1.5 = 150%, etc.)
|
|
282
268
|
*/
|
|
283
269
|
setPageZoom(zoomLevel: number) {
|
|
284
|
-
|
|
270
|
+
ffi.request.webviewSetPageZoom({ id: this.id, zoomLevel });
|
|
285
271
|
}
|
|
286
272
|
|
|
287
273
|
/**
|
|
@@ -289,7 +275,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
289
275
|
* @returns The current zoom level (1.0 = 100%)
|
|
290
276
|
*/
|
|
291
277
|
getPageZoom(): number {
|
|
292
|
-
return
|
|
278
|
+
return ffi.request.webviewGetPageZoom({ id: this.id }) as number;
|
|
293
279
|
}
|
|
294
280
|
|
|
295
281
|
// todo (yoav): move this to a class that also has off, append, prepend, etc.
|
|
@@ -326,9 +312,9 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
326
312
|
if (!sentOverSocket) {
|
|
327
313
|
try {
|
|
328
314
|
const messageString = JSON.stringify(message);
|
|
329
|
-
that.
|
|
315
|
+
that.sendHostMessageToWebviewViaExecute(messageString);
|
|
330
316
|
} catch (error) {
|
|
331
|
-
console.error("
|
|
317
|
+
console.error("host: failed to serialize message to webview", error);
|
|
332
318
|
}
|
|
333
319
|
}
|
|
334
320
|
},
|
|
@@ -342,10 +328,9 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
342
328
|
};
|
|
343
329
|
|
|
344
330
|
remove() {
|
|
345
|
-
if (
|
|
331
|
+
if (this.isRemoved) {
|
|
346
332
|
return;
|
|
347
333
|
}
|
|
348
|
-
const ptr = this.ptr;
|
|
349
334
|
this.isRemoved = true;
|
|
350
335
|
// Drop JS-side references first so late callbacks cannot target a stale view.
|
|
351
336
|
delete BrowserViewMap[this.id];
|
|
@@ -356,16 +341,72 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
356
341
|
unregisterHandler() {},
|
|
357
342
|
});
|
|
358
343
|
this.rpcHandler = undefined;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
344
|
+
try {
|
|
345
|
+
ffi.request.webviewRemove({ id: this.id });
|
|
346
|
+
} catch (error) {
|
|
347
|
+
console.error(`Error removing webview ${this.id}:`, error);
|
|
348
|
+
}
|
|
363
349
|
}
|
|
364
350
|
|
|
365
351
|
static getById(id: number) {
|
|
366
352
|
return BrowserViewMap[id];
|
|
367
353
|
}
|
|
368
354
|
|
|
355
|
+
// Core can create webviews before Bun has constructed a JS wrapper for them.
|
|
356
|
+
// Use this in native/runtime paths that need to ensure a wrapper exists.
|
|
357
|
+
static ensureWrapped<T extends RPCWithTransport = RPCWithTransport>(
|
|
358
|
+
id: number,
|
|
359
|
+
options: Partial<BrowserViewOptions<T>> = {},
|
|
360
|
+
) {
|
|
361
|
+
return (
|
|
362
|
+
(BrowserViewMap[id] as BrowserView<T> | undefined) ??
|
|
363
|
+
BrowserView.adoptExisting(id, options)
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
static adoptExisting<T extends RPCWithTransport = RPCWithTransport>(
|
|
368
|
+
id: number,
|
|
369
|
+
options: Partial<BrowserViewOptions<T>> = {},
|
|
370
|
+
) {
|
|
371
|
+
const existing = BrowserViewMap[id] as BrowserView<T> | undefined;
|
|
372
|
+
if (existing) {
|
|
373
|
+
return existing;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const ptr = ffi.request.getWebviewPointer({ id }) as Pointer | null;
|
|
377
|
+
if (!ptr) {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const view = Object.create(BrowserView.prototype) as BrowserView<T>;
|
|
382
|
+
view.id = id;
|
|
383
|
+
view.hostWebviewId = options.hostWebviewId;
|
|
384
|
+
view.windowId = options.windowId ?? 0;
|
|
385
|
+
view.renderer = options.renderer ?? defaultOptions.renderer ?? "native";
|
|
386
|
+
view.url = options.url ?? defaultOptions.url ?? null;
|
|
387
|
+
view.html = options.html ?? defaultOptions.html ?? null;
|
|
388
|
+
view.preload = options.preload ?? defaultOptions.preload ?? null;
|
|
389
|
+
view.viewsRoot = options.viewsRoot ?? defaultOptions.viewsRoot ?? null;
|
|
390
|
+
view.partition = options.partition ?? null;
|
|
391
|
+
view.frame = {
|
|
392
|
+
x: options.frame?.x ?? defaultOptions.frame!.x,
|
|
393
|
+
y: options.frame?.y ?? defaultOptions.frame!.y,
|
|
394
|
+
width: options.frame?.width ?? defaultOptions.frame!.width,
|
|
395
|
+
height: options.frame?.height ?? defaultOptions.frame!.height,
|
|
396
|
+
};
|
|
397
|
+
view.secretKey = new Uint8Array(0);
|
|
398
|
+
view.rpc = options.rpc;
|
|
399
|
+
view.rpcHandler = undefined;
|
|
400
|
+
view.autoResize = options.autoResize === false ? false : true;
|
|
401
|
+
view.navigationRules = options.navigationRules ?? null;
|
|
402
|
+
view.sandbox = options.sandbox ?? false;
|
|
403
|
+
view.startTransparent = options.startTransparent ?? false;
|
|
404
|
+
view.startPassthrough = options.startPassthrough ?? false;
|
|
405
|
+
view.isRemoved = false;
|
|
406
|
+
BrowserViewMap[id] = view as BrowserView<any>;
|
|
407
|
+
return view;
|
|
408
|
+
}
|
|
409
|
+
|
|
369
410
|
static getAll() {
|
|
370
411
|
return Object.values(BrowserViewMap);
|
|
371
412
|
}
|