@zogien/native-sdk 0.11.0-fork.3 → 0.11.0-fork.5

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 (67) hide show
  1. package/README.md +1 -1
  2. package/build/app.zig +53 -34
  3. package/build/cli.zig +22 -0
  4. package/package.json +10 -10
  5. package/packages/core/package-lock.json +2 -2
  6. package/packages/core/package.json +1 -1
  7. package/schemas/app.schema.json +9 -1
  8. package/skill-data/core/references/bridge-security-native-capabilities.md +4 -0
  9. package/skill-data/ts-core/SKILL.md +2 -0
  10. package/src/app_runner/app_assets.zig +85 -1
  11. package/src/app_runner/root.zig +63 -3
  12. package/src/app_runner/ts_core_main.zig +8 -0
  13. package/src/platform/linux/cef_host.cpp +22 -0
  14. package/src/platform/linux/gtk_host.c +317 -34
  15. package/src/platform/linux/gtk_host.h +37 -0
  16. package/src/platform/linux/root.zig +114 -20
  17. package/src/platform/macos/appkit_host.h +53 -0
  18. package/src/platform/macos/appkit_host.m +667 -67
  19. package/src/platform/macos/cef_host.mm +21 -0
  20. package/src/platform/macos/root.zig +146 -29
  21. package/src/platform/null_platform.zig +136 -0
  22. package/src/platform/root.zig +7 -0
  23. package/src/platform/types.zig +90 -0
  24. package/src/platform/windows/dialog_test.cpp +303 -0
  25. package/src/platform/windows/host_loop_test.cpp +169 -0
  26. package/src/platform/windows/root.zig +262 -39
  27. package/src/platform/windows/session_end_test.cpp +303 -0
  28. package/src/platform/windows/webview2_host.cpp +1435 -306
  29. package/src/primitives/app_manifest/root.zig +1 -0
  30. package/src/primitives/app_manifest/tests.zig +35 -0
  31. package/src/primitives/app_manifest/types.zig +12 -0
  32. package/src/primitives/app_manifest/validation.zig +44 -6
  33. package/src/root.zig +8 -0
  34. package/src/runtime/builtin_bridge.zig +108 -13
  35. package/src/runtime/channel_sources.zig +49 -1
  36. package/src/runtime/core.zig +24 -3
  37. package/src/runtime/dialog_tests.zig +367 -0
  38. package/src/runtime/dialogs.zig +385 -0
  39. package/src/runtime/effects.zig +83 -3
  40. package/src/runtime/effects_host_tests.zig +69 -1
  41. package/src/runtime/flow.zig +20 -6
  42. package/src/runtime/flow_reference.zig +20 -6
  43. package/src/runtime/platform_bridge_tests.zig +1 -0
  44. package/src/runtime/root.zig +9 -0
  45. package/src/runtime/single_instance.zig +893 -0
  46. package/src/runtime/system_services.zig +64 -82
  47. package/src/runtime/ui_app.zig +67 -0
  48. package/src/tooling/manifest.zig +111 -3
  49. package/src/tooling/package.zig +98 -8
  50. package/src/tooling/raw_manifest.zig +7 -0
  51. package/src/tooling/root.zig +2 -0
  52. package/src/tooling/templates.zig +44 -2
  53. package/src/tooling/update.zig +228 -10
  54. package/src/tooling/windows_installer.zig +245 -0
  55. package/src/tooling/zip_writer.zig +211 -0
  56. package/src/updater/engine.zig +936 -0
  57. package/src/updater/feed.zig +261 -12
  58. package/src/updater/macos.zig +187 -0
  59. package/src/updater/root.zig +41 -0
  60. package/src/updater/windows.zig +470 -0
  61. package/src/windows_install/config.zig +247 -0
  62. package/src/windows_install/main.zig +690 -0
  63. package/src/windows_install/payload.zig +146 -0
  64. package/src/windows_install/switch.zig +184 -0
  65. package/src/windows_install/tests.zig +11 -0
  66. package/src/windows_install/win32.zig +248 -0
  67. package/tools/native-sdk/main.zig +10 -9
@@ -3712,6 +3712,27 @@ int native_sdk_appkit_show_message_dialog(native_sdk_appkit_host_t *host, const
3712
3712
  }
3713
3713
  }
3714
3714
 
