electrobun 1.18.1 → 1.18.4-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +37 -14
  2. package/dash.config.ts +28 -0
  3. package/dist/api/browser/global.d.ts +6 -0
  4. package/dist/api/browser/index.ts +162 -44
  5. package/dist/api/{bun → config}/ElectrobunConfig.ts +109 -40
  6. package/dist/api/config/validate.test.ts +31 -0
  7. package/dist/api/config/validate.ts +19 -0
  8. package/dist/api/preload/.generated/compiled.ts +8 -0
  9. package/dist/api/{bun/preload → preload}/build.ts +6 -6
  10. package/dist/api/{bun/preload → preload}/encryption.ts +11 -5
  11. package/dist/api/{bun/preload → preload}/globals.d.ts +8 -0
  12. package/dist/api/{bun/preload → preload}/index.ts +18 -8
  13. package/dist/api/{bun/preload → preload}/webviewTag.ts +32 -3
  14. package/dist/api/{bun/preload → preload}/wgpuTag.ts +33 -2
  15. package/dist/api/{bun → sdks/bun}/__tests__/ffi-contract.test.ts +6 -9
  16. package/dist/api/{bun → sdks/bun}/core/BrowserView.ts +96 -55
  17. package/dist/api/{bun → sdks/bun}/core/BrowserWindow.ts +20 -30
  18. package/dist/api/{bun → sdks/bun}/core/BuildConfig.ts +32 -5
  19. package/dist/api/{bun → sdks/bun}/core/GpuWindow.ts +20 -7
  20. package/dist/api/sdks/bun/core/Socket.ts +22 -0
  21. package/dist/api/{bun → sdks/bun}/core/Tray.ts +33 -32
  22. package/dist/api/{bun → sdks/bun}/core/Updater.ts +10 -10
  23. package/dist/api/{bun → sdks/bun}/core/Utils.ts +3 -5
  24. package/dist/api/{bun → sdks/bun}/core/WGPUView.ts +43 -13
  25. package/dist/api/{bun → sdks/bun}/index.ts +39 -10
  26. package/dist/api/{bun → sdks/bun}/proc/native.ts +942 -746
  27. package/dist/api/{bun → sdks/bun}/webGPU.ts +1 -1
  28. package/dist/api/{bun → sdks/bun}/webgpuAdapter.ts +40 -33
  29. package/dist/api/shared/build-dependencies.test.ts +48 -0
  30. package/dist/api/shared/build-dependencies.ts +46 -0
  31. package/dist/api/shared/go-version.ts +3 -0
  32. package/dist/api/shared/odin-version.ts +6 -0
  33. package/dist/api/shared/rust-version.ts +3 -0
  34. package/dist/go-sdk/callbacks.go +260 -0
  35. package/dist/go-sdk/electrobun.go +2021 -0
  36. package/dist/main.js +35 -28
  37. package/dist/odin-sdk/electrobun/electrobun.odin +2337 -0
  38. package/dist/preload-full.js +948 -0
  39. package/dist/preload-sandboxed.js +111 -0
  40. package/dist/rust-sdk/electrobun.rs +2396 -0
  41. package/dist/zig-sdk/electrobun.zig +2005 -0
  42. package/package.json +5 -29
  43. package/src/cli/index.ts +868 -654
  44. package/bin/electrobun.cjs +0 -169
  45. package/dist/api/bun/core/Socket.ts +0 -205
  46. package/dist/api/bun/core/windowIds.ts +0 -5
  47. package/dist/api/bun/preload/.generated/compiled.ts +0 -8
  48. package/dist/api/shared/bun-version.ts +0 -3
  49. /package/dist/api/{bun/preload → preload}/dragRegions.ts +0 -0
  50. /package/dist/api/{bun/preload → preload}/events.ts +0 -0
  51. /package/dist/api/{bun/preload → preload}/index-sandboxed.ts +0 -0
  52. /package/dist/api/{bun/preload → preload}/internalRpc.ts +0 -0
  53. /package/dist/api/{bun/preload → preload}/overlaySync.ts +0 -0
  54. /package/dist/api/{bun → sdks/bun}/core/ApplicationMenu.ts +0 -0
  55. /package/dist/api/{bun → sdks/bun}/core/ContextMenu.ts +0 -0
  56. /package/dist/api/{bun → sdks/bun}/core/Paths.ts +0 -0
  57. /package/dist/api/{bun → sdks/bun}/core/menuRoles.ts +0 -0
  58. /package/dist/api/{bun → sdks/bun}/events/ApplicationEvents.ts +0 -0
  59. /package/dist/api/{bun → sdks/bun}/events/event.ts +0 -0
  60. /package/dist/api/{bun → sdks/bun}/events/eventEmitter.ts +0 -0
  61. /package/dist/api/{bun → sdks/bun}/events/trayEvents.ts +0 -0
  62. /package/dist/api/{bun → sdks/bun}/events/webviewEvents.ts +0 -0
  63. /package/dist/api/{bun → sdks/bun}/events/windowEvents.ts +0 -0
  64. /package/dist/api/{bun → sdks/bun}/proc/linux.md +0 -0
