electrobun 0.0.19-beta.8 → 0.0.19-beta.80

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 (39) hide show
  1. package/BUILD.md +90 -0
  2. package/bin/electrobun.cjs +165 -0
  3. package/debug.js +5 -0
  4. package/dist/api/browser/builtinrpcSchema.ts +19 -0
  5. package/dist/api/browser/index.ts +409 -0
  6. package/dist/api/browser/rpc/webview.ts +79 -0
  7. package/dist/api/browser/stylesAndElements.ts +3 -0
  8. package/dist/api/browser/webviewtag.ts +534 -0
  9. package/dist/api/bun/core/ApplicationMenu.ts +66 -0
  10. package/dist/api/bun/core/BrowserView.ts +349 -0
  11. package/dist/api/bun/core/BrowserWindow.ts +191 -0
  12. package/dist/api/bun/core/ContextMenu.ts +67 -0
  13. package/dist/api/bun/core/Paths.ts +5 -0
  14. package/dist/api/bun/core/Socket.ts +181 -0
  15. package/dist/api/bun/core/Tray.ts +107 -0
  16. package/dist/api/bun/core/Updater.ts +552 -0
  17. package/dist/api/bun/core/Utils.ts +48 -0
  18. package/dist/api/bun/events/ApplicationEvents.ts +14 -0
  19. package/dist/api/bun/events/event.ts +29 -0
  20. package/dist/api/bun/events/eventEmitter.ts +45 -0
  21. package/dist/api/bun/events/trayEvents.ts +9 -0
  22. package/dist/api/bun/events/webviewEvents.ts +16 -0
  23. package/dist/api/bun/events/windowEvents.ts +12 -0
  24. package/dist/api/bun/index.ts +45 -0
  25. package/dist/api/bun/proc/linux.md +43 -0
  26. package/dist/api/bun/proc/native.ts +1220 -0
  27. package/dist/api/shared/platform.ts +48 -0
  28. package/dist/main.js +53 -0
  29. package/package.json +15 -7
  30. package/src/cli/index.ts +1034 -210
  31. package/templates/hello-world/README.md +57 -0
  32. package/templates/hello-world/bun.lock +63 -0
  33. package/templates/hello-world/electrobun.config +18 -0
  34. package/templates/hello-world/package.json +16 -0
  35. package/templates/hello-world/src/bun/index.ts +15 -0
  36. package/templates/hello-world/src/mainview/index.css +124 -0
  37. package/templates/hello-world/src/mainview/index.html +47 -0
  38. package/templates/hello-world/src/mainview/index.ts +5 -0
  39. package/bin/electrobun +0 -0
