electrobun 0.8.0-beta.0 → 0.9.3-beta.0

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.
@@ -74,6 +74,10 @@ export type InternalWebviewHandlers = RPCSchema<{
74
74
  id: number;
75
75
  hidden: boolean;
76
76
  };
77
+ webviewTagSetNavigationRules: {
78
+ id: number;
79
+ rules: string[];
80
+ };
77
81
  };
78
82
  }>;
79
83
 
@@ -559,7 +559,19 @@ const ConfigureWebviewTags = (
559
559
  id: this.webviewId,
560
560
  hidden: this.hidden|| Boolean(hidden),
561
561
  });
562
- }
562
+ }
563
+
564
+ setNavigationRules(rules: string[]) {
565
+ if (!this.webviewId) {
566
+ console.warn('setNavigationRules called on removed webview');
567
+ return;
568
+ }
569
+
570
+ this.internalRpc.send.webviewTagSetNavigationRules({
571
+ id: this.webviewId,
572
+ rules: rules,
573
+ });
574
+ }
563
575
  }
564
576
 
565
577
  customElements.define("electrobun-webview", WebviewTag);
@@ -12,17 +12,35 @@ export interface ElectrobunConfig {
12
12
  * The display name of your application
13
13
  */
14
14
  name: string;
15
-
15
+
16
16
  /**
17
17
  * Unique identifier for your application (e.g., "com.example.myapp")
18
18
  * Used for platform-specific identifiers
19
19
  */
20
20
  identifier: string;
21
-
21
+
22
22
  /**
23
23
  * Application version string (e.g., "1.0.0")
24
24
  */
25
25
  version: string;
26
+
27
+ /**
28
+ * Custom URL schemes to register for deep linking (e.g., ["myapp", "myapp-dev"])
29
+ * This allows your app to be opened via URLs like myapp://some/path
30
+ *
31
+ * Platform support:
32
+ * - macOS: Fully supported. App must be in /Applications folder for registration to work.
33
+ * - Windows: Not yet supported
34
+ * - Linux: Not yet supported
35
+ *
36
+ * To handle incoming URLs, listen for the "open-url" event:
37
+ * ```typescript
38
+ * Electrobun.events.on("open-url", (e) => {
39
+ * console.log("Opened with URL:", e.data.url);
40
+ * });
41
+ * ```
42
+ */
43
+ urlSchemes?: string[];
26
44
  };
27
45
 