@@ -0,0 +1,2337 @@
1
+ // Electrobun Odin SDK.
2
+ //
3
+ // A faithful port of package/src/sdks/zig/electrobun.zig. Dynamically loads the
4
+ // Electrobun native wrapper shared library (libElectrobunCore) at runtime and
5
+ // wraps its C ABI entry points.
6
+ //
7
+ // Naming follows the zig SDK (camelCase procs) so that porting a zig main to
8
+ // Odin is mechanical: zig `core.createWindow(options)` becomes
9
+ // `electrobun.createWindow(&core, options)`.
10
+ package electrobun
11
+
12
+ import "base:runtime"
13
+ import "core:c"
14
+ import "core:dynlib"
15
+ import "core:encoding/json"
16
+ import "core:fmt"
17
+ import "core:os"
18
+ import "core:path/filepath"
19
+ import "core:reflect"
20
+ import "core:strings"
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Errors
24
+ // ---------------------------------------------------------------------------
25
+
26
+ Error :: enum {
27
+ None,
28
+ MissingCoreSymbol,
29
+ LibraryLoadFailed,
30
+ ElectrobunCoreFailure,
31
+ InvalidExePath,
32
+ InvalidRectJson,
33
+ InvalidJson,
34
+ FileReadFailed,
35
+ EnvVarNotFound,
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // C ABI callback types (must match the zig SDK's callconv(.C) handler types)
40
+ // ---------------------------------------------------------------------------
41
+
42
+ WindowCloseHandler :: proc "c" (u32)
43
+ WindowMoveHandler :: proc "c" (u32, f64, f64)
44
+ WindowResizeHandler :: proc "c" (u32, f64, f64, f64, f64)
45
+ WindowFocusHandler :: proc "c" (u32)
46
+ WindowBlurHandler :: proc "c" (u32)
47
+ WindowKeyHandler :: proc "c" (u32, u32, u32, u32, u32)
48
+ DecideNavigationHandler :: proc "c" (u32, cstring) -> u32
49
+ WebviewEventHandler :: proc "c" (u32, cstring, cstring)
50
+ WebviewPostMessageHandler :: proc "c" (u32, cstring)
51
+ StatusItemHandler :: proc "c" (u32, cstring)
52
+ GlobalShortcutHandler :: proc "c" (cstring)
53
+ QuitRequestedHandler :: proc "c" ()
54
+ URLOpenHandler :: proc "c" (cstring)
55
+ AppReopenHandler :: proc "c" ()
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Public data types
59
+ // ---------------------------------------------------------------------------
60
+
61
+ Renderer :: enum {
62
+ native,
63
+ cef,
64
+ }
65
+
66
+ rendererName :: proc(renderer: Renderer) -> string {
67
+ switch renderer {
68
+ case .native:
69
+ return "native"
70
+ case .cef:
71
+ return "cef"
72
+ }
73
+ return "native"
74
+ }
75
+
76
+ AppInfo :: struct {
77
+ identifier: string,
78
+ name: string,
79
+ channel: string,
80
+ }
81
+
82
+ OwnedAppInfo :: struct {
83
+ identifier: string,
84
+ name: string,
85
+ channel: string,
86
+ }
87
+
88
+ ownedAppInfoDeinit :: proc(self: ^OwnedAppInfo, allocator: runtime.Allocator) {
89
+ delete(self.identifier, allocator)
90
+ delete(self.name, allocator)
91
+ delete(self.channel, allocator)
92
+ }
93
+
94
+ borrowed :: proc(self: OwnedAppInfo) -> AppInfo {
95
+ return {identifier = self.identifier, name = self.name, channel = self.channel}
96
+ }
97
+
98
+ Rect :: struct {
99
+ x: f64,
100
+ y: f64,
101
+ width: f64,
102
+ height: f64,
103
+ }
104
+
105
+ DEFAULT_RECT :: Rect{0, 0, 800, 600}
106
+
107
+ TrafficLightOffset :: struct {
108
+ x: f64,
109
+ y: f64,
110
+ }
111
+
112
+ WindowStyle :: struct {
113
+ borderless: bool,
114
+ titled: bool,
115
+ closable: bool,
116
+ miniaturizable: bool,
117
+ resizable: bool,
118
+ unified_title_and_toolbar: bool,
119
+ full_screen: bool,
120
+ full_size_content_view: bool,
121
+ utility_window: bool,
122
+ doc_modal_window: bool,
123
+ nonactivating_panel: bool,
124
+ hud_window: bool,
125
+ }
126
+
127
+ DEFAULT_WINDOW_STYLE :: WindowStyle {
128
+ titled = true,
129
+ closable = true,
130
+ miniaturizable = true,
131
+ resizable = true,
132
+ }
133
+
134
+ WindowCallbacks :: struct {
135
+ close: WindowCloseHandler,
136
+ move: WindowMoveHandler,
137
+ resize: WindowResizeHandler,
138
+ focus: WindowFocusHandler,
139
+ blur: WindowBlurHandler,
140
+ key: WindowKeyHandler,
141
+ }
142
+
143
+ WindowOptions :: struct {
144
+ title: string,
145
+ frame: Rect,
146
+ style: WindowStyle,
147
+ title_bar_style: string,
148
+ transparent: bool,
149
+ hidden: bool,
150
+ activate: bool,
151
+ traffic_light_offset: TrafficLightOffset,
152
+ callbacks: WindowCallbacks,
153
+ }
154
+
155
+ // Mirrors the zig SDK's WindowOptions default field values.
156
+ defaultWindowOptions :: proc(title: string) -> WindowOptions {
157
+ return {
158
+ title = title,
159
+ frame = DEFAULT_RECT,
160
+ style = DEFAULT_WINDOW_STYLE,
161
+ title_bar_style = "default",
162
+ activate = true,
163
+ }
164
+ }
165
+
166
+ WebviewCallbacks :: struct {
167
+ decide_navigation: DecideNavigationHandler,
168
+ event: WebviewEventHandler,
169
+ event_bridge: WebviewPostMessageHandler,
170
+ host_bridge: WebviewPostMessageHandler,
171
+ bun_bridge: WebviewPostMessageHandler,
172
+ internal_bridge: WebviewPostMessageHandler,
173
+ }
174
+
175
+ WebviewOptions :: struct {
176
+ window_id: u32,
177
+ host_webview_id: u32,
178
+ renderer: Renderer,
179
+ url: string,
180
+ frame: Rect,
181
+ auto_resize: bool,
182
+ partition: string,
183
+ callbacks: WebviewCallbacks,
184
+ secret_key: string,
185
+ preload: string,
186
+ views_root: string,
187
+ sandbox: bool,
188
+ start_transparent: bool,
189
+ start_passthrough: bool,
190
+ }
191
+
192
+ // Mirrors the zig SDK's WebviewOptions default field values.
193
+ defaultWebviewOptions :: proc(window_id: u32) -> WebviewOptions {
194
+ return {
195
+ window_id = window_id,
196
+ renderer = .native,
197
+ frame = DEFAULT_RECT,
198
+ auto_resize = true,
199
+ partition = "persist:default",
200
+ sandbox = true,
201
+ }
202
+ }
203
+
204
+ WGPUViewOptions :: struct {
205
+ window_id: u32,
206
+ frame: Rect,
207
+ auto_resize: bool,
208
+ start_transparent: bool,
209
+ start_passthrough: bool,
210
+ }
211
+
212
+ defaultWGPUViewOptions :: proc(window_id: u32) -> WGPUViewOptions {
213
+ return {window_id = window_id, frame = DEFAULT_RECT, auto_resize = true}
214
+ }
215
+
216
+ TrayOptions :: struct {
217
+ title: string,
218
+ image: string,
219
+ is_template: bool,
220
+ width: u32,
221
+ height: u32,
222
+ }
223
+
224
+ defaultTrayOptions :: proc(image: string) -> TrayOptions {
225
+ return {image = image, width = 18, height = 18}
226
+ }
227
+
228
+ Display :: struct {
229
+ id: i64,
230
+ bounds: Rect,
231
+ workArea: Rect,
232
+ scaleFactor: f64,
233
+ isPrimary: bool,
234
+ }
235
+
236
+ Point :: struct {
237
+ x: f64,
238
+ y: f64,
239
+ }
240
+
241
+ NotificationOptions :: struct {
242
+ title: string,
243
+ body: string,
244
+ subtitle: string,
245
+ silent: bool,
246
+ }
247
+
248
+ Cookie :: struct {
249
+ name: string,
250
+ value: string,
251
+ domain: Maybe(string),
252
+ path: Maybe(string),
253
+ secure: Maybe(bool),
254
+ httpOnly: Maybe(bool),
255
+ sameSite: Maybe(string),
256
+ expirationDate: Maybe(f64),
257
+ }
258
+
259
+ CookieFilter :: struct {
260
+ url: Maybe(string),
261
+ name: Maybe(string),
262
+ domain: Maybe(string),
263
+ path: Maybe(string),
264
+ secure: Maybe(bool),
265
+ session: Maybe(bool),
266
+ }
267
+
268
+ StorageType :: enum {
269
+ cookies,
270
+ localStorage,
271
+ sessionStorage,
272
+ indexedDB,
273
+ webSQL,
274
+ cache,
275
+ all,
276
+ }
277
+
278
+ @(private = "file")
279
+ STORAGE_TYPE_NAMES :: [StorageType]string {
280
+ .cookies = "cookies",
281
+ .localStorage = "localStorage",
282
+ .sessionStorage = "sessionStorage",
283
+ .indexedDB = "indexedDB",
284
+ .webSQL = "webSQL",
285
+ .cache = "cache",
286
+ .all = "all",
287
+ }
288
+
289
+ OpenFileDialogOptions :: struct {
290
+ starting_folder: string,
291
+ allowed_file_types: string,
292
+ can_choose_files: bool,
293
+ can_choose_directory: bool,
294
+ allows_multiple_selection: bool,
295
+ }
296
+
297
+ defaultOpenFileDialogOptions :: proc() -> OpenFileDialogOptions {
298
+ return {
299
+ starting_folder = "~/",
300
+ allowed_file_types = "*",
301
+ can_choose_files = true,
302
+ can_choose_directory = true,
303
+ allows_multiple_selection = true,
304
+ }
305
+ }
306
+
307
+ DEFAULT_MESSAGE_BOX_BUTTONS := []string{"OK"}
308
+
309
+ MessageBoxOptions :: struct {
310
+ box_type: string,
311
+ title: string,
312
+ message: string,
313
+ detail: string,
314
+ buttons: []string,
315
+ default_id: c.int,
316
+ cancel_id: c.int,
317
+ }
318
+
319
+ defaultMessageBoxOptions :: proc() -> MessageBoxOptions {
320
+ return {box_type = "info", buttons = DEFAULT_MESSAGE_BOX_BUTTONS, default_id = 0, cancel_id = -1}
321
+ }
322
+
323
+ Paths :: struct {
324
+ home: string,
325
+ appData: string,
326
+ config: string,
327
+ cache: string,
328
+ temp: string,
329
+ logs: string,
330
+ documents: string,
331
+ downloads: string,
332
+ desktop: string,
333
+ pictures: string,
334
+ music: string,
335
+ videos: string,
336
+ userData: string,
337
+ userCache: string,
338
+ userLogs: string,
339
+ }
340
+
341
+ pathsDeinit :: proc(self: ^Paths, allocator: runtime.Allocator) {
342
+ delete(self.home, allocator)
343
+ delete(self.appData, allocator)
344
+ delete(self.config, allocator)
345
+ delete(self.cache, allocator)
346
+ delete(self.temp, allocator)
347
+ delete(self.logs, allocator)
348
+ delete(self.documents, allocator)
349
+ delete(self.downloads, allocator)
350
+ delete(self.desktop, allocator)
351
+ delete(self.pictures, allocator)
352
+ delete(self.music, allocator)
353
+ delete(self.videos, allocator)
354
+ delete(self.userData, allocator)
355
+ delete(self.userCache, allocator)
356
+ delete(self.userLogs, allocator)
357
+ }
358
+
359
+ // Mirrors zig's Paths.resolve.
360
+ resolvePaths :: proc(allocator: runtime.Allocator, app_info: AppInfo) -> (paths: Paths, err: Error) {
361
+ defer if err != .None {
362
+ pathsDeinit(&paths, allocator)
363
+ }
364
+
365
+ paths.home = get_home_dir(allocator) or_return
366
+ paths.appData = get_app_data_dir(allocator, paths.home)
367
+ paths.config = get_config_dir(allocator, paths.home)
368
+ paths.cache = get_cache_dir(allocator, paths.home)
369
+ paths.temp = get_temp_dir(allocator, paths.home)
370
+ paths.logs = get_logs_dir(allocator, paths.home)
371
+
372
+ paths.documents = get_user_dir(allocator, paths.home, "Documents", "Documents", "XDG_DOCUMENTS_DIR", "Documents")
373
+ paths.downloads = get_user_dir(allocator, paths.home, "Downloads", "Downloads", "XDG_DOWNLOAD_DIR", "Downloads")
374
+ paths.desktop = get_user_dir(allocator, paths.home, "Desktop", "Desktop", "XDG_DESKTOP_DIR", "Desktop")
375
+ paths.pictures = get_user_dir(allocator, paths.home, "Pictures", "Pictures", "XDG_PICTURES_DIR", "Pictures")
376
+ paths.music = get_user_dir(allocator, paths.home, "Music", "Music", "XDG_MUSIC_DIR", "Music")
377
+ paths.videos = get_user_dir(allocator, paths.home, "Movies", "Videos", "XDG_VIDEOS_DIR", "Videos")
378
+
379
+ paths.userData = build_app_scoped_dir(allocator, paths.appData, app_info)
380
+ paths.userCache = build_app_scoped_dir(allocator, paths.cache, app_info)
381
+ paths.userLogs = build_app_scoped_dir(allocator, paths.logs, app_info)
382
+
383
+ return paths, .None
384
+ }
385
+
386
+ // ---------------------------------------------------------------------------
387
+ // Bundle paths / app info
388
+ // ---------------------------------------------------------------------------
389
+
390
+ BundlePaths :: struct {
391
+ exe_dir: string,
392
+ resources_dir: string,
393
+ }
394
+
395
+ bundlePathsDeinit :: proc(self: ^BundlePaths, allocator: runtime.Allocator) {
396
+ delete(self.exe_dir, allocator)
397
+ delete(self.resources_dir, allocator)
398
+ }
399
+
400
+ resolveBundlePaths :: proc(allocator := context.allocator) -> (bundle_paths: BundlePaths, err: Error) {
401
+ exe_path, exe_err := os.get_executable_path(allocator)
402
+ if exe_err != nil {
403
+ return {}, .InvalidExePath
404
+ }
405
+ defer delete(exe_path, allocator)
406
+
407
+ exe_dir_name := os.dir(exe_path)
408
+ if len(exe_dir_name) == 0 {
409
+ return {}, .InvalidExePath
410
+ }
411
+
412
+ bundle_paths.exe_dir = clone_string(exe_dir_name, allocator)
413
+ bundle_paths.resources_dir = join_path(allocator, {exe_dir_name, "..", "Resources"})
414
+ return bundle_paths, .None
415
+ }
416
+
417
+ resolveAppInfoFromBundle :: proc(
418
+ allocator: runtime.Allocator,
419
+ bundle_paths: ^BundlePaths,
420
+ ) -> (
421
+ app_info: OwnedAppInfo,
422
+ err: Error,
423
+ ) {
424
+ version_json_path := join_path(allocator, {bundle_paths.resources_dir, "version.json"})
425
+ defer delete(version_json_path, allocator)
426
+
427
+ version_json, read_err := os.read_entire_file(version_json_path, allocator)
428
+ if read_err != nil {
429
+ return {}, .FileReadFailed
430
+ }
431
+ defer delete(version_json, allocator)
432
+
433
+ value, parse_err := json.parse(version_json, json.DEFAULT_SPECIFICATION, true, allocator)
434
+ if parse_err != .None {
435
+ return {}, .InvalidJson
436
+ }
437
+ defer json.destroy_value(value, allocator)
438
+
439
+ obj, is_object := value.(json.Object)
440
+ if !is_object {
441
+ return {}, .InvalidJson
442
+ }
443
+
444
+ identifier, identifier_ok := object_string(obj, "identifier")
445
+ name, name_ok := object_string(obj, "name")
446
+ channel, channel_ok := object_string(obj, "channel")
447
+ if !identifier_ok || !name_ok || !channel_ok {
448
+ return {}, .InvalidJson
449
+ }
450
+
451
+ app_info.identifier = clone_string(identifier, allocator)
452
+ app_info.name = clone_string(name, allocator)
453
+ app_info.channel = clone_string(channel, allocator)
454
+ return app_info, .None
455
+ }
456
+
457
+ // ---------------------------------------------------------------------------
458
+ // Window registry
459
+ // ---------------------------------------------------------------------------
460
+
461
+ BrowserWindowRef :: struct {
462
+ registry: ^WindowRegistry,
463
+ id: u32,
464
+ }
465
+
466
+ WindowRegistry :: struct {
467
+ allocator: runtime.Allocator,
468
+ core: ^Core,
469
+ ids: map[u32]bool,
470
+ }
471
+
472
+ windowRegistryInit :: proc(allocator: runtime.Allocator, core: ^Core) -> WindowRegistry {
473
+ return {allocator = allocator, core = core, ids = make(map[u32]bool, allocator)}
474
+ }
475
+
476
+ windowRegistryDeinit :: proc(self: ^WindowRegistry) {
477
+ delete(self.ids)
478
+ }
479
+
480
+ createBrowserWindow :: proc(self: ^WindowRegistry, options: WindowOptions) -> (window: BrowserWindowRef, err: Error) {
481
+ id := createWindow(self.core, options) or_return
482
+ self.ids[id] = true
483
+ return BrowserWindowRef{registry = self, id = id}, .None
484
+ }
485
+
486
+ getById :: proc(self: ^WindowRegistry, id: u32) -> (window: BrowserWindowRef, ok: bool) {
487
+ if id not_in self.ids {
488
+ return {}, false
489
+ }
490
+ return BrowserWindowRef{registry = self, id = id}, true
491
+ }
492
+
493
+ windowClose :: proc(self: BrowserWindowRef) -> Error {
494
+ closeWindow(self.registry.core, self.id) or_return
495
+ delete_key(&self.registry.ids, self.id)
496
+ return .None
497
+ }
498
+
499
+ getFrame :: proc(self: BrowserWindowRef) -> (Rect, Error) {
500
+ return getWindowFrame(self.registry.core, self.id)
501
+ }
502
+
503
+ windowSetWindowButtonPosition :: proc(self: BrowserWindowRef, x: f64, y: f64) -> Error {
504
+ return coreSetWindowButtonPosition(self.registry.core, self.id, x, y)
505
+ }
506
+
507
+ // ---------------------------------------------------------------------------
508
+ // Sessions
509
+ // ---------------------------------------------------------------------------
510
+
511
+ SessionPartition :: struct {
512
+ core: ^Core,
513
+ partition: string,
514
+ }
515
+
516
+ // Mirrors zig's Session.fromPartition.
517
+ sessionFromPartition :: proc(core: ^Core, partition: string) -> SessionPartition {
518
+ return {core = core, partition = partition}
519
+ }
520
+
521
+ // Mirrors zig's Session.defaultSession.
522
+ defaultSession :: proc(core: ^Core) -> SessionPartition {
523
+ return sessionFromPartition(core, "persist:default")
524
+ }
525
+
526
+ getCookies :: proc(self: SessionPartition, filter: Maybe(CookieFilter) = nil) -> ([]Cookie, Error) {
527
+ filter_value := filter.? or_else CookieFilter{}
528
+ filter_json := marshal_cookie_filter(self.core.allocator, filter_value)
529
+ defer delete(filter_json, self.core.allocator)
530
+ return sessionGetCookies(self.core, self.partition, filter_json)
531
+ }
532
+
533
+ setCookie :: proc(self: SessionPartition, cookie: Cookie) -> bool {
534
+ cookie_json := marshal_cookie(self.core.allocator, cookie)
535
+ defer delete(cookie_json, self.core.allocator)
536
+ return sessionSetCookie(self.core, self.partition, cookie_json)
537
+ }
538
+
539
+ removeCookie :: proc(self: SessionPartition, url: string, name: string) -> bool {
540
+ return sessionRemoveCookie(self.core, self.partition, url, name)
541
+ }
542
+
543
+ clearCookies :: proc(self: SessionPartition) -> Error {
544
+ return sessionClearCookies(self.core, self.partition)
545
+ }
546
+
547
+ clearStorageData :: proc(self: SessionPartition, storage_types: []StorageType) -> Error {
548
+ if len(storage_types) == 0 {
549
+ return sessionClearStorageData(self.core, self.partition, "[\"all\"]")
550
+ }
551
+
552
+ names := STORAGE_TYPE_NAMES
553
+ b := strings.builder_make(self.core.allocator)
554
+ defer strings.builder_destroy(&b)
555
+ strings.write_string(&b, "[")
556
+ for storage_type, index in storage_types {
557
+ if index > 0 {
558
+ strings.write_string(&b, ",")
559
+ }
560
+ write_json_string(&b, names[storage_type])
561
+ }
562
+ strings.write_string(&b, "]")
563
+ return sessionClearStorageData(self.core, self.partition, strings.to_string(b))
564
+ }
565
+
566
+ // ---------------------------------------------------------------------------
567
+ // WGPU (dawn) native library
568
+ // ---------------------------------------------------------------------------
569
+
570
+ WgpuAdapterDevice :: struct {
571
+ adapter: rawptr,
572
+ device: rawptr,
573
+ }
574
+
575
+ WgpuCreateInstanceFn :: proc "c" (rawptr) -> rawptr
576
+ WgpuDeviceGetQueueFn :: proc "c" (rawptr) -> rawptr
577
+
578
+ WgpuSymbols :: struct {
579
+ __handle: dynlib.Library,
580
+ wgpuCreateInstance: WgpuCreateInstanceFn,
581
+ wgpuDeviceGetQueue: WgpuDeviceGetQueueFn,
582
+ }
583
+
584
+ WgpuNative :: struct {
585
+ symbols: WgpuSymbols,
586
+ }
587
+
588
+ when ODIN_OS == .Windows {
589
+ @(private = "file")
590
+ CORE_LIB_NAME :: "ElectrobunCore.dll"
591
+ @(private = "file")
592
+ WGPU_LIB_NAME :: "webgpu_dawn.dll"
593
+ } else when ODIN_OS == .Darwin {
594
+ @(private = "file")
595
+ CORE_LIB_NAME :: "libElectrobunCore.dylib"
596
+ @(private = "file")
597
+ WGPU_LIB_NAME :: "libwebgpu_dawn.dylib"
598
+ } else {
599
+ @(private = "file")
600
+ CORE_LIB_NAME :: "libElectrobunCore.so"
601
+ @(private = "file")
602
+ WGPU_LIB_NAME :: "libwebgpu_dawn.so"
603
+ }
604
+
605
+ // Mirrors zig's WgpuNative.load.
606
+ wgpuNativeLoad :: proc(allocator := context.allocator) -> (native: WgpuNative, err: Error) {
607
+ bundle_paths := resolveBundlePaths(allocator) or_return
608
+ defer bundlePathsDeinit(&bundle_paths, allocator)
609
+
610
+ lib_path := join_path(allocator, {bundle_paths.exe_dir, WGPU_LIB_NAME})
611
+ defer delete(lib_path, allocator)
612
+
613
+ count, _ := dynlib.initialize_symbols(&native.symbols, lib_path)
614
+ if count == -1 || native.symbols.__handle == nil {
615
+ fmt.eprintf("[electrobun-odin] failed to load %s: %s\n", lib_path, dynlib.last_error())
616
+ return {}, .LibraryLoadFailed
617
+ }
618
+ if name, missing := missing_symbol_name(&native.symbols); missing {
619
+ fmt.eprintf("[electrobun-odin] missing wgpu symbol: %s\n", name)
620
+ dynlib.unload_library(native.symbols.__handle)
621
+ return {}, .MissingCoreSymbol
622
+ }
623
+ return native, .None
624
+ }
625
+
626
+ wgpuNativeClose :: proc(self: ^WgpuNative) {
627
+ dynlib.unload_library(self.symbols.__handle)
628
+ self.symbols.__handle = nil
629
+ }
630
+
631
+ createInstance :: proc(self: ^WgpuNative) -> rawptr {
632
+ return self.symbols.wgpuCreateInstance(nil)
633
+ }
634
+
635
+ deviceGetQueue :: proc(self: ^WgpuNative, device: rawptr) -> rawptr {
636
+ return self.symbols.wgpuDeviceGetQueue(device)
637
+ }
638
+
639
+ WgpuContext :: struct {
640
+ view_ptr: rawptr,
641
+ instance_ptr: rawptr,
642
+ surface_ptr: rawptr,
643
+ adapter_ptr: rawptr,
644
+ device_ptr: rawptr,
645
+ }
646
+
647
+ createForView :: proc(core: ^Core, native: ^WgpuNative, view_ptr: rawptr) -> (ctx: WgpuContext, err: Error) {
648
+ instance_ptr := createInstance(native)
649
+ if instance_ptr == nil {
650
+ return {}, .ElectrobunCoreFailure
651
+ }
652
+ surface_ptr := wgpuCreateSurfaceForView(core, instance_ptr, view_ptr) or_return
653
+
654
+ adapter_device := [2]uintptr{0, 0}
655
+ wgpuCreateAdapterDeviceMainThread(core, instance_ptr, surface_ptr, &adapter_device) or_return
656
+
657
+ adapter_ptr := rawptr(adapter_device[0])
658
+ device_ptr := rawptr(adapter_device[1])
659
+ if device_ptr == nil {
660
+ return {}, .ElectrobunCoreFailure
661
+ }
662
+
663
+ return WgpuContext{
664
+ view_ptr = view_ptr,
665
+ instance_ptr = instance_ptr,
666
+ surface_ptr = surface_ptr,
667
+ adapter_ptr = adapter_ptr,
668
+ device_ptr = device_ptr,
669
+ },
670
+ .None
671
+ }
672
+
673
+ createForWgpuView :: proc(core: ^Core, native: ^WgpuNative, wgpu_view_id: u32) -> (ctx: WgpuContext, err: Error) {
674
+ view_ptr := getWGPUViewPointer(core, wgpu_view_id) or_return
675
+ return createForView(core, native, view_ptr)
676
+ }
677
+
678
+ getQueue :: proc(self: WgpuContext, native: ^WgpuNative) -> rawptr {
679
+ return deviceGetQueue(native, self.device_ptr)
680
+ }
681
+
682
+ // ---------------------------------------------------------------------------
683
+ // Core symbol table (field names are the exact C ABI symbol names)
684
+ // ---------------------------------------------------------------------------
685
+
686
+ LastErrorFn :: proc "c" () -> cstring
687
+ RunMainThreadFn :: proc "c" (cstring, cstring, cstring, c.int) -> c.int
688
+ ConfigureWebviewRuntimeFn :: proc "c" (u32, cstring, cstring) -> bool
689
+ GetWindowStyleFn :: proc "c" (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) -> u32
690
+ CreateWindowFn :: proc "c" (f64, f64, f64, f64, u32, cstring, bool, cstring, bool, bool, f64, f64, WindowCloseHandler, WindowMoveHandler, WindowResizeHandler, WindowFocusHandler, WindowBlurHandler, WindowKeyHandler) -> u32
691
+ CreateWebviewFn :: proc "c" (u32, u32, cstring, cstring, f64, f64, f64, f64, bool, cstring, DecideNavigationHandler, WebviewEventHandler, WebviewPostMessageHandler, WebviewPostMessageHandler, WebviewPostMessageHandler, cstring, cstring, cstring, bool, bool, bool) -> u32
692
+ CreateWGPUViewFn :: proc "c" (u32, f64, f64, f64, f64, bool, bool, bool) -> u32
693
+ SetWindowTitleFn :: proc "c" (u32, cstring)
694
+ WindowIdFn :: proc "c" (u32)
695
+ WindowIdBoolFn :: proc "c" (u32) -> bool
696
+ SetWindowBoolFn :: proc "c" (u32, bool)
697
+ SetWindowXYFn :: proc "c" (u32, f64, f64)
698
+ SetWindowFrameFn :: proc "c" (u32, f64, f64, f64, f64)
699
+ GetWindowFrameFn :: proc "c" (u32, ^f64, ^f64, ^f64, ^f64)
700
+ ResizeViewFn :: proc "c" (u32, f64, f64, f64, f64, cstring)
701
+ IdCstringFn :: proc "c" (u32, cstring)
702
+ UpdatePreloadScriptToWebViewFn :: proc "c" (u32, cstring, cstring, bool)
703
+ SendMessageToWebviewFn :: proc "c" (u32, cstring) -> bool
704
+ PopNextQueuedHostMessageFn :: proc "c" (^u32) -> cstring
705
+ FreeCoreStringFn :: proc "c" (cstring)
706
+ WebviewSetPageZoomFn :: proc "c" (u32, f64)
707
+ WebviewGetPageZoomFn :: proc "c" (u32) -> f64
708
+ WebviewFindInPageFn :: proc "c" (u32, cstring, bool, bool)
709
+ GetViewPointerFn :: proc "c" (u32) -> rawptr
710
+ CreateTrayFn :: proc "c" (cstring, cstring, bool, u32, u32, StatusItemHandler) -> u32
711
+ GetTrayBoundsFn :: proc "c" (u32) -> cstring
712
+ SetBoolFn :: proc "c" (bool)
713
+ GetBoolFn :: proc "c" () -> bool
714
+ GetCstringFn :: proc "c" () -> cstring
715
+ CstringToBoolFn :: proc "c" (cstring) -> bool
716
+ CstringVoidFn :: proc "c" (cstring)
717
+ ShowNotificationFn :: proc "c" (cstring, cstring, cstring, bool)
718
+ VoidFn :: proc "c" ()
719
+ SetMenuFn :: proc "c" (cstring, StatusItemHandler)
720
+ OpenFileDialogFn :: proc "c" (cstring, cstring, c.int, c.int, c.int) -> cstring
721
+ ShowMessageBoxFn :: proc "c" (cstring, cstring, cstring, cstring, cstring, c.int, c.int) -> c.int
722
+ SetGlobalShortcutCallbackFn :: proc "c" (GlobalShortcutHandler)
723
+ SessionGetCookiesFn :: proc "c" (cstring, cstring) -> cstring
724
+ SessionSetCookieFn :: proc "c" (cstring, cstring) -> bool
725
+ SessionRemoveCookieFn :: proc "c" (cstring, cstring, cstring) -> bool
726
+ SetURLOpenHandlerFn :: proc "c" (URLOpenHandler)
727
+ SetAppReopenHandlerFn :: proc "c" (AppReopenHandler)
728
+ SetQuitRequestedHandlerFn :: proc "c" (QuitRequestedHandler)
729
+ IntVoidFn :: proc "c" (c.int)
730
+ WgpuCreateSurfaceForViewFn :: proc "c" (rawptr, rawptr) -> rawptr
731
+ WgpuTwoPtrVoidFn :: proc "c" (rawptr, rawptr)
732
+ WgpuThreePtrVoidFn :: proc "c" (rawptr, rawptr, rawptr)
733
+ WgpuPresentFn :: proc "c" (rawptr) -> i32
734
+
735
+ Symbols :: struct {
736
+ __handle: dynlib.Library,
737
+ electrobun_core_last_error: LastErrorFn,
738
+ electrobun_core_run_main_thread: RunMainThreadFn,
739
+ configureWebviewRuntime: ConfigureWebviewRuntimeFn,
740
+ getWindowStyle: GetWindowStyleFn,
741
+ createWindow: CreateWindowFn,
742
+ createWebview: CreateWebviewFn,
743
+ createWGPUView: CreateWGPUViewFn,
744
+ setWindowTitle: SetWindowTitleFn,
745
+ minimizeWindow: WindowIdFn,
746
+ restoreWindow: WindowIdFn,
747
+ isWindowMinimized: WindowIdBoolFn,
748
+ maximizeWindow: WindowIdFn,
749
+ unmaximizeWindow: WindowIdFn,
750
+ isWindowMaximized: WindowIdBoolFn,
751
+ setWindowFullScreen: SetWindowBoolFn,
752
+ isWindowFullScreen: WindowIdBoolFn,
753
+ setWindowAlwaysOnTop: SetWindowBoolFn,
754
+ isWindowAlwaysOnTop: WindowIdBoolFn,
755
+ setWindowVisibleOnAllWorkspaces: SetWindowBoolFn,
756
+ isWindowVisibleOnAllWorkspaces: WindowIdBoolFn,
757
+ showWindow: SetWindowBoolFn,
758
+ activateWindow: WindowIdFn,
759
+ hideWindow: WindowIdFn,
760
+ setWindowButtonPosition: SetWindowXYFn,
761
+ setWindowPosition: SetWindowXYFn,
762
+ setWindowSize: SetWindowXYFn,
763
+ setWindowFrame: SetWindowFrameFn,
764
+ getWindowFrame: GetWindowFrameFn,
765
+ closeWindow: WindowIdFn,
766
+ resizeWebview: ResizeViewFn,
767
+ loadURLInWebView: IdCstringFn,
768
+ loadHTMLInWebView: IdCstringFn,
769
+ updatePreloadScriptToWebView: UpdatePreloadScriptToWebViewFn,
770
+ webviewCanGoBack: WindowIdBoolFn,
771
+ webviewCanGoForward: WindowIdBoolFn,
772
+ webviewGoBack: WindowIdFn,
773
+ webviewGoForward: WindowIdFn,
774
+ webviewReload: WindowIdFn,
775
+ webviewRemove: WindowIdFn,
776
+ setWebviewHTMLContent: IdCstringFn,
777
+ webviewSetTransparent: SetWindowBoolFn,
778
+ webviewSetPassthrough: SetWindowBoolFn,
779
+ webviewSetHidden: SetWindowBoolFn,
780
+ setWebviewNavigationRules: IdCstringFn,
781
+ webviewFindInPage: WebviewFindInPageFn,
782
+ webviewStopFind: WindowIdFn,
783
+ sendInternalMessageToWebview: SendMessageToWebviewFn,
784
+ sendHostMessageToWebviewViaTransport: SendMessageToWebviewFn,
785
+ popNextQueuedHostMessage: PopNextQueuedHostMessageFn,
786
+ freeCoreString: FreeCoreStringFn,
787
+ webviewOpenDevTools: WindowIdFn,
788
+ webviewCloseDevTools: WindowIdFn,
789
+ webviewToggleDevTools: WindowIdFn,
790
+ webviewSetPageZoom: WebviewSetPageZoomFn,
791
+ webviewGetPageZoom: WebviewGetPageZoomFn,
792
+ setWGPUViewFrame: SetWindowFrameFn,
793
+ resizeWGPUView: ResizeViewFn,
794
+ setWGPUViewTransparent: SetWindowBoolFn,
795
+ setWGPUViewPassthrough: SetWindowBoolFn,
796
+ setWGPUViewHidden: SetWindowBoolFn,
797
+ removeWGPUView: WindowIdFn,
798
+ getWGPUViewPointer: GetViewPointerFn,
799
+ getWGPUViewNativeHandle: GetViewPointerFn,
800
+ runWGPUViewTest: WindowIdFn,
801
+ toggleWGPUViewTestShader: WindowIdFn,
802
+ evaluateJavaScriptWithNoCompletion: IdCstringFn,
803
+ createTray: CreateTrayFn,
804
+ showTray: WindowIdBoolFn,
805
+ hideTray: WindowIdFn,
806
+ setTrayTitle: IdCstringFn,
807
+ removeTray: WindowIdFn,
808
+ getTrayBounds: GetTrayBoundsFn,
809
+ setDockIconVisible: SetBoolFn,
810
+ isDockIconVisible: GetBoolFn,
811
+ getPrimaryDisplay: GetCstringFn,
812
+ getAllDisplays: GetCstringFn,
813
+ getCursorScreenPoint: GetCstringFn,
814
+ moveToTrash: CstringToBoolFn,
815
+ showItemInFolder: CstringVoidFn,
816
+ openExternal: CstringToBoolFn,
817
+ openPath: CstringToBoolFn,
818
+ showNotification: ShowNotificationFn,
819
+ clipboardReadText: GetCstringFn,
820
+ clipboardWriteText: CstringVoidFn,
821
+ clipboardClear: VoidFn,
822
+ clipboardAvailableFormats: GetCstringFn,
823
+ setApplicationMenu: SetMenuFn,
824
+ showContextMenu: SetMenuFn,
825
+ openFileDialog: OpenFileDialogFn,
826
+ showMessageBox: ShowMessageBoxFn,
827
+ setGlobalShortcutCallback: SetGlobalShortcutCallbackFn,
828
+ registerGlobalShortcut: CstringToBoolFn,
829
+ unregisterGlobalShortcut: CstringToBoolFn,
830
+ unregisterAllGlobalShortcuts: VoidFn,
831
+ isGlobalShortcutRegistered: CstringToBoolFn,
832
+ sessionGetCookies: SessionGetCookiesFn,
833
+ sessionSetCookie: SessionSetCookieFn,
834
+ sessionRemoveCookie: SessionRemoveCookieFn,
835
+ sessionClearCookies: CstringVoidFn,
836
+ sessionClearStorageData: proc "c" (cstring, cstring),
837
+ setURLOpenHandler: SetURLOpenHandlerFn,
838
+ setAppReopenHandler: SetAppReopenHandlerFn,
839
+ setQuitRequestedHandler: SetQuitRequestedHandlerFn,
840
+ stopEventLoop: VoidFn,
841
+ waitForShutdownComplete: IntVoidFn,
842
+ forceExit: IntVoidFn,
843
+ wgpuCreateSurfaceForView: WgpuCreateSurfaceForViewFn,
844
+ wgpuCreateAdapterDeviceMainThread: WgpuThreePtrVoidFn,
845
+ wgpuSurfaceConfigureMainThread: WgpuTwoPtrVoidFn,
846
+ wgpuSurfaceGetCurrentTextureMainThread: WgpuTwoPtrVoidFn,
847
+ wgpuSurfacePresentMainThread: WgpuPresentFn,
848
+ }
849
+
850
+ // ---------------------------------------------------------------------------
851
+ // Core
852
+ // ---------------------------------------------------------------------------
853
+
854
+ Core :: struct {
855
+ allocator: runtime.Allocator,
856
+ symbols: Symbols,
857
+ }
858
+
859
+ // Mirrors zig's Core.load. Loads libElectrobunCore from the executable's
860
+ // directory and resolves every required symbol.
861
+ coreLoad :: proc(allocator := context.allocator) -> (core: Core, err: Error) {
862
+ bundle_paths := resolveBundlePaths(allocator) or_return
863
+ defer bundlePathsDeinit(&bundle_paths, allocator)
864
+
865
+ lib_path := join_path(allocator, {bundle_paths.exe_dir, CORE_LIB_NAME})
866
+ defer delete(lib_path, allocator)
867
+
868
+ core.allocator = allocator
869
+ count, _ := dynlib.initialize_symbols(&core.symbols, lib_path)
870
+ if count == -1 || core.symbols.__handle == nil {
871
+ fmt.eprintf("[electrobun-odin] failed to load %s: %s\n", lib_path, dynlib.last_error())
872
+ return {}, .LibraryLoadFailed
873
+ }
874
+ if name, missing := missing_symbol_name(&core.symbols); missing {
875
+ fmt.eprintf("[electrobun-odin] missing core symbol: %s\n", name)
876
+ dynlib.unload_library(core.symbols.__handle)
877
+ return {}, .MissingCoreSymbol
878
+ }
879
+ return core, .None
880
+ }
881
+
882
+ coreClose :: proc(self: ^Core) {
883
+ dynlib.unload_library(self.symbols.__handle)
884
+ self.symbols.__handle = nil
885
+ }
886
+
887
+ configureWebviewRuntimeFromExecutableDir :: proc(
888
+ self: ^Core,
889
+ bundle_paths: ^BundlePaths,
890
+ rpc_port: u32,
891
+ ) -> Error {
892
+ full_path := join_path(self.allocator, {bundle_paths.resources_dir, "preload-full.js"})
893
+ defer delete(full_path, self.allocator)
894
+ sandboxed_path := join_path(self.allocator, {bundle_paths.resources_dir, "preload-sandboxed.js"})
895
+ defer delete(sandboxed_path, self.allocator)
896
+
897
+ full_preload, full_err := read_file_cstring(self, full_path)
898
+ if full_err != .None {
899
+ return full_err
900
+ }
901
+ defer delete(full_preload, self.allocator)
902
+ sandboxed_preload, sandboxed_err := read_file_cstring(self, sandboxed_path)
903
+ if sandboxed_err != .None {
904
+ return sandboxed_err
905
+ }
906
+ defer delete(sandboxed_preload, self.allocator)
907
+
908
+ if !self.symbols.configureWebviewRuntime(rpc_port, full_preload, sandboxed_preload) {
909
+ return error_from_last_error(last_error_string(self))
910
+ }
911
+ return .None
912
+ }
913
+
914
+ defaultWindowStyle :: proc(self: ^Core) -> u32 {
915
+ return self.symbols.getWindowStyle(
916
+ false,
917
+ true,
918
+ true,
919
+ true,
920
+ true,
921
+ false,
922
+ false,
923
+ false,
924
+ false,
925
+ false,
926
+ false,
927
+ false,
928
+ )
929
+ }
930
+
931
+ createWindow :: proc(self: ^Core, options: WindowOptions) -> (window_id: u32, err: Error) {
932
+ title_z := dupe_cstring(self, options.title)
933
+ defer delete(title_z, self.allocator)
934
+ title_bar_style_z := dupe_cstring(self, options.title_bar_style)
935
+ defer delete(title_bar_style_z, self.allocator)
936
+
937
+ style_mask := self.symbols.getWindowStyle(
938
+ options.style.borderless,
939
+ options.style.titled,
940
+ options.style.closable,
941
+ options.style.miniaturizable,
942
+ options.style.resizable,
943
+ options.style.unified_title_and_toolbar,
944
+ options.style.full_screen,
945
+ options.style.full_size_content_view,
946
+ options.style.utility_window,
947
+ options.style.doc_modal_window,
948
+ options.style.nonactivating_panel,
949
+ options.style.hud_window,
950
+ )
951
+
952
+ window_id = self.symbols.createWindow(
953
+ options.frame.x,
954
+ options.frame.y,
955
+ options.frame.width,
956
+ options.frame.height,
957
+ style_mask,
958
+ title_bar_style_z,
959
+ options.transparent,
960
+ title_z,
961
+ options.hidden,
962
+ options.activate,
963
+ options.traffic_light_offset.x,
964
+ options.traffic_light_offset.y,
965
+ options.callbacks.close,
966
+ options.callbacks.move,
967
+ options.callbacks.resize,
968
+ options.callbacks.focus,
969
+ options.callbacks.blur,
970
+ options.callbacks.key,
971
+ )
972
+
973
+ if window_id == 0 {
974
+ return 0, error_from_last_error(last_error_string(self))
975
+ }
976
+ return window_id, .None
977
+ }
978
+
979
+ setWindowTitle :: proc(self: ^Core, window_id: u32, title: string) -> Error {
980
+ title_z := dupe_cstring(self, title)
981
+ defer delete(title_z, self.allocator)
982
+ self.symbols.setWindowTitle(window_id, title_z)
983
+ return ensure_last_call_succeeded(self)
984
+ }
985
+
986
+ minimizeWindow :: proc(self: ^Core, window_id: u32) -> Error {
987
+ self.symbols.minimizeWindow(window_id)
988
+ return ensure_last_call_succeeded(self)
989
+ }
990
+
991
+ restoreWindow :: proc(self: ^Core, window_id: u32) -> Error {
992
+ self.symbols.restoreWindow(window_id)
993
+ return ensure_last_call_succeeded(self)
994
+ }
995
+
996
+ isWindowMinimized :: proc(self: ^Core, window_id: u32) -> bool {
997
+ return self.symbols.isWindowMinimized(window_id)
998
+ }
999
+
1000
+ maximizeWindow :: proc(self: ^Core, window_id: u32) -> Error {
1001
+ self.symbols.maximizeWindow(window_id)
1002
+ return ensure_last_call_succeeded(self)
1003
+ }
1004
+
1005
+ unmaximizeWindow :: proc(self: ^Core, window_id: u32) -> Error {
1006
+ self.symbols.unmaximizeWindow(window_id)
1007
+ return ensure_last_call_succeeded(self)
1008
+ }
1009
+
1010
+ isWindowMaximized :: proc(self: ^Core, window_id: u32) -> bool {
1011
+ return self.symbols.isWindowMaximized(window_id)
1012
+ }
1013
+
1014
+ setWindowFullScreen :: proc(self: ^Core, window_id: u32, full_screen: bool) -> Error {
1015
+ self.symbols.setWindowFullScreen(window_id, full_screen)
1016
+ return ensure_last_call_succeeded(self)
1017
+ }
1018
+
1019
+ isWindowFullScreen :: proc(self: ^Core, window_id: u32) -> bool {
1020
+ return self.symbols.isWindowFullScreen(window_id)
1021
+ }
1022
+
1023
+ setWindowAlwaysOnTop :: proc(self: ^Core, window_id: u32, always_on_top: bool) -> Error {
1024
+ self.symbols.setWindowAlwaysOnTop(window_id, always_on_top)
1025
+ return ensure_last_call_succeeded(self)
1026
+ }
1027
+
1028
+ isWindowAlwaysOnTop :: proc(self: ^Core, window_id: u32) -> bool {
1029
+ return self.symbols.isWindowAlwaysOnTop(window_id)
1030
+ }
1031
+
1032
+ setWindowVisibleOnAllWorkspaces :: proc(self: ^Core, window_id: u32, visible: bool) -> Error {
1033
+ self.symbols.setWindowVisibleOnAllWorkspaces(window_id, visible)
1034
+ return ensure_last_call_succeeded(self)
1035
+ }
1036
+
1037
+ isWindowVisibleOnAllWorkspaces :: proc(self: ^Core, window_id: u32) -> bool {
1038
+ return self.symbols.isWindowVisibleOnAllWorkspaces(window_id)
1039
+ }
1040
+
1041
+ showWindow :: proc(self: ^Core, window_id: u32, activate: bool) -> Error {
1042
+ self.symbols.showWindow(window_id, activate)
1043
+ return ensure_last_call_succeeded(self)
1044
+ }
1045
+
1046
+ activateWindow :: proc(self: ^Core, window_id: u32) -> Error {
1047
+ self.symbols.activateWindow(window_id)
1048
+ return ensure_last_call_succeeded(self)
1049
+ }
1050
+
1051
+ hideWindow :: proc(self: ^Core, window_id: u32) -> Error {
1052
+ self.symbols.hideWindow(window_id)
1053
+ return ensure_last_call_succeeded(self)
1054
+ }
1055
+
1056
+ coreSetWindowButtonPosition :: proc(self: ^Core, window_id: u32, x: f64, y: f64) -> Error {
1057
+ self.symbols.setWindowButtonPosition(window_id, x, y)
1058
+ return ensure_last_call_succeeded(self)
1059
+ }
1060
+
1061
+ setWindowPosition :: proc(self: ^Core, window_id: u32, x: f64, y: f64) -> Error {
1062
+ self.symbols.setWindowPosition(window_id, x, y)
1063
+ return ensure_last_call_succeeded(self)
1064
+ }
1065
+
1066
+ setWindowSize :: proc(self: ^Core, window_id: u32, width: f64, height: f64) -> Error {
1067
+ self.symbols.setWindowSize(window_id, width, height)
1068
+ return ensure_last_call_succeeded(self)
1069
+ }
1070
+
1071
+ setWindowFrame :: proc(self: ^Core, window_id: u32, frame: Rect) -> Error {
1072
+ self.symbols.setWindowFrame(window_id, frame.x, frame.y, frame.width, frame.height)
1073
+ return ensure_last_call_succeeded(self)
1074
+ }
1075
+
1076
+ getWindowFrame :: proc(self: ^Core, window_id: u32) -> (frame: Rect, err: Error) {
1077
+ x, y, width, height: f64
1078
+ self.symbols.getWindowFrame(window_id, &x, &y, &width, &height)
1079
+ ensure_last_call_succeeded(self) or_return
1080
+ return Rect{x = x, y = y, width = width, height = height}, .None
1081
+ }
1082
+
1083
+ closeWindow :: proc(self: ^Core, window_id: u32) -> Error {
1084
+ self.symbols.closeWindow(window_id)
1085
+ return ensure_last_call_succeeded(self)
1086
+ }
1087
+
1088
+ createWebview :: proc(self: ^Core, options: WebviewOptions) -> (webview_id: u32, err: Error) {
1089
+ renderer_z := dupe_cstring(self, rendererName(options.renderer))
1090
+ defer delete(renderer_z, self.allocator)
1091
+ url_z := dupe_cstring(self, options.url)
1092
+ defer delete(url_z, self.allocator)
1093
+ partition_z := dupe_cstring(self, options.partition)
1094
+ defer delete(partition_z, self.allocator)
1095
+ secret_key_z := dupe_cstring(self, options.secret_key)
1096
+ defer delete(secret_key_z, self.allocator)
1097
+ preload_z := dupe_cstring(self, options.preload)
1098
+ defer delete(preload_z, self.allocator)
1099
+ views_root_z := dupe_cstring(self, options.views_root)
1100
+ defer delete(views_root_z, self.allocator)
1101
+
1102
+ host_bridge := options.callbacks.host_bridge
1103
+ if host_bridge == nil {
1104
+ host_bridge = options.callbacks.bun_bridge
1105
+ }
1106
+
1107
+ webview_id = self.symbols.createWebview(
1108
+ options.window_id,
1109
+ options.host_webview_id,
1110
+ renderer_z,
1111
+ url_z,
1112
+ options.frame.x,
1113
+ options.frame.y,
1114
+ options.frame.width,
1115
+ options.frame.height,
1116
+ options.auto_resize,
1117
+ partition_z,
1118
+ options.callbacks.decide_navigation,
1119
+ options.callbacks.event,
1120
+ options.callbacks.event_bridge,
1121
+ host_bridge,
1122
+ options.callbacks.internal_bridge,
1123
+ secret_key_z,
1124
+ preload_z,
1125
+ views_root_z,
1126
+ options.sandbox,
1127
+ options.start_transparent,
1128
+ options.start_passthrough,
1129
+ )
1130
+
1131
+ if webview_id == 0 {
1132
+ return 0, error_from_last_error(last_error_string(self))
1133
+ }
1134
+ return webview_id, .None
1135
+ }
1136
+
1137
+ resizeWebview :: proc(self: ^Core, webview_id: u32, frame: Rect, masks_json: string) -> Error {
1138
+ masks_json_z := dupe_cstring(self, masks_json)
1139
+ defer delete(masks_json_z, self.allocator)
1140
+ self.symbols.resizeWebview(webview_id, frame.x, frame.y, frame.width, frame.height, masks_json_z)
1141
+ return ensure_last_call_succeeded(self)
1142
+ }
1143
+
1144
+ loadURLInWebview :: proc(self: ^Core, webview_id: u32, url: string) -> Error {
1145
+ url_z := dupe_cstring(self, url)
1146
+ defer delete(url_z, self.allocator)
1147
+ self.symbols.loadURLInWebView(webview_id, url_z)
1148
+ return ensure_last_call_succeeded(self)
1149
+ }
1150
+
1151
+ loadHTMLInWebview :: proc(self: ^Core, webview_id: u32, html: string) -> Error {
1152
+ html_z := dupe_cstring(self, html)
1153
+ defer delete(html_z, self.allocator)
1154
+ self.symbols.loadHTMLInWebView(webview_id, html_z)
1155
+ return ensure_last_call_succeeded(self)
1156
+ }
1157
+
1158
+ updatePreloadScriptToWebview :: proc(
1159
+ self: ^Core,
1160
+ webview_id: u32,
1161
+ script_identifier: string,
1162
+ script: string,
1163
+ all_frames: bool,
1164
+ ) -> Error {
1165
+ script_identifier_z := dupe_cstring(self, script_identifier)
1166
+ defer delete(script_identifier_z, self.allocator)
1167
+ script_z := dupe_cstring(self, script)
1168
+ defer delete(script_z, self.allocator)
1169
+ self.symbols.updatePreloadScriptToWebView(webview_id, script_identifier_z, script_z, all_frames)
1170
+ return ensure_last_call_succeeded(self)
1171
+ }
1172
+
1173
+ canWebviewGoBack :: proc(self: ^Core, webview_id: u32) -> bool {
1174
+ return self.symbols.webviewCanGoBack(webview_id)
1175
+ }
1176
+
1177
+ canWebviewGoForward :: proc(self: ^Core, webview_id: u32) -> bool {
1178
+ return self.symbols.webviewCanGoForward(webview_id)
1179
+ }
1180
+
1181
+ webviewGoBack :: proc(self: ^Core, webview_id: u32) -> Error {
1182
+ self.symbols.webviewGoBack(webview_id)
1183
+ return ensure_last_call_succeeded(self)
1184
+ }
1185
+
1186
+ webviewGoForward :: proc(self: ^Core, webview_id: u32) -> Error {
1187
+ self.symbols.webviewGoForward(webview_id)
1188
+ return ensure_last_call_succeeded(self)
1189
+ }
1190
+
1191
+ reloadWebview :: proc(self: ^Core, webview_id: u32) -> Error {
1192
+ self.symbols.webviewReload(webview_id)
1193
+ return ensure_last_call_succeeded(self)
1194
+ }
1195
+
1196
+ removeWebview :: proc(self: ^Core, webview_id: u32) -> Error {
1197
+ self.symbols.webviewRemove(webview_id)
1198
+ return ensure_last_call_succeeded(self)
1199
+ }
1200
+
1201
+ setWebviewHTMLContent :: proc(self: ^Core, webview_id: u32, html: string) -> Error {
1202
+ html_z := dupe_cstring(self, html)
1203
+ defer delete(html_z, self.allocator)
1204
+ self.symbols.setWebviewHTMLContent(webview_id, html_z)
1205
+ return ensure_last_call_succeeded(self)
1206
+ }
1207
+
1208
+ setWebviewTransparent :: proc(self: ^Core, webview_id: u32, transparent: bool) -> Error {
1209
+ self.symbols.webviewSetTransparent(webview_id, transparent)
1210
+ return ensure_last_call_succeeded(self)
1211
+ }
1212
+
1213
+ setWebviewPassthrough :: proc(self: ^Core, webview_id: u32, passthrough: bool) -> Error {
1214
+ self.symbols.webviewSetPassthrough(webview_id, passthrough)
1215
+ return ensure_last_call_succeeded(self)
1216
+ }
1217
+
1218
+ setWebviewHidden :: proc(self: ^Core, webview_id: u32, hidden: bool) -> Error {
1219
+ self.symbols.webviewSetHidden(webview_id, hidden)
1220
+ return ensure_last_call_succeeded(self)
1221
+ }
1222
+
1223
+ setWebviewNavigationRules :: proc(self: ^Core, webview_id: u32, rules_json: string) -> Error {
1224
+ rules_json_z := dupe_cstring(self, rules_json)
1225
+ defer delete(rules_json_z, self.allocator)
1226
+ self.symbols.setWebviewNavigationRules(webview_id, rules_json_z)
1227
+ return ensure_last_call_succeeded(self)
1228
+ }
1229
+
1230
+ webviewFindInPage :: proc(self: ^Core, webview_id: u32, search_text: string, forward: bool, match_case: bool) -> Error {
1231
+ search_text_z := dupe_cstring(self, search_text)
1232
+ defer delete(search_text_z, self.allocator)
1233
+ self.symbols.webviewFindInPage(webview_id, search_text_z, forward, match_case)
1234
+ return ensure_last_call_succeeded(self)
1235
+ }
1236
+
1237
+ webviewStopFind :: proc(self: ^Core, webview_id: u32) -> Error {
1238
+ self.symbols.webviewStopFind(webview_id)
1239
+ return ensure_last_call_succeeded(self)
1240
+ }
1241
+
1242
+ openWebviewDevTools :: proc(self: ^Core, webview_id: u32) -> Error {
1243
+ self.symbols.webviewOpenDevTools(webview_id)
1244
+ return ensure_last_call_succeeded(self)
1245
+ }
1246
+
1247
+ closeWebviewDevTools :: proc(self: ^Core, webview_id: u32) -> Error {
1248
+ self.symbols.webviewCloseDevTools(webview_id)
1249
+ return ensure_last_call_succeeded(self)
1250
+ }
1251
+
1252
+ toggleWebviewDevTools :: proc(self: ^Core, webview_id: u32) -> Error {
1253
+ self.symbols.webviewToggleDevTools(webview_id)
1254
+ return ensure_last_call_succeeded(self)
1255
+ }
1256
+
1257
+ setWebviewPageZoom :: proc(self: ^Core, webview_id: u32, zoom_level: f64) -> Error {
1258
+ self.symbols.webviewSetPageZoom(webview_id, zoom_level)
1259
+ return ensure_last_call_succeeded(self)
1260
+ }
1261
+
1262
+ getWebviewPageZoom :: proc(self: ^Core, webview_id: u32) -> f64 {
1263
+ return self.symbols.webviewGetPageZoom(webview_id)
1264
+ }
1265
+
1266
+ createWGPUView :: proc(self: ^Core, options: WGPUViewOptions) -> (wgpu_view_id: u32, err: Error) {
1267
+ wgpu_view_id = self.symbols.createWGPUView(
1268
+ options.window_id,
1269
+ options.frame.x,
1270
+ options.frame.y,
1271
+ options.frame.width,
1272
+ options.frame.height,
1273
+ options.auto_resize,
1274
+ options.start_transparent,
1275
+ options.start_passthrough,
1276
+ )
1277
+ if wgpu_view_id == 0 {
1278
+ return 0, error_from_last_error(last_error_string(self))
1279
+ }
1280
+ return wgpu_view_id, .None
1281
+ }
1282
+
1283
+ setWGPUViewFrame :: proc(self: ^Core, wgpu_view_id: u32, frame: Rect) -> Error {
1284
+ self.symbols.setWGPUViewFrame(wgpu_view_id, frame.x, frame.y, frame.width, frame.height)
1285
+ return ensure_last_call_succeeded(self)
1286
+ }
1287
+
1288
+ resizeWGPUView :: proc(self: ^Core, wgpu_view_id: u32, frame: Rect, masks_json: string) -> Error {
1289
+ masks_json_z := dupe_cstring(self, masks_json)
1290
+ defer delete(masks_json_z, self.allocator)
1291
+ self.symbols.resizeWGPUView(wgpu_view_id, frame.x, frame.y, frame.width, frame.height, masks_json_z)
1292
+ return ensure_last_call_succeeded(self)
1293
+ }
1294
+
1295
+ setWGPUViewTransparent :: proc(self: ^Core, wgpu_view_id: u32, transparent: bool) -> Error {
1296
+ self.symbols.setWGPUViewTransparent(wgpu_view_id, transparent)
1297
+ return ensure_last_call_succeeded(self)
1298
+ }
1299
+
1300
+ setWGPUViewPassthrough :: proc(self: ^Core, wgpu_view_id: u32, passthrough: bool) -> Error {
1301
+ self.symbols.setWGPUViewPassthrough(wgpu_view_id, passthrough)
1302
+ return ensure_last_call_succeeded(self)
1303
+ }
1304
+
1305
+ setWGPUViewHidden :: proc(self: ^Core, wgpu_view_id: u32, hidden: bool) -> Error {
1306
+ self.symbols.setWGPUViewHidden(wgpu_view_id, hidden)
1307
+ return ensure_last_call_succeeded(self)
1308
+ }
1309
+
1310
+ removeWGPUView :: proc(self: ^Core, wgpu_view_id: u32) -> Error {
1311
+ self.symbols.removeWGPUView(wgpu_view_id)
1312
+ return ensure_last_call_succeeded(self)
1313
+ }
1314
+
1315
+ getWGPUViewPointer :: proc(self: ^Core, wgpu_view_id: u32) -> (handle: rawptr, err: Error) {
1316
+ handle = self.symbols.getWGPUViewPointer(wgpu_view_id)
1317
+ ensure_last_call_succeeded(self) or_return
1318
+ return handle, .None
1319
+ }
1320
+
1321
+ getWGPUViewNativeHandle :: proc(self: ^Core, wgpu_view_id: u32) -> (handle: rawptr, err: Error) {
1322
+ handle = self.symbols.getWGPUViewNativeHandle(wgpu_view_id)
1323
+ ensure_last_call_succeeded(self) or_return
1324
+ return handle, .None
1325
+ }
1326
+
1327
+ runWGPUViewTest :: proc(self: ^Core, wgpu_view_id: u32) -> Error {
1328
+ self.symbols.runWGPUViewTest(wgpu_view_id)
1329
+ return ensure_last_call_succeeded(self)
1330
+ }
1331
+
1332
+ toggleWGPUViewTestShader :: proc(self: ^Core, wgpu_view_id: u32) -> Error {
1333
+ self.symbols.toggleWGPUViewTestShader(wgpu_view_id)
1334
+ return ensure_last_call_succeeded(self)
1335
+ }
1336
+
1337
+ evaluateJavaScriptWithNoCompletion :: proc(self: ^Core, webview_id: u32, js: string) -> Error {
1338
+ js_z := dupe_cstring(self, js)
1339
+ defer delete(js_z, self.allocator)
1340
+ self.symbols.evaluateJavaScriptWithNoCompletion(webview_id, js_z)
1341
+ return ensure_last_call_succeeded(self)
1342
+ }
1343
+
1344
+ // `message` is JSON-encoded with core:encoding/json (mirrors zig's
1345
+ // std.json.stringifyAlloc of an anytype message).
1346
+ sendHostMessageToWebview :: proc(self: ^Core, webview_id: u32, message: any) -> Error {
1347
+ message_json_bytes, marshal_err := json.marshal(message, {}, self.allocator)
1348
+ if marshal_err != nil {
1349
+ return .InvalidJson
1350
+ }
1351
+ defer delete(message_json_bytes, self.allocator)
1352
+ message_json := string(message_json_bytes)
1353
+
1354
+ message_json_z := dupe_cstring(self, message_json)
1355
+ defer delete(message_json_z, self.allocator)
1356
+
1357
+ if self.symbols.sendHostMessageToWebviewViaTransport(webview_id, message_json_z) {
1358
+ return .None
1359
+ }
1360
+
1361
+ js := fmt.aprintf(
1362
+ "window.__electrobun.receiveMessageFromHost(%s);",
1363
+ message_json,
1364
+ allocator = self.allocator,
1365
+ )
1366
+ defer delete(js, self.allocator)
1367
+ return evaluateJavaScriptWithNoCompletion(self, webview_id, js)
1368
+ }
1369
+
1370
+ sendMessageToWebview :: proc(self: ^Core, webview_id: u32, message: any) -> Error {
1371
+ return sendHostMessageToWebview(self, webview_id, message)
1372
+ }
1373
+
1374
+ // The returned cstring is owned by the native layer; release it with
1375
+ // freeCoreString. Returns nil when the queue is empty.
1376
+ popNextQueuedHostMessage :: proc(self: ^Core, out_webview_id: ^u32) -> cstring {
1377
+ return self.symbols.popNextQueuedHostMessage(out_webview_id)
1378
+ }
1379
+
1380
+ freeCoreString :: proc(self: ^Core, value: cstring) {
1381
+ self.symbols.freeCoreString(value)
1382
+ }
1383
+
1384
+ sendInternalMessageToWebview :: proc(self: ^Core, webview_id: u32, message: any) -> Error {
1385
+ message_json_bytes, marshal_err := json.marshal(message, {}, self.allocator)
1386
+ if marshal_err != nil {
1387
+ return .InvalidJson
1388
+ }
1389
+ defer delete(message_json_bytes, self.allocator)
1390
+
1391
+ message_json_z := dupe_cstring(self, string(message_json_bytes))
1392
+ defer delete(message_json_z, self.allocator)
1393
+
1394
+ if !self.symbols.sendInternalMessageToWebview(webview_id, message_json_z) {
1395
+ return error_from_last_error(last_error_string(self))
1396
+ }
1397
+ return .None
1398
+ }
1399
+
1400
+ createTray :: proc(self: ^Core, options: TrayOptions) -> (tray_id: u32, err: Error) {
1401
+ title_z := dupe_cstring(self, options.title)
1402
+ defer delete(title_z, self.allocator)
1403
+ image_z := dupe_cstring(self, options.image)
1404
+ defer delete(image_z, self.allocator)
1405
+
1406
+ tray_id = self.symbols.createTray(title_z, image_z, options.is_template, options.width, options.height, nil)
1407
+ if tray_id == 0 {
1408
+ return 0, error_from_last_error(last_error_string(self))
1409
+ }
1410
+ return tray_id, .None
1411
+ }
1412
+
1413
+ setApplicationMenuJson :: proc(self: ^Core, menu_json: string, handler: StatusItemHandler) -> Error {
1414
+ menu_json_z := dupe_cstring(self, menu_json)
1415
+ defer delete(menu_json_z, self.allocator)
1416
+ self.symbols.setApplicationMenu(menu_json_z, handler)
1417
+ return ensure_last_call_succeeded(self)
1418
+ }
1419
+
1420
+ showContextMenuJson :: proc(self: ^Core, menu_json: string, handler: StatusItemHandler) -> Error {
1421
+ menu_json_z := dupe_cstring(self, menu_json)
1422
+ defer delete(menu_json_z, self.allocator)
1423
+ self.symbols.showContextMenu(menu_json_z, handler)
1424
+ return ensure_last_call_succeeded(self)
1425
+ }
1426
+
1427
+ showTray :: proc(self: ^Core, tray_id: u32) -> Error {
1428
+ if !self.symbols.showTray(tray_id) {
1429
+ return error_from_last_error(last_error_string(self))
1430
+ }
1431
+ return .None
1432
+ }
1433
+
1434
+ hideTray :: proc(self: ^Core, tray_id: u32) -> Error {
1435
+ self.symbols.hideTray(tray_id)
1436
+ return ensure_last_call_succeeded(self)
1437
+ }
1438
+
1439
+ setTrayTitle :: proc(self: ^Core, tray_id: u32, title: string) -> Error {
1440
+ title_z := dupe_cstring(self, title)
1441
+ defer delete(title_z, self.allocator)
1442
+ self.symbols.setTrayTitle(tray_id, title_z)
1443
+ return ensure_last_call_succeeded(self)
1444
+ }
1445
+
1446
+ getTrayBounds :: proc(self: ^Core, tray_id: u32) -> (Rect, Error) {
1447
+ bounds_json := self.symbols.getTrayBounds(tray_id)
1448
+ return parse_rect_json(self.allocator, string(bounds_json))
1449
+ }
1450
+
1451
+ removeTray :: proc(self: ^Core, tray_id: u32) -> Error {
1452
+ self.symbols.removeTray(tray_id)
1453
+ return ensure_last_call_succeeded(self)
1454
+ }
1455
+
1456
+ setDockIconVisible :: proc(self: ^Core, visible: bool) -> Error {
1457
+ self.symbols.setDockIconVisible(visible)
1458
+ return ensure_last_call_succeeded(self)
1459
+ }
1460
+
1461
+ isDockIconVisible :: proc(self: ^Core) -> bool {
1462
+ return self.symbols.isDockIconVisible()
1463
+ }
1464
+
1465
+ getPrimaryDisplay :: proc(self: ^Core) -> (display: Display, err: Error) {
1466
+ json_text := self.symbols.getPrimaryDisplay()
1467
+ if json_text == nil {
1468
+ return {}, .ElectrobunCoreFailure
1469
+ }
1470
+ return parse_display_json(self.allocator, string(json_text))
1471
+ }
1472
+
1473
+ // The returned slice is allocated with core.allocator; free with delete().
1474
+ getAllDisplays :: proc(self: ^Core) -> (displays: []Display, err: Error) {
1475
+ json_text := self.symbols.getAllDisplays()
1476
+ if json_text == nil {
1477
+ return nil, .ElectrobunCoreFailure
1478
+ }
1479
+ return parse_displays_json(self.allocator, string(json_text))
1480
+ }
1481
+
1482
+ getCursorScreenPoint :: proc(self: ^Core) -> (point: Point, err: Error) {
1483
+ json_text := self.symbols.getCursorScreenPoint()
1484
+ if json_text == nil {
1485
+ return {}, .ElectrobunCoreFailure
1486
+ }
1487
+ return parse_point_json(self.allocator, string(json_text))
1488
+ }
1489
+
1490
+ moveToTrash :: proc(self: ^Core, path: string) -> bool {
1491
+ path_z := dupe_cstring(self, path)
1492
+ defer delete(path_z, self.allocator)
1493
+ return self.symbols.moveToTrash(path_z)
1494
+ }
1495
+
1496
+ showItemInFolder :: proc(self: ^Core, path: string) -> Error {
1497
+ path_z := dupe_cstring(self, path)
1498
+ defer delete(path_z, self.allocator)
1499
+ self.symbols.showItemInFolder(path_z)
1500
+ return ensure_last_call_succeeded(self)
1501
+ }
1502
+
1503
+ openExternal :: proc(self: ^Core, url: string) -> bool {
1504
+ url_z := dupe_cstring(self, url)
1505
+ defer delete(url_z, self.allocator)
1506
+ return self.symbols.openExternal(url_z)
1507
+ }
1508
+
1509
+ openPath :: proc(self: ^Core, path: string) -> bool {
1510
+ path_z := dupe_cstring(self, path)
1511
+ defer delete(path_z, self.allocator)
1512
+ return self.symbols.openPath(path_z)
1513
+ }
1514
+
1515
+ // Returns a comma-separated list of selected paths ("" when cancelled).
1516
+ // The returned string is allocated with core.allocator; free with delete().
1517
+ openFileDialog :: proc(self: ^Core, options: OpenFileDialogOptions) -> string {
1518
+ starting_folder_z := dupe_cstring(self, options.starting_folder)
1519
+ defer delete(starting_folder_z, self.allocator)
1520
+ allowed_file_types_z := dupe_cstring(self, options.allowed_file_types)
1521
+ defer delete(allowed_file_types_z, self.allocator)
1522
+
1523
+ result := self.symbols.openFileDialog(
1524
+ starting_folder_z,
1525
+ allowed_file_types_z,
1526
+ c.int(1) if options.can_choose_files else c.int(0),
1527
+ c.int(1) if options.can_choose_directory else c.int(0),
1528
+ c.int(1) if options.allows_multiple_selection else c.int(0),
1529
+ )
1530
+ if result == nil {
1531
+ return clone_string("", self.allocator)
1532
+ }
1533
+ return clone_string(string(result), self.allocator)
1534
+ }
1535
+
1536
+ showMessageBox :: proc(self: ^Core, options: MessageBoxOptions) -> (response: c.int, err: Error) {
1537
+ box_type_z := dupe_cstring(self, options.box_type)
1538
+ defer delete(box_type_z, self.allocator)
1539
+ title_z := dupe_cstring(self, options.title)
1540
+ defer delete(title_z, self.allocator)
1541
+ message_z := dupe_cstring(self, options.message)
1542
+ defer delete(message_z, self.allocator)
1543
+ detail_z := dupe_cstring(self, options.detail)
1544
+ defer delete(detail_z, self.allocator)
1545
+ buttons_joined := strings.join(options.buttons, ",", self.allocator)
1546
+ defer delete(buttons_joined, self.allocator)
1547
+ buttons_z := dupe_cstring(self, buttons_joined)
1548
+ defer delete(buttons_z, self.allocator)
1549
+
1550
+ response = self.symbols.showMessageBox(
1551
+ box_type_z,
1552
+ title_z,
1553
+ message_z,
1554
+ detail_z,
1555
+ buttons_z,
1556
+ options.default_id,
1557
+ options.cancel_id,
1558
+ )
1559
+ ensure_last_call_succeeded(self) or_return
1560
+ return response, .None
1561
+ }
1562
+
1563
+ showNotification :: proc(self: ^Core, options: NotificationOptions) -> Error {
1564
+ title_z := dupe_cstring(self, options.title)
1565
+ defer delete(title_z, self.allocator)
1566
+ body_z := dupe_cstring(self, options.body)
1567
+ defer delete(body_z, self.allocator)
1568
+ subtitle_z := dupe_cstring(self, options.subtitle)
1569
+ defer delete(subtitle_z, self.allocator)
1570
+
1571
+ self.symbols.showNotification(title_z, body_z, subtitle_z, options.silent)
1572
+ return ensure_last_call_succeeded(self)
1573
+ }
1574
+
1575
+ setGlobalShortcutCallback :: proc(self: ^Core, callback: GlobalShortcutHandler) -> Error {
1576
+ self.symbols.setGlobalShortcutCallback(callback)
1577
+ return ensure_last_call_succeeded(self)
1578
+ }
1579
+
1580
+ registerGlobalShortcut :: proc(self: ^Core, accelerator: string) -> bool {
1581
+ accelerator_z := dupe_cstring(self, accelerator)
1582
+ defer delete(accelerator_z, self.allocator)
1583
+ return self.symbols.registerGlobalShortcut(accelerator_z)
1584
+ }
1585
+
1586
+ unregisterGlobalShortcut :: proc(self: ^Core, accelerator: string) -> bool {
1587
+ accelerator_z := dupe_cstring(self, accelerator)
1588
+ defer delete(accelerator_z, self.allocator)
1589
+ return self.symbols.unregisterGlobalShortcut(accelerator_z)
1590
+ }
1591
+
1592
+ unregisterAllGlobalShortcuts :: proc(self: ^Core) -> Error {
1593
+ self.symbols.unregisterAllGlobalShortcuts()
1594
+ return ensure_last_call_succeeded(self)
1595
+ }
1596
+
1597
+ isGlobalShortcutRegistered :: proc(self: ^Core, accelerator: string) -> bool {
1598
+ accelerator_z := dupe_cstring(self, accelerator)
1599
+ defer delete(accelerator_z, self.allocator)
1600
+ return self.symbols.isGlobalShortcutRegistered(accelerator_z)
1601
+ }
1602
+
1603
+ // Returns (text, true) when the clipboard has text. The returned string is
1604
+ // allocated with core.allocator; free with delete().
1605
+ clipboardReadText :: proc(self: ^Core) -> (text: string, has_text: bool) {
1606
+ value := self.symbols.clipboardReadText()
1607
+ if value == nil {
1608
+ return "", false
1609
+ }
1610
+ return clone_string(string(value), self.allocator), true
1611
+ }
1612
+
1613
+ clipboardWriteText :: proc(self: ^Core, text: string) -> Error {
1614
+ text_z := dupe_cstring(self, text)
1615
+ defer delete(text_z, self.allocator)
1616
+ self.symbols.clipboardWriteText(text_z)
1617
+ return ensure_last_call_succeeded(self)
1618
+ }
1619
+
1620
+ clipboardClear :: proc(self: ^Core) -> Error {
1621
+ self.symbols.clipboardClear()
1622
+ return ensure_last_call_succeeded(self)
1623
+ }
1624
+
1625
+ // The returned string is allocated with core.allocator; free with delete().
1626
+ clipboardAvailableFormatsCsv :: proc(self: ^Core) -> string {
1627
+ formats := self.symbols.clipboardAvailableFormats()
1628
+ if formats == nil {
1629
+ return clone_string("", self.allocator)
1630
+ }
1631
+ return clone_string(string(formats), self.allocator)
1632
+ }
1633
+
1634
+ // The returned slice (and its strings) are allocated with core.allocator.
1635
+ sessionGetCookies :: proc(self: ^Core, partition: string, filter_json: string) -> (cookies: []Cookie, err: Error) {
1636
+ partition_z := dupe_cstring(self, partition)
1637
+ defer delete(partition_z, self.allocator)
1638
+ filter_json_z := dupe_cstring(self, filter_json)
1639
+ defer delete(filter_json_z, self.allocator)
1640
+
1641
+ json_text := self.symbols.sessionGetCookies(partition_z, filter_json_z)
1642
+ if json_text == nil {
1643
+ return make([]Cookie, 0, self.allocator), .None
1644
+ }
1645
+ return parse_cookies_json(self.allocator, string(json_text))
1646
+ }
1647
+
1648
+ sessionSetCookie :: proc(self: ^Core, partition: string, cookie_json: string) -> bool {
1649
+ partition_z := dupe_cstring(self, partition)
1650
+ defer delete(partition_z, self.allocator)
1651
+ cookie_json_z := dupe_cstring(self, cookie_json)
1652
+ defer delete(cookie_json_z, self.allocator)
1653
+ return self.symbols.sessionSetCookie(partition_z, cookie_json_z)
1654
+ }
1655
+
1656
+ sessionRemoveCookie :: proc(self: ^Core, partition: string, url: string, name: string) -> bool {
1657
+ partition_z := dupe_cstring(self, partition)
1658
+ defer delete(partition_z, self.allocator)
1659
+ url_z := dupe_cstring(self, url)
1660
+ defer delete(url_z, self.allocator)
1661
+ name_z := dupe_cstring(self, name)
1662
+ defer delete(name_z, self.allocator)
1663
+ return self.symbols.sessionRemoveCookie(partition_z, url_z, name_z)
1664
+ }
1665
+
1666
+ sessionClearCookies :: proc(self: ^Core, partition: string) -> Error {
1667
+ partition_z := dupe_cstring(self, partition)
1668
+ defer delete(partition_z, self.allocator)
1669
+ self.symbols.sessionClearCookies(partition_z)
1670
+ return ensure_last_call_succeeded(self)
1671
+ }
1672
+
1673
+ sessionClearStorageData :: proc(self: ^Core, partition: string, storage_types_json: string) -> Error {
1674
+ partition_z := dupe_cstring(self, partition)
1675
+ defer delete(partition_z, self.allocator)
1676
+ storage_types_json_z := dupe_cstring(self, storage_types_json)
1677
+ defer delete(storage_types_json_z, self.allocator)
1678
+ self.symbols.sessionClearStorageData(partition_z, storage_types_json_z)
1679
+ return ensure_last_call_succeeded(self)
1680
+ }
1681
+
1682
+ setURLOpenHandler :: proc(self: ^Core, handler: URLOpenHandler) -> Error {
1683
+ self.symbols.setURLOpenHandler(handler)
1684
+ return ensure_last_call_succeeded(self)
1685
+ }
1686
+
1687
+ setAppReopenHandler :: proc(self: ^Core, handler: AppReopenHandler) -> Error {
1688
+ self.symbols.setAppReopenHandler(handler)
1689
+ return ensure_last_call_succeeded(self)
1690
+ }
1691
+
1692
+ setQuitRequestedHandler :: proc(self: ^Core, handler: QuitRequestedHandler) -> Error {
1693
+ self.symbols.setQuitRequestedHandler(handler)
1694
+ return ensure_last_call_succeeded(self)
1695
+ }
1696
+
1697
+ stopEventLoop :: proc(self: ^Core) -> Error {
1698
+ self.symbols.stopEventLoop()
1699
+ return ensure_last_call_succeeded(self)
1700
+ }
1701
+
1702
+ waitForShutdownComplete :: proc(self: ^Core, timeout_ms: c.int) -> Error {
1703
+ self.symbols.waitForShutdownComplete(timeout_ms)
1704
+ return ensure_last_call_succeeded(self)
1705
+ }
1706
+
1707
+ forceExit :: proc(self: ^Core, code: c.int) -> ! {
1708
+ self.symbols.forceExit(code)
1709
+ os.exit(int(code))
1710
+ }
1711
+
1712
+ quitGracefully :: proc(self: ^Core, code: c.int) -> ! {
1713
+ _ = stopEventLoop(self)
1714
+ _ = waitForShutdownComplete(self, 5000)
1715
+ forceExit(self, code)
1716
+ }
1717
+
1718
+ wgpuCreateSurfaceForView :: proc(self: ^Core, instance_ptr: rawptr, view_ptr: rawptr) -> (surface_ptr: rawptr, err: Error) {
1719
+ surface_ptr = self.symbols.wgpuCreateSurfaceForView(instance_ptr, view_ptr)
1720
+ ensure_last_call_succeeded(self) or_return
1721
+ return surface_ptr, .None
1722
+ }
1723
+
1724
+ wgpuCreateAdapterDeviceMainThread :: proc(
1725
+ self: ^Core,
1726
+ instance_ptr: rawptr,
1727
+ surface_ptr: rawptr,
1728
+ out_adapter_device: rawptr,
1729
+ ) -> Error {
1730
+ self.symbols.wgpuCreateAdapterDeviceMainThread(instance_ptr, surface_ptr, out_adapter_device)
1731
+ return ensure_last_call_succeeded(self)
1732
+ }
1733
+
1734
+ wgpuSurfaceConfigureMainThread :: proc(self: ^Core, surface_ptr: rawptr, config_ptr: rawptr) -> Error {
1735
+ self.symbols.wgpuSurfaceConfigureMainThread(surface_ptr, config_ptr)
1736
+ return ensure_last_call_succeeded(self)
1737
+ }
1738
+
1739
+ wgpuSurfaceGetCurrentTextureMainThread :: proc(self: ^Core, surface_ptr: rawptr, surface_texture_ptr: rawptr) -> Error {
1740
+ self.symbols.wgpuSurfaceGetCurrentTextureMainThread(surface_ptr, surface_texture_ptr)
1741
+ return ensure_last_call_succeeded(self)
1742
+ }
1743
+
1744
+ wgpuSurfacePresentMainThread :: proc(self: ^Core, surface_ptr: rawptr) -> (result: i32, err: Error) {
1745
+ result = self.symbols.wgpuSurfacePresentMainThread(surface_ptr)
1746
+ ensure_last_call_succeeded(self) or_return
1747
+ return result, .None
1748
+ }
1749
+
1750
+ // Blocks running the native event loop until the app quits.
1751
+ runMainThread :: proc(self: ^Core, app_info: AppInfo) -> Error {
1752
+ identifier_z := dupe_cstring(self, app_info.identifier)
1753
+ defer delete(identifier_z, self.allocator)
1754
+ name_z := dupe_cstring(self, app_info.name)
1755
+ defer delete(name_z, self.allocator)
1756
+ channel_z := dupe_cstring(self, app_info.channel)
1757
+ defer delete(channel_z, self.allocator)
1758
+
1759
+ status := self.symbols.electrobun_core_run_main_thread(identifier_z, name_z, channel_z, 0)
1760
+ if status != 0 {
1761
+ return error_from_last_error(last_error_string(self))
1762
+ }
1763
+ return .None
1764
+ }
1765
+
1766
+ // ---------------------------------------------------------------------------
1767
+ // Convenience callbacks / process helpers
1768
+ // ---------------------------------------------------------------------------
1769
+
1770
+ quit :: proc(code: int) -> ! {
1771
+ os.exit(code)
1772
+ }
1773
+
1774
+ allowAllNavigation :: proc "c" (_: u32, _: cstring) -> u32 {
1775
+ return 1
1776
+ }
1777
+
1778
+ noopWebviewEvent :: proc "c" (_: u32, _: cstring, _: cstring) {
1779
+ }
1780
+
1781
+ noopWebviewPostMessage :: proc "c" (_: u32, _: cstring) {
1782
+ }
1783
+
1784
+ // ---------------------------------------------------------------------------
1785
+ // Overload groups / aliases mirroring the zig SDK's method names
1786
+ // ---------------------------------------------------------------------------
1787
+
1788
+ // zig: Core.load
1789
+ load :: coreLoad
1790
+ // zig: Core.close / WgpuNative.close / BrowserWindowRef.close
1791
+ close :: proc {
1792
+ coreClose,
1793
+ wgpuNativeClose,
1794
+ windowClose,
1795
+ }
1796
+ // zig: Core.setWindowButtonPosition / BrowserWindowRef.setWindowButtonPosition
1797
+ setWindowButtonPosition :: proc {
1798
+ coreSetWindowButtonPosition,
1799
+ windowSetWindowButtonPosition,
1800
+ }
1801
+ // zig: OwnedAppInfo.deinit / Paths.deinit / WindowRegistry.deinit / BundlePaths.deinit
1802
+ deinit :: proc {
1803
+ ownedAppInfoDeinit,
1804
+ pathsDeinit,
1805
+ windowRegistryDeinit,
1806
+ bundlePathsDeinit,
1807
+ }
1808
+
1809
+ // ---------------------------------------------------------------------------
1810
+ // Private helpers
1811
+ // ---------------------------------------------------------------------------
1812
+
1813
+ @(private = "file")
1814
+ clone_string :: proc(value: string, allocator: runtime.Allocator) -> string {
1815
+ result, _ := strings.clone(value, allocator)
1816
+ return result
1817
+ }
1818
+
1819
+ @(private = "file")
1820
+ join_path :: proc(allocator: runtime.Allocator, parts: []string) -> string {
1821
+ result, _ := filepath.join(parts, allocator)
1822
+ return result
1823
+ }
1824
+
1825
+ @(private = "file")
1826
+ dupe_cstring :: proc(self: ^Core, value: string) -> cstring {
1827
+ result, _ := strings.clone_to_cstring(value, self.allocator)
1828
+ return result
1829
+ }
1830
+
1831
+ @(private = "file")
1832
+ last_error_string :: proc(self: ^Core) -> string {
1833
+ return string(self.symbols.electrobun_core_last_error())
1834
+ }
1835
+
1836
+ @(private = "file")
1837
+ ensure_last_call_succeeded :: proc(self: ^Core) -> Error {
1838
+ message := last_error_string(self)
1839
+ if len(message) != 0 {
1840
+ return error_from_last_error(message)
1841
+ }
1842
+ return .None
1843
+ }
1844
+
1845
+ @(private = "file")
1846
+ error_from_last_error :: proc(message: string) -> Error {
1847
+ if len(message) != 0 {
1848
+ fmt.eprintf("[electrobun-odin] core error: %s\n", message)
1849
+ }
1850
+ return .ElectrobunCoreFailure
1851
+ }
1852
+
1853
+ @(private = "file")
1854
+ missing_symbol_name :: proc(table: ^$T) -> (name: string, missing: bool) {
1855
+ for field in reflect.struct_fields_zipped(T) {
1856
+ if field.name == "__handle" || !reflect.is_procedure(field.type) {
1857
+ continue
1858
+ }
1859
+ field_ptr := rawptr(uintptr(table) + field.offset)
1860
+ if (^rawptr)(field_ptr)^ == nil {
1861
+ return field.name, true
1862
+ }
1863
+ }
1864
+ return "", false
1865
+ }
1866
+
1867
+ @(private = "file")
1868
+ read_file_cstring :: proc(self: ^Core, path: string) -> (result: cstring, err: Error) {
1869
+ data, read_err := os.read_entire_file(path, self.allocator)
1870
+ if read_err != nil {
1871
+ return nil, .FileReadFailed
1872
+ }
1873
+ defer delete(data, self.allocator)
1874
+ result, _ = strings.clone_to_cstring(string(data), self.allocator)
1875
+ return result, .None
1876
+ }
1877
+
1878
+ // --- JSON helpers ----------------------------------------------------------
1879
+
1880
+ @(private = "file")
1881
+ write_json_string :: proc(b: ^strings.Builder, value: string) {
1882
+ data, err := json.marshal(value, {}, context.temp_allocator)
1883
+ if err == nil {
1884
+ strings.write_string(b, string(data))
1885
+ } else {
1886
+ strings.write_string(b, "\"\"")
1887
+ }
1888
+ }
1889
+
1890
+ @(private = "file")
1891
+ write_opt_string_value :: proc(b: ^strings.Builder, value: Maybe(string)) {
1892
+ if v, ok := value.?; ok {
1893
+ write_json_string(b, v)
1894
+ } else {
1895
+ strings.write_string(b, "null")
1896
+ }
1897
+ }
1898
+
1899
+ @(private = "file")
1900
+ write_opt_bool_value :: proc(b: ^strings.Builder, value: Maybe(bool)) {
1901
+ if v, ok := value.?; ok {
1902
+ strings.write_string(b, "true" if v else "false")
1903
+ } else {
1904
+ strings.write_string(b, "null")
1905
+ }
1906
+ }
1907
+
1908
+ @(private = "file")
1909
+ write_opt_f64_value :: proc(b: ^strings.Builder, value: Maybe(f64)) {
1910
+ if v, ok := value.?; ok {
1911
+ strings.write_f64(b, v, 'g')
1912
+ } else {
1913
+ strings.write_string(b, "null")
1914
+ }
1915
+ }
1916
+
1917
+ // Emits every field, using null for unset optionals (mirrors zig's
1918
+ // std.json.stringify of the Cookie struct).
1919
+ @(private = "file")
1920
+ marshal_cookie :: proc(allocator: runtime.Allocator, cookie: Cookie) -> string {
1921
+ b := strings.builder_make(allocator)
1922
+ strings.write_string(&b, "{\"name\":")
1923
+ write_json_string(&b, cookie.name)
1924
+ strings.write_string(&b, ",\"value\":")
1925
+ write_json_string(&b, cookie.value)
1926
+ strings.write_string(&b, ",\"domain\":")
1927
+ write_opt_string_value(&b, cookie.domain)
1928
+ strings.write_string(&b, ",\"path\":")
1929
+ write_opt_string_value(&b, cookie.path)
1930
+ strings.write_string(&b, ",\"secure\":")
1931
+ write_opt_bool_value(&b, cookie.secure)
1932
+ strings.write_string(&b, ",\"httpOnly\":")
1933
+ write_opt_bool_value(&b, cookie.httpOnly)
1934
+ strings.write_string(&b, ",\"sameSite\":")
1935
+ write_opt_string_value(&b, cookie.sameSite)
1936
+ strings.write_string(&b, ",\"expirationDate\":")
1937
+ write_opt_f64_value(&b, cookie.expirationDate)
1938
+ strings.write_string(&b, "}")
1939
+ return strings.to_string(b)
1940
+ }
1941
+
1942
+ @(private = "file")
1943
+ marshal_cookie_filter :: proc(allocator: runtime.Allocator, filter: CookieFilter) -> string {
1944
+ b := strings.builder_make(allocator)
1945
+ strings.write_string(&b, "{\"url\":")
1946
+ write_opt_string_value(&b, filter.url)
1947
+ strings.write_string(&b, ",\"name\":")
1948
+ write_opt_string_value(&b, filter.name)
1949
+ strings.write_string(&b, ",\"domain\":")
1950
+ write_opt_string_value(&b, filter.domain)
1951
+ strings.write_string(&b, ",\"path\":")
1952
+ write_opt_string_value(&b, filter.path)
1953
+ strings.write_string(&b, ",\"secure\":")
1954
+ write_opt_bool_value(&b, filter.secure)
1955
+ strings.write_string(&b, ",\"session\":")
1956
+ write_opt_bool_value(&b, filter.session)
1957
+ strings.write_string(&b, "}")
1958
+ return strings.to_string(b)
1959
+ }
1960
+
1961
+ @(private = "file")
1962
+ number_from_value :: proc(value: json.Value) -> (result: f64, ok: bool) {
1963
+ #partial switch v in value {
1964
+ case json.Float:
1965
+ return f64(v), true
1966
+ case json.Integer:
1967
+ return f64(v), true
1968
+ }
1969
+ return 0, false
1970
+ }
1971
+
1972
+ @(private = "file")
1973
+ object_string :: proc(obj: json.Object, key: string) -> (result: string, ok: bool) {
1974
+ value, found := obj[key]
1975
+ if !found {
1976
+ return "", false
1977
+ }
1978
+ s, is_string := value.(json.String)
1979
+ if !is_string {
1980
+ return "", false
1981
+ }
1982
+ return s, true
1983
+ }
1984
+
1985
+ @(private = "file")
1986
+ object_number :: proc(obj: json.Object, key: string) -> (result: f64, ok: bool) {
1987
+ value, found := obj[key]
1988
+ if !found {
1989
+ return 0, false
1990
+ }
1991
+ return number_from_value(value)
1992
+ }
1993
+
1994
+ @(private = "file")
1995
+ object_bool :: proc(obj: json.Object, key: string) -> (result: bool, ok: bool) {
1996
+ value, found := obj[key]
1997
+ if !found {
1998
+ return false, false
1999
+ }
2000
+ v, is_bool := value.(json.Boolean)
2001
+ if !is_bool {
2002
+ return false, false
2003
+ }
2004
+ return v, true
2005
+ }
2006
+
2007
+ @(private = "file")
2008
+ rect_from_value :: proc(value: json.Value) -> (rect: Rect, ok: bool) {
2009
+ obj, is_object := value.(json.Object)
2010
+ if !is_object {
2011
+ return {}, false
2012
+ }
2013
+ x, x_ok := object_number(obj, "x")
2014
+ y, y_ok := object_number(obj, "y")
2015
+ width, width_ok := object_number(obj, "width")
2016
+ height, height_ok := object_number(obj, "height")
2017
+ if !x_ok || !y_ok || !width_ok || !height_ok {
2018
+ return {}, false
2019
+ }
2020
+ return Rect{x = x, y = y, width = width, height = height}, true
2021
+ }
2022
+
2023
+ @(private = "file")
2024
+ parse_rect_json :: proc(allocator: runtime.Allocator, text: string) -> (rect: Rect, err: Error) {
2025
+ value, parse_err := json.parse_string(text, json.DEFAULT_SPECIFICATION, true, allocator)
2026
+ if parse_err != .None {
2027
+ return {}, .InvalidRectJson
2028
+ }
2029
+ defer json.destroy_value(value, allocator)
2030
+
2031
+ result, ok := rect_from_value(value)
2032
+ if !ok {
2033
+ return {}, .InvalidRectJson
2034
+ }
2035
+ return result, .None
2036
+ }
2037
+
2038
+ @(private = "file")
2039
+ display_from_value :: proc(value: json.Value) -> (display: Display, ok: bool) {
2040
+ obj, is_object := value.(json.Object)
2041
+ if !is_object {
2042
+ return {}, false
2043
+ }
2044
+ id, id_ok := object_number(obj, "id")
2045
+ if !id_ok {
2046
+ return {}, false
2047
+ }
2048
+ bounds, bounds_ok := rect_from_value(obj["bounds"])
2049
+ work_area, work_area_ok := rect_from_value(obj["workArea"])
2050
+ if !bounds_ok || !work_area_ok {
2051
+ return {}, false
2052
+ }
2053
+ scale_factor, _ := object_number(obj, "scaleFactor")
2054
+ is_primary, _ := object_bool(obj, "isPrimary")
2055
+ return Display{
2056
+ id = i64(id),
2057
+ bounds = bounds,
2058
+ workArea = work_area,
2059
+ scaleFactor = scale_factor,
2060
+ isPrimary = is_primary,
2061
+ },
2062
+ true
2063
+ }
2064
+
2065
+ @(private = "file")
2066
+ parse_display_json :: proc(allocator: runtime.Allocator, text: string) -> (display: Display, err: Error) {
2067
+ value, parse_err := json.parse_string(text, json.DEFAULT_SPECIFICATION, true, allocator)
2068
+ if parse_err != .None {
2069
+ return {}, .InvalidJson
2070
+ }
2071
+ defer json.destroy_value(value, allocator)
2072
+
2073
+ result, ok := display_from_value(value)
2074
+ if !ok {
2075
+ return {}, .InvalidJson
2076
+ }
2077
+ return result, .None
2078
+ }
2079
+
2080
+ @(private = "file")
2081
+ parse_displays_json :: proc(allocator: runtime.Allocator, text: string) -> (displays: []Display, err: Error) {
2082
+ value, parse_err := json.parse_string(text, json.DEFAULT_SPECIFICATION, true, allocator)
2083
+ if parse_err != .None {
2084
+ return nil, .InvalidJson
2085
+ }
2086
+ defer json.destroy_value(value, allocator)
2087
+
2088
+ arr, is_array := value.(json.Array)
2089
+ if !is_array {
2090
+ return nil, .InvalidJson
2091
+ }
2092
+
2093
+ result := make([]Display, len(arr), allocator)
2094
+ for item, index in arr {
2095
+ display, ok := display_from_value(item)
2096
+ if !ok {
2097
+ delete(result, allocator)
2098
+ return nil, .InvalidJson
2099
+ }
2100
+ result[index] = display
2101
+ }
2102
+ return result, .None
2103
+ }
2104
+
2105
+ @(private = "file")
2106
+ parse_point_json :: proc(allocator: runtime.Allocator, text: string) -> (point: Point, err: Error) {
2107
+ value, parse_err := json.parse_string(text, json.DEFAULT_SPECIFICATION, true, allocator)
2108
+ if parse_err != .None {
2109
+ return {}, .InvalidJson
2110
+ }
2111
+ defer json.destroy_value(value, allocator)
2112
+
2113
+ obj, is_object := value.(json.Object)
2114
+ if !is_object {
2115
+ return {}, .InvalidJson
2116
+ }
2117
+ x, x_ok := object_number(obj, "x")
2118
+ y, y_ok := object_number(obj, "y")
2119
+ if !x_ok || !y_ok {
2120
+ return {}, .InvalidJson
2121
+ }
2122
+ return Point{x = x, y = y}, .None
2123
+ }
2124
+
2125
+ @(private = "file")
2126
+ cookie_from_object :: proc(allocator: runtime.Allocator, obj: json.Object) -> (cookie: Cookie) {
2127
+ if name, ok := object_string(obj, "name"); ok {
2128
+ cookie.name = clone_string(name, allocator)
2129
+ }
2130
+ if value, ok := object_string(obj, "value"); ok {
2131
+ cookie.value = clone_string(value, allocator)
2132
+ }
2133
+ if domain, ok := object_string(obj, "domain"); ok {
2134
+ cookie.domain = clone_string(domain, allocator)
2135
+ }
2136
+ if path, ok := object_string(obj, "path"); ok {
2137
+ cookie.path = clone_string(path, allocator)
2138
+ }
2139
+ if secure, ok := object_bool(obj, "secure"); ok {
2140
+ cookie.secure = secure
2141
+ }
2142
+ if http_only, ok := object_bool(obj, "httpOnly"); ok {
2143
+ cookie.httpOnly = http_only
2144
+ }
2145
+ if same_site, ok := object_string(obj, "sameSite"); ok {
2146
+ cookie.sameSite = clone_string(same_site, allocator)
2147
+ }
2148
+ if expiration_date, ok := object_number(obj, "expirationDate"); ok {
2149
+ cookie.expirationDate = expiration_date
2150
+ }
2151
+ return cookie
2152
+ }
2153
+
2154
+ @(private = "file")
2155
+ parse_cookies_json :: proc(allocator: runtime.Allocator, text: string) -> (cookies: []Cookie, err: Error) {
2156
+ value, parse_err := json.parse_string(text, json.DEFAULT_SPECIFICATION, true, allocator)
2157
+ if parse_err != .None {
2158
+ return nil, .InvalidJson
2159
+ }
2160
+ defer json.destroy_value(value, allocator)
2161
+
2162
+ arr, is_array := value.(json.Array)
2163
+ if !is_array {
2164
+ return nil, .InvalidJson
2165
+ }
2166
+
2167
+ result := make([]Cookie, len(arr), allocator)
2168
+ for item, index in arr {
2169
+ obj, is_object := item.(json.Object)
2170
+ if !is_object {
2171
+ continue
2172
+ }
2173
+ result[index] = cookie_from_object(allocator, obj)
2174
+ }
2175
+ return result, .None
2176
+ }
2177
+
2178
+ // --- Paths helpers ---------------------------------------------------------
2179
+
2180
+ @(private = "file")
2181
+ get_home_dir :: proc(allocator: runtime.Allocator) -> (home: string, err: Error) {
2182
+ when ODIN_OS == .Windows {
2183
+ if value, found := os.lookup_env("USERPROFILE", allocator); found {
2184
+ return value, .None
2185
+ }
2186
+ }
2187
+ if value, found := os.lookup_env("HOME", allocator); found {
2188
+ return value, .None
2189
+ }
2190
+ return "", .EnvVarNotFound
2191
+ }
2192
+
2193
+ @(private = "file")
2194
+ env_or_join :: proc(allocator: runtime.Allocator, env_name: string, fallback_parts: []string) -> string {
2195
+ if value, found := os.lookup_env(env_name, allocator); found {
2196
+ return value
2197
+ }
2198
+ return join_path(allocator, fallback_parts)
2199
+ }
2200
+
2201
+ @(private = "file")
2202
+ get_app_data_dir :: proc(allocator: runtime.Allocator, home: string) -> string {
2203
+ when ODIN_OS == .Darwin {
2204
+ return join_path(allocator, {home, "Library", "Application Support"})
2205
+ } else when ODIN_OS == .Windows {
2206
+ return env_or_join(allocator, "LOCALAPPDATA", {home, "AppData", "Local"})
2207
+ } else {
2208
+ return env_or_join(allocator, "XDG_DATA_HOME", {home, ".local", "share"})
2209
+ }
2210
+ }
2211
+
2212
+ @(private = "file")
2213
+ get_cache_dir :: proc(allocator: runtime.Allocator, home: string) -> string {
2214
+ when ODIN_OS == .Darwin {
2215
+ return join_path(allocator, {home, "Library", "Caches"})
2216
+ } else when ODIN_OS == .Windows {
2217
+ return env_or_join(allocator, "LOCALAPPDATA", {home, "AppData", "Local"})
2218
+ } else {
2219
+ return env_or_join(allocator, "XDG_CACHE_HOME", {home, ".cache"})
2220
+ }
2221
+ }
2222
+
2223
+ @(private = "file")
2224
+ get_logs_dir :: proc(allocator: runtime.Allocator, home: string) -> string {
2225
+ when ODIN_OS == .Darwin {
2226
+ return join_path(allocator, {home, "Library", "Logs"})
2227
+ } else when ODIN_OS == .Windows {
2228
+ return env_or_join(allocator, "LOCALAPPDATA", {home, "AppData", "Local"})
2229
+ } else {
2230
+ return env_or_join(allocator, "XDG_STATE_HOME", {home, ".local", "state"})
2231
+ }
2232
+ }
2233
+
2234
+ @(private = "file")
2235
+ get_config_dir :: proc(allocator: runtime.Allocator, home: string) -> string {
2236
+ when ODIN_OS == .Darwin {
2237
+ return join_path(allocator, {home, "Library", "Application Support"})
2238
+ } else when ODIN_OS == .Windows {
2239
+ return env_or_join(allocator, "APPDATA", {home, "AppData", "Roaming"})
2240
+ } else {
2241
+ return env_or_join(allocator, "XDG_CONFIG_HOME", {home, ".config"})
2242
+ }
2243
+ }
2244
+
2245
+ @(private = "file")
2246
+ get_temp_dir :: proc(allocator: runtime.Allocator, home: string) -> string {
2247
+ when ODIN_OS == .Windows {
2248
+ if value, found := os.lookup_env("TEMP", allocator); found {
2249
+ return value
2250
+ }
2251
+ if value, found := os.lookup_env("TMP", allocator); found {
2252
+ return value
2253
+ }
2254
+ return join_path(allocator, {home, "AppData", "Local", "Temp"})
2255
+ } else {
2256
+ if value, found := os.lookup_env("TMPDIR", allocator); found {
2257
+ return value
2258
+ }
2259
+ return clone_string("/tmp", allocator)
2260
+ }
2261
+ }
2262
+
2263
+ @(private = "file")
2264
+ get_user_dir :: proc(
2265
+ allocator: runtime.Allocator,
2266
+ home: string,
2267
+ mac_name: string,
2268
+ win_name: string,
2269
+ xdg_key: string,
2270
+ fallback_name: string,
2271
+ ) -> string {
2272
+ when ODIN_OS == .Darwin {
2273
+ return join_path(allocator, {home, mac_name})
2274
+ } else when ODIN_OS == .Windows {
2275
+ return join_path(allocator, {home, win_name})
2276
+ } else {
2277
+ return linux_xdg_user_dir(allocator, home, xdg_key, fallback_name)
2278
+ }
2279
+ }
2280
+
2281
+ @(private = "file")
2282
+ linux_xdg_user_dir :: proc(
2283
+ allocator: runtime.Allocator,
2284
+ home: string,
2285
+ key: string,
2286
+ fallback_name: string,
2287
+ ) -> string {
2288
+ config_path := join_path(allocator, {home, ".config", "user-dirs.dirs"})
2289
+ defer delete(config_path, allocator)
2290
+
2291
+ fallback := join_path(allocator, {home, fallback_name})
2292
+
2293
+ content, read_err := os.read_entire_file(config_path, allocator)
2294
+ if read_err != nil {
2295
+ return fallback
2296
+ }
2297
+ defer delete(content, allocator)
2298
+
2299
+ iterator := string(content)
2300
+ for line in strings.split_lines_iterator(&iterator) {
2301
+ trimmed := strings.trim(line, " \t\r")
2302
+ if len(trimmed) == 0 || trimmed[0] == '#' {
2303
+ continue
2304
+ }
2305
+
2306
+ eq_index := strings.index_byte(trimmed, '=')
2307
+ if eq_index < 0 {
2308
+ continue
2309
+ }
2310
+ line_key := trimmed[:eq_index]
2311
+ if line_key != key {
2312
+ continue
2313
+ }
2314
+
2315
+ value := strings.trim(trimmed[eq_index + 1:], " \t\r")
2316
+ if len(value) >= 2 && value[0] == '"' && value[len(value) - 1] == '"' {
2317
+ value = value[1:len(value) - 1]
2318
+ }
2319
+
2320
+ replaced, was_allocation := strings.replace_all(value, "$HOME", home, allocator)
2321
+ if !was_allocation {
2322
+ replaced = clone_string(replaced, allocator)
2323
+ }
2324
+ delete(fallback, allocator)
2325
+ return replaced
2326
+ }
2327
+
2328
+ return fallback
2329
+ }
2330
+
2331
+ @(private = "file")
2332
+ build_app_scoped_dir :: proc(allocator: runtime.Allocator, base: string, app_info: AppInfo) -> string {
2333
+ if len(app_info.identifier) == 0 || len(app_info.channel) == 0 {
2334
+ return clone_string(base, allocator)
2335
+ }
2336
+ return join_path(allocator, {base, app_info.identifier, app_info.channel})
2337
+ }