electrobun 0.0.18 → 0.0.19-beta.6

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.
@@ -1,417 +0,0 @@
1
- import { zigRPC } from "../proc/zig";
2
- import * as fs from "fs";
3
- import { execSync } from "child_process";
4
- import electrobunEventEmitter from "../events/eventEmitter";
5
- import {
6
- type RPCSchema,
7
- type RPCRequestHandler,
8
- type RPCMessageHandlerFn,
9
- type WildcardRPCMessageHandlerFn,
10
- type RPCOptions,
11
- createRPC,
12
- } from "rpc-anywhere";
13
- import { Updater } from "./Updater";
14
- import type { BuiltinBunToWebviewSchema } from "../../browser/builtinrpcSchema";
15
- import { rpcPort, sendMessageToWebviewViaSocket } from "./Socket";
16
- import { randomBytes } from "crypto";
17
-
18
- const BrowserViewMap: {
19
- [id: number]: BrowserView<any>;
20
- } = {};
21
- let nextWebviewId = 1;
22
-
23
- const CHUNK_SIZE = 1024 * 4; // 4KB
24
-
25
- type BrowserViewOptions<T = undefined> = {
26
- url: string | null;
27
- html: string | null;
28
- preload: string | null;
29
- partition: string | null;
30
- frame: {
31
- x: number;
32
- y: number;
33
- width: number;
34
- height: number;
35
- };
36
- rpc: T;
37
- syncRpc: { [method: string]: (params: any) => any };
38
- hostWebviewId: number;
39
- autoResize: boolean;
40
- };
41
-
42
- interface ElectrobunWebviewRPCSChema {
43
- bun: RPCSchema;
44
- webview: RPCSchema;
45
- }
46
-
47
- const defaultOptions: Partial<BrowserViewOptions> = {
48
- url: "https://electrobun.dev",
49
- html: null,
50
- preload: null,
51
- frame: {
52
- x: 0,
53
- y: 0,
54
- width: 800,
55
- height: 600,
56
- },
57
- };
58
-
59
- const internalSyncRpcHandlers = {
60
- webviewTagInit: ({
61
- hostWebviewId,
62
- windowId,
63
- url,
64
- html,
65
- preload,
66
- partition,
67
- frame,
68
- }: BrowserViewOptions & { windowId: number }) => {
69
- const webviewForTag = new BrowserView({
70
- url,
71
- html,
72
- preload,
73
- partition,
74
- frame,
75
- hostWebviewId,
76
- autoResize: false,
77
- });
78
-
79
- // Note: we have to give it a couple of ticks to fully create the browserview
80
- // which has a settimout init() which calls rpc that has a settimeout and all the serialization/deserialization
81
-
82
- // TODO: we really need a better way to handle the whole view creation flow and
83
- // maybe an onready event or something.
84
- setTimeout(() => {
85
- zigRPC.request.addWebviewToWindow({
86
- windowId: windowId,
87
- webviewId: webviewForTag.id,
88
- });
89
-
90
- if (url) {
91
- webviewForTag.loadURL(url);
92
- } else if (html) {
93
- webviewForTag.loadHTML(html);
94
- }
95
- }, 100);
96
-
97
- return webviewForTag.id;
98
- },
99
- };
100
-
101
- const hash = await Updater.localInfo.hash();
102
- // Note: we use the build's hash to separate from different apps and different builds
103
- // but we also want a randomId to separate different instances of the same app
104
- const randomId = Math.random().toString(36).substring(7);
105
-
106
- export class BrowserView<T> {
107
- id: number = nextWebviewId++;
108
- hostWebviewId?: number;
109
- url: string | null = null;
110
- html: string | null = null;
111
- preload: string | null = null;
112
- partition: string | null = null;
113
- autoResize: boolean = true;
114
- frame: {
115
- x: number;
116
- y: number;
117
- width: number;
118
- height: number;
119
- } = {
120
- x: 0,
121
- y: 0,
122
- width: 800,
123
- height: 600,
124
- };
125
- pipePrefix: string;
126
- inStream: fs.WriteStream;
127
- outStream: ReadableStream<Uint8Array>;
128
- secretKey: Uint8Array;
129
- rpc?: T;
130
- syncRpc?: { [method: string]: (params: any) => any };
131
- rpcHandler?: (msg: any) => void;
132
-
133
- constructor(options: Partial<BrowserViewOptions<T>> = defaultOptions) {
134
- this.url = options.url || defaultOptions.url || null;
135
- this.html = options.html || defaultOptions.html || null;
136
- this.preload = options.preload || defaultOptions.preload || null;
137
- this.frame = options.frame
138
- ? { ...defaultOptions.frame, ...options.frame }
139
- : { ...defaultOptions.frame };
140
- this.rpc = options.rpc;
141
- this.secretKey = new Uint8Array(randomBytes(32));
142
- this.syncRpc = { ...(options.syncRpc || {}), ...internalSyncRpcHandlers };
143
- this.partition = options.partition || null;
144
- // todo (yoav): since collisions can crash the app add a function that checks if the
145
- // file exists first
146
- this.pipePrefix = `/private/tmp/electrobun_ipc_pipe_${hash}_${randomId}_${this.id}`;
147
- this.hostWebviewId = options.hostWebviewId;
148
- this.autoResize = options.autoResize === false ? false : true;
149
-
150
- this.init();
151
- }
152
-
153
- init() {
154
- // TODO: add a then to this that fires an onReady event
155
- zigRPC.request.createWebview({
156
- id: this.id,
157
- rpcPort: rpcPort,
158
- // todo: consider sending secretKey as base64
159
- secretKey: this.secretKey.toString(),
160
- hostWebviewId: this.hostWebviewId || null,
161
- pipePrefix: this.pipePrefix,
162
- partition: this.partition,
163
- // TODO: decide whether we want to keep sending url/html
164
- // here, if we're manually calling loadURL/loadHTML below
165
- // then we can remove it from the api here
166
- url: this.url,
167
- html: this.html,
168
- preload: this.preload,
169
- frame: {
170
- width: this.frame.width,
171
- height: this.frame.height,
172
- x: this.frame.x,
173
- y: this.frame.y,
174
- },
175
- autoResize: this.autoResize,
176
- });
177
-
178
- this.createStreams();
179
-
180
- BrowserViewMap[this.id] = this;
181
- }
182
-
183
- createStreams() {
184
- const webviewPipeIn = this.pipePrefix + "_in";
185
- const webviewPipeOut = this.pipePrefix + "_out";
186
-
187
- try {
188
- execSync("mkfifo " + webviewPipeOut);
189
- } catch (e) {
190
- console.log("pipe out already exists");
191
- }
192
-
193
- try {
194
- execSync("mkfifo " + webviewPipeIn);
195
- } catch (e) {
196
- console.log("pipe in already exists");
197
- }
198
-
199
- const inStream = fs.createWriteStream(webviewPipeIn, {
200
- flags: "r+",
201
- });
202
-
203
- // todo: something has to be written to it to open it
204
- // look into this
205
- inStream.write("\n");
206
-
207
- this.inStream = inStream;
208
-
209
- // Open the named pipe for reading
210
- const outStream = Bun.file(webviewPipeOut).stream();
211
- this.outStream = outStream;
212
-
213
- if (this.rpc) {
214
- this.rpc.setTransport(this.createTransport());
215
- }
216
- }
217
-
218
- sendMessageToWebviewViaExecute(jsonMessage) {
219
- const stringifiedMessage =
220
- typeof jsonMessage === "string"
221
- ? jsonMessage
222
- : JSON.stringify(jsonMessage);
223
- // todo (yoav): make this a shared const with the browser api
224
- const wrappedMessage = `window.__electrobun.receiveMessageFromBun(${stringifiedMessage})`;
225
- this.executeJavascript(wrappedMessage);
226
- }
227
-
228
- // Note: the OS has a buffer limit on named pipes. If we overflow it
229
- // it won't trigger the kevent for zig to read the pipe and we'll be stuck.
230
- // so we have to chunk it
231
- executeJavascript(js: string) {
232
- let offset = 0;
233
- while (offset < js.length) {
234
- const chunk = js.slice(offset, offset + CHUNK_SIZE);
235
- this.inStream.write(chunk);
236
- offset += CHUNK_SIZE;
237
- }
238
-
239
- // Ensure the newline is written after all chunks
240
- this.inStream.write("\n");
241
- }
242
-
243
- loadURL(url: string) {
244
- this.url = url;
245
- zigRPC.request.loadURL({ webviewId: this.id, url: this.url });
246
- }
247
-
248
- loadHTML(html: string) {
249
- this.html = html;
250
- zigRPC.request.loadHTML({ webviewId: this.id, html: this.html });
251
- }
252
-
253
- // todo (yoav): move this to a class that also has off, append, prepend, etc.
254
- // name should only allow browserView events
255
- // Note: normalize event names to willNavigate instead of ['will-navigate'] to save
256
- // 5 characters per usage and allow minification to be more effective.
257
- on(
258
- name:
259
- | "will-navigate"
260
- | "did-navigate"
261
- | "did-navigate-in-page"
262
- | "did-commit-navigation"
263
- | "dom-ready",
264
- handler
265
- ) {
266
- const specificName = `${name}-${this.id}`;
267
- electrobunEventEmitter.on(specificName, handler);
268
- }
269
-
270
- createTransport = () => {
271
- const that = this;
272
-
273
- return {
274
- send(message: any) {
275
- const sentOverSocket = sendMessageToWebviewViaSocket(that.id, message);
276
-
277
- if (!sentOverSocket) {
278
- try {
279
- const messageString = JSON.stringify(message);
280
- that.sendMessageToWebviewViaExecute(messageString);
281
- } catch (error) {
282
- console.error("bun: failed to serialize message to webview", error);
283
- }
284
- }
285
- },
286
- registerHandler(handler) {
287
- that.rpcHandler = handler;
288
-
289
- async function readFromPipe(
290
- reader: ReadableStreamDefaultReader<Uint8Array>
291
- ) {
292
- let buffer = "";
293
- while (true) {
294
- const { done, value } = await reader.read();
295
- if (done) break;
296
-
297
- buffer += new TextDecoder().decode(value);
298
- let eolIndex;
299
-
300
- while ((eolIndex = buffer.indexOf("\n")) >= 0) {
301
- const line = buffer.slice(0, eolIndex).trim();
302
- buffer = buffer.slice(eolIndex + 1);
303
- if (line) {
304
- try {
305
- const event = JSON.parse(line);
306
- handler(event);
307
- } catch (error) {
308
- console.error("webview: ", line);
309
- }
310
- }
311
- }
312
- }
313
- }
314
-
315
- const reader = that.outStream.getReader();
316
- readFromPipe(reader);
317
- },
318
- };
319
- };
320
-
321
- static getById(id: number) {
322
- return BrowserViewMap[id];
323
- }
324
-
325
- static getAll() {
326
- return Object.values(BrowserViewMap);
327
- }
328
-
329
- static defineRPC<
330
- Schema extends ElectrobunWebviewRPCSChema,
331
- BunSchema extends RPCSchema = Schema["bun"],
332
- WebviewSchema extends RPCSchema = Schema["webview"]
333
- >(config: {
334
- maxRequestTime?: number;
335
- handlers: {
336
- requests?: RPCRequestHandler<BunSchema["requests"]>;
337
- messages?: {
338
- [key in keyof BunSchema["messages"]]: RPCMessageHandlerFn<
339
- BunSchema["messages"],
340
- key
341
- >;
342
- } & {
343
- "*"?: WildcardRPCMessageHandlerFn<BunSchema["messages"]>;
344
- };
345
- };
346
- }) {
347
- // Note: RPC Anywhere requires defining the requests that a schema handles and the messages that a schema sends.
348
- // eg: BunSchema {
349
- // requests: // ... requests bun handles, sent by webview
350
- // messages: // ... messages bun sends, handled by webview
351
- // }
352
- // In some generlized contexts that makes sense,
353
- // In the Electrobun context it can feel a bit counter-intuitive so we swap this around a bit. In Electrobun, the
354
- // webview and bun are known endpoints so we simplify schema definitions by combining them.
355
- // Schema {
356
- // bun: BunSchema {
357
- // requests: // ... requests bun handles, sent by webview,
358
- // messages: // ... messages bun handles, sent by webview
359
- // },
360
- // webview: WebviewSchema {
361
- // requests: // ... requests webview handles, sent by bun,
362
- // messages: // ... messages webview handles, sent by bun
363
- // },
364
- // }
365
- // This way from bun, webview.rpc.request.getTitle() and webview.rpc.send.someMessage maps to the schema
366
- // MySchema.webview.requests.getTitle and MySchema.webview.messages.someMessage
367
- // and in the webview, Electroview.rpc.request.getFileContents maps to
368
- // MySchema.bun.requests.getFileContents.
369
- // electrobun also treats messages as "requests that we don't wait for to complete", and normalizes specifying the
370
- // handlers for them alongside request handlers.
371
-
372
- type mixedWebviewSchema = {
373
- requests: BunSchema["requests"];
374
- messages: WebviewSchema["messages"];
375
- };
376
-
377
- type mixedBunSchema = {
378
- requests: WebviewSchema["requests"] &
379
- BuiltinBunToWebviewSchema["requests"];
380
- messages: BunSchema["messages"];
381
- };
382
-
383
- const rpcOptions = {
384
- maxRequestTime: config.maxRequestTime,
385
- requestHandler: config.handlers.requests,
386
- transport: {
387
- // Note: RPC Anywhere will throw if you try add a message listener if transport.registerHandler is falsey
388
- registerHandler: () => {},
389
- },
390
- } as RPCOptions<mixedWebviewSchema, mixedBunSchema>;
391
-
392
- const rpc = createRPC<mixedWebviewSchema, mixedBunSchema>(rpcOptions);
393
-
394
- const messageHandlers = config.handlers.messages;
395
- if (messageHandlers) {
396
- // note: this can only be done once there is a transport
397
- // @ts-ignore - this is due to all the schema mixing we're doing, fine to ignore
398
- // while types in here are borked, they resolve correctly/bubble up to the defineRPC call site.
399
- rpc.addMessageListener(
400
- "*",
401
- (messageName: keyof BunSchema["messages"], payload) => {
402
- const globalHandler = messageHandlers["*"];
403
- if (globalHandler) {
404
- globalHandler(messageName, payload);
405
- }
406
-
407
- const messageHandler = messageHandlers[messageName];
408
- if (messageHandler) {
409
- messageHandler(payload);
410
- }
411
- }
412
- );
413
- }
414
-
415
- return rpc;
416
- }
417
- }
@@ -1,178 +0,0 @@
1
- import { zigRPC } from "../proc/zig";
2
- import electrobunEventEmitter from "../events/eventEmitter";
3
- import { BrowserView } from "./BrowserView";
4
- import { type RPC } from "rpc-anywhere";
5
-
6
- let nextWindowId = 1;
7
-
8
- // todo (yoav): if we default to builtInSchema, we don't want dev to have to define custom handlers
9
- // for the built-in schema stuff.
10
- type WindowOptionsType<T = undefined> = {
11
- title: string;
12
- frame: {
13
- x: number;
14
- y: number;
15
- width: number;
16
- height: number;
17
- };
18
- url: string | null;
19
- html: string | null;
20
- preload: string | null;
21
- rpc?: T;
22
- syncRpc?: { [method: string]: (params: any) => any };
23
- styleMask?: {};
24
- // TODO: implement all of them
25
- titleBarStyle: "hiddenInset" | "default";
26
- };
27
-
28
- const defaultOptions: WindowOptionsType = {
29
- title: "Electrobun",
30
- frame: {
31
- x: 0,
32
- y: 0,
33
- width: 800,
34
- height: 600,
35
- },
36
- url: "https://electrobun.dev",
37
- html: null,
38
- preload: null,
39
- titleBarStyle: "default",
40
- };
41
-
42
- const BrowserWindowMap = {};
43
-
44
- // todo (yoav): do something where the type extends the default schema
45
- // that way we can provide built-in requests/messages and devs can extend it
46
-
47
- export class BrowserWindow<T> {
48
- id: number = nextWindowId++;
49
- title: string = "Electrobun";
50
- state: "creating" | "created" = "creating";
51
- url: string | null = null;
52
- html: string | null = null;
53
- preload: string | null = null;
54
- frame: {
55
- x: number;
56
- y: number;
57
- width: number;
58
- height: number;
59
- } = {
60
- x: 0,
61
- y: 0,
62
- width: 800,
63
- height: 600,
64
- };
65
- // todo (yoav): make this an array of ids or something
66
- webviewId: number;
67
-
68
- constructor(options: Partial<WindowOptionsType<T>> = defaultOptions) {
69
- this.title = options.title || "New Window";
70
- this.frame = options.frame
71
- ? { ...defaultOptions.frame, ...options.frame }
72
- : { ...defaultOptions.frame };
73
- this.url = options.url || null;
74
- this.html = options.html || null;
75
- this.preload = options.preload || null;
76
-
77
- this.init(options);
78
- }
79
-
80
- init({
81
- rpc,
82
- syncRpc,
83
- styleMask,
84
- titleBarStyle,
85
- }: Partial<WindowOptionsType<T>>) {
86
- zigRPC.request.createWindow({
87
- id: this.id,
88
- title: this.title,
89
- url: this.url,
90
- html: this.html,
91
- frame: {
92
- width: this.frame.width,
93
- height: this.frame.height,
94
- x: this.frame.x,
95
- y: this.frame.y,
96
- },
97
- styleMask: {
98
- Borderless: false,
99
- Titled: true,
100
- Closable: true,
101
- Miniaturizable: true,
102
- Resizable: true,
103
- UnifiedTitleAndToolbar: false,
104
- FullScreen: false,
105
- FullSizeContentView: false,
106
- UtilityWindow: false,
107
- DocModalWindow: false,
108
- NonactivatingPanel: false,
109
- HUDWindow: false,
110
- ...(styleMask || {}),
111
- ...(titleBarStyle === "hiddenInset"
112
- ? {
113
- Titled: true,
114
- FullSizeContentView: true,
115
- }
116
- : {}),
117
- },
118
- titleBarStyle: titleBarStyle || "default",
119
- });
120
-
121
- // todo (yoav): user should be able to override this and pass in their
122
- // own webview instance, or instances for attaching to the window.
123
- const webview = new BrowserView({
124
- // TODO: decide whether we want to keep sending url/html
125
- // here, if we're manually calling loadURL/loadHTML below
126
- // then we can remove it from the api here
127
- url: this.url,
128
- html: this.html,
129
- preload: this.preload,
130
- // frame: this.frame,
131
- frame: {
132
- x: 0,
133
- y: 0,
134
- width: this.frame.width,
135
- height: this.frame.height,
136
- },
137
- rpc,
138
- syncRpc,
139
- });
140
-
141
- this.webviewId = webview.id;
142
-
143
- zigRPC.request.addWebviewToWindow({
144
- windowId: this.id,
145
- webviewId: webview.id,
146
- });
147
-
148
- if (this.url) {
149
- webview.loadURL(this.url);
150
- } else if (this.html) {
151
- webview.loadHTML(this.html);
152
- }
153
-
154
- BrowserWindowMap[this.id] = this;
155
- }
156
-
157
- get webview() {
158
- // todo (yoav): we don't want this to be undefined, so maybe we should just
159
- // link directly to the browserview object instead of a getter
160
- return BrowserView.getById(this.webviewId) as BrowserView<T>;
161
- }
162
-
163
- setTitle(title: string) {
164
- this.title = title;
165
- return zigRPC.request.setTitle({ winId: this.id, title });
166
- }
167
-
168
- close() {
169
- return zigRPC.request.closeWindow({ winId: this.id });
170
- }
171
-
172
- // todo (yoav): move this to a class that also has off, append, prepend, etc.
173
- // name should only allow browserWindow events
174
- on(name, handler) {
175
- const specificName = `${name}-${this.id}`;
176
- electrobunEventEmitter.on(specificName, handler);
177
- }
178
- }
@@ -1,67 +0,0 @@
1
- // TODO: have a context specific menu that excludes role
2
- import { zigRPC, type ApplicationMenuItemConfig } from "../proc/zig";
3
- import electrobunEventEmitter from "../events/eventEmitter";
4
-
5
- export const showContextMenu = (menu: Array<ApplicationMenuItemConfig>) => {
6
- const menuWithDefaults = menuConfigWithDefaults(menu);
7
- zigRPC.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
- };
@@ -1,5 +0,0 @@
1
- import { resolve } from "path";
2
-
3
- const RESOURCES_FOLDER = resolve("../Resources/");
4
-
5
- export const VIEWS_FOLDER = resolve(RESOURCES_FOLDER, "app/views");