@@ -0,0 +1,552 @@
1
+ import { join, dirname, resolve, basename } from "path";
2
+ import { homedir } from "os";
3
+ import { renameSync, unlinkSync, mkdirSync, rmdirSync, statSync, readdirSync } from "fs";
4
+ import tar from "tar";
5
+ import { ZstdInit } from "@oneidentity/zstd-js/wasm";
6
+ import { OS as currentOS, ARCH as currentArch } from '../../shared/platform';
7
+
8
+ // Cross-platform app data directory
9
+ function getAppDataDir(): string {
10
+ switch (currentOS) {
11
+ case 'macos':
12
+ return join(homedir(), "Library", "Application Support");
13
+ case 'win':
14
+ // Use LOCALAPPDATA to match extractor location
15
+ return process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local");
16
+ case 'linux':
17
+ // Use XDG_DATA_HOME or fallback to ~/.local/share to match extractor
18
+ return process.env.XDG_DATA_HOME || join(homedir(), ".local", "share");
19
+ default:
20
+ // Fallback to home directory with .config
21
+ return join(homedir(), ".config");
22
+ }
23
+ }
24
+
25
+ // todo (yoav): share type with cli
26
+ let localInfo: {
27
+ version: string;
28
+ hash: string;
29
+ bucketUrl: string;
30
+ channel: string;
31
+ name: string;
32
+ identifier: string;
33
+ };
34
+
35
+ let updateInfo: {
36
+ version: string;
37
+ hash: string;
38
+ updateAvailable: boolean;
39
+ updateReady: boolean;
40
+ error: string;
41
+ };
42
+
43
+ const Updater = {
44
+ // workaround for some weird state stuff in this old version of bun
45
+ // todo: revisit after updating to the latest bun
46
+ updateInfo: () => {
47
+ return updateInfo;
48
+ },
49
+ // todo: allow switching channels, by default will check the current channel
50
+ checkForUpdate: async () => {
51
+ const localInfo = await Updater.getLocallocalInfo();
52
+
53
+ if (localInfo.channel === "dev") {
54
+ return {
55
+ version: localInfo.version,
56
+ hash: localInfo.hash,
57
+ updateAvailable: false,
58
+ updateReady: false,
59
+ error: "",
60
+ };
61
+ }
62
+
63
+ const channelBucketUrl = await Updater.channelBucketUrl();
64
+ const cacheBuster = Math.random().toString(36).substring(7);
65
+ const platformFolder = `${localInfo.channel}-${currentOS}-${currentArch}`;
66
+ const updateInfoUrl = join(localInfo.bucketUrl, platformFolder, `update.json?${cacheBuster}`);
67
+
68
+ try {
69
+ const updateInfoResponse = await fetch(updateInfoUrl);
70
+
71
+ if (updateInfoResponse.ok) {
72
+ // todo: this seems brittle
73
+ updateInfo = await updateInfoResponse.json();
74
+
75
+ if (updateInfo.hash !== localInfo.hash) {
76
+ updateInfo.updateAvailable = true;
77
+ }
78
+ } else {
79
+ return {
80
+ version: "",
81
+ hash: "",
82
+ updateAvailable: false,
83
+ updateReady: false,
84
+ error: `Failed to fetch update info from ${updateInfoUrl}`,
85
+ };
86
+ }
87
+ } catch (error) {
88
+ return {
89
+ version: "",
90
+ hash: "",
91
+ updateAvailable: false,
92
+ updateReady: false,
93
+ error: `Failed to fetch update info from ${updateInfoUrl}`,
94
+ };
95
+ }
96
+
97
+ return updateInfo;
98
+ },
99
+
100
+ downloadUpdate: async () => {
101
+ const appDataFolder = await Updater.appDataFolder();
102
+ const channelBucketUrl = await Updater.channelBucketUrl();
103
+ const appFileName = localInfo.name;
104
+
105
+ let currentHash = (await Updater.getLocallocalInfo()).hash;
106
+ let latestHash = (await Updater.checkForUpdate()).hash;
107
+
108
+ const extractionFolder = join(appDataFolder, "self-extraction");
109
+ if (!(await Bun.file(extractionFolder).exists())) {
110
+ mkdirSync(extractionFolder, { recursive: true });
111
+ }
112
+
113
+ let currentTarPath = join(extractionFolder, `${currentHash}.tar`);
114
+ const latestTarPath = join(extractionFolder, `${latestHash}.tar`);
115
+
116
+ const seenHashes = [];
117
+
118
+ // todo (yoav): add a check to the while loop that checks for a hash we've seen before
119
+ // so that update loops that are cyclical can be broken
120
+ if (!(await Bun.file(latestTarPath).exists())) {
121
+ while (currentHash !== latestHash) {
122
+ seenHashes.push(currentHash);
123
+ const currentTar = Bun.file(currentTarPath);
124
+
125
+ if (!(await currentTar.exists())) {
126
+ // tar file of the current version not found
127
+ // so we can't patch it. We need the byte-for-byte tar file
128
+ // so break out and download the full version
129
+ break;
130
+ }
131
+
132
+ // check if there's a patch file for it
133
+ const platformFolder = `${localInfo.channel}-${currentOS}-${currentArch}`;
134
+ const patchResponse = await fetch(
135
+ join(localInfo.bucketUrl, platformFolder, `${currentHash}.patch`)
136
+ );
137
+
138
+ if (!patchResponse.ok) {
139
+ // patch not found
140
+ break;
141
+ }
142
+
143
+ // The patch file's name is the hash of the "from" version
144
+ const patchFilePath = join(
145
+ appDataFolder,
146
+ "self-extraction",
147
+ `${currentHash}.patch`
148
+ );
149
+ await Bun.write(patchFilePath, await patchResponse.arrayBuffer());
150
+ // patch it to a tmp name
151
+ const tmpPatchedTarFilePath = join(
152
+ appDataFolder,
153
+ "self-extraction",
154
+ `from-${currentHash}.tar`
155
+ );
156
+
157
+ // Note: cwd should be Contents/MacOS/ where the binaries are in the amc app bundle
158
+ try {
159
+ Bun.spawnSync([
160
+ "bspatch",
161
+ currentTarPath,
162
+ tmpPatchedTarFilePath,
163
+ patchFilePath,
164
+ ]);
165
+ } catch (error) {
166
+ break;
167
+ }
168
+
169
+ let versionSubpath = "";
170
+ const untarDir = join(appDataFolder, "self-extraction", "tmpuntar");
171
+ mkdirSync(untarDir, { recursive: true });
172
+
173
+ // extract just the version.json from the patched tar file so we can see what hash it is now
174
+ const resourcesDir = 'Resources'; // Always use capitalized Resources
175
+ await tar.x({
176
+ // gzip: false,
177
+ file: tmpPatchedTarFilePath,
178
+ cwd: untarDir,
179
+ filter: (path, stat) => {
180
+ if (path.endsWith(`${resourcesDir}/version.json`)) {
181
+ versionSubpath = path;
182
+ return true;
183
+ } else {
184
+ return false;
185
+ }
186
+ },
187
+ });
188
+
189
+ const currentVersionJson = await Bun.file(
190
+ join(untarDir, versionSubpath)
191
+ ).json();
192
+ const nextHash = currentVersionJson.hash;
193
+
194
+ if (seenHashes.includes(nextHash)) {
195
+ console.log("Warning: cyclical update detected");
196
+ break;
197
+ }
198
+
199
+ seenHashes.push(nextHash);
200
+
201
+ if (!nextHash) {
202
+ break;
203
+ }
204
+ // Sync the patched tar file to the new hash
205
+ const updatedTarPath = join(
206
+ appDataFolder,
207
+ "self-extraction",
208
+ `${nextHash}.tar`
209
+ );
210
+ renameSync(tmpPatchedTarFilePath, updatedTarPath);
211
+
212
+ // delete the old tar file
213
+ unlinkSync(currentTarPath);
214
+ unlinkSync(patchFilePath);
215
+ rmdirSync(untarDir, { recursive: true });
216
+
217
+ currentHash = nextHash;
218
+ currentTarPath = join(
219
+ appDataFolder,
220
+ "self-extraction",
221
+ `${currentHash}.tar`
222
+ );
223
+ // loop through applying patches until we reach the latest version
224
+ // if we get stuck then exit and just download the full latest version
225
+ }
226
+
227
+ // If we weren't able to apply patches to the current version,
228
+ // then just download it and unpack it
229
+ if (currentHash !== latestHash) {
230
+ const cacheBuster = Math.random().toString(36).substring(7);
231
+ const platformFolder = `${localInfo.channel}-${currentOS}-${currentArch}`;
232
+ // Platform-specific tarball naming
233
+ let tarballName: string;
234
+ if (currentOS === 'macos') {
235
+ tarballName = `${appFileName}.app.tar.zst`;
236
+ } else if (currentOS === 'win') {
237
+ tarballName = `${appFileName}.tar.zst`;
238
+ } else {
239
+ tarballName = `${appFileName}.tar.zst`;
240
+ }
241
+
242
+ const urlToLatestTarball = join(
243
+ localInfo.bucketUrl,
244
+ platformFolder,
245
+ tarballName
246
+ );
247
+ const prevVersionCompressedTarballPath = join(
248
+ appDataFolder,
249
+ "self-extraction",
250
+ "latest.tar.zst"
251
+ );
252
+ const response = await fetch(urlToLatestTarball + `?${cacheBuster}`);
253
+
254
+ if (response.ok && response.body) {
255
+ const reader = response.body.getReader();
256
+
257
+ const writer = Bun.file(prevVersionCompressedTarballPath).writer();
258
+
259
+ while (true) {
260
+ const { done, value } = await reader.read();
261
+ if (done) break;
262
+ await writer.write(value);
263
+ }
264
+ await writer.flush();
265
+ writer.end();
266
+ } else {
267
+ console.log("latest version not found at: ", urlToLatestTarball);
268
+ }
269
+
270
+ await ZstdInit().then(async ({ ZstdSimple }) => {
271
+ const data = new Uint8Array(
272
+ await Bun.file(prevVersionCompressedTarballPath).arrayBuffer()
273
+ );
274
+ const uncompressedData = ZstdSimple.decompress(data);
275
+
276
+ await Bun.write(latestTarPath, uncompressedData);
277
+ });
278
+
279
+ unlinkSync(prevVersionCompressedTarballPath);
280
+ try {
281
+ unlinkSync(currentTarPath);
282
+ } catch (error) {
283
+ // Note: ignore the error. it may have already been deleted by the patching process
284
+ // if the patching process only got halfway
285
+ }
286
+ }
287
+ }
288
+
289
+ // Note: Bun.file().exists() caches the result, so we nee d an new instance of Bun.file() here
290
+ // to check again
291
+ if (await Bun.file(latestTarPath).exists()) {
292
+ // download patch for this version, apply it.
293
+ // check for patch from that tar and apply it, until it matches the latest version
294
+ // as a fallback it should just download and unpack the latest version
295
+ updateInfo.updateReady = true;
296
+ } else {
297
+ updateInfo.error = "Failed to download latest version";
298
+ }
299
+ },
300
+
301
+ // todo (yoav): this should emit an event so app can cleanup or block the restart
302
+ // todo (yoav): rename this to quitAndApplyUpdate or something
303
+ applyUpdate: async () => {
304
+ if (updateInfo?.updateReady) {
305
+ const appDataFolder = await Updater.appDataFolder();
306
+ const extractionFolder = join(appDataFolder, "self-extraction");
307
+ if (!(await Bun.file(extractionFolder).exists())) {
308
+ mkdirSync(extractionFolder, { recursive: true });
309
+ }
310
+
311
+ let latestHash = (await Updater.checkForUpdate()).hash;
312
+ const latestTarPath = join(extractionFolder, `${latestHash}.tar`);
313
+
314
+ let appBundleSubpath: string = "";
315
+
316
+ if (await Bun.file(latestTarPath).exists()) {
317
+ await tar.x({
318
+ // gzip: false,
319
+ file: latestTarPath,
320
+ cwd: extractionFolder,
321
+ onentry: (entry) => {
322
+ if (currentOS === 'macos') {
323
+ // find the first .app bundle in the tarball
324
+ // Some apps may have nested .app bundles
325
+ if (!appBundleSubpath && entry.path.endsWith(".app/")) {
326
+ appBundleSubpath = entry.path;
327
+ }
328
+ } else {
329
+ // For Windows/Linux, look for the main executable
330
+ // This assumes the tarball contains the app at the root
331
+ if (!appBundleSubpath) {
332
+ appBundleSubpath = "./";
333
+ }
334
+ }
335
+ },
336
+ });
337
+
338
+ if (!appBundleSubpath) {
339
+ console.error("Failed to find app in tarball");
340
+ return;
341
+ }
342
+
343
+ // Note: resolve here removes the extra trailing / that the tar file adds
344
+ const extractedAppPath = resolve(
345
+ join(extractionFolder, appBundleSubpath)
346
+ );
347
+
348
+ // Platform-specific path handling
349
+ let newAppBundlePath: string;
350
+ if (currentOS === 'linux' || currentOS === 'win') {
351
+ // On Linux/Windows, the actual app is inside a subdirectory
352
+ // Use same sanitization as extractor: remove spaces and dots
353
+ // Note: localInfo.name already includes the channel (e.g., "test1-canary")
354
+ const appBundleName = localInfo.name.replace(/ /g, "").replace(/\./g, "-");
355
+ newAppBundlePath = join(extractionFolder, appBundleName);
356
+
357
+ // Verify the extracted app exists
358
+ if (!statSync(newAppBundlePath, { throwIfNoEntry: false })) {
359
+ console.error(`Extracted app not found at: ${newAppBundlePath}`);
360
+ return;
361
+ }
362
+ } else {
363
+ // On macOS, use the extracted app path directly
364
+ newAppBundlePath = extractedAppPath;
365
+ }
366
+ // Platform-specific app path calculation
367
+ let runningAppBundlePath: string;
368
+ if (currentOS === 'macos') {
369
+ // On macOS, executable is at Contents/MacOS/binary inside .app bundle
370
+ runningAppBundlePath = resolve(
371
+ dirname(process.execPath),
372
+ "..",
373
+ ".."
374
+ );
375
+ } else if (currentOS === 'linux') {
376
+ // On Linux, executable is at app/bin/launcher
377
+ runningAppBundlePath = resolve(
378
+ dirname(process.execPath),
379
+ "..",
380
+ ".."
381
+ );
382
+ } else {
383
+ // On Windows, handle versioned app folders
384
+ // Current executable is at app-<hash>/bin/launcher.exe
385
+ runningAppBundlePath = resolve(
386
+ dirname(process.execPath),
387
+ "..",
388
+ ".."
389
+ );
390
+ }
391
+ // Platform-specific backup handling
392
+ let backupPath: string;
393
+ if (currentOS === 'macos') {
394
+ // On macOS, backup in extraction folder with .app extension
395
+ backupPath = join(extractionFolder, "backup.app");
396
+ } else {
397
+ // On Linux/Windows, create a tar backup of the current app
398
+ backupPath = join(extractionFolder, "backup.tar");
399
+ }
400
+
401
+ try {
402
+ if (currentOS === 'macos') {
403
+ // On macOS, use rename approach
404
+ // Remove existing backup if it exists
405
+ if (statSync(backupPath, { throwIfNoEntry: false })) {
406
+ rmdirSync(backupPath, { recursive: true });
407
+ }
408
+
409
+ // Move current running app to backup
410
+ renameSync(runningAppBundlePath, backupPath);
411
+
412
+ // Move new app to running location
413
+ renameSync(newAppBundlePath, runningAppBundlePath);
414
+ } else if (currentOS === 'linux') {
415
+ // On Linux, create tar backup and replace
416
+ // Remove existing backup.tar if it exists
417
+ if (statSync(backupPath, { throwIfNoEntry: false })) {
418
+ unlinkSync(backupPath);
419
+ }
420
+
421
+ // Create tar backup of current app
422
+ await tar.c(
423
+ {
424
+ file: backupPath,
425
+ cwd: dirname(runningAppBundlePath),
426
+ },
427
+ [basename(runningAppBundlePath)]
428
+ );
429
+
430
+ // Remove current app
431
+ rmdirSync(runningAppBundlePath, { recursive: true });
432
+
433
+ // Move new app to app location
434
+ renameSync(newAppBundlePath, runningAppBundlePath);
435
+ } else {
436
+ // On Windows, use versioned app folders
437
+ const parentDir = dirname(runningAppBundlePath);
438
+ const newVersionDir = join(parentDir, `app-${latestHash}`);
439
+
440
+ // Move new app to versioned directory
441
+ renameSync(newAppBundlePath, newVersionDir);
442
+
443
+ // Create/update the launcher batch file
444
+ const launcherPath = join(parentDir, "run.bat");
445
+ const launcherContent = `@echo off
446
+ :: Electrobun App Launcher
447
+ :: This file launches the current version and cleans up old versions
448
+
449
+ :: Set current version
450
+ set CURRENT_HASH=${latestHash}
451
+ set APP_DIR=%~dp0app-%CURRENT_HASH%
452
+
453
+ :: Clean up old app versions (keep current and one backup)
454
+ for /d %%D in ("%~dp0app-*") do (
455
+ if not "%%~nxD"=="app-%CURRENT_HASH%" (
456
+ echo Removing old version: %%~nxD
457
+ rmdir /s /q "%%D" 2>nul
458
+ )
459
+ )
460
+
461
+ :: Launch the app
462
+ cd /d "%APP_DIR%\\bin"
463
+ start "" launcher.exe
464
+ `;
465
+
466
+ await Bun.write(launcherPath, launcherContent);
467
+
468
+ // Update desktop shortcuts to point to run.bat
469
+ // This is handled by the running app, not the updater
470
+
471
+ runningAppBundlePath = newVersionDir;
472
+ }
473
+ } catch (error) {
474
+ console.error("Failed to replace app with new version", error);
475
+ return;
476
+ }
477
+
478
+ // Cross-platform app launch
479
+ switch (currentOS) {
480
+ case 'macos':
481
+ await Bun.spawn(["open", runningAppBundlePath]);
482
+ break;
483
+ case 'win':
484
+ // On Windows, launch the run.bat file which handles versioning
485
+ const parentDir = dirname(runningAppBundlePath);
486
+ const runBatPath = join(parentDir, "run.bat");
487
+ await Bun.spawn(["cmd", "/c", runBatPath], { detached: true });
488
+ break;
489
+ case 'linux':
490
+ // On Linux, launch the launcher inside the app directory
491
+ const linuxLauncher = join(runningAppBundlePath, "bin", "launcher");
492
+ await Bun.spawn([linuxLauncher]);
493
+ break;
494
+ }
495
+ process.exit(0);
496
+ }
497
+ }
498
+ },
499
+
500
+ channelBucketUrl: async () => {
501
+ await Updater.getLocallocalInfo();
502
+ const platformFolder = `${localInfo.channel}-${currentOS}-${currentArch}`;
503
+ return join(localInfo.bucketUrl, platformFolder);
504
+ },
505
+
506
+ appDataFolder: async () => {
507
+ await Updater.getLocallocalInfo();
508
+ const appDataFolder = join(
509
+ getAppDataDir(),
510
+ localInfo.identifier,
511
+ localInfo.name
512
+ );
513
+
514
+ return appDataFolder;
515
+ },
516
+
517
+ // TODO: consider moving this from "Updater.localInfo" to "BuildVars"
518
+ localInfo: {
519
+ version: async () => {
520
+ return (await Updater.getLocallocalInfo()).version;
521
+ },
522
+ hash: async () => {
523
+ return (await Updater.getLocallocalInfo()).hash;
524
+ },
525
+ channel: async () => {
526
+ return (await Updater.getLocallocalInfo()).channel;
527
+ },
528
+ bucketUrl: async () => {
529
+ return (await Updater.getLocallocalInfo()).bucketUrl;
530
+ },
531
+ },
532
+
533
+ getLocallocalInfo: async () => {
534
+ if (localInfo) {
535
+ return localInfo;
536
+ }
537
+
538
+ try {
539
+ const resourcesDir = 'Resources'; // Always use capitalized Resources
540
+ localInfo = await Bun.file(`../${resourcesDir}/version.json`).json();
541
+ return localInfo;
542
+ } catch (error) {
543
+ // Handle the error
544
+ console.error("Failed to read version.json", error);
545
+
546
+ // Then rethrow so the app crashes
547
+ throw error;
548
+ }
549
+ },
550
+ };
551
+
552
+ export { Updater };
@@ -0,0 +1,48 @@
1
+ import { ffi } from "../proc/native";
2
+
3
+ // TODO: move this to a more appropriate namespace
4
+ export const moveToTrash = (path: string) => {
5
+ return ffi.request.moveToTrash({ path });
6
+ };
7
+
8
+ export const showItemInFolder = (path: string) => {
9
+ return ffi.request.showItemInFolder({ path });
10
+ };
11
+
12
+ export const openFileDialog = async (
13
+ opts: {
14
+ startingFolder?: string;
15
+ allowedFileTypes?: string;
16
+ canChooseFiles?: boolean;
17
+ canChooseDirectory?: boolean;
18
+ allowsMultipleSelection?: boolean;
19
+ } = {}
20
+ ): Promise<string[]> => {
21
+ const optsWithDefault = {
22
+ ...{
23
+ startingFolder: "~/",
24
+ allowedFileTypes: "*",
25
+ canChooseFiles: true,
26
+ canChooseDirectory: true,
27
+ allowsMultipleSelection: true,
28
+ },
29
+ ...opts,
30
+ };
31
+
32
+ // todo: extend the timeout for this one (this version of rpc-anywhere doesn't seem to be able to set custom timeouts per request)
33
+ // we really want it to be infinity since the open file dialog blocks everything anyway.
34
+ // todo: there's the timeout between bun and zig, and the timeout between browser and bun since user likely requests
35
+ // from a browser context
36
+ const result = await ffi.request.openFileDialog({
37
+ startingFolder: optsWithDefault.startingFolder,
38
+ allowedFileTypes: optsWithDefault.allowedFileTypes,
39
+ canChooseFiles: optsWithDefault.canChooseFiles,
40
+ canChooseDirectory: optsWithDefault.canChooseDirectory,
41
+ allowsMultipleSelection: optsWithDefault.allowsMultipleSelection,
42
+ });
43
+
44
+ const filePaths = result.split(",");
45
+
46
+ // todo: it's nested like this due to zig union types. needs a zig refactor and revisit
47
+ return filePaths;
48
+ };
@@ -0,0 +1,14 @@
1
+ import ElectrobunEvent from "./event";
2
+
3
+ export default {
4
+ applicationMenuClicked: (data) =>
5
+ new ElectrobunEvent<{ id: number; action: string }, { allow: boolean }>(
6
+ "application-menu-clicked",
7
+ data
8
+ ),
9
+ contextMenuClicked: (data) =>
10
+ new ElectrobunEvent<{ id: number; action: string }, { allow: boolean }>(
11
+ "context-menu-clicked",
12
+ data
13
+ ),
14
+ };
@@ -0,0 +1,29 @@
1
+ export default class ElectrobunEvent<DataType, ResponseType> {
2
+ // todo (yoav): make most of these readonly except for response
3
+ name: string;
4
+ data: DataType;
5
+ // todo (yoav): define getters and setters for response
6
+ _response: ResponseType | undefined;
7
+ responseWasSet: boolean = false;
8
+
9
+ constructor(name: string, data: DataType) {
10
+ this.name = name;
11
+ this.data = data;
12
+ }
13
+
14
+ // Getter for response
15
+ get response(): ResponseType | undefined {
16
+ return this._response;
17
+ }
18
+
19
+ // Setter for response
20
+ set response(value: ResponseType) {
21
+ this._response = value;
22
+ this.responseWasSet = true; // Update flag when response is set
23
+ }
24
+
25
+ clearResponse() {
26
+ this._response = undefined;
27
+ this.responseWasSet = false;
28
+ }
29
+ }
@@ -0,0 +1,45 @@
1
+ import EventEmitter from "events";
2
+ import windowEvents from "./windowEvents";
3
+ import webviewEvents from "./webviewEvents";
4
+ import trayEvents from "./trayEvents";
5
+ import applicationEvents from "./ApplicationEvents";
6
+ import ElectrobunEvent from "./event";
7
+
8
+ class ElectrobunEventEmitter extends EventEmitter {
9
+ constructor() {
10
+ super();
11
+ }
12
+
13
+ // optionally pass in a specifier to make the event name specific.
14
+ // eg: will-navigate is listened to globally for all webviews, but
15
+ // will-navigate-1 is listened to for a specific webview with id 1
16
+ emitEvent(
17
+ ElectrobunEvent: ElectrobunEvent<any, any>,
18
+ specifier?: number | string
19
+ ) {
20
+ if (specifier) {
21
+ this.emit(`${ElectrobunEvent.name}-${specifier}`, ElectrobunEvent);
22
+ } else {
23
+ this.emit(ElectrobunEvent.name, ElectrobunEvent);
24
+ }
25
+ }
26
+
27
+ events = {
28
+ window: {
29
+ ...windowEvents,
30
+ },
31
+ webview: {
32
+ ...webviewEvents,
33
+ },
34
+ tray: {
35
+ ...trayEvents,
36
+ },
37
+ app: {
38
+ ...applicationEvents,
39
+ },
40
+ };
41
+ }
42
+
43
+ export const electrobunEventEmitter = new ElectrobunEventEmitter();
44
+
45
+ export default electrobunEventEmitter;
@@ -0,0 +1,9 @@
1
+ import ElectrobunEvent from "./event";
2
+
3
+ export default {
4
+ trayClicked: (data) =>
5
+ new ElectrobunEvent<{ id: number; action: string }, { allow: boolean }>(
6
+ "tray-clicked",
7
+ data
8
+ ),
9
+ };