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.
- package/dist/api/browser/rpc/webview.ts +4 -0
- package/dist/api/browser/webviewtag.ts +13 -1
- package/dist/api/bun/ElectrobunConfig.ts +20 -2
- package/dist/api/bun/core/BrowserView.ts +16 -1
- package/dist/api/bun/core/BrowserWindow.ts +40 -5
- package/dist/api/bun/core/Utils.ts +216 -0
- package/dist/api/bun/events/ApplicationEvents.ts +5 -0
- package/dist/api/bun/index.ts +17 -0
- package/dist/api/bun/proc/native.ts +725 -16
- package/package.json +1 -1
- package/src/cli/index.ts +31 -6
|
@@ -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
|
+
};
|
package/dist/api/bun/index.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
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
|
|