@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.
- package/README.md +1 -1
- package/build/app.zig +53 -34
- package/build/cli.zig +22 -0
- package/package.json +10 -10
- package/packages/core/package-lock.json +2 -2
- package/packages/core/package.json +1 -1
- package/schemas/app.schema.json +9 -1
- package/skill-data/core/references/bridge-security-native-capabilities.md +4 -0
- package/skill-data/ts-core/SKILL.md +2 -0
- package/src/app_runner/app_assets.zig +85 -1
- package/src/app_runner/root.zig +63 -3
- package/src/app_runner/ts_core_main.zig +8 -0
- package/src/platform/linux/cef_host.cpp +22 -0
- package/src/platform/linux/gtk_host.c +317 -34
- package/src/platform/linux/gtk_host.h +37 -0
- package/src/platform/linux/root.zig +114 -20
- package/src/platform/macos/appkit_host.h +53 -0
- package/src/platform/macos/appkit_host.m +667 -67
- package/src/platform/macos/cef_host.mm +21 -0
- package/src/platform/macos/root.zig +146 -29
- package/src/platform/null_platform.zig +136 -0
- package/src/platform/root.zig +7 -0
- package/src/platform/types.zig +90 -0
- package/src/platform/windows/dialog_test.cpp +303 -0
- package/src/platform/windows/host_loop_test.cpp +169 -0
- package/src/platform/windows/root.zig +262 -39
- package/src/platform/windows/session_end_test.cpp +303 -0
- package/src/platform/windows/webview2_host.cpp +1435 -306
- package/src/primitives/app_manifest/root.zig +1 -0
- package/src/primitives/app_manifest/tests.zig +35 -0
- package/src/primitives/app_manifest/types.zig +12 -0
- package/src/primitives/app_manifest/validation.zig +44 -6
- package/src/root.zig +8 -0
- package/src/runtime/builtin_bridge.zig +108 -13
- package/src/runtime/channel_sources.zig +49 -1
- package/src/runtime/core.zig +24 -3
- package/src/runtime/dialog_tests.zig +367 -0
- package/src/runtime/dialogs.zig +385 -0
- package/src/runtime/effects.zig +83 -3
- package/src/runtime/effects_host_tests.zig +69 -1
- package/src/runtime/flow.zig +20 -6
- package/src/runtime/flow_reference.zig +20 -6
- package/src/runtime/platform_bridge_tests.zig +1 -0
- package/src/runtime/root.zig +9 -0
- package/src/runtime/single_instance.zig +893 -0
- package/src/runtime/system_services.zig +64 -82
- package/src/runtime/ui_app.zig +67 -0
- package/src/tooling/manifest.zig +111 -3
- package/src/tooling/package.zig +98 -8
- package/src/tooling/raw_manifest.zig +7 -0
- package/src/tooling/root.zig +2 -0
- package/src/tooling/templates.zig +44 -2
- package/src/tooling/update.zig +228 -10
- package/src/tooling/windows_installer.zig +245 -0
- package/src/tooling/zip_writer.zig +211 -0
- package/src/updater/engine.zig +936 -0
- package/src/updater/feed.zig +261 -12
- package/src/updater/macos.zig +187 -0
- package/src/updater/root.zig +41 -0
- package/src/updater/windows.zig +470 -0
- package/src/windows_install/config.zig +247 -0
- package/src/windows_install/main.zig +690 -0
- package/src/windows_install/payload.zig +146 -0
- package/src/windows_install/switch.zig +184 -0
- package/src/windows_install/tests.zig +11 -0
- package/src/windows_install/win32.zig +248 -0
- 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
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
|
2687
|
-
const
|
|
2688
|
-
|
|
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
|
|
2709
|
-
const
|
|
2710
|
-
|
|
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
|
|
2729
|
-
|
|
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;
|
package/src/platform/root.zig
CHANGED
|
@@ -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;
|
package/src/platform/types.zig
CHANGED
|
@@ -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);
|