28
46
  /**
@@ -215,7 +215,7 @@ export class BrowserView<T> {
215
215
  loadHTML(html: string) {
216
216
  this.html = html;
217
217
  console.log(`DEBUG: Setting HTML content for webview ${this.id}:`, html.substring(0, 50) + '...');
218
-
218
+
219
219
  if (this.renderer === 'cef') {
220
220
  // For CEF, store HTML content in native map and use scheme handler
221
221
  native.symbols.setWebviewHTMLContent(this.id, toCString(html));
@@ -226,6 +226,21 @@ export class BrowserView<T> {
226
226
  }
227
227
  }
228
228
 
229
+ setNavigationRules(rules: string[]) {
230
+ this.navigationRules = JSON.stringify(rules);
231
+ const rulesJson = JSON.stringify(rules);
232
+ native.symbols.setWebviewNavigationRules(this.ptr, toCString(rulesJson));
233
+ }
234
+
235
+ findInPage(searchText: string, options?: {forward?: boolean; matchCase?: boolean}) {
236
+ const forward = options?.forward ?? true;
237
+ const matchCase = options?.matchCase ?? false;
238
+ native.symbols.webviewFindInPage(this.ptr, toCString(searchText), forward, matchCase);
239
+ }
240
+
241
+ stopFindInPage() {
242
+ native.symbols.webviewStopFind(this.ptr);
243
+ }
229
244
 
230
245
  // todo (yoav): move this to a class that also has off, append, prepend, etc.
231
246
  // name should only allow browserView events
@@ -6,8 +6,6 @@ import {FFIType} from 'bun:ffi'
6
6
 
7
7
  let nextWindowId = 1;
8
8
 
9
- // todo (yoav): if we default to builtInSchema, we don't want dev to have to define custom handlers
10
- // for the built-in schema stuff.
11
9
  type WindowOptionsType<T = undefined> = {
12
10
  title: string;
13
11
  frame: {
@@ -45,9 +43,6 @@ const defaultOptions: WindowOptionsType = {
45
43
 
46
44
  export const BrowserWindowMap = {};
47
45
 
48
- // todo (yoav): do something where the type extends the default schema
49
- // that way we can provide built-in requests/messages and devs can extend it
50
-
51
46
  export class BrowserWindow<T> {
52
47
  id: number = nextWindowId++;
53
48
  ptr: FFIType.ptr;
@@ -185,6 +180,46 @@ export class BrowserWindow<T> {
185
180
  return ffi.request.focusWindow({ winId: this.id });
186
181
  }
187
182
 
183
+ minimize() {
184
+ return ffi.request.minimizeWindow({ winId: this.id });
185
+ }
186
+
187
+ unminimize() {
188
+ return ffi.request.unminimizeWindow({ winId: this.id });
189
+ }
190
+
191
+ isMinimized(): boolean {
192
+ return ffi.request.isWindowMinimized({ winId: this.id });
193
+ }
194
+
195
+ maximize() {
196
+ return ffi.request.maximizeWindow({ winId: this.id });
197
+ }
198
+
199
+ unmaximize() {
200
+ return ffi.request.unmaximizeWindow({ winId: this.id });
201
+ }
202
+
203
+ isMaximized(): boolean {
204
+ return ffi.request.isWindowMaximized({ winId: this.id });
205
+ }
206
+
207
+ setFullScreen(fullScreen: boolean) {
208
+ return ffi.request.setWindowFullScreen({ winId: this.id, fullScreen });
209
+ }
210
+
211
+ isFullScreen(): boolean {
212
+ return ffi.request.isWindowFullScreen({ winId: this.id });
213
+ }
214
+
215
+ setAlwaysOnTop(alwaysOnTop: boolean) {
216
+ return ffi.request.setWindowAlwaysOnTop({ winId: this.id, alwaysOnTop });
217
+ }
218
+
219
+ isAlwaysOnTop(): boolean {
220
+ return ffi.request.isWindowAlwaysOnTop({ winId: this.id });
221
+ }
222
+
188
223
  // todo (yoav): move this to a class that also has off, append, prepend, etc.
189
224
  // name should only allow browserWindow events
190
225
  on(name, handler) {
@@ -9,6 +9,104 @@ export const showItemInFolder = (path: string) => {
9
9
  return ffi.request.showItemInFolder({ path });
10
10
  };
11
11
 
12
+ /**
13
+ * Open a URL in the default browser or appropriate application.
14
+ * Works with http/https URLs, mailto: links, custom URL schemes, etc.
15
+ *
16
+ * @param url - The URL to open (e.g., "https://example.com", "mailto:test@example.com")
17
+ * @returns true if the URL was opened successfully, false otherwise
18
+ *
19
+ * @example
20
+ * // Open a website
21
+ * openExternal("https://example.com");
22
+ *
23
+ * // Open an email
24
+ * openExternal("mailto:support@example.com?subject=Help");
25
+ *
26
+ * // Open a custom URL scheme
27
+ * openExternal("slack://open");
28
+ */
29
+ export const openExternal = (url: string): boolean => {
30
+ return ffi.request.openExternal({ url });
31
+ };
32
+
33
+ /**
34
+ * Open a file or folder with the default application.
35
+ * For files, opens with the associated application (e.g., .pdf with PDF reader).
36
+ * For folders, opens in the file manager.
37
+ *
38
+ * @param path - The absolute path to the file or folder
39
+ * @returns true if the path was opened successfully, false otherwise
40
+ *
41
+ * @example
42
+ * // Open a document with default app
43
+ * openPath("/Users/me/Documents/report.pdf");
44
+ *
45
+ * // Open a folder in file manager
46
+ * openPath("/Users/me/Downloads");
47
+ */
48
+ export const openPath = (path: string): boolean => {
49
+ return ffi.request.openPath({ path });
50
+ };
51
+
52
+ export type NotificationOptions = {
53
+ /**
54
+ * The title of the notification (required)
55
+ */
56
+ title: string;
57
+ /**
58
+ * The main body text of the notification
59
+ */
60
+ body?: string;
61
+ /**
62
+ * A subtitle displayed below the title (macOS only, shown as additional line on other platforms)
63
+ */
64
+ subtitle?: string;
65
+ /**
66
+ * If true, the notification will not play a sound
67
+ */
68
+ silent?: boolean;
69
+ };
70
+
71
+ /**
72
+ * Show a native desktop notification.
73
+ *
74
+ * @param options - Notification options
75
+ * @param options.title - The title of the notification (required)
76
+ * @param options.body - The main body text
77
+ * @param options.subtitle - A subtitle (macOS shows this between title and body)
78
+ * @param options.silent - If true, no sound will be played
79
+ *
80
+ * @example
81
+ * // Simple notification
82
+ * showNotification({ title: "Download Complete" });
83
+ *
84
+ * // Notification with body
85
+ * showNotification({
86
+ * title: "New Message",
87
+ * body: "You have a new message from John"
88
+ * });
89
+ *
90
+ * // Full notification
91
+ * showNotification({
92
+ * title: "Reminder",
93
+ * subtitle: "Calendar Event",
94
+ * body: "Team meeting in 15 minutes",
95
+ * silent: false
96
+ * });
97
+ *
98
+ * // Silent notification
99
+ * showNotification({
100
+ * title: "Sync Complete",
101
+ * body: "All files have been synchronized",
102
+ * silent: true
103
+ * });
104
+ */
105
+ export const showNotification = (options: NotificationOptions): void => {
106
+ const { title, body, subtitle, silent } = options;
107
+ ffi.request.showNotification({ title, body, subtitle, silent });
108
+ };
109
+
12
110
  export const quit = () => {
13
111
  // Use native killApp for graceful shutdown
14
112
  native.symbols.killApp();
@@ -51,3 +149,121 @@ export const openFileDialog = async (
51
149
  // todo: it's nested like this due to zig union types. needs a zig refactor and revisit
52
150
  return filePaths;
53
151
  };
152
+
153
+ export type MessageBoxOptions = {
154
+ type?: "info" | "warning" | "error" | "question";
155
+ title?: string;
156
+ message?: string;
157
+ detail?: string;
158
+ buttons?: string[];
159
+ defaultId?: number;
160
+ cancelId?: number;
161
+ };
162
+
163
+ export type MessageBoxResponse = {
164
+ response: number; // Index of the clicked button
165
+ };
166
+
167
+ /**
168
+ * Shows a message box dialog and returns which button was clicked.
169
+ * Similar to Electron's dialog.showMessageBox()
170
+ *
171
+ * @param opts - Options for the message box
172
+ * @param opts.type - The type of dialog: "info", "warning", "error", or "question"
173
+ * @param opts.title - The title of the dialog window
174
+ * @param opts.message - The main message to display
175
+ * @param opts.detail - Additional detail text (displayed smaller on some platforms)
176
+ * @param opts.buttons - Array of button labels (e.g., ["OK", "Cancel"])
177
+ * @param opts.defaultId - Index of the default button (focused on open)
178
+ * @param opts.cancelId - Index of the button to trigger on Escape key or dialog close
179
+ * @returns Promise resolving to an object with `response` (0-based button index clicked)
180
+ *
181
+ * @example
182
+ * const { response } = await showMessageBox({
183
+ * type: "question",
184
+ * title: "Confirm",
185
+ * message: "Are you sure you want to delete this file?",
186
+ * buttons: ["Delete", "Cancel"],
187
+ * defaultId: 1,
188
+ * cancelId: 1
189
+ * });
190
+ * if (response === 0) {
191
+ * // User clicked Delete
192
+ * }
193
+ */
194
+ export const showMessageBox = async (
195
+ opts: MessageBoxOptions = {}
196
+ ): Promise<MessageBoxResponse> => {
197
+ const {
198
+ type = "info",
199
+ title = "",
200
+ message = "",
201
+ detail = "",
202
+ buttons = ["OK"],
203
+ defaultId = 0,
204
+ cancelId = -1,
205
+ } = opts;
206
+
207
+ const response = ffi.request.showMessageBox({
208
+ type,
209
+ title,
210
+ message,
211
+ detail,
212
+ buttons,
213
+ defaultId,
214
+ cancelId,
215
+ });
216
+
217
+ return { response };
218
+ };
219
+
220
+ // ============================================================================
221
+ // Clipboard API
222
+ // ============================================================================
223
+
224
+ /**
225
+ * Read text from the system clipboard.
226
+ * @returns The clipboard text, or null if no text is available
227
+ */
228
+ export const clipboardReadText = (): string | null => {
229
+ return ffi.request.clipboardReadText();
230
+ };
231
+
232
+ /**
233
+ * Write text to the system clipboard.
234
+ * @param text - The text to write to the clipboard
235
+ */
236
+ export const clipboardWriteText = (text: string): void => {
237
+ ffi.request.clipboardWriteText({ text });
238
+ };
239
+
240
+ /**
241
+ * Read image from the system clipboard as PNG data.
242
+ * @returns PNG image data as Uint8Array, or null if no image is available
243
+ */
244
+ export const clipboardReadImage = (): Uint8Array | null => {
245
+ return ffi.request.clipboardReadImage();
246
+ };
247
+
248
+ /**
249
+ * Write PNG image data to the system clipboard.
250
+ * @param pngData - PNG image data as Uint8Array
251
+ */
252
+ export const clipboardWriteImage = (pngData: Uint8Array): void => {
253
+ ffi.request.clipboardWriteImage({ pngData });
254
+ };
255
+
256
+ /**
257
+ * Clear the system clipboard.
258
+ */
259
+ export const clipboardClear = (): void => {
260
+ ffi.request.clipboardClear();
261
+ };
262
+
263
+ /**
264
+ * Get the available formats in the clipboard.
265
+ * @returns Array of format names (e.g., ["text", "image", "files", "html"])
266
+ */
267
+ export const clipboardAvailableFormats = (): string[] => {
268
+ return ffi.request.clipboardAvailableFormats();
269
+ };
@@ -11,4 +11,9 @@ export default {
11
11
  "context-menu-clicked",
12
12
  data
13
13
  ),
14
+ openUrl: (data) =>
15
+ new ElectrobunEvent<{ url: string }, void>(
16
+ "open-url",
17
+ data
18
+ ),
14
19
  };
