electrobun 1.18.1 → 1.18.4-beta.10

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 (64) hide show
  1. package/README.md +37 -14
  2. package/dash.config.ts +28 -0
  3. package/dist/api/browser/global.d.ts +6 -0
  4. package/dist/api/browser/index.ts +162 -44
  5. package/dist/api/{bun → config}/ElectrobunConfig.ts +109 -40
  6. package/dist/api/config/validate.test.ts +31 -0
  7. package/dist/api/config/validate.ts +19 -0
  8. package/dist/api/preload/.generated/compiled.ts +8 -0
  9. package/dist/api/{bun/preload → preload}/build.ts +6 -6
  10. package/dist/api/{bun/preload → preload}/encryption.ts +11 -5
  11. package/dist/api/{bun/preload → preload}/globals.d.ts +8 -0
  12. package/dist/api/{bun/preload → preload}/index.ts +18 -8
  13. package/dist/api/{bun/preload → preload}/webviewTag.ts +32 -3
  14. package/dist/api/{bun/preload → preload}/wgpuTag.ts +33 -2
  15. package/dist/api/{bun → sdks/bun}/__tests__/ffi-contract.test.ts +6 -9
  16. package/dist/api/{bun → sdks/bun}/core/BrowserView.ts +96 -55
  17. package/dist/api/{bun → sdks/bun}/core/BrowserWindow.ts +20 -30
  18. package/dist/api/{bun → sdks/bun}/core/BuildConfig.ts +32 -5
  19. package/dist/api/{bun → sdks/bun}/core/GpuWindow.ts +20 -7
  20. package/dist/api/sdks/bun/core/Socket.ts +22 -0
  21. package/dist/api/{bun → sdks/bun}/core/Tray.ts +33 -32
  22. package/dist/api/{bun → sdks/bun}/core/Updater.ts +10 -10
  23. package/dist/api/{bun → sdks/bun}/core/Utils.ts +3 -5
  24. package/dist/api/{bun → sdks/bun}/core/WGPUView.ts +43 -13
  25. package/dist/api/{bun → sdks/bun}/index.ts +39 -10
  26. package/dist/api/{bun → sdks/bun}/proc/native.ts +942 -746
  27. package/dist/api/{bun → sdks/bun}/webGPU.ts +1 -1
  28. package/dist/api/{bun → sdks/bun}/webgpuAdapter.ts +40 -33
  29. package/dist/api/shared/build-dependencies.test.ts +48 -0
  30. package/dist/api/shared/build-dependencies.ts +46 -0
  31. package/dist/api/shared/go-version.ts +3 -0
  32. package/dist/api/shared/odin-version.ts +6 -0
  33. package/dist/api/shared/rust-version.ts +3 -0
  34. package/dist/go-sdk/callbacks.go +260 -0
  35. package/dist/go-sdk/electrobun.go +2021 -0
  36. package/dist/main.js +35 -28
  37. package/dist/odin-sdk/electrobun/electrobun.odin +2337 -0
  38. package/dist/preload-full.js +948 -0
  39. package/dist/preload-sandboxed.js +111 -0
  40. package/dist/rust-sdk/electrobun.rs +2396 -0
  41. package/dist/zig-sdk/electrobun.zig +2005 -0
  42. package/package.json +5 -29
  43. package/src/cli/index.ts +868 -654
  44. package/bin/electrobun.cjs +0 -169
  45. package/dist/api/bun/core/Socket.ts +0 -205
  46. package/dist/api/bun/core/windowIds.ts +0 -5
  47. package/dist/api/bun/preload/.generated/compiled.ts +0 -8
  48. package/dist/api/shared/bun-version.ts +0 -3
  49. /package/dist/api/{bun/preload → preload}/dragRegions.ts +0 -0
  50. /package/dist/api/{bun/preload → preload}/events.ts +0 -0
  51. /package/dist/api/{bun/preload → preload}/index-sandboxed.ts +0 -0
  52. /package/dist/api/{bun/preload → preload}/internalRpc.ts +0 -0
  53. /package/dist/api/{bun/preload → preload}/overlaySync.ts +0 -0
  54. /package/dist/api/{bun → sdks/bun}/core/ApplicationMenu.ts +0 -0
  55. /package/dist/api/{bun → sdks/bun}/core/ContextMenu.ts +0 -0
  56. /package/dist/api/{bun → sdks/bun}/core/Paths.ts +0 -0
  57. /package/dist/api/{bun → sdks/bun}/core/menuRoles.ts +0 -0
  58. /package/dist/api/{bun → sdks/bun}/events/ApplicationEvents.ts +0 -0
  59. /package/dist/api/{bun → sdks/bun}/events/event.ts +0 -0
  60. /package/dist/api/{bun → sdks/bun}/events/eventEmitter.ts +0 -0
  61. /package/dist/api/{bun → sdks/bun}/events/trayEvents.ts +0 -0
  62. /package/dist/api/{bun → sdks/bun}/events/webviewEvents.ts +0 -0
  63. /package/dist/api/{bun → sdks/bun}/events/windowEvents.ts +0 -0
  64. /package/dist/api/{bun → sdks/bun}/proc/linux.md +0 -0
