electrobun 0.0.19-beta.8 → 0.0.19-beta.81

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 +547 -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,79 @@
1
+ import {
2
+ type RPCSchema,
3
+ } from "rpc-anywhere";
4
+
5
+
6
+ // todo (yoav): move this stuff to browser/rpc/webview.ts
7
+ export type InternalWebviewHandlers = RPCSchema<{
8
+ requests: {
9
+ webviewTagCallAsyncJavaScript: {
10
+ params: {
11
+ messageId: string;
12
+ webviewId: number;
13
+ hostWebviewId: number;
14
+ script: string;
15
+ };
16
+ response: void;
17
+ };
18
+ };
19
+ }>;
20
+
21
+ export type WebviewTagHandlers = RPCSchema<{
22
+ requests: {};
23
+ messages: {
24
+ webviewTagResize: {
25
+ id: number;
26
+ frame: {
27
+ x: number;
28
+ y: number;
29
+ width: number;
30
+ height: number;
31
+ };
32
+ masks: string;
33
+ };
34
+ webviewTagUpdateSrc: {
35
+ id: number;
36
+ url: string;
37
+ };
38
+ webviewTagUpdateHtml: {
39
+ id: number;
40
+ html: string;
41
+ }
42
+ webviewTagGoBack: {
43
+ id: number;
44
+ };
45
+ webviewTagGoForward: {
46
+ id: number;
47
+ };
48
+ webviewTagReload: {
49
+ id: number;
50
+ };
51
+ webviewTagRemove: {
52
+ id: number;
53
+ };
54
+ startWindowMove: {
55
+ id: number;
56
+ };
57
+ stopWindowMove: {
58
+ id: number;
59
+ };
60
+ moveWindowBy: {
61
+ id: number;
62
+ x: number;
63
+ y: number;
64
+ };
65
+ webviewTagSetTransparent: {
66
+ id: number;
67
+ transparent: boolean;
68
+ };
69
+ webviewTagSetPassthrough: {
70
+ id: number;
71
+ enablePassthrough: boolean;
72
+ };
73
+ webviewTagSetHidden: {
74
+ id: number;
75
+ hidden: boolean;
76
+ };
77
+ };
78
+ }>;
79
+
@@ -0,0 +1,3 @@
1
+ export const isAppRegionDrag = (e: MouseEvent) => {
2
+ return e.target?.classList.contains("electrobun-webkit-app-region-drag");
3
+ };
@@ -0,0 +1,534 @@
1
+ type WebviewEventTypes =
2
+ | "did-navigate"
3
+ | "did-navigate-in-page"
4
+ | "did-commit-navigation"
5
+ | "dom-ready";
6
+
7
+ type Rect = { x: number; y: number; width: number; height: number };
8
+
9
+ const ConfigureWebviewTags = (
10
+ enableWebviewTags: boolean,
11
+ internalRpc: (params: any) => any,
12
+ bunRpc: (params: any) => any
13
+ ) => {
14
+ if (!enableWebviewTags) {
15
+ return;
16
+ }
17
+
18
+ // todo: provide global types for <electrobun-webview> tag elements (like querySelector results etc.)
19
+
20
+ class WebviewTag extends HTMLElement {
21
+ // todo (yoav): come up with a better mechanism to eliminate collisions with bun created
22
+ // webviews
23
+ webviewId?: number; // = nextWebviewId++;
24
+
25
+ // rpc
26
+ internalRpc: any;
27
+ bunRpc: any;
28
+
29
+ // querySelectors for elements that you want to appear
30
+ // in front of the webview.
31
+ maskSelectors: Set<string> = new Set();
32
+
33
+ // observers
34
+ resizeObserver?: ResizeObserver;
35
+ // intersectionObserver?: IntersectionObserver;
36
+ // mutationObserver?: MutationObserver;
37
+
38
+ positionCheckLoop?: Timer;
39
+ positionCheckLoopReset?: Timer;
40
+
41
+ lastRect = {
42
+ x: 0,
43
+ y: 0,
44
+ width: 0,
45
+ height: 0,
46
+ };
47
+
48
+ lastMasksJSON: string = "";
49
+ lastMasks: Rect[] = [];
50
+
51
+ transparent: boolean = false;
52
+ passthroughEnabled: boolean = false;
53
+ hidden: boolean = false;
54
+ hiddenMirrorMode: boolean = false;
55
+ wasZeroRect: boolean = false;
56
+ isMirroring: boolean = false;
57
+
58
+ partition: string | null = null;
59
+
60
+ constructor() {
61
+ super();
62
+ this.internalRpc = internalRpc;
63
+ this.bunRpc = bunRpc;
64
+
65
+ // Give it a frame to be added to the dom and render before measuring
66
+ requestAnimationFrame(() => {
67
+ this.initWebview();
68
+ });
69
+ }
70
+
71
+ addMaskSelector(selector: string) {
72
+ this.maskSelectors.add(selector);
73
+ this.syncDimensions();
74
+ }
75
+
76
+ removeMaskSelector(selector: string) {
77
+ this.maskSelectors.delete(selector);
78
+ this.syncDimensions();
79
+ }
80
+
81
+ async initWebview() {
82
+ const rect = this.getBoundingClientRect();
83
+ this.lastRect = rect;
84
+
85
+ const url = this.src || this.getAttribute("src");
86
+ const html = this.html || this.getAttribute("html");
87
+
88
+ const webviewId = await this.internalRpc.request.webviewTagInit({
89
+ hostWebviewId: window.__electrobunWebviewId,
90
+ windowId: window.__electrobunWindowId,
91
+ renderer: this.renderer,
92
+ url: url,
93
+ html: html,
94
+ preload: this.preload || this.getAttribute("preload") || null,
95
+ partition: this.partition || this.getAttribute("partition") || null,
96
+ frame: {
97
+ width: rect.width,
98
+ height: rect.height,
99
+ x: rect.x,
100
+ y: rect.y,
101
+ },
102
+ // todo: wire up to a param and a method to update them
103
+ navigationRules: null,
104
+ });
105
+
106
+ this.webviewId = webviewId;
107
+ this.id = `electrobun-webview-${webviewId}`;
108
+ // todo: replace bun -> webviewtag communication with a global instead of
109
+ // queryselector based on id
110
+ this.setAttribute("id", this.id);
111
+ }
112
+
113
+ asyncResolvers: {
114
+ [id: string]: { resolve: (arg: any) => void; reject: (arg: any) => void };
115
+ } = {};
116
+
117
+ callAsyncJavaScript({ script }: { script: string }) {
118
+ return new Promise((resolve, reject) => {
119
+ const messageId = "" + Date.now() + Math.random();
120
+ this.asyncResolvers[messageId] = {
121
+ resolve,
122
+ reject,
123
+ };
124
+
125
+ this.internalRpc.request.webviewTagCallAsyncJavaScript({
126
+ messageId,
127
+ webviewId: this.webviewId,
128
+ hostWebviewId: window.__electrobunWebviewId,
129
+ script,
130
+ });
131
+ });
132
+ }
133
+
134
+ setCallAsyncJavaScriptResponse(messageId: string, response: any) {
135
+ const resolvers = this.asyncResolvers[messageId];
136
+ delete this.asyncResolvers[messageId];
137
+ try {
138
+ response = JSON.parse(response);
139
+
140
+ if (response.result) {
141
+ resolvers.resolve(response.result);
142
+ } else {
143
+ resolvers.reject(response.error);
144
+ }
145
+ } catch (e: any) {
146
+ resolvers.reject(e.message);
147
+ }
148
+ }
149
+
150
+ async canGoBack() {
151
+ return this.internalRpc.request.webviewTagCanGoBack({ id: this.webviewId });
152
+ }
153
+
154
+ async canGoForward() {
155
+ return this.internalRpc.request.webviewTagCanGoForward({
156
+ id: this.webviewId,
157
+ });
158
+ }
159
+
160
+ // propertie setters/getters. keeps them in sync with dom attributes
161
+ updateAttr(name: string, value: string | null) {
162
+ if (value) {
163
+ this.setAttribute(name, value);
164
+ } else {
165
+ this.removeAttribute(name);
166
+ }
167
+ }
168
+
169
+ get src() {
170
+ return this.getAttribute("src");
171
+ }
172
+
173
+ set src(value) {
174
+ this.updateAttr("src", value);
175
+ }
176
+
177
+ get html() {
178
+ return this.getAttribute("html");
179
+ }
180
+
181
+ set html(value) {
182
+ this.updateAttr("html", value);
183
+ }
184
+
185
+ get preload() {
186
+ return this.getAttribute("preload");
187
+ }
188
+
189
+ set preload(value) {
190
+ this.updateAttr("preload", value);
191
+ }
192
+
193
+ get renderer() {
194
+ const _renderer = this.getAttribute("renderer") === "cef" ? "cef" : "native";
195
+ return _renderer;
196
+ }
197
+
198
+ set renderer(value: 'cef' | 'native') {
199
+ const _renderer = value === "cef" ? "cef" : "native";
200
+ this.updateAttr("renderer", _renderer);
201
+ }
202
+
203
+ // Note: since <electrobun-webview> is an anchor for a native webview
204
+ // on osx even if we hide it, enable mouse passthrough etc. There
205
+ // are still events like drag events which are natively handled deep in the window manager
206
+ // and will be handled incorrectly. To get around this for now we need to
207
+ // move the webview off screen during delegate mode.
208
+ adjustDimensionsForHiddenMirrorMode(rect: DOMRect) {
209
+ if (this.hiddenMirrorMode) {
210
+ rect.x = 0 - rect.width;
211
+ }
212
+
213
+ return rect;
214
+ }
215
+
216
+ // Note: in the brwoser-context we can ride on the dom element's uilt in event emitter for managing custom events
217
+ on(event: WebviewEventTypes, listener: () => {}) {
218
+ this.addEventListener(event, listener);
219
+ }
220
+
221
+ off(event: WebviewEventTypes, listener: () => {}) {
222
+ this.removeEventListener(event, listener);
223
+ }
224
+
225
+ // This is typically called by injected js from bun
226
+ emit(event: WebviewEventTypes, detail: any) {
227
+ this.dispatchEvent(new CustomEvent(event, { detail }));
228
+ }
229
+
230
+ // Call this via document.querySelector('electrobun-webview').syncDimensions();
231
+ // That way the host can trigger an alignment with the nested webview when they
232
+ // know that they're chaning something in order to eliminate the lag that the
233
+ // catch all loop will catch
234
+ syncDimensions(force: boolean = false) {
235
+ if (!this.webviewId || (!force && this.hidden)) {
236
+ return;
237
+ }
238
+
239
+ const rect = this.getBoundingClientRect();
240
+ const { x, y, width, height } =
241
+ this.adjustDimensionsForHiddenMirrorMode(rect);
242
+ const lastRect = this.lastRect;
243
+
244
+ if (width === 0 && height === 0) {
245
+ if (this.wasZeroRect === false) {
246
+ this.wasZeroRect = true;
247
+ this.toggleHidden(true, true);
248
+ }
249
+ return;
250
+ }
251
+
252
+ const masks: Rect[] = [];
253
+ this.maskSelectors.forEach((selector) => {
254
+ const els = document.querySelectorAll(selector);
255
+
256
+ for (let i = 0; i < els.length; i++) {
257
+ const el = els[i];
258
+
259
+ if (el) {
260
+ const maskRect = el.getBoundingClientRect();
261
+
262
+ masks.push({
263
+ // reposition the bounding rect to be relative to the webview rect
264
+ // so objc can apply the mask correctly and handle the actual overlap
265
+ x: maskRect.x - x,
266
+ y: maskRect.y - y,
267
+ width: maskRect.width,
268
+ height: maskRect.height,
269
+ });
270
+ }
271
+ }
272
+ });
273
+
274
+ // store jsonStringified last masks value to compare
275
+ const masksJson = masks.length ? JSON.stringify(masks) : "";
276
+
277
+ if (
278
+ force ||
279
+ lastRect.x !== x ||
280
+ lastRect.y !== y ||
281
+ lastRect.width !== width ||
282
+ lastRect.height !== height ||
283
+ this.lastMasksJSON !== masksJson
284
+ ) {
285
+ // let it know we're still accelerating
286
+ this.setPositionCheckLoop(true);
287
+
288
+ this.lastRect = rect;
289
+ this.lastMasks = masks;
290
+ this.lastMasksJSON = masksJson;
291
+
292
+ this.internalRpc.send.webviewTagResize({
293
+ id: this.webviewId,
294
+ frame: {
295
+ width: width,
296
+ height: height,
297
+ x: x,
298
+ y: y,
299
+ },
300
+ masks: masksJson,
301
+ });
302
+ }
303
+
304
+ if (this.wasZeroRect) {
305
+ this.wasZeroRect = false;
306
+ this.toggleHidden(false, true);
307
+ }
308
+ }
309
+
310
+ boundSyncDimensions = () => this.syncDimensions();
311
+ boundForceSyncDimensions = () => this.syncDimensions(true);
312
+
313
+ setPositionCheckLoop(accelerate = false) {
314
+ if (this.positionCheckLoop) {
315
+ clearInterval(this.positionCheckLoop);
316
+ this.positionCheckLoop = undefined;
317
+ }
318
+
319
+ if (this.positionCheckLoopReset) {
320
+ clearTimeout(this.positionCheckLoopReset);
321
+ this.positionCheckLoopReset = undefined;
322
+ }
323
+
324
+ const delay = accelerate ? 0 : 300;
325
+
326
+ if (accelerate) {
327
+ this.positionCheckLoopReset = setTimeout(() => {
328
+ this.setPositionCheckLoop(false);
329
+ }, 2000);
330
+ }
331
+ // Note: Since there's not catch all way to listen for x/y changes
332
+ // we have a 400ms interval to check
333
+ // on m1 max this 400ms interval for one nested webview
334
+ // only uses around 0.1% cpu
335
+
336
+ // Note: We also listen for resize events and changes to
337
+ // certain properties to get reactive repositioning for
338
+ // many cases.
339
+
340
+ // todo: consider having an option to disable this and let user
341
+ // trigger position sync for high performance cases (like
342
+ // a browser with a hundred tabs)
343
+ this.positionCheckLoop = setInterval(() => this.syncDimensions(), delay);
344
+ }
345
+
346
+ connectedCallback() {
347
+ this.setPositionCheckLoop();
348
+
349
+ this.resizeObserver = new ResizeObserver(() => {
350
+ this.syncDimensions();
351
+ });
352
+ // Note: In objc the webview is positioned in the window from the bottom-left corner
353
+ // the html anchor is positioned in the webview from the top-left corner
354
+ // In those cases the getBoundingClientRect() will return the same value, but
355
+ // we still need to send it to objc to calculate from its bottom left position
356
+ // otherwise it'll move around unexpectedly.
357
+ window.addEventListener("resize", this.boundForceSyncDimensions);
358
+ window.addEventListener("scroll", this.boundSyncDimensions);
359
+
360
+ // todo: For chromium webviews (windows native or chromium bundled)
361
+ // should be able to use performanceObservers on layout-shift to
362
+ // call syncDimensions more reactively
363
+ }
364
+
365
+ disconnectedCallback() {
366
+ // removed from the dom
367
+ clearInterval(this.positionCheckLoop);
368
+
369
+ this.resizeObserver?.disconnect();
370
+ // this.intersectionObserver?.disconnect();
371
+ // this.mutationObserver?.disconnect();
372
+ window.removeEventListener("resize", this.boundForceSyncDimensions);
373
+ window.removeEventListener("scroll", this.boundSyncDimensions);
374
+ this.internalRpc.send.webviewTagRemove({ id: this.webviewId });
375
+ }
376
+
377
+ static get observedAttributes() {
378
+ // TODO: support html, preload, and other stuff here
379
+ return ["src", "html", "preload", "class", "style"];
380
+ }
381
+
382
+ attributeChangedCallback(name, oldValue, newValue) {
383
+ if (name === "src" && oldValue !== newValue) {
384
+ this.updateIFrameSrc(newValue);
385
+ } else if (name === "html" && oldValue !== newValue) {
386
+ this.updateIFrameHtml(newValue);
387
+ } else if (name === "preload" && oldValue !== newValue) {
388
+ this.updateIFramePreload(newValue);
389
+ } else {
390
+ this.syncDimensions();
391
+ }
392
+ }
393
+
394
+ updateIFrameSrc(src: string) {
395
+ if (!this.webviewId) {
396
+ return;
397
+ }
398
+ this.internalRpc.send.webviewTagUpdateSrc({
399
+ id: this.webviewId,
400
+ url: src,
401
+ });
402
+ }
403
+
404
+ updateIFrameHtml(html: string) {
405
+ if (!this.webviewId) {
406
+ return;
407
+ }
408
+
409
+ this.internalRpc.send.webviewTagUpdateHtml({
410
+ id: this.webviewId,
411
+ html: html,
412
+ });
413
+ }
414
+
415
+ updateIFramePreload(preload: string) {
416
+ if (!this.webviewId) {
417
+ return;
418
+ }
419
+ this.internalRpc.send.webviewTagUpdatePreload({
420
+ id: this.webviewId,
421
+ preload,
422
+ });
423
+ }
424
+
425
+ goBack() {
426
+ this.internalRpc.send.webviewTagGoBack({ id: this.webviewId });
427
+ }
428
+
429
+ goForward() {
430
+ this.internalRpc.send.webviewTagGoForward({ id: this.webviewId });
431
+ }
432
+
433
+ reload() {
434
+ this.internalRpc.send.webviewTagReload({ id: this.webviewId });
435
+ }
436
+ loadURL(url: string) {
437
+ this.setAttribute("src", url);
438
+ this.internalRpc.send.webviewTagUpdateSrc({
439
+ id: this.webviewId,
440
+ url,
441
+ });
442
+ }
443
+ loadHTML(html: string) {
444
+ this.setAttribute("html", html);
445
+ this.internalRpc.send.webviewTagUpdateHtml({
446
+ id: this.webviewId,
447
+ html,
448
+ })
449
+ }
450
+
451
+ // This sets the native webview hovering over the dom to be transparent
452
+ toggleTransparent(transparent?: boolean, bypassState?: boolean) {
453
+ if (!bypassState) {
454
+ if (typeof transparent === "undefined") {
455
+ this.transparent = !this.transparent;
456
+ } else {
457
+ this.transparent = transparent;
458
+ }
459
+ }
460
+
461
+ this.internalRpc.send.webviewTagSetTransparent({
462
+ id: this.webviewId,
463
+ transparent: this.transparent || Boolean(transparent),
464
+ });
465
+ }
466
+ togglePassthrough(enablePassthrough?: boolean, bypassState?: boolean) {
467
+ if (!bypassState) {
468
+ if (typeof enablePassthrough === "undefined") {
469
+ this.passthroughEnabled = !this.passthroughEnabled;
470
+ } else {
471
+ this.passthroughEnabled = enablePassthrough;
472
+ }
473
+ }
474
+
475
+ this.internalRpc.send.webviewTagSetPassthrough({
476
+ id: this.webviewId,
477
+ enablePassthrough:
478
+ this.passthroughEnabled || Boolean(enablePassthrough),
479
+ });
480
+ }
481
+
482
+ toggleHidden(hidden?: boolean, bypassState?: boolean) {
483
+ if (!bypassState) {
484
+ if (typeof hidden === "undefined") {
485
+ this.hidden = !this.hidden;
486
+ } else {
487
+ this.hidden = hidden;
488
+ }
489
+ }
490
+
491
+ this.internalRpc.send.webviewTagSetHidden({
492
+ id: this.webviewId,
493
+ hidden: this.hidden || Boolean(hidden),
494
+ });
495
+ }
496
+ }
497
+
498
+ customElements.define("electrobun-webview", WebviewTag);
499
+
500
+ insertWebviewTagNormalizationStyles();
501
+ };
502
+
503
+ // Give <electrobun-webview>s some default styles that can
504
+ // be easily overridden in the host document
505
+ const insertWebviewTagNormalizationStyles = () => {
506
+ var style = document.createElement("style");
507
+ style.type = "text/css";
508
+
509
+ var css = `
510
+ electrobun-webview {
511
+ display: block;
512
+ width: 800px;
513
+ height: 300px;
514
+ background: #fff;
515
+ background-repeat: no-repeat!important;
516
+ overflow: hidden;
517
+ }
518
+ `;
519
+
520
+ style.appendChild(document.createTextNode(css));
521
+
522
+ var head = document.getElementsByTagName("head")[0];
523
+ if (!head) {
524
+ return;
525
+ }
526
+
527
+ if (head.firstChild) {
528
+ head.insertBefore(style, head.firstChild);
529
+ } else {
530
+ head.appendChild(style);
531
+ }
532
+ };
533
+
534
+ export { ConfigureWebviewTags };
@@ -0,0 +1,66 @@
1
+ import { ffi, type ApplicationMenuItemConfig } from "../proc/native";
2
+ import electrobunEventEmitter from "../events/eventEmitter";
3
+
4
+ export const setApplicationMenu = (menu: Array<ApplicationMenuItemConfig>) => {
5
+ const menuWithDefaults = menuConfigWithDefaults(menu);
6
+ ffi.request.setApplicationMenu({
7
+ menuConfig: JSON.stringify(menuWithDefaults),
8
+ });
9
+ };
10
+
11
+ export const on = (name: "application-menu-clicked", handler) => {
12
+ const specificName = `${name}`;
13
+ electrobunEventEmitter.on(specificName, handler);
14
+ };
15
+
16
+ const roleLabelMap = {
17
+ quit: "Quit",
18
+ hide: "Hide",
19
+ hideOthers: "Hide Others",
20
+ showAll: "Show All",
21
+ undo: "Undo",
22
+ redo: "Redo",
23
+ cut: "Cut",
24
+ copy: "Copy",
25
+ paste: "Paste",
26
+ pasteAndMatchStyle: "Paste And Match Style",
27
+ delete: "Delete",
28
+ selectAll: "Select All",
29
+ startSpeaking: "Start Speaking",
30
+ stopSpeaking: "Stop Speaking",
31
+ enterFullScreen: "Enter FullScreen",
32
+ exitFullScreen: "Exit FullScreen",
33
+ toggleFullScreen: "Toggle Full Screen",
34
+ minimize: "Minimize",
35
+ zoom: "Zoom",
36
+ bringAllToFront: "Bring All To Front",
37
+ close: "Close",
38
+ cycleThroughWindows: "Cycle Through Windows",
39
+ showHelp: "Show Help",
40
+ };
41
+
42
+ const menuConfigWithDefaults = (
43
+ menu: Array<ApplicationMenuItemConfig>
44
+ ): Array<ApplicationMenuItemConfig> => {
45
+ return menu.map((item) => {
46
+ if (item.type === "divider" || item.type === "separator") {
47
+ return { type: "divider" };
48
+ } else {
49
+ return {
50
+ label: item.label || roleLabelMap[item.role] || "",
51
+ type: item.type || "normal",
52
+ // application menus can either have an action or a role. not both.
53
+ ...(item.role ? { role: item.role } : { action: item.action || "" }),
54
+ // default enabled to true unless explicitly set to false
55
+ enabled: item.enabled === false ? false : true,
56
+ checked: Boolean(item.checked),
57
+ hidden: Boolean(item.hidden),
58
+ tooltip: item.tooltip || undefined,
59
+ accelerator: item.accelerator || undefined,
60
+ ...(item.submenu
61
+ ? { submenu: menuConfigWithDefaults(item.submenu) }
62
+ : {}),
63
+ };
64
+ }
65
+ });
66
+ };