@@ -6,17 +6,28 @@ import * as ApplicationMenu from "./core/ApplicationMenu";
6
6
  import * as ContextMenu from "./core/ContextMenu";
7
7
  import { Updater } from "./core/Updater";
8
8
  import * as Utils from "./core/Utils";
9
+ import type { MessageBoxOptions, MessageBoxResponse } from "./core/Utils";
9
10
  import { type RPCSchema, createRPC } from "rpc-anywhere";
10
11
  import type ElectrobunEvent from "./events/event";
11
12
  import * as PATHS from "./core/Paths";
12
13
  import * as Socket from "./core/Socket";
13
14
  import type { ElectrobunConfig } from "./ElectrobunConfig";
15
+ import { GlobalShortcut, Screen, Session } from "./proc/native";
16
+ import type { Display, Rectangle, Point, Cookie, CookieFilter, StorageType } from "./proc/native";
14
17
 
15
18
  // Named Exports
16
19
  export {
17
20
  type RPCSchema,
18
21
  type ElectrobunEvent,
19
22
  type ElectrobunConfig,
23
+ type MessageBoxOptions,
24
+ type MessageBoxResponse,
25
+ type Display,
26
+ type Rectangle,
27
+ type Point,
28
+ type Cookie,
29
+ type CookieFilter,
30
+ type StorageType,
20
31
  createRPC,
21
32
  BrowserWindow,
22
33
  BrowserView,
@@ -27,6 +38,9 @@ export {
27
38
  ContextMenu,
28
39
  PATHS,
29
40
  Socket,
41
+ GlobalShortcut,
42
+ Screen,
43
+ Session,
30
44
  };
31
45
 
32
46
  // Default Export
@@ -38,6 +52,9 @@ const Electrobun = {
38
52
  Utils,
39
53
  ApplicationMenu,
40
54
  ContextMenu,
55
+ GlobalShortcut,
56
+ Screen,
57
+ Session,
41
58
  events: electobunEventEmmitter,
42
59
  PATHS,
43
60
  Socket,
@@ -98,6 +98,46 @@ export const native = (() => {
98
98
  ],
99
99
  returns: FFIType.void,
100
100
  },
101
+ minimizeNSWindow: {
102
+ args: [FFIType.ptr],
103
+ returns: FFIType.void,
104
+ },
105
+ unminimizeNSWindow: {
106
+ args: [FFIType.ptr],
107
+ returns: FFIType.void,
108
+ },
109
+ isNSWindowMinimized: {
110
+ args: [FFIType.ptr],
111
+ returns: FFIType.bool,
112
+ },
113
+ maximizeNSWindow: {
114
+ args: [FFIType.ptr],
115
+ returns: FFIType.void,
116
+ },
117
+ unmaximizeNSWindow: {
118
+ args: [FFIType.ptr],
119
+ returns: FFIType.void,
120
+ },
121
+ isNSWindowMaximized: {
122
+ args: [FFIType.ptr],
123
+ returns: FFIType.bool,
124
+ },
125
+ setNSWindowFullScreen: {
126
+ args: [FFIType.ptr, FFIType.bool],
127
+ returns: FFIType.void,
128
+ },
129
+ isNSWindowFullScreen: {
130
+ args: [FFIType.ptr],
131
+ returns: FFIType.bool,
132
+ },
133
+ setNSWindowAlwaysOnTop: {
134
+ args: [FFIType.ptr, FFIType.bool],
135
+ returns: FFIType.void,
136
+ },
137
+ isNSWindowAlwaysOnTop: {
138
+ args: [FFIType.ptr],
139
+ returns: FFIType.bool,
140
+ },
101
141
  // webview
102
142
  initWebview: {
103
143
  args: [
@@ -207,7 +247,19 @@ export const native = (() => {
207
247
  webviewSetHidden: {
208
248
  args: [FFIType.ptr, FFIType.bool],
209
249
  returns: FFIType.void
210
- },
250
+ },
251
+ setWebviewNavigationRules: {
252
+ args: [FFIType.ptr, FFIType.cstring],
253
+ returns: FFIType.void
254
+ },
255
+ webviewFindInPage: {
256
+ args: [FFIType.ptr, FFIType.cstring, FFIType.bool, FFIType.bool],
257
+ returns: FFIType.void
258
+ },
259
+ webviewStopFind: {
260
+ args: [FFIType.ptr],
261
+ returns: FFIType.void
262
+ },
211
263
  evaluateJavaScriptWithNoCompletion: {
212
264
  args: [FFIType.ptr, FFIType.cstring],
213
265
  returns: FFIType.void
@@ -256,7 +308,61 @@ export const native = (() => {
256
308
  showItemInFolder: {
257
309
  args: [FFIType.cstring],
258
310
  returns: FFIType.void
259
- },
311
+ },
312
+ openExternal: {
313
+ args: [FFIType.cstring],
314
+ returns: FFIType.bool
315
+ },
316
+ openPath: {
317
+ args: [FFIType.cstring],
318
+ returns: FFIType.bool
319
+ },
320
+ showNotification: {
321
+ args: [
322
+ FFIType.cstring, // title
323
+ FFIType.cstring, // body
324
+ FFIType.cstring, // subtitle
325
+ FFIType.bool, // silent
326
+ ],
327
+ returns: FFIType.void
328
+ },
329
+
330
+ // Global keyboard shortcuts
331
+ setGlobalShortcutCallback: {
332
+ args: [FFIType.function],
333
+ returns: FFIType.void
334
+ },
335
+ registerGlobalShortcut: {
336
+ args: [FFIType.cstring],
337
+ returns: FFIType.bool
338
+ },
339
+ unregisterGlobalShortcut: {
340
+ args: [FFIType.cstring],
341
+ returns: FFIType.bool
342
+ },
343
+ unregisterAllGlobalShortcuts: {
344
+ args: [],
345
+ returns: FFIType.void
346
+ },
347
+ isGlobalShortcutRegistered: {
348
+ args: [FFIType.cstring],
349
+ returns: FFIType.bool
350
+ },
351
+
352
+ // Screen API
353
+ getAllDisplays: {
354
+ args: [],
355
+ returns: FFIType.cstring
356
+ },
357
+ getPrimaryDisplay: {
358
+ args: [],
359
+ returns: FFIType.cstring
360
+ },
361
+ getCursorScreenPoint: {
362
+ args: [],
363
+ returns: FFIType.cstring
364
+ },
365
+
260
366
  openFileDialog: {
261
367
  args: [
262
368
  FFIType.cstring,
@@ -266,8 +372,74 @@ export const native = (() => {
266
372
  FFIType.int,
267
373
  ],
268
374
  returns: FFIType.cstring
269
- },
270
-
375
+ },
376
+ showMessageBox: {
377
+ args: [
378
+ FFIType.cstring, // type
379
+ FFIType.cstring, // title
380
+ FFIType.cstring, // message
381
+ FFIType.cstring, // detail
382
+ FFIType.cstring, // buttons (comma-separated)
383
+ FFIType.int, // defaultId
384
+ FFIType.int, // cancelId
385
+ ],
386
+ returns: FFIType.int
387
+ },
388
+
389
+ // Clipboard API
390
+ clipboardReadText: {
391
+ args: [],
392
+ returns: FFIType.cstring
393
+ },
394
+ clipboardWriteText: {
395
+ args: [FFIType.cstring],
396
+ returns: FFIType.void
397
+ },
398
+ clipboardReadImage: {
399
+ args: [FFIType.ptr], // pointer to size_t for output size
400
+ returns: FFIType.ptr // pointer to PNG data
401
+ },
402
+ clipboardWriteImage: {
403
+ args: [FFIType.ptr, FFIType.u64], // PNG data pointer, size
404
+ returns: FFIType.void
405
+ },
406
+ clipboardClear: {
407
+ args: [],
408
+ returns: FFIType.void
409
+ },
410
+ clipboardAvailableFormats: {
411
+ args: [],
412
+ returns: FFIType.cstring
413
+ },
414
+
415
+ // Session/Cookie API
416
+ sessionGetCookies: {
417
+ args: [FFIType.cstring, FFIType.cstring],
418
+ returns: FFIType.cstring
419
+ },
420
+ sessionSetCookie: {
421
+ args: [FFIType.cstring, FFIType.cstring],
422
+ returns: FFIType.bool
423
+ },
424
+ sessionRemoveCookie: {
425
+ args: [FFIType.cstring, FFIType.cstring, FFIType.cstring],
426
+ returns: FFIType.bool
427
+ },
428
+ sessionClearCookies: {
429
+ args: [FFIType.cstring],
430
+ returns: FFIType.void
431
+ },
432
+ sessionClearStorageData: {
433
+ args: [FFIType.cstring, FFIType.cstring],
434
+ returns: FFIType.void
435
+ },
436
+
437
+ // URL scheme handler (macOS only)
438
+ setURLOpenHandler: {
439
+ args: [FFIType.function], // handler callback
440
+ returns: FFIType.void
441
+ },
442
+
271
443
  // MacOS specific native utils
272
444
  getNSWindowStyleMask: {
273
445
  args: [
@@ -441,14 +613,124 @@ export const ffi = {
441
613
  focusWindow: (params: {winId: number}) => {
442
614
  const {winId} = params;
443
615
  const windowPtr = BrowserWindow.getById(winId)?.ptr;
444
-
616
+
445
617
  if (!windowPtr) {
446
618
  throw `Can't focus window. Window no longer exists`;
447
619
  }
448
-
620
+
449
621
  native.symbols.makeNSWindowKeyAndOrderFront(windowPtr);
450
622
  },
451
623
 
624
+ minimizeWindow: (params: {winId: number}) => {
625
+ const {winId} = params;
626
+ const windowPtr = BrowserWindow.getById(winId)?.ptr;
627
+
628
+ if (!windowPtr) {
629
+ throw `Can't minimize window. Window no longer exists`;
630
+ }
631
+
632
+ native.symbols.minimizeNSWindow(windowPtr);
633
+ },
634
+
635
+ unminimizeWindow: (params: {winId: number}) => {
636
+ const {winId} = params;
637
+ const windowPtr = BrowserWindow.getById(winId)?.ptr;
638
+
639
+ if (!windowPtr) {
640
+ throw `Can't unminimize window. Window no longer exists`;
641
+ }
642
+
643
+ native.symbols.unminimizeNSWindow(windowPtr);
644
+ },
645
+
646
+ isWindowMinimized: (params: {winId: number}): boolean => {
647
+ const {winId} = params;
648
+ const windowPtr = BrowserWindow.getById(winId)?.ptr;
649
+
650
+ if (!windowPtr) {
651
+ return false;
652
+ }
653
+
654
+ return native.symbols.isNSWindowMinimized(windowPtr);
655
+ },
656
+
657
+ maximizeWindow: (params: {winId: number}) => {
658
+ const {winId} = params;
659
+ const windowPtr = BrowserWindow.getById(winId)?.ptr;
660
+
661
+ if (!windowPtr) {
662
+ throw `Can't maximize window. Window no longer exists`;
663
+ }
664
+
665
+ native.symbols.maximizeNSWindow(windowPtr);
666
+ },
667
+
668
+ unmaximizeWindow: (params: {winId: number}) => {
669
+ const {winId} = params;
670
+ const windowPtr = BrowserWindow.getById(winId)?.ptr;
671
+
672
+ if (!windowPtr) {
673
+ throw `Can't unmaximize window. Window no longer exists`;
674
+ }
675
+
676
+ native.symbols.unmaximizeNSWindow(windowPtr);
677
+ },
678
+
679
+ isWindowMaximized: (params: {winId: number}): boolean => {
680
+ const {winId} = params;
681
+ const windowPtr = BrowserWindow.getById(winId)?.ptr;
682
+
683
+ if (!windowPtr) {
684
+ return false;
685
+ }
686
+
687
+ return native.symbols.isNSWindowMaximized(windowPtr);
688
+ },
689
+
690
+ setWindowFullScreen: (params: {winId: number; fullScreen: boolean}) => {
691
+ const {winId, fullScreen} = params;
692
+ const windowPtr = BrowserWindow.getById(winId)?.ptr;
693
+
694
+ if (!windowPtr) {
695
+ throw `Can't set fullscreen. Window no longer exists`;
696
+ }
697
+
698
+ native.symbols.setNSWindowFullScreen(windowPtr, fullScreen);
699
+ },
700
+
701
+ isWindowFullScreen: (params: {winId: number}): boolean => {
702
+ const {winId} = params;
703
+ const windowPtr = BrowserWindow.getById(winId)?.ptr;
704
+
705
+ if (!windowPtr) {
706
+ return false;
707
+ }
708
+
709
+ return native.symbols.isNSWindowFullScreen(windowPtr);
710
+ },
711
+
712
+ setWindowAlwaysOnTop: (params: {winId: number; alwaysOnTop: boolean}) => {
713
+ const {winId, alwaysOnTop} = params;
714
+ const windowPtr = BrowserWindow.getById(winId)?.ptr;
715
+
716
+ if (!windowPtr) {
717
+ throw `Can't set always on top. Window no longer exists`;
718
+ }
719
+
720
+ native.symbols.setNSWindowAlwaysOnTop(windowPtr, alwaysOnTop);
721
+ },
722
+
723
+ isWindowAlwaysOnTop: (params: {winId: number}): boolean => {
724
+ const {winId} = params;
725
+ const windowPtr = BrowserWindow.getById(winId)?.ptr;
726
+
727
+ if (!windowPtr) {
728
+ return false;
729
+ }
730
+
731
+ return native.symbols.isNSWindowAlwaysOnTop(windowPtr);
732
+ },
733
+
452
734
  createWebview: (params: {
453
735
  id: number;
454
736
  windowId: number;
@@ -858,7 +1140,24 @@ export const ffi = {
858
1140
  path
859
1141
  } = params;
860
1142
 
861
- native.symbols.showItemInFolder(toCString(path));
1143
+ native.symbols.showItemInFolder(toCString(path));
1144
+ },
1145
+ openExternal: (params: {url: string}): boolean => {
1146
+ const { url } = params;
1147
+ return native.symbols.openExternal(toCString(url));
1148
+ },
1149
+ openPath: (params: {path: string}): boolean => {
1150
+ const { path } = params;
1151
+ return native.symbols.openPath(toCString(path));
1152
+ },
1153
+ showNotification: (params: {title: string, body?: string, subtitle?: string, silent?: boolean}): void => {
1154
+ const { title, body = '', subtitle = '', silent = false } = params;
1155
+ native.symbols.showNotification(
1156
+ toCString(title),
1157
+ toCString(body),
1158
+ toCString(subtitle),
1159
+ silent
1160
+ );
862
1161
  },
863
1162
  openFileDialog: (params: {startingFolder: string, allowedFileTypes: string, canChooseFiles: boolean, canChooseDirectory: boolean, allowsMultipleSelection: boolean}): string => {
864
1163
  const {
@@ -867,12 +1166,79 @@ export const ffi = {
867
1166
  canChooseFiles,
868
1167
  canChooseDirectory,
869
1168
  allowsMultipleSelection,
870
- } = params;
871
- const filePath = native.symbols.openFileDialog(toCString(startingFolder), toCString(allowedFileTypes), canChooseFiles, canChooseDirectory, allowsMultipleSelection);
872
-
1169
+ } = params;
1170
+ const filePath = native.symbols.openFileDialog(toCString(startingFolder), toCString(allowedFileTypes), canChooseFiles, canChooseDirectory, allowsMultipleSelection);
1171
+
873
1172
  return filePath.toString();
874
1173
  },
875
-
1174
+ showMessageBox: (params: {type?: string, title?: string, message?: string, detail?: string, buttons?: string[], defaultId?: number, cancelId?: number}): number => {
1175
+ const {
1176
+ type = 'info',
1177
+ title = '',
1178
+ message = '',
1179
+ detail = '',
1180
+ buttons = ['OK'],
1181
+ defaultId = 0,
1182
+ cancelId = -1,
1183
+ } = params;
1184
+ // Convert buttons array to comma-separated string
1185
+ const buttonsStr = buttons.join(',');
1186
+ return native.symbols.showMessageBox(
1187
+ toCString(type),
1188
+ toCString(title),
1189
+ toCString(message),
1190
+ toCString(detail),
1191
+ toCString(buttonsStr),
1192
+ defaultId,
1193
+ cancelId
1194
+ );
1195
+ },
1196
+
1197
+ // Clipboard API
1198
+ clipboardReadText: (): string | null => {
1199
+ const result = native.symbols.clipboardReadText();
1200
+ if (!result) return null;
1201
+ return result.toString();
1202
+ },
1203
+ clipboardWriteText: (params: {text: string}): void => {
1204
+ native.symbols.clipboardWriteText(toCString(params.text));
1205
+ },
1206
+ clipboardReadImage: (): Uint8Array | null => {
1207
+ // Allocate a buffer for the size output
1208
+ const sizeBuffer = new BigUint64Array(1);
1209
+ const dataPtr = native.symbols.clipboardReadImage(ptr(sizeBuffer));
1210
+
1211
+ if (!dataPtr) return null;
1212
+
1213
+ const size = Number(sizeBuffer[0]);
1214
+ if (size === 0) return null;
1215
+
1216
+ // Copy the data to a Uint8Array
1217
+ const result = new Uint8Array(size);
1218
+ const sourceView = new Uint8Array(toArrayBuffer(dataPtr, 0, size));
1219
+ result.set(sourceView);
1220
+
1221
+ // Note: The native code allocated this memory with malloc
1222
+ // We should free it, but Bun's FFI doesn't expose free directly
1223
+ // The memory will be reclaimed when the process exits
1224
+
1225
+ return result;
1226
+ },
1227
+ clipboardWriteImage: (params: {pngData: Uint8Array}): void => {
1228
+ const { pngData } = params;
1229
+ native.symbols.clipboardWriteImage(ptr(pngData), BigInt(pngData.length));
1230
+ },
1231
+ clipboardClear: (): void => {
1232
+ native.symbols.clipboardClear();
1233
+ },
1234
+ clipboardAvailableFormats: (): string[] => {
1235
+ const result = native.symbols.clipboardAvailableFormats();
1236
+ if (!result) return [];
1237
+ const formatsStr = result.toString();
1238
+ if (!formatsStr) return [];
1239
+ return formatsStr.split(',').filter(f => f.length > 0);
1240
+ },
1241
+
876
1242
  // ffifunc: (params: {}): void => {
877
1243
  // const {
878
1244
 
@@ -1047,15 +1413,333 @@ const getHTMLForWebviewSync = new JSCallback((webviewId) => {
1047
1413
 
1048
1414
  native.symbols.setJSUtils(getMimeType, getHTMLForWebviewSync);
1049
1415
 
1050
- // TODO XX: revisit this as integrated into the will-navigate handler
1416
+ // URL scheme open handler (macOS only)
1417
+ // Receives URLs when the app is opened via custom URL schemes (e.g., myapp://path)
1418
+ const urlOpenCallback = new JSCallback(
1419
+ (urlPtr) => {
1420
+ const url = new CString(urlPtr).toString();
1421
+ const handler = electrobunEventEmitter.events.app.openUrl;
1422
+ const event = handler({ url });
1423
+ electrobunEventEmitter.emitEvent(event);
1424
+ },
1425
+ {
1426
+ args: [FFIType.cstring],
1427
+ returns: "void",
1428
+ threadsafe: true,
1429
+ }
1430
+ );
1431
+
1432
+ // Register the URL open handler with native code (macOS only)
1433
+ if (process.platform === 'darwin') {
1434
+ native.symbols.setURLOpenHandler(urlOpenCallback);
1435
+ }
1436
+
1437
+ // Global shortcut storage and callback
1438
+ const globalShortcutHandlers = new Map<string, () => void>();
1439
+
1440
+ const globalShortcutCallback = new JSCallback(
1441
+ (acceleratorPtr) => {
1442
+ const accelerator = new CString(acceleratorPtr).toString();
1443
+ const handler = globalShortcutHandlers.get(accelerator);
1444
+ if (handler) {
1445
+ handler();
1446
+ }
1447
+ },
1448
+ {
1449
+ args: [FFIType.cstring],
1450
+ returns: "void",
1451
+ threadsafe: true,
1452
+ }
1453
+ );
1454
+
1455
+ // Set up the global shortcut callback
1456
+ native.symbols.setGlobalShortcutCallback(globalShortcutCallback);
1457
+
1458
+ // GlobalShortcut module for external use
1459
+ export const GlobalShortcut = {
1460
+ /**
1461
+ * Register a global keyboard shortcut
1462
+ * @param accelerator - The shortcut string (e.g., "CommandOrControl+Shift+Space")
1463
+ * @param callback - Function to call when the shortcut is triggered
1464
+ * @returns true if registered successfully, false otherwise
1465
+ */
1466
+ register: (accelerator: string, callback: () => void): boolean => {
1467
+ if (globalShortcutHandlers.has(accelerator)) {
1468
+ return false; // Already registered
1469
+ }
1470
+
1471
+ const result = native.symbols.registerGlobalShortcut(toCString(accelerator));
1472
+ if (result) {
1473
+ globalShortcutHandlers.set(accelerator, callback);
1474
+ }
1475
+ return result;
1476
+ },
1477
+
1478
+ /**
1479
+ * Unregister a global keyboard shortcut
1480
+ * @param accelerator - The shortcut string to unregister
1481
+ * @returns true if unregistered successfully, false otherwise
1482
+ */
1483
+ unregister: (accelerator: string): boolean => {
1484
+ const result = native.symbols.unregisterGlobalShortcut(toCString(accelerator));
1485
+ if (result) {
1486
+ globalShortcutHandlers.delete(accelerator);
1487
+ }
1488
+ return result;
1489
+ },
1490
+
1491
+ /**
1492
+ * Unregister all global keyboard shortcuts
1493
+ */
1494
+ unregisterAll: (): void => {
1495
+ native.symbols.unregisterAllGlobalShortcuts();
1496
+ globalShortcutHandlers.clear();
1497
+ },
1498
+
1499
+ /**
1500
+ * Check if a shortcut is registered
1501
+ * @param accelerator - The shortcut string to check
1502
+ * @returns true if registered, false otherwise
1503
+ */
1504
+ isRegistered: (accelerator: string): boolean => {
1505
+ return native.symbols.isGlobalShortcutRegistered(toCString(accelerator));
1506
+ },
1507
+ };
1508
+
1509
+ // Types for Screen API
1510
+ export interface Rectangle {
1511
+ x: number;
1512
+ y: number;
1513
+ width: number;
1514
+ height: number;
1515
+ }
1516
+
1517
+ export interface Display {
1518
+ id: number;
1519
+ bounds: Rectangle;
1520
+ workArea: Rectangle;
1521
+ scaleFactor: number;
1522
+ isPrimary: boolean;
1523
+ }
1524
+
1525
+ export interface Point {
1526
+ x: number;
1527
+ y: number;
1528
+ }
1529
+
1530
+ // Screen module for display and cursor information
1531
+ export const Screen = {
1532
+ /**
1533
+ * Get the primary display
1534
+ * @returns Display object for the primary monitor
1535
+ */
1536
+ getPrimaryDisplay: (): Display => {
1537
+ const jsonStr = native.symbols.getPrimaryDisplay();
1538
+ if (!jsonStr) {
1539
+ return {
1540
+ id: 0,
1541
+ bounds: { x: 0, y: 0, width: 0, height: 0 },
1542
+ workArea: { x: 0, y: 0, width: 0, height: 0 },
1543
+ scaleFactor: 1,
1544
+ isPrimary: true,
1545
+ };
1546
+ }
1547
+ try {
1548
+ return JSON.parse(jsonStr);
1549
+ } catch {
1550
+ return {
1551
+ id: 0,
1552
+ bounds: { x: 0, y: 0, width: 0, height: 0 },
1553
+ workArea: { x: 0, y: 0, width: 0, height: 0 },
1554
+ scaleFactor: 1,
1555
+ isPrimary: true,
1556
+ };
1557
+ }
1558
+ },
1559
+
1560
+ /**
1561
+ * Get all connected displays
1562
+ * @returns Array of Display objects
1563
+ */
1564
+ getAllDisplays: (): Display[] => {
1565
+ const jsonStr = native.symbols.getAllDisplays();
1566
+ if (!jsonStr) {
1567
+ return [];
1568
+ }
1569
+ try {
1570
+ return JSON.parse(jsonStr);
1571
+ } catch {
1572
+ return [];
1573
+ }
1574
+ },
1575
+
1576
+ /**
1577
+ * Get the current cursor position in screen coordinates
1578
+ * @returns Point with x and y coordinates
1579
+ */
1580
+ getCursorScreenPoint: (): Point => {
1581
+ const jsonStr = native.symbols.getCursorScreenPoint();
1582
+ if (!jsonStr) {
1583
+ return { x: 0, y: 0 };
1584
+ }
1585
+ try {
1586
+ return JSON.parse(jsonStr);
1587
+ } catch {
1588
+ return { x: 0, y: 0 };
1589
+ }
1590
+ },
1591
+ };
1592
+
1593
+ // Types for Session/Cookie API
1594
+ export interface Cookie {
1595
+ name: string;
1596
+ value: string;
1597
+ domain?: string;
1598
+ path?: string;
1599
+ secure?: boolean;
1600
+ httpOnly?: boolean;
1601
+ sameSite?: 'no_restriction' | 'lax' | 'strict';
1602
+ expirationDate?: number; // Unix timestamp in seconds
1603
+ }
1604
+
1605
+ export interface CookieFilter {
1606
+ url?: string;
1607
+ name?: string;
1608
+ domain?: string;
1609
+ path?: string;
1610
+ secure?: boolean;
1611
+ session?: boolean;
1612
+ }
1613
+
1614
+ export type StorageType =
1615
+ | 'cookies'
1616
+ | 'localStorage'
1617
+ | 'sessionStorage'
1618
+ | 'indexedDB'
1619
+ | 'webSQL'
1620
+ | 'cache'
1621
+ | 'all';
1622
+
1623
+ // Cookies API for a session
1624
+ class SessionCookies {
1625
+ private partitionId: string;
1626
+
1627
+ constructor(partitionId: string) {
1628
+ this.partitionId = partitionId;
1629
+ }
1630
+
1631
+ /**
1632
+ * Get cookies matching the filter criteria
1633
+ * @param filter - Optional filter to match cookies
1634
+ * @returns Array of matching cookies
1635
+ */
1636
+ get(filter?: CookieFilter): Cookie[] {
1637
+ const filterJson = JSON.stringify(filter || {});
1638
+ const result = native.symbols.sessionGetCookies(
1639
+ toCString(this.partitionId),
1640
+ toCString(filterJson)
1641
+ );
1642
+ if (!result) return [];
1643
+ try {
1644
+ return JSON.parse(result);
1645
+ } catch {
1646
+ return [];
1647
+ }
1648
+ }
1649
+
1650
+ /**
1651
+ * Set a cookie
1652
+ * @param cookie - The cookie to set
1653
+ * @returns true if the cookie was set successfully
1654
+ */
1655
+ set(cookie: Cookie): boolean {
1656
+ const cookieJson = JSON.stringify(cookie);
1657
+ return native.symbols.sessionSetCookie(
1658
+ toCString(this.partitionId),
1659
+ toCString(cookieJson)
1660
+ );
1661
+ }
1662
+
1663
+ /**
1664
+ * Remove a specific cookie
1665
+ * @param url - The URL associated with the cookie
1666
+ * @param name - The name of the cookie
1667
+ * @returns true if the cookie was removed successfully
1668
+ */
1669
+ remove(url: string, name: string): boolean {
1670
+ return native.symbols.sessionRemoveCookie(
1671
+ toCString(this.partitionId),
1672
+ toCString(url),
1673
+ toCString(name)
1674
+ );
1675
+ }
1676
+
1677
+ /**
1678
+ * Clear all cookies for this session
1679
+ */
1680
+ clear(): void {
1681
+ native.symbols.sessionClearCookies(toCString(this.partitionId));
1682
+ }
1683
+ }
1684
+
1685
+ // Session class representing a storage partition
1686
+ class SessionInstance {
1687
+ readonly partition: string;
1688
+ readonly cookies: SessionCookies;
1689
+
1690
+ constructor(partition: string) {
1691
+ this.partition = partition;
1692
+ this.cookies = new SessionCookies(partition);
1693
+ }
1694
+
1695
+ /**
1696
+ * Clear storage data for this session
1697
+ * @param types - Array of storage types to clear, or 'all' to clear everything
1698
+ */
1699
+ clearStorageData(types: StorageType[] | 'all' = 'all'): void {
1700
+ const typesArray = types === 'all' ? ['all'] : types;
1701
+ native.symbols.sessionClearStorageData(
1702
+ toCString(this.partition),
1703
+ toCString(JSON.stringify(typesArray))
1704
+ );
1705
+ }
1706
+ }
1707
+
1708
+ // Cache of session instances
1709
+ const sessionCache = new Map<string, SessionInstance>();
1710
+
1711
+ // Session module for storage/cookie management
1712
+ export const Session = {
1713
+ /**
1714
+ * Get or create a session for a given partition
1715
+ * @param partition - The partition identifier (e.g., "persist:myapp" or "ephemeral")
1716
+ * @returns Session instance for the partition
1717
+ */
1718
+ fromPartition: (partition: string): SessionInstance => {
1719
+ let session = sessionCache.get(partition);
1720
+ if (!session) {
1721
+ session = new SessionInstance(partition);
1722
+ sessionCache.set(partition, session);
1723
+ }
1724
+ return session;
1725
+ },
1726
+
1727
+ /**
1728
+ * Get the default session (persist:default partition)
1729
+ */
1730
+ get defaultSession(): SessionInstance {
1731
+ return Session.fromPartition('persist:default');
1732
+ },
1733
+ };
1734
+
1735
+ // DEPRECATED: This callback is no longer used for navigation decisions.
1736
+ // Navigation rules are now stored in native code and evaluated synchronously
1737
+ // without calling back to Bun. Use webview.setNavigationRules() instead.
1738
+ // This callback is kept for FFI signature compatibility but is not called.
1051
1739
  const webviewDecideNavigation = new JSCallback((webviewId, url) => {
1052
- console.log('TODO: webviewDecideNavigation', webviewId, new CString(url))
1053
1740
  return true;
1054
1741
  }, {
1055
1742
  args: [FFIType.u32, FFIType.cstring],
1056
- // NOTE: In Objc true is YES which is so dumb, but that doesn't work with Bun's FFIType.bool
1057
- // in JSCallbacks right now (it always infers false) so to make this cross platform we have to use
1058
- // FFIType.u32 and uint32_t and then just treat it as a boolean in code.
1059
1743
  returns: FFIType.u32,
1060
1744
  threadsafe: true
1061
1745
  });
@@ -1496,6 +2180,31 @@ export const internalRpcHandlers = {
1496
2180
  }
1497
2181
  native.symbols.webviewSetHidden(webview.ptr, params.hidden);
1498
2182
  },
2183
+ webviewTagSetNavigationRules: (params: {id: number; rules: string[]}) => {
2184
+ const webview = BrowserView.getById(params.id);
2185
+ if (!webview || !webview.ptr) {
2186
+ console.error(`webviewTagSetNavigationRules: BrowserView not found or has no ptr for id ${params.id}`);
2187
+ return;
2188
+ }
2189
+ const rulesJson = JSON.stringify(params.rules);
2190
+ native.symbols.setWebviewNavigationRules(webview.ptr, toCString(rulesJson));
2191
+ },
2192
+ webviewTagFindInPage: (params: {id: number; searchText: string; forward: boolean; matchCase: boolean}) => {
2193
+ const webview = BrowserView.getById(params.id);
2194
+ if (!webview || !webview.ptr) {
2195
+ console.error(`webviewTagFindInPage: BrowserView not found or has no ptr for id ${params.id}`);
2196
+ return;
2197
+ }
2198
+ native.symbols.webviewFindInPage(webview.ptr, toCString(params.searchText), params.forward, params.matchCase);
2199
+ },
2200
+ webviewTagStopFind: (params: {id: number}) => {
2201
+ const webview = BrowserView.getById(params.id);
2202
+ if (!webview || !webview.ptr) {
2203
+ console.error(`webviewTagStopFind: BrowserView not found or has no ptr for id ${params.id}`);
2204
+ return;
2205
+ }
2206
+ native.symbols.webviewStopFind(webview.ptr);
2207
+ },
1499
2208
  webviewEvent: (params) => {
1500
2209
  console.log('-----------------+webviewEvent', params)
1501
2210
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrobun",
3
- "version": "0.8.0-beta.0",
3
+ "version": "0.9.3-beta.0",
4
4
  "description": "Build ultra fast, tiny, and cross-platform desktop apps with Typescript.",
5
5
  "license": "MIT",
6
6
  "author": "Blackboard Technologies Inc.",
package/src/cli/index.ts CHANGED
@@ -609,22 +609,45 @@ function escapeXml(str: string): string {
609
609
  // Helper function to generate usage description entries for Info.plist
610
610
  function generateUsageDescriptions(entitlements: Record<string, boolean | string>): string {
611
611
  const usageEntries: string[] = [];
612
-
612
+
613
613
  for (const [entitlement, value] of Object.entries(entitlements)) {
614
614
  const plistKey = ENTITLEMENT_TO_PLIST_KEY[entitlement];
615
615
  if (plistKey && value) {
616
616
  // Use the string value as description, or a default if it's just true
617
- const description = typeof value === "string"
617
+ const description = typeof value === "string"
618
618
  ? escapeXml(value)
619
619
  : `This app requires access for ${entitlement.split('.').pop()?.replace('-', ' ')}`;
620
-
620
+
621
621
  usageEntries.push(` <key>${plistKey}</key>\n <string>${description}</string>`);
622
622
  }
623
623
  }
624
-
624
+
625
625
  return usageEntries.join('\n');
626
626
  }
627
627
 
628
+ // Helper function to generate CFBundleURLTypes for custom URL schemes
629
+ function generateURLTypes(urlSchemes: string[] | undefined, identifier: string): string {
630
+ if (!urlSchemes || urlSchemes.length === 0) {
631
+ return '';
632
+ }
633
+
634
+ const schemesXml = urlSchemes.map(scheme => ` <string>${escapeXml(scheme)}</string>`).join('\n');
635
+
636
+ return ` <key>CFBundleURLTypes</key>
637
+ <array>
638
+ <dict>
639
+ <key>CFBundleURLName</key>
640
+ <string>${escapeXml(identifier)}</string>
641
+ <key>CFBundleTypeRole</key>
642
+ <string>Viewer</string>
643
+ <key>CFBundleURLSchemes</key>
644
+ <array>
645
+ ${schemesXml}
646
+ </array>
647
+ </dict>
648
+ </array>`;
649
+ }
650
+
628
651
  const command = commandDefaults[commandArg];
629
652
 
630
653
  if (!command) {
@@ -1064,7 +1087,9 @@ if (commandArg === "init") {
1064
1087
  // provide methods to help segment data in those folders based on channel/environment
1065
1088
  // Generate usage descriptions from entitlements
1066
1089
  const usageDescriptions = generateUsageDescriptions(config.build.mac.entitlements || {});
1067
-
1090
+ // Generate URL scheme handlers
1091
+ const urlTypes = generateURLTypes(config.app.urlSchemes, config.app.identifier);
1092
+
1068
1093
  const InfoPlistContents = `<?xml version="1.0" encoding="UTF-8"?>
1069
1094
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1070
1095
  <plist version="1.0">
@@ -1080,7 +1105,7 @@ if (commandArg === "init") {
1080
1105
  <key>CFBundlePackageType</key>
1081
1106
  <string>APPL</string>
1082
1107
  <key>CFBundleIconFile</key>
1083
- <string>AppIcon</string>${usageDescriptions ? '\n' + usageDescriptions : ''}
1108
+ <string>AppIcon</string>${usageDescriptions ? '\n' + usageDescriptions : ''}${urlTypes ? '\n' + urlTypes : ''}
1084
1109
  </dict>
1085
1110
  </plist>`;
1086
1111