electrobun 1.17.3-beta.11 → 1.17.3-beta.12
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 +22 -4
- package/bin/electrobun.cjs +11 -1
- package/dist/api/bun/ElectrobunConfig.ts +42 -1
- package/dist/api/bun/__tests__/ffi-contract.test.ts +105 -0
- package/dist/api/bun/core/BrowserView.ts +5 -3
- package/dist/api/bun/core/BrowserWindow.ts +34 -2
- package/dist/api/bun/core/GpuWindow.ts +30 -2
- package/dist/api/bun/core/Paths.ts +1 -1
- package/dist/api/bun/core/Updater.ts +19 -12
- package/dist/api/bun/index.ts +2 -0
- package/dist/api/bun/proc/native.ts +84 -24
- package/dist/api/bun/webGPU.ts +7 -32
- package/dist/api/bun/webgpuAdapter.ts +0 -5
- package/dist/api/shared/bun-version.ts +1 -1
- package/dist/api/shared/cef-version.ts +2 -2
- package/package.json +2 -2
- package/src/cli/index.ts +299 -31
package/README.md
CHANGED
|
@@ -14,16 +14,27 @@
|
|
|
14
14
|
## What is Electrobun?
|
|
15
15
|
|
|
16
16
|
Electrobun aims to be a complete **solution-in-a-box** for building, updating, and shipping ultra fast, tiny, and cross-platform desktop applications written in Typescript.
|
|
17
|
-
Under the hood it uses <a href="https://bun.sh">bun</a> to execute the main process and to bundle webview typescript, and has native bindings written in <a href="https://ziglang.org/">zig</a>.
|
|
17
|
+
Under the hood it uses <a href="https://bun.sh">bun</a> to execute the main process and to bundle webview typescript, and has native bindings written in Objc, C++, and several core parts written in <a href="https://ziglang.org/">zig</a>.
|
|
18
18
|
|
|
19
|
-
Visit <a href="https://
|
|
19
|
+
Visit <a href="https://docs.electrobunny.ai/electrobun/">https://docs.electrobunny.ai/electrobun/</a> to see api documentation, guides, and more.
|
|
20
|
+
|
|
21
|
+
You use it via npm.
|
|
22
|
+
|
|
23
|
+
Don't miss our:
|
|
24
|
+
- self-extracting bundles that use ZSTD compression for more compact distributables as small as 16MB
|
|
25
|
+
- zig optimized BSDIFF implementation that lets you ship tiny app updates as small as 4KB
|
|
26
|
+
- `bundleCEF` flag to bundle and pin Chromium for those that want that tradeoff of consistency over file size
|
|
27
|
+
- `bundleWGPU` that lets you use Bun Typescript -> WGPU to control a native GPU surface without a webview
|
|
28
|
+
- Our Three.js and Babylon.js adapters that work right in Bun
|
|
29
|
+
- Our `<electrobun-webview>` and `<electrobun-wpgu>` html elements that let you composit proper OOPIFs and native GPU surfaces into your UIs
|
|
30
|
+
- so much more.
|
|
20
31
|
|
|
21
32
|
**Project Goals**
|
|
22
33
|
|
|
23
34
|
- Write typescript for the main process and webviews without having to think about it.
|
|
24
35
|
- Isolation between main and webview processes with fast, typed, easy to implement RPC between them.
|
|
25
|
-
- Small self-extracting app bundles ~
|
|
26
|
-
- Even smaller app updates as small as
|
|
36
|
+
- Small self-extracting app bundles ~14MB (when using system webview, most of this is the bun runtime)
|
|
37
|
+
- Even smaller app updates as small as 4KB (using bsdiff it only downloads tiny patches between versions)
|
|
27
38
|
- Provide everything you need in one tightly integrated workflow to start writing code in 5 minutes and distribute in 10.
|
|
28
39
|
|
|
29
40
|
## Apps Built with Electrobun
|
|
@@ -42,8 +53,10 @@ Visit <a href="https://blackboard.sh/electrobun/">https://blackboard.sh/electrob
|
|
|
42
53
|
- [Codex Agents Composer](https://github.com/MrLesk/codex-agents-composer) - desktop app for managing your Codex agents and their skills
|
|
43
54
|
- [codex-devtools](https://github.com/gulivan/codex-devtools) - desktop inspector for Codex session data; browse conversations, search messages, and analyze agent activity
|
|
44
55
|
- [Deskdown](https://github.com/guarana-studio/deskdown) - transform any web address into a desktop app in under 20 seconds
|
|
56
|
+
- [Dictate](https://github.com/siddhantparadox/dictate) - Windows dictation app with local and BYOK cloud transcription
|
|
45
57
|
- [dev-3.0](https://github.com/h0x91b/dev-3.0) - helps you not get lost while managing multiple AI agents across projects
|
|
46
58
|
- [DOOM](https://github.com/blackboardsh/electrobun-doom) - DOOM implemented in 2 ways: bun -> (c doom -> bundled wgpu) and (full ts port bun -> bundled wgpu)
|
|
59
|
+
- [dotlock](https://github.com/tsconfigdotjson/dotlock) - macOS desktop app for managing `.env` files across your projects
|
|
47
60
|
- [electrobun-pdf](https://github.com/GijungKim/electrobun-pdf) - local-first PDF & DOCX editor for opening, annotating, and exporting documents without leaving your machine
|
|
48
61
|
- [electrobun-rms](https://github.com/khanhthanhdev/electrobun-rms) - fast Electrobun desktop app template with React, Tailwind CSS, and Vite
|
|
49
62
|
- [golb](https://github.com/chrisdadev13/golb) - desktop AI coding workspace built with React, Vite, and Tailwind
|
|
@@ -55,6 +68,7 @@ Visit <a href="https://blackboard.sh/electrobun/">https://blackboard.sh/electrob
|
|
|
55
68
|
- [md-browse](https://github.com/needle-tools/md-browse) - a markdown-first browser that converts web pages to clean markdown
|
|
56
69
|
- [Patchline](https://github.com/adwaithks/Patchline) - lightweight desktop Git client for reading patches and line diffs, then staging and committing changes
|
|
57
70
|
- [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
|
+
- [PiBun](https://github.com/khairold/pibun) - desktop GUI for the Pi coding agent with chat, terminal, git integration, and plugin system
|
|
58
72
|
- [PLEXI](https://github.com/ianjamesburke/PLEXI) - a multi-dimensional terminal multiplexer for the agentic era
|
|
59
73
|
- [Prometheus](https://github.com/opensourcectl/prometheus) - desktop utility toolbox for file cleanup, document manipulation, and image processing
|
|
60
74
|
- [Quiver](https://ataraxy-labs.github.io/quiver/) - desktop app for GitHub PR reviews, merge conflict resolution, and AI commit messages
|
|
@@ -67,6 +81,7 @@ Visit <a href="https://blackboard.sh/electrobun/">https://blackboard.sh/electrob
|
|
|
67
81
|
- [VibesOS](https://github.com/popmechanic/VibesOS) - A GUI for Claude Code that makes it easy to vibe code simple, un-hackable apps
|
|
68
82
|
- [VoiceVault](https://github.com/PJH720/VoiceVault) - AI-powered voice recorder with transcription, summarization, and RAG search
|
|
69
83
|
- [warren](https://github.com/Loa212/warren) - open-source, peer-to-peer terminal mesh for accessing your machines from any device without SSH keys or config files
|
|
84
|
+
- [whatsapp-reminder](https://github.com/FatahChan/whatsapp-reminder) - managed scheduled WhatsApp messages
|
|
70
85
|
|
|
71
86
|
### Video Demos
|
|
72
87
|
|
|
@@ -81,8 +96,11 @@ Visit <a href="https://blackboard.sh/electrobun/">https://blackboard.sh/electrob
|
|
|
81
96
|
[](https://www.star-history.com/#blackboardsh/electrobun&type=date&legend=top-left)
|
|
82
97
|
|
|
83
98
|
## Contributing
|
|
99
|
+
Electrobun is one piece of a vision I'm building. I'm optimizing for focus and execution. Issues and PRs can be used to share ideas, but there should be no expectation that I will review, respond to, or merge them.
|
|
100
|
+
|
|
84
101
|
Ways to get involved:
|
|
85
102
|
|
|
103
|
+
- Read the [Contribution guidelines](./CONTRIBUTING.md)
|
|
86
104
|
- Follow us on X for updates <a href="https://twitter.com/BlackboardTech">@BlackboardTech</a> and <a href="https://twitter.com/YoavCodes">@YoavCodes</a> or on bluesky <a href="https://bsky.app/profile/yoav.codes">@yoav.codes</a>
|
|
87
105
|
- Join the conversation on <a href="https://discord.gg/ueKE4tjaCE">Discord</a>
|
|
88
106
|
- Create and participate in Github issues and discussions
|
package/bin/electrobun.cjs
CHANGED
|
@@ -32,6 +32,16 @@ const platform = getPlatform();
|
|
|
32
32
|
const arch = platform === 'win' ? 'x64' : getArch();
|
|
33
33
|
const binExt = platform === 'win' ? '.exe' : '';
|
|
34
34
|
|
|
35
|
+
function getTarCommand() {
|
|
36
|
+
if (platform !== 'win') {
|
|
37
|
+
return 'tar';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Git Bash tar can treat C:\... as a remote path. Force the built-in Windows tar.
|
|
41
|
+
const systemTar = join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe');
|
|
42
|
+
return existsSync(systemTar) ? `"${systemTar}"` : 'tar';
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
// Paths
|
|
36
46
|
const electrobunDir = join(__dirname, '..');
|
|
37
47
|
const cacheDir = join(electrobunDir, '.cache');
|
|
@@ -98,7 +108,7 @@ async function ensureCliBinary() {
|
|
|
98
108
|
await downloadFile(tarballUrl, tarballPath);
|
|
99
109
|
|
|
100
110
|
// Extract using system tar (available on macOS, Linux, and Windows 10+)
|
|
101
|
-
execSync(
|
|
111
|
+
execSync(`${getTarCommand()} -xzf "${tarballPath}"`, { cwd: cacheDir, stdio: 'pipe' });
|
|
102
112
|
|
|
103
113
|
// Clean up tarball
|
|
104
114
|
unlinkSync(tarballPath);
|
|
@@ -56,6 +56,39 @@ export interface ElectrobunConfig {
|
|
|
56
56
|
* ```
|
|
57
57
|
*/
|
|
58
58
|
urlSchemes?: string[];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* File type associations for the application.
|
|
62
|
+
* Registers document types so the OS can open files with your app
|
|
63
|
+
* (e.g., double-click in Finder, "Open With" menu, drag-to-dock).
|
|
64
|
+
*
|
|
65
|
+
* Platform support:
|
|
66
|
+
* - macOS: Fully supported. Generates CFBundleDocumentTypes in Info.plist.
|
|
67
|
+
* - Windows/Linux: Not yet supported.
|
|
68
|
+
*
|
|
69
|
+
* Files arrive as file:// URLs via the existing "open-url" event:
|
|
70
|
+
* ```typescript
|
|
71
|
+
* Electrobun.events.on("open-url", (e) => {
|
|
72
|
+
* if (e.data.url.startsWith("file://")) {
|
|
73
|
+
* console.log("Opened file:", e.data.url);
|
|
74
|
+
* }
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
fileAssociations?: Array<{
|
|
79
|
+
/** File extensions without the leading dot (e.g., ["dotlock", "json"]) */
|
|
80
|
+
ext: string[];
|
|
81
|
+
/** Human-readable name for this file type (e.g., "DotLock Document") */
|
|
82
|
+
name: string;
|
|
83
|
+
/** The app's role for this file type. @default "Viewer" */
|
|
84
|
+
role?: "Editor" | "Viewer" | "Shell" | "None";
|
|
85
|
+
/**
|
|
86
|
+
* Path to an .icns file for this document type (macOS only).
|
|
87
|
+
* The file is automatically copied into the app bundle's Resources folder
|
|
88
|
+
* during the build. Only the filename (without path) is written to Info.plist.
|
|
89
|
+
*/
|
|
90
|
+
icon?: string;
|
|
91
|
+
}>;
|
|
59
92
|
};
|
|
60
93
|
|
|
61
94
|
/**
|
|
@@ -278,7 +311,15 @@ export interface ElectrobunConfig {
|
|
|
278
311
|
entitlements?: Record<string, boolean | string | string[]>;
|
|
279
312
|
|
|
280
313
|
/**
|
|
281
|
-
* Path to .iconset folder
|
|
314
|
+
* Path to .iconset folder or .icon file (from Icon Composer)
|
|
315
|
+
* containing app icons.
|
|
316
|
+
*
|
|
317
|
+
* - `.iconset` folders are converted to .icns via iconutil
|
|
318
|
+
* (requires Command Line Tools)
|
|
319
|
+
* - `.icon` files are compiled via actool, producing Assets.car
|
|
320
|
+
* for Liquid Glass on macOS 26+ and a .icns fallback for older
|
|
321
|
+
* macOS versions (requires Xcode)
|
|
322
|
+
*
|
|
282
323
|
* @default "icon.iconset"
|
|
283
324
|
*/
|
|
284
325
|
icons?: string;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Contract tests for the bun:ffi APIs that electrobun depends on.
|
|
2
|
+
//
|
|
3
|
+
// Bun bumps don't usually break electrobun, but when they do, the breakage
|
|
4
|
+
// almost always lives in this surface — JSCallback marshaling, FFIType
|
|
5
|
+
// encoding, dlopen behavior. These tests are a tripwire: if a new Bun release
|
|
6
|
+
// breaks any of them, we want to know before cutting an electrobun release,
|
|
7
|
+
// not after a user reports a crash.
|
|
8
|
+
//
|
|
9
|
+
// Skipped on Windows for now since the bun-check workflow runs on Linux and
|
|
10
|
+
// the system library paths differ. If we add a Windows runner later, switch
|
|
11
|
+
// the libc path resolution to include msvcrt/ucrtbase.
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from "bun:test";
|
|
14
|
+
import {
|
|
15
|
+
CString,
|
|
16
|
+
FFIType,
|
|
17
|
+
JSCallback,
|
|
18
|
+
dlopen,
|
|
19
|
+
ptr,
|
|
20
|
+
toArrayBuffer,
|
|
21
|
+
type Pointer,
|
|
22
|
+
} from "bun:ffi";
|
|
23
|
+
|
|
24
|
+
const isUnix =
|
|
25
|
+
process.platform === "darwin" || process.platform === "linux";
|
|
26
|
+
|
|
27
|
+
const libcPath =
|
|
28
|
+
process.platform === "darwin" ? "libSystem.B.dylib" : "libc.so.6";
|
|
29
|
+
|
|
30
|
+
(isUnix ? describe : describe.skip)(
|
|
31
|
+
"bun:ffi contract used by electrobun",
|
|
32
|
+
() => {
|
|
33
|
+
it("dlopen + FFIType.cstring + FFIType.u64 (strlen)", () => {
|
|
34
|
+
const lib = dlopen(libcPath, {
|
|
35
|
+
strlen: {
|
|
36
|
+
args: [FFIType.cstring],
|
|
37
|
+
returns: FFIType.u64,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const len = lib.symbols.strlen(
|
|
42
|
+
new TextEncoder().encode("hello\0"),
|
|
43
|
+
);
|
|
44
|
+
expect(Number(len)).toBe(5);
|
|
45
|
+
|
|
46
|
+
lib.close();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("ptr + toArrayBuffer round-trip", () => {
|
|
50
|
+
const src = new Uint8Array([1, 2, 3, 4, 5]);
|
|
51
|
+
const back = new Uint8Array(
|
|
52
|
+
toArrayBuffer(ptr(src), 0, src.byteLength),
|
|
53
|
+
);
|
|
54
|
+
expect(Array.from(back)).toEqual([1, 2, 3, 4, 5]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("CString reads null-terminated bytes", () => {
|
|
58
|
+
const buf = new Uint8Array([72, 105, 33, 0]); // "Hi!\0"
|
|
59
|
+
const s = new CString(ptr(buf));
|
|
60
|
+
expect(s.toString()).toBe("Hi!");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("JSCallback: native invokes JS via function pointer (qsort)", () => {
|
|
64
|
+
const lib = dlopen(libcPath, {
|
|
65
|
+
qsort: {
|
|
66
|
+
args: [
|
|
67
|
+
FFIType.ptr,
|
|
68
|
+
FFIType.u64,
|
|
69
|
+
FFIType.u64,
|
|
70
|
+
FFIType.function,
|
|
71
|
+
],
|
|
72
|
+
returns: FFIType.void,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
let comparisonCount = 0;
|
|
77
|
+
const compare = new JSCallback(
|
|
78
|
+
(aPtr: Pointer, bPtr: Pointer) => {
|
|
79
|
+
comparisonCount++;
|
|
80
|
+
const a = new Int32Array(toArrayBuffer(aPtr, 0, 4))[0]!;
|
|
81
|
+
const b = new Int32Array(toArrayBuffer(bPtr, 0, 4))[0]!;
|
|
82
|
+
return a - b;
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
args: [FFIType.ptr, FFIType.ptr],
|
|
86
|
+
returns: FFIType.i32,
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const arr = new Int32Array([5, 2, 8, 1, 3]);
|
|
91
|
+
lib.symbols.qsort(
|
|
92
|
+
ptr(arr),
|
|
93
|
+
BigInt(arr.length),
|
|
94
|
+
4n,
|
|
95
|
+
compare.ptr,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(Array.from(arr)).toEqual([1, 2, 3, 5, 8]);
|
|
99
|
+
expect(comparisonCount).toBeGreaterThan(0);
|
|
100
|
+
|
|
101
|
+
compare.close();
|
|
102
|
+
lib.close();
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
);
|
|
@@ -74,7 +74,7 @@ const randomId = Math.random().toString(36).substring(7);
|
|
|
74
74
|
|
|
75
75
|
export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
76
76
|
id: number = nextWebviewId++;
|
|
77
|
-
ptr
|
|
77
|
+
ptr: Pointer | null = null;
|
|
78
78
|
hostWebviewId?: number;
|
|
79
79
|
windowId!: number;
|
|
80
80
|
renderer!: "cef" | "native";
|
|
@@ -361,8 +361,10 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
361
361
|
unregisterHandler() {},
|
|
362
362
|
});
|
|
363
363
|
this.rpcHandler = undefined;
|
|
364
|
-
|
|
365
|
-
|
|
364
|
+
|
|
365
|
+
this.rpcHandler = undefined;
|
|
366
|
+
this.ptr = null;
|
|
367
|
+
native!.symbols.webviewRemove(ptr);
|
|
366
368
|
}
|
|
367
369
|
|
|
368
370
|
static getById(id: number) {
|
|
@@ -12,6 +12,11 @@ import { WGPUView } from "./WGPUView";
|
|
|
12
12
|
const buildConfig = await BuildConfig.get();
|
|
13
13
|
|
|
14
14
|
export type WindowOptionsType<T = undefined> = {
|
|
15
|
+
trafficLightOffset?: {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
};
|
|
19
|
+
activate?: boolean;
|
|
15
20
|
title: string;
|
|
16
21
|
frame: {
|
|
17
22
|
x: number;
|
|
@@ -123,6 +128,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
123
128
|
transparent: boolean = false;
|
|
124
129
|
passthrough: boolean = false;
|
|
125
130
|
hidden: boolean = false;
|
|
131
|
+
trafficLightOffset: { x: number; y: number } = { x: 0, y: 0 };
|
|
126
132
|
navigationRules: string | null = null;
|
|
127
133
|
// Sandbox mode disables RPC and only allows event emission (for untrusted content)
|
|
128
134
|
sandbox: boolean = false;
|
|
@@ -153,6 +159,10 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
153
159
|
this.transparent = options.transparent ?? false;
|
|
154
160
|
this.passthrough = options.passthrough ?? false;
|
|
155
161
|
this.hidden = options.hidden ?? false;
|
|
162
|
+
this.trafficLightOffset = {
|
|
163
|
+
x: options.trafficLightOffset?.x ?? 0,
|
|
164
|
+
y: options.trafficLightOffset?.y ?? 0,
|
|
165
|
+
};
|
|
156
166
|
this.navigationRules = options.navigationRules || null;
|
|
157
167
|
this.sandbox = options.sandbox ?? false;
|
|
158
168
|
|
|
@@ -165,6 +175,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
165
175
|
titleBarStyle,
|
|
166
176
|
transparent,
|
|
167
177
|
hidden,
|
|
178
|
+
activate,
|
|
168
179
|
}: Partial<WindowOptionsType<T>>) {
|
|
169
180
|
this.ptr = ffi.request.createWindow({
|
|
170
181
|
id: this.id,
|
|
@@ -208,6 +219,8 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
208
219
|
titleBarStyle: titleBarStyle || "default",
|
|
209
220
|
transparent: transparent ?? false,
|
|
210
221
|
hidden: hidden ?? false,
|
|
222
|
+
activate: activate ?? true,
|
|
223
|
+
trafficLightOffset: this.trafficLightOffset,
|
|
211
224
|
}) as Pointer;
|
|
212
225
|
|
|
213
226
|
BrowserWindowMap[this.id] = this;
|
|
@@ -263,12 +276,27 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
263
276
|
return ffi.request.closeWindow({ winId: this.id });
|
|
264
277
|
}
|
|
265
278
|
|
|
279
|
+
activate() {
|
|
280
|
+
return ffi.request.activateWindow({ winId: this.id });
|
|
281
|
+
}
|
|
282
|
+
|
|
266
283
|
focus() {
|
|
267
|
-
|
|
284
|
+
console.log(
|
|
285
|
+
"[electrobun] BrowserWindow.focus() is deprecated. Use window.activate() instead.",
|
|
286
|
+
);
|
|
287
|
+
return this.activate();
|
|
268
288
|
}
|
|
269
289
|
|
|
270
290
|
show() {
|
|
271
|
-
return ffi.request.
|
|
291
|
+
return ffi.request.showWindow({ winId: this.id, activate: true });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
showInactive() {
|
|
295
|
+
return ffi.request.showWindow({ winId: this.id, activate: false });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
hide() {
|
|
299
|
+
return ffi.request.hideWindow({ winId: this.id });
|
|
272
300
|
}
|
|
273
301
|
|
|
274
302
|
minimize() {
|
|
@@ -325,6 +353,10 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
325
353
|
return ffi.request.setWindowPosition({ winId: this.id, x, y });
|
|
326
354
|
}
|
|
327
355
|
|
|
356
|
+
setWindowButtonPosition(x: number, y: number) {
|
|
357
|
+
return ffi.request.setWindowButtonPosition({ winId: this.id, x, y });
|
|
358
|
+
}
|
|
359
|
+
|
|
328
360
|
setSize(width: number, height: number) {
|
|
329
361
|
this.frame.width = width;
|
|
330
362
|
this.frame.height = height;
|
|
@@ -6,6 +6,11 @@ import { getNextWindowId } from "./windowIds";
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
export type GpuWindowOptionsType = {
|
|
9
|
+
trafficLightOffset?: {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
};
|
|
13
|
+
activate?: boolean;
|
|
9
14
|
title: string;
|
|
10
15
|
frame: {
|
|
11
16
|
x: number;
|
|
@@ -54,6 +59,7 @@ export class GpuWindow {
|
|
|
54
59
|
title: string = "Electrobun";
|
|
55
60
|
state: "creating" | "created" = "creating";
|
|
56
61
|
transparent: boolean = false;
|
|
62
|
+
trafficLightOffset: { x: number; y: number } = { x: 0, y: 0 };
|
|
57
63
|
frame: {
|
|
58
64
|
x: number;
|
|
59
65
|
y: number;
|
|
@@ -73,6 +79,10 @@ export class GpuWindow {
|
|
|
73
79
|
? { ...defaultOptions.frame, ...options.frame }
|
|
74
80
|
: { ...defaultOptions.frame };
|
|
75
81
|
this.transparent = options.transparent ?? false;
|
|
82
|
+
this.trafficLightOffset = {
|
|
83
|
+
x: options.trafficLightOffset?.x ?? 0,
|
|
84
|
+
y: options.trafficLightOffset?.y ?? 0,
|
|
85
|
+
};
|
|
76
86
|
|
|
77
87
|
this.init(options);
|
|
78
88
|
}
|
|
@@ -81,6 +91,7 @@ export class GpuWindow {
|
|
|
81
91
|
styleMask,
|
|
82
92
|
titleBarStyle,
|
|
83
93
|
transparent,
|
|
94
|
+
activate,
|
|
84
95
|
}: Partial<GpuWindowOptionsType>) {
|
|
85
96
|
this.ptr = ffi.request.createWindow({
|
|
86
97
|
id: this.id,
|
|
@@ -123,6 +134,8 @@ export class GpuWindow {
|
|
|
123
134
|
},
|
|
124
135
|
titleBarStyle: titleBarStyle || "default",
|
|
125
136
|
transparent: transparent ?? false,
|
|
137
|
+
activate: activate ?? true,
|
|
138
|
+
trafficLightOffset: this.trafficLightOffset,
|
|
126
139
|
}) as Pointer;
|
|
127
140
|
|
|
128
141
|
GpuWindowMap[this.id] = this;
|
|
@@ -160,12 +173,23 @@ export class GpuWindow {
|
|
|
160
173
|
return ffi.request.closeWindow({ winId: this.id });
|
|
161
174
|
}
|
|
162
175
|
|
|
176
|
+
activate() {
|
|
177
|
+
return ffi.request.activateWindow({ winId: this.id });
|
|
178
|
+
}
|
|
179
|
+
|
|
163
180
|
focus() {
|
|
164
|
-
|
|
181
|
+
console.log(
|
|
182
|
+
"[electrobun] GpuWindow.focus() is deprecated. Use window.activate() instead.",
|
|
183
|
+
);
|
|
184
|
+
return this.activate();
|
|
165
185
|
}
|
|
166
186
|
|
|
167
187
|
show() {
|
|
168
|
-
return ffi.request.
|
|
188
|
+
return ffi.request.showWindow({ winId: this.id, activate: true });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
showInactive() {
|
|
192
|
+
return ffi.request.showWindow({ winId: this.id, activate: false });
|
|
169
193
|
}
|
|
170
194
|
|
|
171
195
|
minimize() {
|
|
@@ -214,6 +238,10 @@ export class GpuWindow {
|
|
|
214
238
|
return ffi.request.setWindowPosition({ winId: this.id, x, y });
|
|
215
239
|
}
|
|
216
240
|
|
|
241
|
+
setWindowButtonPosition(x: number, y: number) {
|
|
242
|
+
return ffi.request.setWindowButtonPosition({ winId: this.id, x, y });
|
|
243
|
+
}
|
|
244
|
+
|
|
217
245
|
setSize(width: number, height: number) {
|
|
218
246
|
this.frame.width = width;
|
|
219
247
|
this.frame.height = height;
|
|
@@ -179,7 +179,7 @@ const Updater = {
|
|
|
179
179
|
// todo: allow switching channels, by default will check the current channel
|
|
180
180
|
checkForUpdate: async () => {
|
|
181
181
|
emitStatus("checking", "Checking for updates...");
|
|
182
|
-
const localInfo = await Updater.
|
|
182
|
+
const localInfo = await Updater.getLocalInfo();
|
|
183
183
|
|
|
184
184
|
if (localInfo.channel === "dev") {
|
|
185
185
|
emitStatus("no-update", "Dev channel - updates disabled", {
|
|
@@ -283,7 +283,7 @@ const Updater = {
|
|
|
283
283
|
await Updater.channelBucketUrl(); // Ensure localInfo is loaded
|
|
284
284
|
const appFileName = localInfo.name;
|
|
285
285
|
|
|
286
|
-
let currentHash = (await Updater.
|
|
286
|
+
let currentHash = (await Updater.getLocalInfo()).hash;
|
|
287
287
|
let latestHash = (await Updater.checkForUpdate()).hash;
|
|
288
288
|
|
|
289
289
|
const extractionFolder = join(appDataFolder, "self-extraction");
|
|
@@ -1074,14 +1074,14 @@ del "%~f0"
|
|
|
1074
1074
|
},
|
|
1075
1075
|
|
|
1076
1076
|
channelBucketUrl: async () => {
|
|
1077
|
-
await Updater.
|
|
1077
|
+
await Updater.getLocalInfo();
|
|
1078
1078
|
// With flat prefix-based naming, channelBucketUrl is just the baseUrl
|
|
1079
1079
|
// Users can also use Updater.localInfo.baseUrl() directly
|
|
1080
1080
|
return localInfo.baseUrl;
|
|
1081
1081
|
},
|
|
1082
1082
|
|
|
1083
1083
|
appDataFolder: async () => {
|
|
1084
|
-
await Updater.
|
|
1084
|
+
await Updater.getLocalInfo();
|
|
1085
1085
|
// Use identifier + channel for the app data folder
|
|
1086
1086
|
// e.g., ~/Library/Application Support/sh.blackboard.myapp/canary/
|
|
1087
1087
|
const appDataFolder = join(
|
|
@@ -1096,20 +1096,20 @@ del "%~f0"
|
|
|
1096
1096
|
// TODO: consider moving this from "Updater.localInfo" to "BuildVars"
|
|
1097
1097
|
localInfo: {
|
|
1098
1098
|
version: async () => {
|
|
1099
|
-
return (await Updater.
|
|
1099
|
+
return (await Updater.getLocalInfo()).version;
|
|
1100
1100
|
},
|
|
1101
1101
|
hash: async () => {
|
|
1102
|
-
return (await Updater.
|
|
1102
|
+
return (await Updater.getLocalInfo()).hash;
|
|
1103
1103
|
},
|
|
1104
1104
|
channel: async () => {
|
|
1105
|
-
return (await Updater.
|
|
1105
|
+
return (await Updater.getLocalInfo()).channel;
|
|
1106
1106
|
},
|
|
1107
1107
|
baseUrl: async () => {
|
|
1108
|
-
return (await Updater.
|
|
1108
|
+
return (await Updater.getLocalInfo()).baseUrl;
|
|
1109
1109
|
},
|
|
1110
1110
|
},
|
|
1111
1111
|
|
|
1112
|
-
|
|
1112
|
+
getLocalInfo: async () => {
|
|
1113
1113
|
if (localInfo) {
|
|
1114
1114
|
return localInfo;
|
|
1115
1115
|
}
|
|
@@ -1122,8 +1122,15 @@ del "%~f0"
|
|
|
1122
1122
|
console.error("Failed to read version.json", error);
|
|
1123
1123
|
localInfo = { identifier: "", channel: "", version: "", hash: "", baseUrl: "", name: "" };
|
|
1124
1124
|
return localInfo;
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1125
|
+
}
|
|
1126
|
+
},
|
|
1127
|
+
getLocallocalInfo: async () => {
|
|
1128
|
+
console.error(
|
|
1129
|
+
"[Electrobun] Updater.getLocallocalInfo() is deprecated. Use Updater.getLocalInfo() instead.",
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
return Updater.getLocalInfo();
|
|
1133
|
+
},
|
|
1134
|
+
};
|
|
1128
1135
|
|
|
1129
1136
|
export { Updater };
|
package/dist/api/bun/index.ts
CHANGED
|
@@ -229,6 +229,7 @@ export {
|
|
|
229
229
|
Screen,
|
|
230
230
|
Session,
|
|
231
231
|
WGPUBridge,
|
|
232
|
+
|
|
232
233
|
BuildConfig,
|
|
233
234
|
};
|
|
234
235
|
|
|
@@ -247,6 +248,7 @@ const Electrobun = {
|
|
|
247
248
|
Screen,
|
|
248
249
|
Session,
|
|
249
250
|
WGPUBridge,
|
|
251
|
+
|
|
250
252
|
BuildConfig,
|
|
251
253
|
events: electobunEventEmmitter,
|
|
252
254
|
PATHS,
|
|
@@ -97,6 +97,8 @@ export const native = (() => {
|
|
|
97
97
|
FFIType.u32, // styleMask
|
|
98
98
|
FFIType.cstring, // titleBarStyle
|
|
99
99
|
FFIType.bool, // transparent
|
|
100
|
+
FFIType.f64, // trafficLightOffsetX
|
|
101
|
+
FFIType.f64, // trafficLightOffsetY
|
|
100
102
|
FFIType.function, // closeHandler
|
|
101
103
|
FFIType.function, // moveHandler
|
|
102
104
|
FFIType.function, // resizeHandler
|
|
@@ -116,9 +118,20 @@ export const native = (() => {
|
|
|
116
118
|
showWindow: {
|
|
117
119
|
args: [
|
|
118
120
|
FFIType.ptr, // window ptr
|
|
121
|
+
FFIType.bool, // activate
|
|
119
122
|
],
|
|
120
123
|
returns: FFIType.void,
|
|
121
124
|
},
|
|
125
|
+
activateWindow: {
|
|
126
|
+
args: [
|
|
127
|
+
FFIType.ptr, // window ptr
|
|
128
|
+
],
|
|
129
|
+
returns: FFIType.void,
|
|
130
|
+
},
|
|
131
|
+
hideWindow: {
|
|
132
|
+
args: [FFIType.ptr],
|
|
133
|
+
returns: FFIType.void,
|
|
134
|
+
},
|
|
122
135
|
closeWindow: {
|
|
123
136
|
args: [
|
|
124
137
|
FFIType.ptr, // window ptr
|
|
@@ -173,14 +186,18 @@ export const native = (() => {
|
|
|
173
186
|
args: [FFIType.ptr],
|
|
174
187
|
returns: FFIType.bool,
|
|
175
188
|
},
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
189
|
+
setWindowPosition: {
|
|
190
|
+
args: [FFIType.ptr, FFIType.f64, FFIType.f64],
|
|
191
|
+
returns: FFIType.void,
|
|
192
|
+
},
|
|
193
|
+
setWindowButtonPosition: {
|
|
194
|
+
args: [FFIType.ptr, FFIType.f64, FFIType.f64],
|
|
195
|
+
returns: FFIType.void,
|
|
196
|
+
},
|
|
197
|
+
setWindowSize: {
|
|
198
|
+
args: [FFIType.ptr, FFIType.f64, FFIType.f64],
|
|
199
|
+
returns: FFIType.void,
|
|
200
|
+
},
|
|
184
201
|
setWindowFrame: {
|
|
185
202
|
args: [FFIType.ptr, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.f64],
|
|
186
203
|
returns: FFIType.void,
|
|
@@ -835,6 +852,11 @@ const _ffiImpl = {
|
|
|
835
852
|
titleBarStyle: string;
|
|
836
853
|
transparent: boolean;
|
|
837
854
|
hidden?: boolean;
|
|
855
|
+
activate?: boolean;
|
|
856
|
+
trafficLightOffset?: {
|
|
857
|
+
x: number;
|
|
858
|
+
y: number;
|
|
859
|
+
};
|
|
838
860
|
}): FFIType.ptr => {
|
|
839
861
|
const {
|
|
840
862
|
id,
|
|
@@ -858,6 +880,8 @@ const _ffiImpl = {
|
|
|
858
880
|
titleBarStyle,
|
|
859
881
|
transparent,
|
|
860
882
|
hidden = false,
|
|
883
|
+
activate = true,
|
|
884
|
+
trafficLightOffset = { x: 0, y: 0 },
|
|
861
885
|
} = params;
|
|
862
886
|
|
|
863
887
|
const styleMask = native_.symbols.getWindowStyle(
|
|
@@ -886,6 +910,8 @@ const _ffiImpl = {
|
|
|
886
910
|
// style
|
|
887
911
|
toCString(titleBarStyle),
|
|
888
912
|
transparent,
|
|
913
|
+
trafficLightOffset.x,
|
|
914
|
+
trafficLightOffset.y,
|
|
889
915
|
// callbacks
|
|
890
916
|
windowCloseCallback,
|
|
891
917
|
windowMoveCallback,
|
|
@@ -901,7 +927,7 @@ const _ffiImpl = {
|
|
|
901
927
|
|
|
902
928
|
native_.symbols.setWindowTitle(windowPtr, toCString(title));
|
|
903
929
|
if (!hidden) {
|
|
904
|
-
native_.symbols.showWindow(windowPtr);
|
|
930
|
+
native_.symbols.showWindow(windowPtr, activate);
|
|
905
931
|
}
|
|
906
932
|
|
|
907
933
|
return windowPtr;
|
|
@@ -930,15 +956,37 @@ const _ffiImpl = {
|
|
|
930
956
|
// Note: Cleanup of BrowserWindowMap happens in the windowCloseCallback
|
|
931
957
|
},
|
|
932
958
|
|
|
933
|
-
|
|
959
|
+
showWindow: (params: { winId: number; activate?: boolean }) => {
|
|
960
|
+
const { winId } = params;
|
|
961
|
+
const windowPtr = getWindowPtr(winId);
|
|
962
|
+
|
|
963
|
+
if (!windowPtr) {
|
|
964
|
+
throw `Can't show window. Window no longer exists`;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
native_.symbols.showWindow(windowPtr, params.activate ?? true);
|
|
968
|
+
},
|
|
969
|
+
|
|
970
|
+
activateWindow: (params: { winId: number }) => {
|
|
934
971
|
const { winId } = params;
|
|
935
972
|
const windowPtr = getWindowPtr(winId);
|
|
936
973
|
|
|
937
974
|
if (!windowPtr) {
|
|
938
|
-
throw `Can't
|
|
975
|
+
throw `Can't activate window. Window no longer exists`;
|
|
939
976
|
}
|
|
940
977
|
|
|
941
|
-
native_.symbols.
|
|
978
|
+
native_.symbols.activateWindow(windowPtr);
|
|
979
|
+
},
|
|
980
|
+
|
|
981
|
+
hideWindow: (params: { winId: number }) => {
|
|
982
|
+
const { winId } = params;
|
|
983
|
+
const windowPtr = getWindowPtr(winId);
|
|
984
|
+
|
|
985
|
+
if (!windowPtr) {
|
|
986
|
+
throw `Can't hide window. Window no longer exists`;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
native_.symbols.hideWindow(windowPtr);
|
|
942
990
|
},
|
|
943
991
|
|
|
944
992
|
minimizeWindow: (params: { winId: number }) => {
|
|
@@ -1079,21 +1127,32 @@ const _ffiImpl = {
|
|
|
1079
1127
|
return native_.symbols.isWindowVisibleOnAllWorkspaces(windowPtr);
|
|
1080
1128
|
},
|
|
1081
1129
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1130
|
+
setWindowPosition: (params: { winId: number; x: number; y: number }) => {
|
|
1131
|
+
const { winId, x, y } = params;
|
|
1132
|
+
const windowPtr = getWindowPtr(winId);
|
|
1085
1133
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1134
|
+
if (!windowPtr) {
|
|
1135
|
+
throw `Can't set window position. Window no longer exists`;
|
|
1136
|
+
}
|
|
1089
1137
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1138
|
+
native_.symbols.setWindowPosition(windowPtr, x, y);
|
|
1139
|
+
},
|
|
1092
1140
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1141
|
+
setWindowButtonPosition: (params: { winId: number; x: number; y: number }) => {
|
|
1142
|
+
const { winId, x, y } = params;
|
|
1143
|
+
const windowPtr = getWindowPtr(winId);
|
|
1144
|
+
|
|
1145
|
+
if (!windowPtr) {
|
|
1146
|
+
throw `Can't set window button position. Window no longer exists`;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
native_.symbols.setWindowButtonPosition(windowPtr, x, y);
|
|
1150
|
+
},
|
|
1151
|
+
|
|
1152
|
+
setWindowSize: (params: {
|
|
1153
|
+
winId: number;
|
|
1154
|
+
width: number;
|
|
1155
|
+
height: number;
|
|
1097
1156
|
}) => {
|
|
1098
1157
|
const { winId, width, height } = params;
|
|
1099
1158
|
const windowPtr = getWindowPtr(winId);
|
|
@@ -1814,6 +1873,7 @@ export const WGPUBridge = {
|
|
|
1814
1873
|
},
|
|
1815
1874
|
};
|
|
1816
1875
|
|
|
1876
|
+
|
|
1817
1877
|
// Worker management. Move to a different file
|
|
1818
1878
|
process.on("uncaughtException", (err) => {
|
|
1819
1879
|
console.error("Uncaught exception in worker:", err);
|
package/dist/api/bun/webGPU.ts
CHANGED
|
@@ -291,43 +291,20 @@ const WGPU_LIB_NAMES: Record<string, string[]> = {
|
|
|
291
291
|
};
|
|
292
292
|
|
|
293
293
|
function findWgpuLibraryPath(): string | null {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
if (envPath && existsSync(envPath)) {
|
|
297
|
-
if (debug) console.log("[WGPU] using ELECTROBUN_WGPU_PATH:", envPath);
|
|
298
|
-
return envPath;
|
|
299
|
-
} else if (envPath && debug) {
|
|
300
|
-
console.warn("[WGPU] ELECTROBUN_WGPU_PATH not found:", envPath);
|
|
301
|
-
}
|
|
294
|
+
const envPath = process.env['ELECTROBUN_WGPU_PATH'];
|
|
295
|
+
if (envPath && existsSync(envPath)) return envPath;
|
|
302
296
|
|
|
303
297
|
const names = WGPU_LIB_NAMES[process.platform] ?? ["libwebgpu_dawn." + suffix];
|
|
304
298
|
for (const name of names) {
|
|
305
299
|
const cwdCandidate = join(process.cwd(), name);
|
|
306
|
-
if (existsSync(cwdCandidate))
|
|
307
|
-
if (debug) console.log("[WGPU] found in cwd:", cwdCandidate);
|
|
308
|
-
return cwdCandidate;
|
|
309
|
-
}
|
|
300
|
+
if (existsSync(cwdCandidate)) return cwdCandidate;
|
|
310
301
|
const execDir = dirname(process.execPath);
|
|
311
302
|
const macCandidate = join(execDir, "..", "MacOS", name);
|
|
312
|
-
if (existsSync(macCandidate))
|
|
313
|
-
if (debug) console.log("[WGPU] found in bundle MacOS:", macCandidate);
|
|
314
|
-
return macCandidate;
|
|
315
|
-
}
|
|
303
|
+
if (existsSync(macCandidate)) return macCandidate;
|
|
316
304
|
const resCandidate = join(execDir, "..", "Resources", name);
|
|
317
|
-
if (existsSync(resCandidate))
|
|
318
|
-
if (debug) console.log("[WGPU] found in bundle Resources:", resCandidate);
|
|
319
|
-
return resCandidate;
|
|
320
|
-
}
|
|
305
|
+
if (existsSync(resCandidate)) return resCandidate;
|
|
321
306
|
const execCandidate = join(execDir, name);
|
|
322
|
-
if (existsSync(execCandidate))
|
|
323
|
-
if (debug) console.log("[WGPU] found next to exec:", execCandidate);
|
|
324
|
-
return execCandidate;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (debug) {
|
|
329
|
-
console.warn("[WGPU] not found. platform:", process.platform, "execPath:", process.execPath, "cwd:", process.cwd());
|
|
330
|
-
console.warn("[WGPU] names:", names);
|
|
307
|
+
if (existsSync(execCandidate)) return execCandidate;
|
|
331
308
|
}
|
|
332
309
|
|
|
333
310
|
return null;
|
|
@@ -352,9 +329,7 @@ export const native = (() => {
|
|
|
352
329
|
symbols: lib.symbols,
|
|
353
330
|
close: lib.close,
|
|
354
331
|
};
|
|
355
|
-
} catch
|
|
356
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
357
|
-
console.warn("[WGPU] dlopen failed:", libPath, message);
|
|
332
|
+
} catch {
|
|
358
333
|
return {
|
|
359
334
|
available: false,
|
|
360
335
|
path: libPath,
|
|
@@ -811,11 +811,6 @@ function makeCommandBufferArray(cmdPtr: number) {
|
|
|
811
811
|
return { buffer, ptr: ptr(buffer) };
|
|
812
812
|
}
|
|
813
813
|
|
|
814
|
-
function makeSurfaceTexture() {
|
|
815
|
-
const buffer = new ArrayBuffer(24);
|
|
816
|
-
return { buffer, view: new DataView(buffer), ptr: ptr(buffer) };
|
|
817
|
-
}
|
|
818
|
-
|
|
819
814
|
function makeSurfaceCapabilities() {
|
|
820
815
|
const buffer = new ArrayBuffer(64);
|
|
821
816
|
const view = new DataView(buffer);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Default CEF version shipped with this Electrobun release.
|
|
2
2
|
// All platforms use the same version. Update this single pair when bumping CEF.
|
|
3
|
-
export const CEF_VERSION = `
|
|
4
|
-
export const CHROMIUM_VERSION = `
|
|
3
|
+
export const CEF_VERSION = `147.0.10+gd58e84d`;
|
|
4
|
+
export const CHROMIUM_VERSION = `147.0.7727.118`;
|
|
5
5
|
export const DEFAULT_CEF_VERSION_STRING = `${CEF_VERSION}+chromium-${CHROMIUM_VERSION}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "electrobun",
|
|
3
|
-
"version": "1.17.3-beta.
|
|
3
|
+
"version": "1.17.3-beta.12",
|
|
4
4
|
"description": "Build ultra fast, tiny, and cross-platform desktop apps with Typescript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Blackboard Technologies Inc.",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"push:stable": "bun run typecheck && bun scripts/push-version.js stable",
|
|
47
47
|
"build:push:artifacts": "bun scripts/build-and-upload-artifacts.js",
|
|
48
48
|
"test": "bun install && bun build:dev && bun build:cli && cd ../tests && bun install && bun build:dev && bun start",
|
|
49
|
-
"test:unit": "bun test src/shared",
|
|
49
|
+
"test:unit": "bun test src/shared src/bun",
|
|
50
50
|
"bump-cef": "bun scripts/update-cef-version.ts"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
package/src/cli/index.ts
CHANGED
|
@@ -1462,6 +1462,13 @@ const _commandDefaults = {
|
|
|
1462
1462
|
},
|
|
1463
1463
|
};
|
|
1464
1464
|
|
|
1465
|
+
type FileAssociation = {
|
|
1466
|
+
ext: string[];
|
|
1467
|
+
name: string;
|
|
1468
|
+
role?: "Editor" | "Viewer" | "Shell" | "None";
|
|
1469
|
+
icon?: string;
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1465
1472
|
// Default values merged with user's electrobun.config.ts
|
|
1466
1473
|
// For the user-facing type, see ElectrobunConfig in src/bun/ElectrobunConfig.ts
|
|
1467
1474
|
const defaultConfig = {
|
|
@@ -1471,6 +1478,7 @@ const defaultConfig = {
|
|
|
1471
1478
|
version: "0.1.0",
|
|
1472
1479
|
description: "" as string | undefined,
|
|
1473
1480
|
urlSchemes: undefined as string[] | undefined,
|
|
1481
|
+
fileAssociations: undefined as FileAssociation[] | undefined,
|
|
1474
1482
|
},
|
|
1475
1483
|
build: {
|
|
1476
1484
|
buildFolder: "build",
|
|
@@ -1779,6 +1787,151 @@ ${schemesXml}
|
|
|
1779
1787
|
</array>`;
|
|
1780
1788
|
}
|
|
1781
1789
|
|
|
1790
|
+
// Generates CFBundleDocumentTypes and UTExportedTypeDeclarations for file associations.
|
|
1791
|
+
// Each association gets a UTI derived from the app identifier (e.g., com.example.app.myext).
|
|
1792
|
+
// LSItemContentTypes in CFBundleDocumentTypes references these UTIs so Launch Services
|
|
1793
|
+
// properly associates files with the app on modern macOS.
|
|
1794
|
+
function generateDocumentTypes(
|
|
1795
|
+
fileAssociations: FileAssociation[] | undefined,
|
|
1796
|
+
projectRoot: string,
|
|
1797
|
+
appIdentifier: string,
|
|
1798
|
+
): string {
|
|
1799
|
+
if (!fileAssociations || fileAssociations.length === 0) {
|
|
1800
|
+
return "";
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
const validAssociations = fileAssociations.filter((assoc) => {
|
|
1804
|
+
if (!assoc.ext || assoc.ext.length === 0) {
|
|
1805
|
+
console.log(
|
|
1806
|
+
`WARNING: fileAssociations entry "${assoc.name || "(unnamed)"}" has no extensions — skipping`,
|
|
1807
|
+
);
|
|
1808
|
+
return false;
|
|
1809
|
+
}
|
|
1810
|
+
if (!assoc.name) {
|
|
1811
|
+
console.log(
|
|
1812
|
+
`WARNING: fileAssociations entry with extensions [${assoc.ext.join(", ")}] has no name — skipping`,
|
|
1813
|
+
);
|
|
1814
|
+
return false;
|
|
1815
|
+
}
|
|
1816
|
+
return true;
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
if (validAssociations.length === 0) {
|
|
1820
|
+
return "";
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// Clean extensions and warn about leading dots
|
|
1824
|
+
const cleaned = validAssociations.map((assoc) => ({
|
|
1825
|
+
...assoc,
|
|
1826
|
+
ext: assoc.ext.map((ext) => {
|
|
1827
|
+
const clean = ext.replace(/^\./, "");
|
|
1828
|
+
if (clean !== ext) {
|
|
1829
|
+
console.log(
|
|
1830
|
+
`WARNING: fileAssociations ext "${ext}" has a leading dot — stripping to "${clean}"`,
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
return clean;
|
|
1834
|
+
}),
|
|
1835
|
+
}));
|
|
1836
|
+
|
|
1837
|
+
// Generate CFBundleDocumentTypes with LSItemContentTypes
|
|
1838
|
+
const docTypes = cleaned
|
|
1839
|
+
.map((assoc) => {
|
|
1840
|
+
const role = assoc.role || "Viewer";
|
|
1841
|
+
// Resolve icon: only reference if file exists to avoid dangling plist entries
|
|
1842
|
+
let iconName = "";
|
|
1843
|
+
if (assoc.icon) {
|
|
1844
|
+
const iconSourcePath = join(projectRoot, assoc.icon);
|
|
1845
|
+
if (existsSync(iconSourcePath)) {
|
|
1846
|
+
iconName = basename(assoc.icon).replace(/\.icns$/i, "");
|
|
1847
|
+
} else {
|
|
1848
|
+
console.log(
|
|
1849
|
+
`WARNING: Document type icon not found: ${iconSourcePath} — skipping icon reference`,
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
const iconLine = iconName
|
|
1854
|
+
? ` <key>CFBundleTypeIconFile</key>\n <string>${escapeXml(iconName)}</string>\n`
|
|
1855
|
+
: "";
|
|
1856
|
+
// One UTI per extension, all listed under LSItemContentTypes
|
|
1857
|
+
const utiXml = assoc.ext
|
|
1858
|
+
.map(
|
|
1859
|
+
(ext) =>
|
|
1860
|
+
` <string>${escapeXml(appIdentifier)}.${escapeXml(ext)}</string>`,
|
|
1861
|
+
)
|
|
1862
|
+
.join("\n");
|
|
1863
|
+
const extsXml = assoc.ext
|
|
1864
|
+
.map(
|
|
1865
|
+
(ext) =>
|
|
1866
|
+
` <string>${escapeXml(ext)}</string>`,
|
|
1867
|
+
)
|
|
1868
|
+
.join("\n");
|
|
1869
|
+
|
|
1870
|
+
return ` <dict>
|
|
1871
|
+
<key>CFBundleTypeName</key>
|
|
1872
|
+
<string>${escapeXml(assoc.name)}</string>
|
|
1873
|
+
<key>CFBundleTypeRole</key>
|
|
1874
|
+
<string>${escapeXml(role)}</string>
|
|
1875
|
+
${iconLine} <key>LSItemContentTypes</key>
|
|
1876
|
+
<array>
|
|
1877
|
+
${utiXml}
|
|
1878
|
+
</array>
|
|
1879
|
+
<key>CFBundleTypeExtensions</key>
|
|
1880
|
+
<array>
|
|
1881
|
+
${extsXml}
|
|
1882
|
+
</array>
|
|
1883
|
+
</dict>`;
|
|
1884
|
+
})
|
|
1885
|
+
.join("\n");
|
|
1886
|
+
|
|
1887
|
+
// Generate UTExportedTypeDeclarations — one per extension
|
|
1888
|
+
const utiDecls = cleaned
|
|
1889
|
+
.flatMap((assoc) => {
|
|
1890
|
+
let iconName = "";
|
|
1891
|
+
if (assoc.icon) {
|
|
1892
|
+
const iconSourcePath = join(projectRoot, assoc.icon);
|
|
1893
|
+
if (existsSync(iconSourcePath)) {
|
|
1894
|
+
iconName = basename(assoc.icon).replace(/\.icns$/i, "");
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
const iconLine = iconName
|
|
1898
|
+
? ` <key>UTTypeIconFiles</key>
|
|
1899
|
+
<array>
|
|
1900
|
+
<string>${escapeXml(iconName)}</string>
|
|
1901
|
+
</array>\n`
|
|
1902
|
+
: "";
|
|
1903
|
+
return assoc.ext.map(
|
|
1904
|
+
(ext) => ` <dict>
|
|
1905
|
+
<key>UTTypeIdentifier</key>
|
|
1906
|
+
<string>${escapeXml(appIdentifier)}.${escapeXml(ext)}</string>
|
|
1907
|
+
<key>UTTypeDescription</key>
|
|
1908
|
+
<string>${escapeXml(assoc.name)}</string>
|
|
1909
|
+
<key>UTTypeConformsTo</key>
|
|
1910
|
+
<array>
|
|
1911
|
+
<string>public.data</string>
|
|
1912
|
+
</array>
|
|
1913
|
+
${iconLine} <key>UTTypeTagSpecification</key>
|
|
1914
|
+
<dict>
|
|
1915
|
+
<key>public.filename-extension</key>
|
|
1916
|
+
<array>
|
|
1917
|
+
<string>${escapeXml(ext)}</string>
|
|
1918
|
+
</array>
|
|
1919
|
+
</dict>
|
|
1920
|
+
</dict>`,
|
|
1921
|
+
);
|
|
1922
|
+
})
|
|
1923
|
+
.join("\n");
|
|
1924
|
+
|
|
1925
|
+
return ` <key>CFBundleDocumentTypes</key>
|
|
1926
|
+
<array>
|
|
1927
|
+
${docTypes}
|
|
1928
|
+
</array>
|
|
1929
|
+
<key>UTExportedTypeDeclarations</key>
|
|
1930
|
+
<array>
|
|
1931
|
+
${utiDecls}
|
|
1932
|
+
</array>`;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1782
1935
|
// Execute command handling
|
|
1783
1936
|
(async () => {
|
|
1784
1937
|
if (commandArg === "init") {
|
|
@@ -1913,7 +2066,7 @@ ${schemesXml}
|
|
|
1913
2066
|
console.log(
|
|
1914
2067
|
"Different architecture, different APIs. Do not use Electron patterns.",
|
|
1915
2068
|
);
|
|
1916
|
-
console.log("Docs: https://
|
|
2069
|
+
console.log("Docs: https://docs.electrobunny.ai/electrobun/llms.txt");
|
|
1917
2070
|
console.log(
|
|
1918
2071
|
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
1919
2072
|
);
|
|
@@ -1932,9 +2085,7 @@ ${schemesXml}
|
|
|
1932
2085
|
try {
|
|
1933
2086
|
await runBuild(config, buildEnvironment);
|
|
1934
2087
|
} catch (error) {
|
|
1935
|
-
|
|
1936
|
-
console.error(error.message);
|
|
1937
|
-
}
|
|
2088
|
+
console.error("Build failed:", error);
|
|
1938
2089
|
process.exit(1);
|
|
1939
2090
|
}
|
|
1940
2091
|
} else if (commandArg === "run") {
|
|
@@ -1950,9 +2101,7 @@ ${schemesXml}
|
|
|
1950
2101
|
try {
|
|
1951
2102
|
await runBuild(config, "dev");
|
|
1952
2103
|
} catch (error) {
|
|
1953
|
-
|
|
1954
|
-
console.error(error.message);
|
|
1955
|
-
}
|
|
2104
|
+
console.error("Build failed:", error);
|
|
1956
2105
|
process.exit(1);
|
|
1957
2106
|
}
|
|
1958
2107
|
await runAppWithSignalHandling(config);
|
|
@@ -2059,18 +2208,97 @@ ${schemesXml}
|
|
|
2059
2208
|
const iconDestPath = join(appBundleFolderResourcesPath, "AppIcon.icns");
|
|
2060
2209
|
if (existsSync(iconSourceFolder)) {
|
|
2061
2210
|
if (OS === "macos") {
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
stdio: ["ignore", "
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2211
|
+
if (config.build.mac.icons.endsWith(".icon")) {
|
|
2212
|
+
// .icon format (Icon Composer) — compile with actool
|
|
2213
|
+
// Produces Assets.car (Liquid Glass on macOS 26+) and .icns fallback
|
|
2214
|
+
const actoolCheck = Bun.spawnSync(
|
|
2215
|
+
["xcrun", "--find", "actool"],
|
|
2216
|
+
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
2217
|
+
);
|
|
2218
|
+
if (actoolCheck.exitCode !== 0) {
|
|
2219
|
+
throw new Error(
|
|
2220
|
+
"Building .icon files requires Xcode (actool is not available from Command Line Tools alone). " +
|
|
2221
|
+
"Install Xcode from the App Store, or set mac.icons to an .iconset folder instead.",
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
const iconStem = basename(config.build.mac.icons, ".icon");
|
|
2226
|
+
const partialPlistPath = join(
|
|
2227
|
+
buildFolder,
|
|
2228
|
+
".actool-partial-info.plist",
|
|
2229
|
+
);
|
|
2230
|
+
|
|
2231
|
+
console.log(
|
|
2232
|
+
"Compiling .icon file with actool (requires Xcode)...",
|
|
2233
|
+
);
|
|
2234
|
+
const result = Bun.spawnSync(
|
|
2235
|
+
[
|
|
2236
|
+
"xcrun",
|
|
2237
|
+
"actool",
|
|
2238
|
+
"--compile",
|
|
2239
|
+
appBundleFolderResourcesPath,
|
|
2240
|
+
"--app-icon",
|
|
2241
|
+
iconStem,
|
|
2242
|
+
"--platform",
|
|
2243
|
+
"macosx",
|
|
2244
|
+
"--minimum-deployment-target",
|
|
2245
|
+
"11.0",
|
|
2246
|
+
"--output-partial-info-plist",
|
|
2247
|
+
partialPlistPath,
|
|
2248
|
+
iconSourceFolder,
|
|
2249
|
+
],
|
|
2250
|
+
{
|
|
2251
|
+
cwd: projectRoot,
|
|
2252
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
2253
|
+
env: {
|
|
2254
|
+
...process.env,
|
|
2255
|
+
ELECTROBUN_BUILD_ENV: buildEnvironment,
|
|
2256
|
+
},
|
|
2071
2257
|
},
|
|
2072
|
-
|
|
2073
|
-
|
|
2258
|
+
);
|
|
2259
|
+
|
|
2260
|
+
if (result.exitCode !== 0) {
|
|
2261
|
+
throw new Error(
|
|
2262
|
+
`actool failed to compile ${config.build.mac.icons} (exit code ${result.exitCode})`,
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// actool produces <stem>.icns — rename to AppIcon.icns so
|
|
2267
|
+
// CFBundleIconFile ("AppIcon") resolves correctly
|
|
2268
|
+
const actoolIcns = join(
|
|
2269
|
+
appBundleFolderResourcesPath,
|
|
2270
|
+
`${iconStem}.icns`,
|
|
2271
|
+
);
|
|
2272
|
+
if (existsSync(actoolIcns) && actoolIcns !== iconDestPath) {
|
|
2273
|
+
renameSync(actoolIcns, iconDestPath);
|
|
2274
|
+
}
|
|
2275
|
+
} else {
|
|
2276
|
+
// Use iconutil to convert .iconset folder to .icns
|
|
2277
|
+
const result = Bun.spawnSync(
|
|
2278
|
+
[
|
|
2279
|
+
"iconutil",
|
|
2280
|
+
"-c",
|
|
2281
|
+
"icns",
|
|
2282
|
+
"-o",
|
|
2283
|
+
iconDestPath,
|
|
2284
|
+
iconSourceFolder,
|
|
2285
|
+
],
|
|
2286
|
+
{
|
|
2287
|
+
cwd: appBundleFolderResourcesPath,
|
|
2288
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
2289
|
+
env: {
|
|
2290
|
+
...process.env,
|
|
2291
|
+
ELECTROBUN_BUILD_ENV: buildEnvironment,
|
|
2292
|
+
},
|
|
2293
|
+
},
|
|
2294
|
+
);
|
|
2295
|
+
|
|
2296
|
+
if (result.exitCode !== 0) {
|
|
2297
|
+
throw new Error(
|
|
2298
|
+
`iconutil failed to convert ${config.build.mac.icons} (exit code ${result.exitCode})`,
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2074
2302
|
} else {
|
|
2075
2303
|
console.log(
|
|
2076
2304
|
`WARNING: Cannot build macOS icons on ${OS} - iconutil is only available on macOS`,
|
|
@@ -2137,6 +2365,26 @@ Categories=Utility;Application;
|
|
|
2137
2365
|
cpSync(iconPath, targetIconPath, { dereference: true });
|
|
2138
2366
|
}
|
|
2139
2367
|
}
|
|
2368
|
+
|
|
2369
|
+
// Copy document type icon files to the app bundle Resources folder
|
|
2370
|
+
if (targetOS === "macos" && config.app.fileAssociations) {
|
|
2371
|
+
for (const assoc of config.app.fileAssociations) {
|
|
2372
|
+
if (assoc.icon) {
|
|
2373
|
+
const iconSourcePath = join(projectRoot, assoc.icon);
|
|
2374
|
+
if (existsSync(iconSourcePath)) {
|
|
2375
|
+
const iconFileName = basename(iconSourcePath);
|
|
2376
|
+
const iconDestPath = join(
|
|
2377
|
+
appBundleFolderResourcesPath,
|
|
2378
|
+
iconFileName,
|
|
2379
|
+
);
|
|
2380
|
+
cpSync(iconSourcePath, iconDestPath, {
|
|
2381
|
+
dereference: true,
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
// Missing icon warning is handled by generateDocumentTypes
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2140
2388
|
};
|
|
2141
2389
|
|
|
2142
2390
|
// Run preBuild hook before anything starts
|
|
@@ -2212,8 +2460,20 @@ Categories=Utility;Application;
|
|
|
2212
2460
|
config.app.urlSchemes,
|
|
2213
2461
|
config.app.identifier,
|
|
2214
2462
|
);
|
|
2463
|
+
// Generate document type associations
|
|
2464
|
+
const documentTypes = generateDocumentTypes(
|
|
2465
|
+
config.app.fileAssociations,
|
|
2466
|
+
projectRoot,
|
|
2467
|
+
config.app.identifier,
|
|
2468
|
+
);
|
|
2469
|
+
|
|
2470
|
+
// When using .icon format, CFBundleIconName is needed for Assets.car lookup
|
|
2471
|
+
const iconName = config.build.mac?.icons?.endsWith(".icon")
|
|
2472
|
+
? basename(config.build.mac.icons, ".icon")
|
|
2473
|
+
: null;
|
|
2215
2474
|
|
|
2216
2475
|
InfoPlistContents = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2476
|
+
|
|
2217
2477
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2218
2478
|
<plist version="1.0">
|
|
2219
2479
|
<dict>
|
|
@@ -2228,7 +2488,9 @@ Categories=Utility;Application;
|
|
|
2228
2488
|
<key>CFBundlePackageType</key>
|
|
2229
2489
|
<string>APPL</string>
|
|
2230
2490
|
<key>CFBundleIconFile</key>
|
|
2231
|
-
<string>AppIcon</string>${
|
|
2491
|
+
<string>AppIcon</string>${iconName ? `\n <key>CFBundleIconName</key>\n <string>${iconName}</string>` : ""}${usageDescriptions ? "\n" +
|
|
2492
|
+
usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
2493
|
+
"\n" + documentTypes : ""}
|
|
2232
2494
|
</dict>
|
|
2233
2495
|
</plist>`;
|
|
2234
2496
|
|
|
@@ -2328,10 +2590,12 @@ Categories=Utility;Application;
|
|
|
2328
2590
|
}
|
|
2329
2591
|
|
|
2330
2592
|
// Use rcedit to embed the icon into launcher.exe
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2593
|
+
const { execFileSync } = await import("child_process");
|
|
2594
|
+
const rceditPkgPath = require.resolve("rcedit/package.json");
|
|
2595
|
+
const rceditDir = dirname(rceditPkgPath);
|
|
2596
|
+
const rceditX64 = join(rceditDir, "bin", "rcedit-x64.exe");
|
|
2597
|
+
const rceditExe = existsSync(rceditX64) ? rceditX64 : join(rceditDir, "bin", "rcedit.exe");
|
|
2598
|
+
execFileSync(rceditExe, [bunCliLauncherDestination, "--set-icon", iconPath]);
|
|
2335
2599
|
console.log(`Successfully embedded icon into launcher.exe`);
|
|
2336
2600
|
|
|
2337
2601
|
// Clean up temp ICO file
|
|
@@ -2425,10 +2689,12 @@ Categories=Utility;Application;
|
|
|
2425
2689
|
}
|
|
2426
2690
|
|
|
2427
2691
|
// Use rcedit to embed the icon into bun.exe
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2692
|
+
const { execFileSync } = await import("child_process");
|
|
2693
|
+
const rceditPkgPath = require.resolve("rcedit/package.json");
|
|
2694
|
+
const rceditDir = dirname(rceditPkgPath);
|
|
2695
|
+
const rceditX64 = join(rceditDir, "bin", "rcedit-x64.exe");
|
|
2696
|
+
const rceditExe = existsSync(rceditX64) ? rceditX64 : join(rceditDir, "bin", "rcedit.exe");
|
|
2697
|
+
execFileSync(rceditExe, [bunBinaryDestInBundlePath, "--set-icon", iconPath]);
|
|
2432
2698
|
console.log(`Successfully embedded icon into bun.exe`);
|
|
2433
2699
|
|
|
2434
2700
|
// Clean up temp ICO file
|
|
@@ -4610,10 +4876,12 @@ Categories=Utility;Application;
|
|
|
4610
4876
|
}
|
|
4611
4877
|
|
|
4612
4878
|
// Use rcedit to embed the icon
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4879
|
+
const { execFileSync } = await import("child_process");
|
|
4880
|
+
const rceditPkgPath = require.resolve("rcedit/package.json");
|
|
4881
|
+
const rceditDir = dirname(rceditPkgPath);
|
|
4882
|
+
const rceditX64 = join(rceditDir, "bin", "rcedit-x64.exe");
|
|
4883
|
+
const rceditExe = existsSync(rceditX64) ? rceditX64 : join(rceditDir, "bin", "rcedit.exe");
|
|
4884
|
+
execFileSync(rceditExe, [outputExePath, "--set-icon", iconPath]);
|
|
4617
4885
|
console.log(`Successfully embedded icon into ${setupFileName}`);
|
|
4618
4886
|
|
|
4619
4887
|
// Clean up temp ICO file
|