3715
+ /* Asynchronous dialogs are not offered here: root.zig leaves them unbound
3716
+ * for the Chromium engine, so the runtime shows the blocking ones above. */
3717
+ int native_sdk_appkit_request_dialog(native_sdk_appkit_host_t *host, const native_sdk_appkit_dialog_request_t *request) {
3718
+ (void)host;
3719
+ (void)request;
3720
+ return 0;
3721
+ }
3722
+
3723
+ void native_sdk_appkit_cancel_dialog(native_sdk_appkit_host_t *host, uint64_t id) {
3724
+ (void)host;
3725
+ (void)id;
3726
+ }
3727
+
3728
+ int native_sdk_appkit_poll_dialog_result(native_sdk_appkit_host_t *host, char *buffer, size_t buffer_len, native_sdk_appkit_dialog_result_t *result) {
3729
+ (void)host;
3730
+ (void)buffer;
3731
+ (void)buffer_len;
3732
+ (void)result;
3733
+ return 0;
3734
+ }
3735
+
3715
3736
  static NSImage *NativeSdkCefTrayImageWithOpacity(NSImage *source, double opacity) {
3716
3737
  if (!source) return nil;
3717
3738
  const CGFloat fraction = MAX(0.0, MIN(1.0, opacity));
@@ -9,6 +9,7 @@ const canvas = @import("canvas");
9
9
  // macOS mask/inset render so the Dock tile matches `native package`.
10
10
  const app_icon = canvas.app_icon;
11
11
  const updater_c_api = @import("../../updater/c_api.zig");
12
+ const update_feed = @import("../../updater/feed.zig");
12
13
 
13
14
  comptime {
14
15
  _ = updater_c_api.native_sdk_update_verify_feed;
@@ -355,6 +356,23 @@ const AppKitMessageDialogOpts = extern struct {
355
356
  tertiary_button_len: usize,
356
357
  };
357
358
 
359
+ const AppKitDialogRequest = extern struct {
360
+ id: u64,
361
+ parent_window_id: u64,
362
+ kind: c_int,
363
+ open_opts: ?*const AppKitOpenDialogOpts,
364
+ save_opts: ?*const AppKitSaveDialogOpts,
365
+ message_opts: ?*const AppKitMessageDialogOpts,
366
+ };
367
+
368
+ const AppKitDialogResult = extern struct {
369
+ id: u64,
370
+ outcome: c_int,
371
+ button: c_int,
372
+ count: usize,
373
+ bytes_written: usize,
374
+ };
375
+
358
376
  const AppKitWidgetAccessibilityNode = extern struct {
359
377
  id: u64,
360
378
  parent_id: u64,
@@ -461,6 +479,9 @@ const AppKitTrayChartRow = extern struct {
461
479
  extern fn native_sdk_appkit_show_open_dialog(host: *AppKitHost, opts: *const AppKitOpenDialogOpts, buffer: [*]u8, buffer_len: usize) AppKitOpenDialogResult;
462
480
  extern fn native_sdk_appkit_show_save_dialog(host: *AppKitHost, opts: *const AppKitSaveDialogOpts, buffer: [*]u8, buffer_len: usize) usize;
463
481
  extern fn native_sdk_appkit_show_message_dialog(host: *AppKitHost, opts: *const AppKitMessageDialogOpts) c_int;
482
+ extern fn native_sdk_appkit_request_dialog(host: *AppKitHost, request: *const AppKitDialogRequest) c_int;
483
+ extern fn native_sdk_appkit_cancel_dialog(host: *AppKitHost, id: u64) void;
484
+ extern fn native_sdk_appkit_poll_dialog_result(host: *AppKitHost, buffer: [*]u8, buffer_len: usize, result: *AppKitDialogResult) c_int;
464
485
  extern fn native_sdk_appkit_create_tray(host: *AppKitHost, status_item_id: u32, icon_path: [*]const u8, icon_path_len: usize, title: [*]const u8, title_len: usize, tooltip: [*]const u8, tooltip_len: usize, visible: c_int, width: f64, tone: c_int, icon_opacity: f64, monospaced: c_int, font_size: f64, font_weight: c_int, activation_command: [*]const u8, activation_command_len: usize, alternate_activation_command: [*]const u8, alternate_activation_command_len: usize, open_command: [*]const u8, open_command_len: usize) void;
465
486
  extern fn native_sdk_appkit_update_tray_shell(host: *AppKitHost, status_item_id: u32, icon_path: [*]const u8, icon_path_len: usize, tooltip: [*]const u8, tooltip_len: usize, visible: c_int, activation_command: [*]const u8, activation_command_len: usize, alternate_activation_command: [*]const u8, alternate_activation_command_len: usize, open_command: [*]const u8, open_command_len: usize) void;
466
487
  extern fn native_sdk_appkit_update_tray_menu(host: *AppKitHost, status_item_id: u32, item_ids: [*]const u32, labels: [*]const [*]const u8, label_lens: [*]const usize, separators: [*]const c_int, enabled_flags: [*]const c_int, details: [*]const [*]const u8, detail_lens: [*]const usize, roles: [*]const c_int, keys: [*]const [*]const u8, key_lens: [*]const usize, modifiers: [*]const u32, count: usize) void;
@@ -685,12 +706,10 @@ pub const MacPlatform = struct {
685
706
  const dock_icon = planDockIcon(app_info.icon_path);
686
707
  const icon_path = if (dock_icon == .host_file) app_info.icon_path else "";
687
708
  const host = native_sdk_appkit_create(app_info.app_name.ptr, app_info.app_name.len, display_name.ptr, display_name.len, app_info.version.ptr, app_info.version.len, app_info.description.ptr, app_info.description.len, if (app_info.has_web_content) 1 else 0, if (app_info.dock_visible) 1 else 0, window_title.ptr, window_title.len, app_info.bundle_id.ptr, app_info.bundle_id.len, icon_path.ptr, icon_path.len, window_options.label.ptr, window_options.label.len, frame.x, frame.y, frame.width, frame.height, if (window_options.restore_state) 1 else 0, initialPlacementInt(window_options.initial_placement), restorePolicyInt(window_options.restore_policy), if (window_options.resizable) 1 else 0, titlebarStyleInt(window_options.titlebar), showModeInt(window_options.show), windowFlags(window_options)) orelse return error.CreateFailed;
688
- const update_target = comptime switch (builtin.cpu.arch) {
689
- .aarch64 => "macos-aarch64",
690
- .x86_64 => "macos-x86_64",
691
- else => "macos-unsupported",
692
- };
693
- native_sdk_appkit_configure_updates(host, app_info.update_feed_url.ptr, app_info.update_feed_url.len, app_info.update_public_key.ptr, app_info.update_public_key.len, if (app_info.update_check_on_start) 1 else 0, update_target.ptr, update_target.len);
709
+ const update_target = comptime update_feed.currentTarget() orelse "macos-unsupported";
710
+ var update_feed_url_buffer: [update_feed.max_feed_url_bytes]u8 = undefined;
711
+ const update_feed_url = expandedUpdateFeedUrl(&update_feed_url_buffer, app_info.update_feed_url, update_target, app_info.version);
712
+ native_sdk_appkit_configure_updates(host, update_feed_url.ptr, update_feed_url.len, app_info.update_public_key.ptr, app_info.update_public_key.len, if (app_info.update_check_on_start) 1 else 0, update_target.ptr, update_target.len);
694
713
  switch (dock_icon) {
695
714
  .host_file => {},
696
715
  .masked_render => spawnDevDockIconRender(host, app_info.icon_path),
@@ -805,6 +824,9 @@ pub const MacPlatform = struct {
805
824
  .show_open_dialog_fn = showOpenDialog,
806
825
  .show_save_dialog_fn = showSaveDialog,
807
826
  .show_message_dialog_fn = showMessageDialog,
827
+ .request_dialog_fn = if (self.web_engine == .system) requestDialog else null,
828
+ .cancel_dialog_fn = if (self.web_engine == .system) cancelDialog else null,
829
+ .poll_dialog_result_fn = if (self.web_engine == .system) pollDialogResult else null,
808
830
  .show_notification_fn = showNotification,
809
831
  .set_credential_fn = setCredential,
810
832
  .get_credential_fn = getCredential,
@@ -2390,6 +2412,18 @@ fn checkForUpdates(context: ?*anyopaque, user_initiated: bool) anyerror!void {
2390
2412
  if (native_sdk_appkit_check_for_updates(self.host, if (user_initiated) 1 else 0) == 0) return error.UnsupportedService;
2391
2413
  }
2392
2414
 
2415
+ /// The feed URL the host fetches: the manifest's, with `{target}` and
2416
+ /// `{current_version}` expanded for this install. A URL this build cannot
2417
+ /// expand leaves updates unconfigured rather than fetching some other
2418
+ /// target's feed.
2419
+ fn expandedUpdateFeedUrl(buffer: []u8, template: []const u8, target: []const u8, version: []const u8) []const u8 {
2420
+ if (template.len == 0) return "";
2421
+ return update_feed.expandFeedUrl(buffer, template, target, version) catch |err| {
2422
+ std.log.warn("updates disabled: feed_url cannot be expanded for {s} {s} ({s})", .{ target, version, @errorName(err) });
2423
+ return "";
2424
+ };
2425
+ }
2426
+
2393
2427
  fn configureSecurityPolicy(context: ?*anyopaque, policy: security.Policy) anyerror!void {
2394
2428
  const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2395
2429
  var origins_buffer: [4096]u8 = undefined;
@@ -2683,11 +2717,9 @@ fn viewKindInt(kind: platform_mod.ViewKind) c_int {
2683
2717
  };
2684
2718
  }
2685
2719
 
2686
- fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions, buffer: []u8) anyerror!platform_mod.OpenDialogResult {
2687
- const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2688
- var ext_buf: [1024]u8 = undefined;
2689
- const ext_str = flattenFilters(options.filters, &ext_buf);
2690
- const opts = AppKitOpenDialogOpts{
2720
+ fn appKitOpenDialogOpts(options: platform_mod.OpenDialogOptions, ext_buf: []u8) AppKitOpenDialogOpts {
2721
+ const ext_str = flattenFilters(options.filters, ext_buf);
2722
+ return .{
2691
2723
  .title = options.title.ptr,
2692
2724
  .title_len = options.title.len,
2693
2725
  .default_path = options.default_path.ptr,
@@ -2697,19 +2729,11 @@ fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions,
2697
2729
  .allow_directories = if (options.allow_directories) 1 else 0,
2698
2730
  .allow_multiple = if (options.allow_multiple) 1 else 0,
2699
2731
  };
2700
- const result = native_sdk_appkit_show_open_dialog(self.host, &opts, buffer.ptr, buffer.len);
2701
- if (result.bytes_written > buffer.len) return error.NoSpaceLeft;
2702
- return .{
2703
- .count = result.count,
2704
- .paths = buffer[0..result.bytes_written],
2705
- };
2706
2732
  }
2707
2733
 
2708
- fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions, buffer: []u8) anyerror!?[]const u8 {
2709
- const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2710
- var ext_buf: [1024]u8 = undefined;
2711
- const ext_str = flattenFilters(options.filters, &ext_buf);
2712
- const opts = AppKitSaveDialogOpts{
2734
+ fn appKitSaveDialogOpts(options: platform_mod.SaveDialogOptions, ext_buf: []u8) AppKitSaveDialogOpts {
2735
+ const ext_str = flattenFilters(options.filters, ext_buf);
2736
+ return .{
2713
2737
  .title = options.title.ptr,
2714
2738
  .title_len = options.title.len,
2715
2739
  .default_path = options.default_path.ptr,
@@ -2719,15 +2743,10 @@ fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions,
2719
2743
  .extensions = ext_str.ptr,
2720
2744
  .extensions_len = ext_str.len,
2721
2745
  };
2722
- const written = native_sdk_appkit_show_save_dialog(self.host, &opts, buffer.ptr, buffer.len);
2723
- if (written > buffer.len) return error.NoSpaceLeft;
2724
- if (written == 0) return null;
2725
- return buffer[0..written];
2726
2746
  }
2727
2747
 
2728
- fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOptions) anyerror!platform_mod.MessageDialogResult {
2729
- const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2730
- const opts = AppKitMessageDialogOpts{
2748
+ fn appKitMessageDialogOpts(options: platform_mod.MessageDialogOptions) AppKitMessageDialogOpts {
2749
+ return .{
2731
2750
  .style = @intFromEnum(options.style),
2732
2751
  .title = options.title.ptr,
2733
2752
  .title_len = options.title.len,
@@ -2742,10 +2761,96 @@ fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOp
2742
2761
  .tertiary_button = options.tertiary_button.ptr,
2743
2762
  .tertiary_button_len = options.tertiary_button.len,
2744
2763
  };
2764
+ }
2765
+
2766
+ fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions, buffer: []u8) anyerror!platform_mod.OpenDialogResult {
2767
+ const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2768
+ var ext_buf: [1024]u8 = undefined;
2769
+ const opts = appKitOpenDialogOpts(options, &ext_buf);
2770
+ const result = native_sdk_appkit_show_open_dialog(self.host, &opts, buffer.ptr, buffer.len);
2771
+ if (result.bytes_written > buffer.len) return error.NoSpaceLeft;
2772
+ return .{
2773
+ .count = result.count,
2774
+ .paths = buffer[0..result.bytes_written],
2775
+ };
2776
+ }
2777
+
2778
+ fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions, buffer: []u8) anyerror!?[]const u8 {
2779
+ const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2780
+ var ext_buf: [1024]u8 = undefined;
2781
+ const opts = appKitSaveDialogOpts(options, &ext_buf);
2782
+ const written = native_sdk_appkit_show_save_dialog(self.host, &opts, buffer.ptr, buffer.len);
2783
+ if (written > buffer.len) return error.NoSpaceLeft;
2784
+ if (written == 0) return null;
2785
+ return buffer[0..written];
2786
+ }
2787
+
2788
+ fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOptions) anyerror!platform_mod.MessageDialogResult {
2789
+ const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2790
+ const opts = appKitMessageDialogOpts(options);
2745
2791
  const result = native_sdk_appkit_show_message_dialog(self.host, &opts);
2746
2792
  return @enumFromInt(result);
2747
2793
  }
2748
2794
 
2795
+ // The host copies every option before returning, so they may live on this
2796
+ // stack; the result comes back through pollDialogResult on a later wake.
2797
+ fn requestDialog(context: ?*anyopaque, request: platform_mod.DialogRequest) anyerror!void {
2798
+ const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2799
+ var ext_buf: [1024]u8 = undefined;
2800
+ var open_opts: AppKitOpenDialogOpts = undefined;
2801
+ var save_opts: AppKitSaveDialogOpts = undefined;
2802
+ var message_opts: AppKitMessageDialogOpts = undefined;
2803
+ var native_request = AppKitDialogRequest{
2804
+ .id = request.id,
2805
+ .parent_window_id = request.parent_window_id orelse 0,
2806
+ .kind = @intFromEnum(std.meta.activeTag(request.options)),
2807
+ .open_opts = null,
2808
+ .save_opts = null,
2809
+ .message_opts = null,
2810
+ };
2811
+ switch (request.options) {
2812
+ .open => |options| {
2813
+ open_opts = appKitOpenDialogOpts(options, &ext_buf);
2814
+ native_request.open_opts = &open_opts;
2815
+ },
2816
+ .save => |options| {
2817
+ save_opts = appKitSaveDialogOpts(options, &ext_buf);
2818
+ native_request.save_opts = &save_opts;
2819
+ },
2820
+ .message => |options| {
2821
+ message_opts = appKitMessageDialogOpts(options);
2822
+ native_request.message_opts = &message_opts;
2823
+ },
2824
+ }
2825
+ if (native_sdk_appkit_request_dialog(self.host, &native_request) == 0) return error.DialogUnavailable;
2826
+ }
2827
+
2828
+ fn cancelDialog(context: ?*anyopaque, id: platform_mod.DialogRequestId) void {
2829
+ const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2830
+ native_sdk_appkit_cancel_dialog(self.host, id);
2831
+ }
2832
+
2833
+ fn pollDialogResult(context: ?*anyopaque, buffer: []u8) ?platform_mod.DialogResult {
2834
+ const self: *MacPlatform = @ptrCast(@alignCast(context.?));
2835
+ var result: AppKitDialogResult = undefined;
2836
+ if (native_sdk_appkit_poll_dialog_result(self.host, buffer.ptr, buffer.len, &result) == 0) return null;
2837
+ return .{
2838
+ .id = result.id,
2839
+ .outcome = switch (result.outcome) {
2840
+ 0 => .selected,
2841
+ 1 => .cancelled,
2842
+ else => .failed,
2843
+ },
2844
+ .paths = buffer[0..@min(result.bytes_written, buffer.len)],
2845
+ .count = result.count,
2846
+ .button = switch (result.button) {
2847
+ 1 => .secondary,
2848
+ 2 => .tertiary,
2849
+ else => .primary,
2850
+ },
2851
+ };
2852
+ }
2853
+
2749
2854
  const max_tray_items: usize = 32;
2750
2855
 
2751
2856
  fn createTray(context: ?*anyopaque, status_item_id: platform_mod.StatusItemId, options: platform_mod.TrayOptions) anyerror!void {
@@ -3103,6 +3208,18 @@ test "mac updater hashes archives off the main thread and treats user cancellati
3103
3208
  try std.testing.expect(std.mem.indexOf(u8, download_body, "strongSelf.updateCheckRunning = NO;") != null);
3104
3209
  }
3105
3210
 
3211
+ test "mac updater fetches the feed URL expanded for this install" {
3212
+ var buffer: [update_feed.max_feed_url_bytes]u8 = undefined;
3213
+ try std.testing.expectEqualStrings(
3214
+ "https://example.com/releases/macos-aarch64/native-update.json?from=1.2.3",
3215
+ expandedUpdateFeedUrl(&buffer, "https://example.com/releases/{target}/native-update.json?from={current_version}", "macos-aarch64", "1.2.3"),
3216
+ );
3217
+ try std.testing.expectEqualStrings("", expandedUpdateFeedUrl(&buffer, "", "macos-aarch64", "1.2.3"));
3218
+ // A build with no release format fetches nothing rather than another
3219
+ // target's feed.
3220
+ try std.testing.expectEqualStrings("", expandedUpdateFeedUrl(&buffer, "https://example.com/{target}.json", "macos-unsupported", "1.2.3"));
3221
+ }
3222
+
3106
3223
  test "mac updater binds packaged releases to the installed bundle identity" {
3107
3224
  const host_source = @embedFile("appkit_host.m");
3108
3225
  try std.testing.expect(std.mem.indexOf(u8, host_source, "static NSString *NativeSdkPackagedBundleIdentifier(void)") != null);
@@ -117,8 +117,36 @@ const SaveDialogOptions = types.SaveDialogOptions;
117
117
  const MessageDialogStyle = types.MessageDialogStyle;
118
118
  const MessageDialogResult = types.MessageDialogResult;
119
119
  const MessageDialogOptions = types.MessageDialogOptions;
120
+ const DialogRequestId = types.DialogRequestId;
121
+ const DialogKind = types.DialogKind;
122
+ const DialogRequest = types.DialogRequest;
123
+ const DialogOutcome = types.DialogOutcome;
124
+ const DialogResult = types.DialogResult;
120
125
  const NotificationOptions = types.NotificationOptions;
121
126
  const CredentialKey = types.CredentialKey;
127
+
128
+ const max_null_dialogs = 16;
129
+
130
+ /// One asynchronous dialog the null platform was asked to present.
131
+ pub const NullDialog = struct {
132
+ state: enum { free, presented, finished } = .free,
133
+ id: DialogRequestId = 0,
134
+ kind: DialogKind = .open,
135
+ parent_window_id: ?WindowId = null,
136
+ /// Request order, so `pollDialogResult` reports finishes in order.
137
+ finished_order: u64 = 0,
138
+ outcome: DialogOutcome = .cancelled,
139
+ button: MessageDialogResult = .primary,
140
+ count: usize = 0,
141
+ paths: [512]u8 = undefined,
142
+ paths_len: usize = 0,
143
+ title: [128]u8 = undefined,
144
+ title_len: usize = 0,
145
+
146
+ pub fn titleText(self: *const NullDialog) []const u8 {
147
+ return self.title[0..self.title_len];
148
+ }
149
+ };
122
150
  const Credential = types.Credential;
123
151
  const LocalTimeStyle = types.LocalTimeStyle;
124
152
  const StatusItemId = types.StatusItemId;
@@ -527,6 +555,17 @@ pub const NullPlatform = struct {
527
555
  save_dialog_count: usize = 0,
528
556
  message_dialog_count: usize = 0,
529
557
  message_dialog_result: MessageDialogResult = .primary,
558
+ /// Asynchronous dialogs, once a test opts in (`useAsyncDialogs`):
559
+ /// requests the runtime made, in order, and the results tests finished
560
+ /// them with. Nothing finishes on its own; a test calls `finishDialog`
561
+ /// (or the runtime cancels), which posts a wake like a real host.
562
+ dialogs: [max_null_dialogs]NullDialog = [_]NullDialog{.{}} ** max_null_dialogs,
563
+ dialog_request_count: usize = 0,
564
+ dialog_cancel_count: usize = 0,
565
+ dialog_finish_sequence: u64 = 0,
566
+ /// The next `requestDialog` fails, modelling a host that cannot
567
+ /// present.
568
+ fail_next_dialog_request: bool = false,
530
569
  notification_title: [max_notification_title_bytes]u8 = undefined,
531
570
  notification_title_len: usize = 0,
532
571
  notification_subtitle: [max_notification_subtitle_bytes]u8 = undefined,
@@ -1650,6 +1689,103 @@ pub const NullPlatform = struct {
1650
1689
  return self.message_dialog_result;
1651
1690
  }
1652
1691
 
1692
+ /// Route `services` (this platform's, as the runtime holds them) through
1693
+ /// the asynchronous dialog services. Opt-in: by default the null
1694
+ /// platform answers the blocking dialog calls at once, which app tests
1695
+ /// rely on, and its asynchronous dialogs never finish on their own.
1696
+ pub fn useAsyncDialogs(self: *NullPlatform, services: *PlatformServices) void {
1697
+ std.debug.assert(services.context == @as(?*anyopaque, @ptrCast(self)));
1698
+ services.request_dialog_fn = requestDialog;
1699
+ services.cancel_dialog_fn = cancelDialog;
1700
+ services.poll_dialog_result_fn = pollDialogResult;
1701
+ }
1702
+
1703
+ fn requestDialog(context: ?*anyopaque, request: DialogRequest) anyerror!void {
1704
+ const self: *NullPlatform = @ptrCast(@alignCast(context.?));
1705
+ if (self.fail_next_dialog_request) {
1706
+ self.fail_next_dialog_request = false;
1707
+ return error.DialogUnavailable;
1708
+ }
1709
+ const slot = for (&self.dialogs) |*dialog| {
1710
+ if (dialog.state == .free) break dialog;
1711
+ } else return error.DialogUnavailable;
1712
+ const title = switch (request.options) {
1713
+ .open => |open| open.title,
1714
+ .save => |save| save.title,
1715
+ .message => |message| if (message.title.len > 0) message.title else message.message,
1716
+ };
1717
+ slot.* = .{ .state = .presented, .id = request.id, .kind = request.options, .parent_window_id = request.parent_window_id };
1718
+ slot.title_len = @min(title.len, slot.title.len);
1719
+ @memcpy(slot.title[0..slot.title_len], title[0..slot.title_len]);
1720
+ self.dialog_request_count += 1;
1721
+ }
1722
+
1723
+ fn cancelDialog(context: ?*anyopaque, id: DialogRequestId) void {
1724
+ const self: *NullPlatform = @ptrCast(@alignCast(context.?));
1725
+ const dialog = self.presentedDialog(id) orelse return;
1726
+ self.dialog_cancel_count += 1;
1727
+ self.markDialogFinished(dialog, .cancelled);
1728
+ }
1729
+
1730
+ fn pollDialogResult(context: ?*anyopaque, buffer: []u8) ?DialogResult {
1731
+ const self: *NullPlatform = @ptrCast(@alignCast(context.?));
1732
+ var next: ?*NullDialog = null;
1733
+ for (&self.dialogs) |*dialog| {
1734
+ if (dialog.state != .finished) continue;
1735
+ if (next == null or dialog.finished_order < next.?.finished_order) next = dialog;
1736
+ }
1737
+ const dialog = next orelse return null;
1738
+ const len = @min(dialog.paths_len, buffer.len);
1739
+ @memcpy(buffer[0..len], dialog.paths[0..len]);
1740
+ const result: DialogResult = .{ .id = dialog.id, .outcome = dialog.outcome, .paths = buffer[0..len], .count = dialog.count, .button = dialog.button };
1741
+ dialog.* = .{};
1742
+ return result;
1743
+ }
1744
+
1745
+ fn presentedDialog(self: *NullPlatform, id: DialogRequestId) ?*NullDialog {
1746
+ for (&self.dialogs) |*dialog| {
1747
+ if (dialog.state == .presented and dialog.id == id) return dialog;
1748
+ }
1749
+ return null;
1750
+ }
1751
+
1752
+ fn markDialogFinished(self: *NullPlatform, dialog: *NullDialog, outcome: DialogOutcome) void {
1753
+ dialog.state = .finished;
1754
+ dialog.outcome = outcome;
1755
+ dialog.finished_order = self.dialog_finish_sequence;
1756
+ self.dialog_finish_sequence += 1;
1757
+ _ = self.wake_count.fetchAdd(1, .release);
1758
+ }
1759
+
1760
+ /// Test helper: how many dialogs are showing right now.
1761
+ pub fn presentedDialogCount(self: *const NullPlatform) usize {
1762
+ var count: usize = 0;
1763
+ for (self.dialogs) |dialog| {
1764
+ if (dialog.state == .presented) count += 1;
1765
+ }
1766
+ return count;
1767
+ }
1768
+
1769
+ /// Test helper: the presented dialog with `id`, for inspecting its
1770
+ /// kind, parent and title.
1771
+ pub fn findPresentedDialog(self: *NullPlatform, id: DialogRequestId) ?*const NullDialog {
1772
+ return self.presentedDialog(id);
1773
+ }
1774
+
1775
+ /// Test helper: the user picked `paths` (newline-separated) in an
1776
+ /// open or save dialog, or pressed `button` in a message dialog. Posts
1777
+ /// the wake a real host would; `error.DialogNotPresented` names a
1778
+ /// dialog that is not showing.
1779
+ pub fn finishDialog(self: *NullPlatform, id: DialogRequestId, outcome: DialogOutcome, paths: []const u8, button: MessageDialogResult) error{ DialogNotPresented, NoSpaceLeft }!void {
1780
+ const dialog = self.presentedDialog(id) orelse return error.DialogNotPresented;
1781
+ if (paths.len > dialog.paths.len) return error.NoSpaceLeft;
1782
+ @memcpy(dialog.paths[0..paths.len], paths);
1783
+ dialog.paths_len = paths.len;
1784
+ dialog.count = if (paths.len == 0) 0 else std.mem.count(u8, paths, "\n") + 1;
1785
+ dialog.button = button;
1786
+ self.markDialogFinished(dialog, outcome);
1787
+ }
1788
+
1653
1789
  fn showNotification(context: ?*anyopaque, options: NotificationOptions) anyerror!void {
1654
1790
  const self: *NullPlatform = @ptrCast(@alignCast(context.?));
1655
1791
  self.notification_title = undefined;
@@ -119,6 +119,7 @@ pub const ViewPatch = types.ViewPatch;
119
119
  pub const Cursor = types.Cursor;
120
120
  pub const ViewInfo = types.ViewInfo;
121
121
  pub const AppInfo = types.AppInfo;
122
+ pub const UpdateMode = types.UpdateMode;
122
123
  pub const Surface = types.Surface;
123
124
  pub const BridgeMessage = types.BridgeMessage;
124
125
  pub const ViewFocusEvent = types.ViewFocusEvent;
@@ -136,6 +137,12 @@ pub const SaveDialogOptions = types.SaveDialogOptions;
136
137
  pub const MessageDialogStyle = types.MessageDialogStyle;
137
138
  pub const MessageDialogResult = types.MessageDialogResult;
138
139
  pub const MessageDialogOptions = types.MessageDialogOptions;
140
+ pub const DialogRequestId = types.DialogRequestId;
141
+ pub const DialogKind = types.DialogKind;
142
+ pub const DialogOptions = types.DialogOptions;
143
+ pub const DialogRequest = types.DialogRequest;
144
+ pub const DialogOutcome = types.DialogOutcome;
145
+ pub const DialogResult = types.DialogResult;
139
146
  pub const NotificationOptions = types.NotificationOptions;
140
147
  pub const NotificationCommandEvent = types.NotificationCommandEvent;
141
148
  pub const CredentialKey = types.CredentialKey;
@@ -1289,6 +1289,9 @@ pub const ViewInfo = struct {
1289
1289
  }
1290
1290
  };
1291
1291
 
1292
+ /// Who drives updates on macOS (the manifest's `updates.mode`).
1293
+ pub const UpdateMode = enum { host, app };
1294
+
1292
1295
  pub const AppInfo = struct {
1293
1296
  app_name: []const u8 = "native-sdk",
1294
1297
  /// The human-facing application name (app.zon `display_name`) the
@@ -1323,9 +1326,15 @@ pub const AppInfo = struct {
1323
1326
  window_title: []const u8 = "",
1324
1327
  bundle_id: []const u8 = "dev.native_sdk.app",
1325
1328
  icon_path: []const u8 = "",
1329
+ /// The manifest's feed URL, before the host expands `{target}` and
1330
+ /// `{current_version}` for this install.
1326
1331
  update_feed_url: []const u8 = "",
1332
+ /// The trusted update keys: base64 Ed25519 public keys, comma-joined.
1327
1333
  update_public_key: []const u8 = "",
1328
1334
  update_check_on_start: bool = false,
1335
+ /// `.app`: the app drives updates through `native-sdk.updates` and the
1336
+ /// macOS host shows no update UI of its own.
1337
+ update_mode: UpdateMode = .host,
1329
1338
  main_window: WindowOptions = .{},
1330
1339
  windows: []const WindowOptions = &.{},
1331
1340
 
@@ -1445,6 +1454,56 @@ pub const MessageDialogOptions = struct {
1445
1454
  tertiary_button: []const u8 = "",
1446
1455
  };
1447
1456
 
1457
+ /// Identifies one asynchronous dialog (`PlatformServices.requestDialog`).
1458
+ /// The runtime allocates ids; zero is never used.
1459
+ pub const DialogRequestId = u64;
1460
+
1461
+ pub const DialogKind = enum(u8) {
1462
+ open,
1463
+ save,
1464
+ message,
1465
+ };
1466
+
1467
+ pub const DialogOptions = union(DialogKind) {
1468
+ open: OpenDialogOptions,
1469
+ save: SaveDialogOptions,
1470
+ message: MessageDialogOptions,
1471
+ };
1472
+
1473
+ /// One dialog the runtime asks the platform to present without blocking.
1474
+ /// Strings borrow runtime storage only for the duration of the call; a
1475
+ /// platform that presents later copies what it keeps.
1476
+ pub const DialogRequest = struct {
1477
+ id: DialogRequestId,
1478
+ /// The window the dialog belongs to (a macOS sheet, a Windows owner, a
1479
+ /// GTK transient parent). Null presents it without a parent.
1480
+ parent_window_id: ?WindowId = null,
1481
+ options: DialogOptions,
1482
+ };
1483
+
1484
+ pub const DialogOutcome = enum(u8) {
1485
+ /// Open and save: the user chose `paths`. Message: the user pressed
1486
+ /// `button`.
1487
+ selected,
1488
+ /// Closed without a choice: the user cancelled or dismissed it, or it
1489
+ /// was closed for them (its window closed, the request was cancelled,
1490
+ /// the session is ending).
1491
+ cancelled,
1492
+ /// The platform could not present the dialog.
1493
+ failed,
1494
+ };
1495
+
1496
+ pub const DialogResult = struct {
1497
+ id: DialogRequestId,
1498
+ outcome: DialogOutcome,
1499
+ /// Open: `count` newline-separated paths. Save: one path. Empty
1500
+ /// otherwise.
1501
+ paths: []const u8 = "",
1502
+ count: usize = 0,
1503
+ /// Message dialogs reporting `.selected`.
1504
+ button: MessageDialogResult = .primary,
1505
+ };
1506
+
1448
1507
  pub const NotificationOptions = struct {
1449
1508
  /// Stable host identity. Reissuing a non-empty id replaces the
1450
1509
  /// notification with that id and its action binding.
@@ -2811,6 +2870,18 @@ pub const PlatformServices = struct {
2811
2870
  show_open_dialog_fn: ?*const fn (context: ?*anyopaque, options: OpenDialogOptions, buffer: []u8) anyerror!OpenDialogResult = null,
2812
2871
  show_save_dialog_fn: ?*const fn (context: ?*anyopaque, options: SaveDialogOptions, buffer: []u8) anyerror!?[]const u8 = null,
2813
2872
  show_message_dialog_fn: ?*const fn (context: ?*anyopaque, options: MessageDialogOptions) anyerror!MessageDialogResult = null,
2873
+ /// Asynchronous dialogs. `request_dialog_fn` returns promptly and
2874
+ /// never runs a nested event loop on the caller's stack: the platform
2875
+ /// presents the dialog on its own schedule, and when it finishes it
2876
+ /// keeps the result and posts a wake (`wake_fn`). The runtime drains
2877
+ /// finished dialogs with `poll_dialog_result_fn`, whose result paths
2878
+ /// borrow `buffer`. `cancel_dialog_fn` closes a presented dialog,
2879
+ /// which then finishes as `.cancelled`; unknown ids are a no-op.
2880
+ /// Platforms without these keep the synchronous `show_*_dialog_fn`
2881
+ /// family, which the runtime falls back to.
2882
+ request_dialog_fn: ?*const fn (context: ?*anyopaque, request: DialogRequest) anyerror!void = null,
2883
+ cancel_dialog_fn: ?*const fn (context: ?*anyopaque, id: DialogRequestId) void = null,
2884
+ poll_dialog_result_fn: ?*const fn (context: ?*anyopaque, buffer: []u8) ?DialogResult = null,
2814
2885
  show_notification_fn: ?*const fn (context: ?*anyopaque, options: NotificationOptions) anyerror!void = null,
2815
2886
  set_credential_fn: ?*const fn (context: ?*anyopaque, credential: Credential) anyerror!void = null,
2816
2887
  get_credential_fn: ?*const fn (context: ?*anyopaque, key: CredentialKey, buffer: []u8) anyerror![]const u8 = null,
@@ -3368,6 +3439,25 @@ pub const PlatformServices = struct {
3368
3439
  return msg_fn(self.context, options);
3369
3440
  }
3370
3441
 
3442
+ pub fn supportsAsyncDialogs(self: PlatformServices) bool {
3443
+ return self.request_dialog_fn != null and self.poll_dialog_result_fn != null;
3444
+ }
3445
+
3446
+ pub fn requestDialog(self: PlatformServices, request: DialogRequest) anyerror!void {
3447
+ const request_fn = self.request_dialog_fn orelse return error.UnsupportedService;
3448
+ return request_fn(self.context, request);
3449
+ }
3450
+
3451
+ pub fn cancelDialog(self: PlatformServices, id: DialogRequestId) void {
3452
+ const cancel_fn = self.cancel_dialog_fn orelse return;
3453
+ cancel_fn(self.context, id);
3454
+ }
3455
+
3456
+ pub fn pollDialogResult(self: PlatformServices, buffer: []u8) ?DialogResult {
3457
+ const poll_fn = self.poll_dialog_result_fn orelse return null;
3458
+ return poll_fn(self.context, buffer);
3459
+ }
3460
+
3371
3461
  pub fn showNotification(self: PlatformServices, options: NotificationOptions) anyerror!void {
3372
3462
  const notify_fn = self.show_notification_fn orelse return error.UnsupportedService;
3373
3463
  return notify_fn(self.context, options);