@@ -2,14 +2,12 @@ import { ffi, type MenuItemConfig, type Rectangle } from "../proc/native";
2
2
  import electrobunEventEmitter from "../events/eventEmitter";
3
3
  import { VIEWS_FOLDER } from "./Paths";
4
4
  import { join } from "path";
5
- import { type Pointer } from "bun:ffi";
6
5
 
7
6
  type NonDividerMenuItem = Exclude<
8
7
  MenuItemConfig,
9
8
  { type: "divider" | "separator" }
10
9
  >;
11
10
 
12
- let nextTrayId = 1;
13
11
  const TrayMap: { [id: number]: Tray } = {};
14
12
 
15
13
  export type TrayOptions = {
@@ -21,9 +19,8 @@ export type TrayOptions = {
21
19
  };
22
20
 
23
21
  export class Tray {
24
- id: number = nextTrayId++;
25
- ptr: Pointer | null = null;
26
- visible = true;
22
+ id = 0;
23
+ visible = false;
27
24
  title = "";
28
25
  image = "";
29
26
  template = true;
@@ -44,36 +41,35 @@ export class Tray {
44
41
  this.width = width;
45
42
  this.height = height;
46
43
 
47
- this.createNativeTray();
48
-
49
- TrayMap[this.id] = this;
44
+ if (this.createNativeTray()) {
45
+ TrayMap[this.id] = this;
46
+ }
50
47
  }
51
48
 
52
- private createNativeTray() {
49
+ private createNativeTray(): boolean {
53
50
  try {
54
- this.ptr = ffi.request.createTray({
55
- id: this.id,
51
+ const trayId = ffi.request.createTray({
56
52
  title: this.title,
57
53
  image: this.resolveImagePath(this.image),
58
54
  template: this.template,
59
55
  width: this.width,
60
56
  height: this.height,
61
- }) as Pointer;
57
+ }) as number;
58
+
59
+ if (!trayId) {
60
+ throw new Error("Tray creation returned an invalid id");
61
+ }
62
+
63
+ this.id = trayId;
62
64
  this.visible = true;
65
+ return true;
63
66
  } catch (error) {
64
67
  console.warn("Tray creation failed:", error);
65
68
  console.warn(
66
69
  "System tray functionality may not be available on this platform",
67
70
  );
68
- this.ptr = null;
69
71
  this.visible = false;
70
- }
71
-
72
- if (this.ptr && this.menu) {
73
- ffi.request.setTrayMenu({
74
- id: this.id,
75
- menuConfig: JSON.stringify(menuConfigWithDefaults(this.menu)),
76
- });
72
+ return false;
77
73
  }
78
74
  }
79
75
 
@@ -88,13 +84,13 @@ export class Tray {
88
84
 
89
85
  setTitle(title: string) {
90
86
  this.title = title;
91
- if (!this.ptr) return;
87
+ if (!this.id) return;
92
88
  ffi.request.setTrayTitle({ id: this.id, title });
93
89
  }
94
90
 
95
91
  setImage(imgPath: string) {
96
92
  this.image = imgPath;
97
- if (!this.ptr) return;
93
+ if (!this.id) return;
98
94
  ffi.request.setTrayImage({
99
95
  id: this.id,
100
96
  image: this.resolveImagePath(imgPath),
@@ -103,7 +99,7 @@ export class Tray {
103
99
 
104
100
  setMenu(menu: Array<MenuItemConfig>) {
105
101
  this.menu = menu;
106
- if (!this.ptr) return;
102
+ if (!this.id) return;
107
103
  const menuWithDefaults = menuConfigWithDefaults(menu);
108
104
  ffi.request.setTrayMenu({
109
105
  id: this.id,
@@ -122,15 +118,19 @@ export class Tray {
122
118
  }
123
119
 
124
120
  if (!visible) {
125
- if (this.ptr) {
126
- ffi.request.removeTray({ id: this.id });
127
- this.ptr = null;
128
- }
121
+ if (this.id) ffi.request.hideTray({ id: this.id });
129
122
  this.visible = false;
130
123
  return;
131
124
  }
132
125
 
133
- this.createNativeTray();
126
+ if (!this.id) {
127
+ if (this.createNativeTray()) {
128
+ TrayMap[this.id] = this;
129
+ }
130
+ return;
131
+ }
132
+
133
+ this.visible = ffi.request.showTray({ id: this.id }) as boolean;
134
134
  }
135
135
 
136
136
  getBounds(): Rectangle {
@@ -139,12 +139,13 @@ export class Tray {
139
139
 
140
140
  remove() {
141
141
  console.log("Tray.remove() called for id:", this.id);
142
- if (this.ptr) {
143
- ffi.request.removeTray({ id: this.id });
144
- this.ptr = null;
142
+ const trayId = this.id;
143
+ if (trayId) {
144
+ ffi.request.removeTray({ id: trayId });
145
145
  }
146
146
  this.visible = false;
147
- delete TrayMap[this.id];
147
+ delete TrayMap[trayId];
148
+ this.id = 0;
148
149
  console.log("Tray removed from TrayMap");
149
150
  }
150
151
 
@@ -9,8 +9,8 @@ import {
9
9
  readdirSync,
10
10
  } from "fs";
11
11
  import { execSync } from "child_process";
12
- import { OS as currentOS, ARCH as currentArch } from "../../shared/platform";
13
- import { getPlatformPrefix, getTarballFileName } from "../../shared/naming";
12
+ import { OS as currentOS, ARCH as currentArch } from "../../../shared/platform";
13
+ import { getPlatformPrefix, getTarballFileName } from "../../../shared/naming";
14
14
  import { quit } from "./Utils";
15
15
 
16
16
  // Update status types for granular progress tracking
@@ -855,7 +855,7 @@ const Updater = {
855
855
  } else if (currentOS === "win") {
856
856
  // On Windows, the actual app is inside a subdirectory.
857
857
  // version.json's `name` field already contains the formatted app
858
- // file name (e.g. "MyApp-canary" for non-stable, "MyApp" for stable),
858
+ // file name (e.g. "MyApp-canary" for canary, "MyApp" for production),
859
859
  // so don't re-apply getAppFileName or it doubles the channel suffix.
860
860
  newAppBundlePath = join(extractionDir, localInfo.name);
861
861
 
@@ -934,9 +934,9 @@ const Updater = {
934
934
  }
935
935
 
936
936
  // Also ensure other binaries are executable
937
- const bunPath = join(appBundleDir, "bin", "bun");
938
- if (statSync(bunPath, { throwIfNoEntry: false })) {
939
- execSync(`chmod +x "${bunPath}"`);
937
+ const cottontailPath = join(appBundleDir, "bin", "cottontail");
938
+ if (statSync(cottontailPath, { throwIfNoEntry: false })) {
939
+ execSync(`chmod +x "${cottontailPath}"`);
940
940
  }
941
941
  }
942
942
 
@@ -974,13 +974,13 @@ const Updater = {
974
974
  setlocal
975
975
 
976
976
  :: Wait for the app and any CEF helper processes to fully exit.
977
- :: launcher.exe spawns bun.exe which spawns "bun Helper*.exe" processes that
977
+ :: launcher.exe spawns cottontail.exe and "main Helper*.exe" processes that
978
978
  :: keep libcef.dll locked; if we proceed too early, rmdir partially fails.
979
979
  :waitloop
980
980
  tasklist /FI "IMAGENAME eq launcher.exe" 2>NUL | find /I /N "launcher.exe">NUL && goto waitsleep
981
- tasklist /FI "IMAGENAME eq bun.exe" 2>NUL | find /I /N "bun.exe">NUL && goto waitsleep
982
- tasklist /FI "IMAGENAME eq bun Helper.exe" 2>NUL | find /I /N "bun Helper.exe">NUL && goto waitsleep
983
- tasklist 2>NUL | find /I "bun Helper">NUL && goto waitsleep
981
+ tasklist /FI "IMAGENAME eq cottontail.exe" 2>NUL | find /I /N "cottontail.exe">NUL && goto waitsleep
982
+ tasklist /FI "IMAGENAME eq main Helper.exe" 2>NUL | find /I /N "main Helper.exe">NUL && goto waitsleep
983
+ tasklist 2>NUL | find /I "main Helper">NUL && goto waitsleep
984
984
  goto waitdone
985
985
  :waitsleep
986
986
  timeout /t 1 /nobreak >nul
@@ -3,7 +3,7 @@ import { electrobunEventEmitter } from "../events/eventEmitter";
3
3
  import { homedir, tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { readFileSync } from "node:fs";
6
- import { OS } from "../../shared/platform";
6
+ import { OS } from "../../../shared/platform";
7
7
 
8
8
  export const moveToTrash = (path: string) => {
9
9
  return ffi.request.moveToTrash({ path });
@@ -137,9 +137,7 @@ export const quit = () => {
137
137
  }
138
138
 
139
139
  if (native) {
140
- native.symbols.stopEventLoop();
141
- native.symbols.waitForShutdownComplete(5000);
142
- native.symbols.forceExit(0);
140
+ ffi.request.quitGracefully({ code: 0, timeoutMs: 5000 });
143
141
  } else {
144
142
  process.exit(0);
145
143
  }
@@ -150,7 +148,7 @@ const _originalProcessExit = process.exit;
150
148
  process.exit = ((code?: number) => {
151
149
  if (native) {
152
150
  if (isQuitting) {
153
- native.symbols.forceExit(code ?? 0);
151
+ ffi.request.quitGracefully({ code: code ?? 0, timeoutMs: 0 });
154
152
  return;
155
153
  }
156
154
  quit();
@@ -6,8 +6,6 @@ const WGPUViewMap: {
6
6
  [id: number]: WGPUView;
7
7
  } = {};
8
8
 
9
- let nextWGPUViewId = 1;
10
-
11
9
  export type WGPUViewOptions = {
12
10
  frame: {
13
11
  x: number;
@@ -34,8 +32,7 @@ const defaultOptions: Partial<WGPUViewOptions> = {
34
32
  };
35
33
 
36
34
  export class WGPUView {
37
- id: number = nextWGPUViewId++;
38
- ptr!: Pointer;
35
+ id: number = 0;
39
36
  windowId!: number;
40
37
  autoResize: boolean = true;
41
38
  frame: {
@@ -51,6 +48,14 @@ export class WGPUView {
51
48
  };
52
49
  startTransparent: boolean = false;
53
50
  startPassthrough: boolean = false;
51
+ isRemoved: boolean = false;
52
+
53
+ get ptr(): Pointer | null {
54
+ if (this.isRemoved) {
55
+ return null;
56
+ }
57
+ return ffi.request.getWGPUViewPointer({ id: this.id }) as Pointer | null;
58
+ }
54
59
 
55
60
  constructor(options: Partial<WGPUViewOptions> = defaultOptions) {
56
61
  this.frame = {
@@ -64,13 +69,12 @@ export class WGPUView {
64
69
  this.startTransparent = options.startTransparent ?? false;
65
70
  this.startPassthrough = options.startPassthrough ?? false;
66
71
 
72
+ this.id = this.init() as number;
67
73
  WGPUViewMap[this.id] = this;
68
- this.ptr = this.init() as Pointer;
69
74
  }
70
75
 
71
76
  init() {
72
77
  return ffi.request.createWGPUView({
73
- id: this.id,
74
78
  windowId: this.windowId,
75
79
  frame: {
76
80
  width: this.frame.width,
@@ -107,20 +111,18 @@ export class WGPUView {
107
111
  }
108
112
 
109
113
  remove() {
110
- // Check if already removed
111
- if (this.ptr === null) {
114
+ if (this.isRemoved) {
112
115
  return;
113
116
  }
114
-
117
+
118
+ this.isRemoved = true;
119
+ delete WGPUViewMap[this.id];
120
+
115
121
  try {
116
122
  ffi.request.wgpuViewRemove({ id: this.id });
117
123
  } catch (e) {
118
124
  console.error(`Error removing WGPU view ${this.id}:`, e);
119
125
  }
120
-
121
- delete WGPUViewMap[this.id];
122
- // Clear the pointer to prevent any accidental access
123
- this.ptr = null as any;
124
126
  }
125
127
 
126
128
  getNativeHandle() {
@@ -131,6 +133,34 @@ export class WGPUView {
131
133
  return WGPUViewMap[id];
132
134
  }
133
135
 
136
+ static adoptExisting(id: number, options: Partial<WGPUViewOptions> = {}) {
137
+ const existing = WGPUViewMap[id];
138
+ if (existing) {
139
+ return existing;
140
+ }
141
+
142
+ const ptr = ffi.request.getWGPUViewPointer({ id }) as Pointer | null;
143
+ if (!ptr) {
144
+ return undefined;
145
+ }
146
+
147
+ const view = Object.create(WGPUView.prototype) as WGPUView;
148
+ view.id = id;
149
+ view.windowId = options.windowId ?? 0;
150
+ view.autoResize = options.autoResize === false ? false : true;
151
+ view.frame = {
152
+ x: options.frame?.x ?? defaultOptions.frame!.x,
153
+ y: options.frame?.y ?? defaultOptions.frame!.y,
154
+ width: options.frame?.width ?? defaultOptions.frame!.width,
155
+ height: options.frame?.height ?? defaultOptions.frame!.height,
156
+ };
157
+ view.startTransparent = options.startTransparent ?? false;
158
+ view.startPassthrough = options.startPassthrough ?? false;
159
+ view.isRemoved = false;
160
+ WGPUViewMap[id] = view;
161
+ return view;
162
+ }
163
+
134
164
  static getAll() {
135
165
  return Object.values(WGPUViewMap);
136
166
  }
@@ -23,7 +23,7 @@ import {
23
23
  type ElectrobunRPCSchema,
24
24
  createRPC,
25
25
  defineElectrobunRPC,
26
- } from "../shared/rpc.js";
26
+ } from "../../shared/rpc.js";
27
27
  import type ElectrobunEvent from "./events/event";
28
28
  import * as PATHS from "./core/Paths";
29
29
  import * as Socket from "./core/Socket";
@@ -31,7 +31,7 @@ import WGPU from "./webGPU";
31
31
  import webgpu from "./webgpuAdapter";
32
32
  import * as three from "three";
33
33
  import * as babylon from "@babylonjs/core";
34
- import type { ElectrobunConfig } from "./ElectrobunConfig";
34
+ import type { ElectrobunConfig } from "../../config/ElectrobunConfig";
35
35
  import { GlobalShortcut, Screen, Session, WGPUBridge } from "./proc/native";
36
36
  import type {
37
37
  Display,
@@ -48,7 +48,7 @@ import { bridge, hasFFI } from "./proc/native";
48
48
 
49
49
  // Carrot boot state — populated from __bunnyCarrotBootstrap injected by Bunny Ears
50
50
  let _carrotManifest: Record<string, unknown> | null = null;
51
- let _carrotContext: { currentDir?: string; statePath?: string; logsPath?: string; permissions?: string[]; grantedPermissions?: Record<string, unknown>; authToken?: string | null; channel?: string } | null = null;
51
+ let _carrotContext: { currentDir?: string; statePath?: string; logsPath?: string; authToken?: string | null; channel?: string } | null = null;
52
52
 
53
53
  const _bootstrap = (globalThis as any).__bunnyCarrotBootstrap as { manifest?: any; context?: any } | undefined;
54
54
  if (_bootstrap) {
@@ -104,7 +104,21 @@ export const Carrots = {
104
104
  if (!bridge) throw new Error("Carrots.list() is only available when running as a carrot inside Bunny Ears");
105
105
  return bridge.requestHost<Array<{
106
106
  id: string; name: string; description: string; version: string;
107
- mode: string; permissions: string[]; status: string; devMode: boolean;
107
+ mode: string; status: string; devMode: boolean;
108
+ remoteUIs?: Array<{ id: string; name: string; path: string }>;
109
+ slateUIs?: Array<{ id: string; name: string; path: string }>;
110
+ contributions?: {
111
+ fileActivators?: Array<{
112
+ baseName?: string;
113
+ nodeType?: "file" | "dir" | "any";
114
+ slate: {
115
+ type: string;
116
+ name?: string;
117
+ icon?: string;
118
+ config?: Record<string, unknown>;
119
+ };
120
+ }>;
121
+ };
108
122
  }>>("list-carrots");
109
123
  },
110
124
  async start(carrotId: string) {
@@ -134,12 +148,6 @@ export const app = {
134
148
  get manifest() {
135
149
  return _carrotManifest;
136
150
  },
137
- get permissions() {
138
- return _carrotContext?.permissions ?? [];
139
- },
140
- get grantedPermissions() {
141
- return _carrotContext?.grantedPermissions ?? {};
142
- },
143
151
  get currentDir() {
144
152
  return _carrotContext?.currentDir ?? "";
145
153
  },
@@ -167,6 +175,27 @@ export const app = {
167
175
  _carrotContext.authToken = token;
168
176
  }
169
177
  },
178
+ async setDeviceToken(token: string, tokenId?: string): Promise<void> {
179
+ if (!bridge) return;
180
+ await bridge.requestHost("set-device-token", { token, tokenId });
181
+ },
182
+ async getMachineInfo(): Promise<{ machineId: string; hostname: string; platform: string }> {
183
+ if (!bridge) {
184
+ return { machineId: "", hostname: "", platform: "" };
185
+ }
186
+ return bridge.requestHost<{ machineId: string; hostname: string; platform: string }>(
187
+ "get-machine-info",
188
+ );
189
+ },
190
+ async updateCarrots(): Promise<void> {
191
+ if (!bridge) return;
192
+ await bridge.requestHost("update-carrots");
193
+ },
194
+ async getWebBridgePort(): Promise<number | null> {
195
+ if (!bridge) return null;
196
+ const result = await bridge.requestHost<{ port: number | null }>("get-web-bridge-port");
197
+ return typeof result?.port === "number" ? result.port : null;
198
+ },
170
199
  get channel() {
171
200
  return _carrotContext?.channel ?? "";
172
201
  },