@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
@@ -83,6 +83,7 @@ pub const FileAssociation = types.FileAssociation;
83
83
  pub const UrlScheme = types.UrlScheme;
84
84
  pub const PackageMetadata = types.PackageMetadata;
85
85
  pub const UpdateConfig = types.UpdateConfig;
86
+ pub const UpdateMode = types.UpdateMode;
86
87
  pub const Manifest = types.Manifest;
87
88
  pub const ImagesConfig = types.ImagesConfig;
88
89
  pub const validateManifest = validation.validateManifest;
@@ -125,6 +125,41 @@ test "updates require paired HTTPS feed and Ed25519 public key" {
125
125
  }));
126
126
  }
127
127
 
128
+ test "app-driven updates need a feed to drive" {
129
+ try validateUpdates(.{
130
+ .feed_url = "https://example.com/native-update.json",
131
+ .public_key = "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=",
132
+ .mode = .app,
133
+ });
134
+ try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .mode = .app }));
135
+ }
136
+
137
+ test "update feed URLs may name the target and installed version" {
138
+ const key = "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=";
139
+ try validateUpdates(.{ .feed_url = "https://example.com/releases/{target}/native-update.json", .public_key = key });
140
+ try validateUpdates(.{ .feed_url = "https://example.com/feed?target={target}&from={current_version}", .public_key = key });
141
+ try std.testing.expectError(error.InvalidUrl, validateUpdates(.{ .feed_url = "https://example.com/{arch}/native-update.json", .public_key = key }));
142
+ try std.testing.expectError(error.InvalidUrl, validateUpdates(.{ .feed_url = "https://example.com/{target/native-update.json", .public_key = key }));
143
+ try std.testing.expectError(error.InvalidUrl, validateUpdates(.{ .feed_url = "https://example.com/target}/native-update.json", .public_key = key }));
144
+ try std.testing.expectError(error.InvalidUrl, validateUpdates(.{ .feed_url = "https://{target}.example.com/native-update.json", .public_key = key }));
145
+ }
146
+
147
+ test "update public_keys add distinct trusted keys beside public_key" {
148
+ const key = "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=";
149
+ const other = "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo=";
150
+ const rotated = [_][]const u8{other};
151
+ try validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &rotated });
152
+ try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .public_keys = &rotated }));
153
+ const repeated = [_][]const u8{key};
154
+ try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &repeated }));
155
+ const duplicated = [_][]const u8{ other, other };
156
+ try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &duplicated }));
157
+ const malformed = [_][]const u8{"not-a-public-key"};
158
+ try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &malformed }));
159
+ const too_many = [_][]const u8{ other, "IBLLkMpg6OXY2vZuInLSIz4EhtVX6MZhQe2JIBd9frc=", "+AzM3OSuHAeuIIoq35mjEK5CB+Awb6AjYRCwaCe7uNA=", "n0lDnp2wlbxBEe0l01eV2DG8VaBH9LHX9q7jd3u0EiA=" };
160
+ try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &too_many }));
161
+ }
162
+
128
163
  test "accessory startup requires the tray capability" {
129
164
  const tray_capabilities = [_]Capability{.tray};
130
165
  try validateManifest(.{
@@ -647,9 +647,21 @@ pub const PackageMetadata = struct {
647
647
  pub const UpdateConfig = struct {
648
648
  feed_url: ?[]const u8 = null,
649
649
  public_key: ?[]const u8 = null,
650
+ /// Further trusted keys beside `public_key`, so a release signed with
651
+ /// a key being rotated out still verifies on installs that know it.
652
+ public_keys: []const []const u8 = &.{},
650
653
  check_on_start: bool = false,
654
+ /// Who drives updates on macOS: the host's own prompts, or the app
655
+ /// through the `native-sdk.updates` requests and channel. Windows apps
656
+ /// always drive updates themselves.
657
+ mode: UpdateMode = .host,
651
658
  };
652
659
 
660
+ pub const UpdateMode = enum { host, app };
661
+
662
+ /// `public_key` plus at most this many `public_keys`.
663
+ pub const max_update_public_keys: usize = 3;
664
+
653
665
  pub const PersistRestore = struct {
654
666
  ok: []const u8,
655
667
  none: []const u8,
@@ -657,17 +657,55 @@ pub fn validateSecurity(security: SecurityConfig) ValidationError!void {
657
657
  pub fn validateUpdates(updates: UpdateConfig) ValidationError!void {
658
658
  if ((updates.feed_url == null) != (updates.public_key == null)) return error.MissingRequiredField;
659
659
  if (updates.check_on_start and updates.feed_url == null) return error.MissingRequiredField;
660
+ if (updates.mode == .app and updates.feed_url == null) return error.MissingRequiredField;
661
+ if (updates.public_keys.len > 0 and updates.public_key == null) return error.MissingRequiredField;
662
+ if (updates.public_keys.len > types.max_update_public_keys) return error.MissingRequiredField;
660
663
  if (updates.feed_url) |url| {
661
664
  if (url.len > 4096) return error.InvalidUrl;
662
665
  if (!std.mem.startsWith(u8, url, "https://")) return error.InvalidUrl;
663
666
  try validateUrl(url);
667
+ try validateFeedUrlPlaceholders(url);
668
+ }
669
+ if (updates.public_key) |key| try validateUpdatePublicKey(key);
670
+ for (updates.public_keys, 0..) |key, index| {
671
+ try validateUpdatePublicKey(key);
672
+ if (std.mem.eql(u8, key, updates.public_key.?)) return error.MissingRequiredField;
673
+ for (updates.public_keys[0..index]) |previous| {
674
+ if (std.mem.eql(u8, key, previous)) return error.MissingRequiredField;
675
+ }
664
676
  }
665
- if (updates.public_key) |key| {
666
- var decoded: [std.crypto.sign.Ed25519.PublicKey.encoded_length]u8 = undefined;
667
- const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(key) catch return error.MissingRequiredField;
668
- if (decoded_len != decoded.len) return error.MissingRequiredField;
669
- std.base64.standard.Decoder.decode(&decoded, key) catch return error.MissingRequiredField;
670
- _ = std.crypto.sign.Ed25519.PublicKey.fromBytes(decoded) catch return error.MissingRequiredField;
677
+ }
678
+
679
+ fn validateUpdatePublicKey(key: []const u8) ValidationError!void {
680
+ var decoded: [std.crypto.sign.Ed25519.PublicKey.encoded_length]u8 = undefined;
681
+ const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(key) catch return error.MissingRequiredField;
682
+ if (decoded_len != decoded.len) return error.MissingRequiredField;
683
+ std.base64.standard.Decoder.decode(&decoded, key) catch return error.MissingRequiredField;
684
+ _ = std.crypto.sign.Ed25519.PublicKey.fromBytes(decoded) catch return error.MissingRequiredField;
685
+ }
686
+
687
+ /// A feed_url may name `{target}` and `{current_version}`, which each
688
+ /// installed app expands for itself, in its path or query only.
689
+ fn validateFeedUrlPlaceholders(url: []const u8) ValidationError!void {
690
+ const authority_start = "https://".len;
691
+ const authority_end = std.mem.findAnyPos(u8, url, authority_start, "/?#") orelse url.len;
692
+ if (std.mem.findAny(u8, url[0..authority_end], "{}") != null) return error.InvalidUrl;
693
+ var index = authority_end;
694
+ while (index < url.len) : (index += 1) {
695
+ switch (url[index]) {
696
+ '{' => {
697
+ const rest = url[index..];
698
+ const placeholder = if (std.mem.startsWith(u8, rest, "{target}"))
699
+ "{target}"
700
+ else if (std.mem.startsWith(u8, rest, "{current_version}"))
701
+ "{current_version}"
702
+ else
703
+ return error.InvalidUrl;
704
+ index += placeholder.len - 1;
705
+ },
706
+ '}' => return error.InvalidUrl,
707
+ else => {},
708
+ }
671
709
  }
672
710
  }
673
711
 
package/src/root.zig CHANGED
@@ -157,6 +157,7 @@ pub const EffectChannelSourceState = runtime.EffectChannelSourceState;
157
157
  pub const EffectChannelSourceResult = runtime.EffectChannelSourceResult;
158
158
  pub const max_channel_source_payload = runtime.max_channel_source_payload;
159
159
  pub const ChannelHandle = runtime.ChannelHandle;
160
+ pub const single_instance = runtime.single_instance;
160
161
  pub const max_effect_channels = runtime.max_effect_channels;
161
162
  pub const max_effect_channel_bytes = runtime.max_effect_channel_bytes;
162
163
  pub const max_effect_channel_pending = runtime.max_effect_channel_pending;
@@ -269,6 +270,13 @@ pub const SaveDialogOptions = platform.SaveDialogOptions;
269
270
  pub const MessageDialogStyle = platform.MessageDialogStyle;
270
271
  pub const MessageDialogResult = platform.MessageDialogResult;
271
272
  pub const MessageDialogOptions = platform.MessageDialogOptions;
273
+ pub const DialogRequestId = platform.DialogRequestId;
274
+ pub const DialogKind = platform.DialogKind;
275
+ pub const DialogOptions = platform.DialogOptions;
276
+ pub const DialogOutcome = platform.DialogOutcome;
277
+ pub const DialogResult = platform.DialogResult;
278
+ pub const DialogCompletion = runtime.DialogCompletion;
279
+ pub const DialogRequestOptions = runtime.DialogRequestOptions;
272
280
  pub const NotificationOptions = platform.NotificationOptions;
273
281
  pub const StatusItemId = platform.StatusItemId;
274
282
  pub const primary_status_item_id = platform.primary_status_item_id;
@@ -52,6 +52,7 @@ pub fn RuntimeBuiltinBridge(comptime Runtime: type) type {
52
52
  const App = runtime_api.App(Runtime);
53
53
  const SystemServiceMethods = runtime_system_services.RuntimeSystemServices(Runtime);
54
54
  const WindowViewMethods = runtime_window_views.RuntimeWindowViews(Runtime);
55
+ const AsyncBridgeResponseSlot = @import("async_bridge.zig").AsyncBridgeResponseSlot(Runtime);
55
56
 
56
57
  return struct {
57
58
  const Self = @This();
@@ -125,15 +126,106 @@ pub fn RuntimeBuiltinBridge(comptime Runtime: type) type {
125
126
  return bridge.writeSuccessResponse(response_buffer, request.id, result);
126
127
  }
127
128
 
128
- fn dispatchCompiledDialogBridgeCommand(self: *Runtime, request: bridge.Request, result_buffer: []u8, response_buffer: []u8) []const u8 {
129
+ fn dispatchCompiledDialogBridgeCommand(self: *Runtime, request: bridge.Request, message: platform.BridgeMessage, result_buffer: []u8, response_buffer: []u8) ?[]const u8 {
129
130
  const plan = builtin_coordination.route(self.builtin_coordination_policy.?, .dialog, request.command);
130
- const result = switch (plan.verb()) {
131
- .dialog_openFile => SystemServiceMethods.openFileDialogFromJson(self, request.payload, result_buffer) catch |err| return Self.writeCompiledBuiltinError(self, request.id, err, response_buffer),
132
- .dialog_saveFile => SystemServiceMethods.saveFileDialogFromJson(self, request.payload, result_buffer) catch |err| return Self.writeCompiledBuiltinError(self, request.id, err, response_buffer),
133
- .dialog_showMessage => SystemServiceMethods.showMessageDialogFromJson(self, request.payload, result_buffer) catch |err| return Self.writeCompiledBuiltinError(self, request.id, err, response_buffer),
131
+ const kind: platform.DialogKind = switch (plan.verb()) {
132
+ .dialog_openFile => .open,
133
+ .dialog_saveFile => .save,
134
+ .dialog_showMessage => .message,
134
135
  else => return bridge.writeErrorResponse(response_buffer, request.id, .unknown_command, plan.unknownMessage()),
135
136
  };
136
- return bridge.writeSuccessResponse(response_buffer, request.id, result);
137
+ return Self.startDialogBridgeCommand(self, kind, request, message, result_buffer, response_buffer);
138
+ }
139
+
140
+ /// Queue the dialog and answer the bridge request when it
141
+ /// finishes. Null means the response is deferred; a returned
142
+ /// response is an immediate refusal, or, on a platform without
143
+ /// asynchronous dialogs, the blocking dialog's answer.
144
+ fn startDialogBridgeCommand(self: *Runtime, kind: platform.DialogKind, request: bridge.Request, message: platform.BridgeMessage, result_buffer: []u8, response_buffer: []u8) ?[]const u8 {
145
+ var storage = json.StringStorage.init(result_buffer);
146
+ const options = SystemServiceMethods.dialogOptionsFromJson(kind, request.payload, &storage) catch |err| return Self.writeDialogBridgeError(self, request.id, err, response_buffer);
147
+ if (!self.options.platform.services.supportsAsyncDialogs()) return Self.answerDialogBridgeCommand(self, kind, request.id, options, result_buffer, response_buffer);
148
+ const slot =Self.reserveDialogBridgeResponse(self, message) catch |err| return Self.writeDialogBridgeError(self, request.id, err, response_buffer);
149
+ var tag: [1 + bridge.max_id_bytes]u8 = undefined;
150
+ tag[0] = @intFromEnum(kind);
151
+ @memcpy(tag[1..][0..request.id.len], request.id);
152
+ _ = self.requestDialog(.{
153
+ .options = options,
154
+ .parent_window_id = Self.openWindowId(self, message.window_id),
155
+ }, .{
156
+ .context = slot,
157
+ .tag = tag[0 .. 1 + request.id.len],
158
+ .on_result = Self.finishDialogBridgeCommand,
159
+ }) catch |err| {
160
+ slot.release();
161
+ return Self.writeDialogBridgeError(self, request.id, err, response_buffer);
162
+ };
163
+ return null;
164
+ }
165
+
166
+ fn finishDialogBridgeCommand(context: *anyopaque, runtime: *Runtime, tag: []const u8, result: platform.DialogResult) anyerror!void {
167
+ _ = runtime;
168
+ const slot: *AsyncBridgeResponseSlot = @ptrCast(@alignCast(context));
169
+ const kind: platform.DialogKind = @enumFromInt(tag[0]);
170
+ const id = tag[1..];
171
+ var result_buffer: [bridge.max_result_bytes]u8 = undefined;
172
+ var response_buffer: [bridge.max_response_bytes]u8 = undefined;
173
+ const response = if (result.outcome == .failed)
174
+ bridge.writeErrorResponse(&response_buffer, id, .internal_error, "Dialog could not be shown")
175
+ else if (SystemServiceMethods.writeDialogResultJson(kind, result, &result_buffer)) |value|
176
+ bridge.writeSuccessResponse(&response_buffer, id, value)
177
+ else |err|
178
+ bridge.writeErrorResponse(&response_buffer, id, builtinBridgeErrorCode(err), builtinBridgeErrorMessage(err));
179
+ try slot.respond(response);
180
+ }
181
+
182
+ /// Platforms without asynchronous dialogs (Chromium builds, the
183
+ /// null platform) answer from the blocking call, at once, as they
184
+ /// always have. `options` borrows `result_buffer`, which is free
185
+ /// again once the dialog has returned.
186
+ fn answerDialogBridgeCommand(self: *Runtime, kind: platform.DialogKind, id: []const u8, options: platform.DialogOptions, result_buffer: []u8, response_buffer: []u8) []const u8 {
187
+ var paths: [platform.max_dialog_paths_bytes]u8 = undefined;
188
+ const result: platform.DialogResult = switch (options) {
189
+ .open => |open| opened: {
190
+ const opened = self.showOpenDialog(open, &paths) catch |err| return Self.writeDialogBridgeError(self, id, err, response_buffer);
191
+ break :opened .{ .id = 0, .outcome = if (opened.count == 0) .cancelled else .selected, .paths = opened.paths, .count = opened.count };
192
+ },
193
+ .save => |save| saved: {
194
+ const saved = self.showSaveDialog(save, paths[0..platform.max_dialog_path_bytes]) catch |err| return Self.writeDialogBridgeError(self, id, err, response_buffer);
195
+ const path = saved orelse break :saved .{ .id = 0, .outcome = .cancelled };
196
+ break :saved .{ .id = 0, .outcome = .selected, .paths = path, .count = 1 };
197
+ },
198
+ .message => |dialog| .{
199
+ .id = 0,
200
+ .outcome = .selected,
201
+ .button = self.showMessageDialog(dialog) catch |err| return Self.writeDialogBridgeError(self, id, err, response_buffer),
202
+ },
203
+ };
204
+ const value = SystemServiceMethods.writeDialogResultJson(kind, result, result_buffer) catch |err| return Self.writeDialogBridgeError(self, id, err, response_buffer);
205
+ return bridge.writeSuccessResponse(response_buffer, id, value);
206
+ }
207
+
208
+ fn writeDialogBridgeError(self: *Runtime, id: []const u8, err: anyerror, output: []u8) []const u8 {
209
+ if (self.builtin_coordination_policy != null) return Self.writeCompiledBuiltinError(self, id, err, output);
210
+ return bridge.writeErrorResponse(output, id, builtinBridgeErrorCode(err), builtinBridgeErrorMessage(err));
211
+ }
212
+
213
+ fn reserveDialogBridgeResponse(self: *Runtime, message: platform.BridgeMessage) !*AsyncBridgeResponseSlot {
214
+ for (&self.async_bridge_responses) |*slot| {
215
+ if (slot.in_use) continue;
216
+ try slot.init(self, .{ .origin = message.origin, .window_id = message.window_id, .webview_label = message.webview_label });
217
+ return slot;
218
+ }
219
+ return error.AsyncBridgeResponseLimitReached;
220
+ }
221
+
222
+ /// The requesting window when the runtime knows it as open;
223
+ /// otherwise null lets the broker pick a parent.
224
+ fn openWindowId(self: *Runtime, window_id: platform.WindowId) ?platform.WindowId {
225
+ for (self.windows[0..self.window_count]) |window| {
226
+ if (window.info.id == window_id and window.info.open) return window_id;
227
+ }
228
+ return null;
137
229
  }
138
230
 
139
231
  fn dispatchCompiledOsBridgeCommand(self: *Runtime, request: bridge.Request, origin: []const u8, result_buffer: []u8, response_buffer: []u8) []const u8 {
@@ -306,17 +398,20 @@ pub fn RuntimeBuiltinBridge(comptime Runtime: type) type {
306
398
  return bridge.writeSuccessResponse(response_buffer, request.id, result);
307
399
  }
308
400
 
309
- pub fn dispatchDialogBridgeCommand(self: *Runtime, request: bridge.Request, result_buffer: []u8, response_buffer: []u8) []const u8 {
310
- if (self.builtin_coordination_policy != null) return Self.dispatchCompiledDialogBridgeCommand(self, request, result_buffer, response_buffer);
311
- const result = if (std.mem.eql(u8, request.command, "native-sdk.dialog.openFile"))
312
- SystemServiceMethods.openFileDialogFromJson(self, request.payload, result_buffer) catch |err| return bridge.writeErrorResponse(response_buffer, request.id, builtinBridgeErrorCode(err), builtinBridgeErrorMessage(err))
401
+ /// Dialog commands answer asynchronously: null means the response
402
+ /// will follow when the dialog finishes (see
403
+ /// `startDialogBridgeCommand`).
404
+ pub fn dispatchDialogBridgeCommand(self: *Runtime, request: bridge.Request, message: platform.BridgeMessage, result_buffer: []u8, response_buffer: []u8) ?[]const u8 {
405
+ if (self.builtin_coordination_policy != null) return Self.dispatchCompiledDialogBridgeCommand(self, request, message, result_buffer, response_buffer);
406
+ const kind: platform.DialogKind = if (std.mem.eql(u8, request.command, "native-sdk.dialog.openFile"))
407
+ .open
313
408
  else if (std.mem.eql(u8, request.command, "native-sdk.dialog.saveFile"))
314
- SystemServiceMethods.saveFileDialogFromJson(self, request.payload, result_buffer) catch |err| return bridge.writeErrorResponse(response_buffer, request.id, builtinBridgeErrorCode(err), builtinBridgeErrorMessage(err))
409
+ .save
315
410
  else if (std.mem.eql(u8, request.command, "native-sdk.dialog.showMessage"))
316
- SystemServiceMethods.showMessageDialogFromJson(self, request.payload, result_buffer) catch |err| return bridge.writeErrorResponse(response_buffer, request.id, builtinBridgeErrorCode(err), builtinBridgeErrorMessage(err))
411
+ .message
317
412
  else
318
413
  return bridge.writeErrorResponse(response_buffer, request.id, .unknown_command, "Unknown dialog command");
319
- return bridge.writeSuccessResponse(response_buffer, request.id, result);
414
+ return Self.startDialogBridgeCommand(self, kind, request, message, result_buffer, response_buffer);
320
415
  }
321
416
 
322
417
  pub fn dispatchOsBridgeCommand(self: *Runtime, request: bridge.Request, origin: []const u8, result_buffer: []u8, response_buffer: []u8) []const u8 {
@@ -1,19 +1,67 @@
1
1
  //! Explicit native external-producer capabilities. Portable app state and
2
- //! channel lifecycle stay in the app; this source owns a detached OS sampler.
2
+ //! channel lifecycle stay in the app; `native-sdk.process.samples` owns a
3
+ //! detached OS sampler, `native-sdk.app.launches` hands the channel to
4
+ //! the single-instance listener (single_instance.zig), and
5
+ //! `native-sdk.updates` receives the updater's status records
6
+ //! (updater/engine.zig).
3
7
  const std = @import("std");
4
8
  const builtin = @import("builtin");
5
9
  const effects = @import("effects.zig");
6
10
  const clock = @import("clock.zig");
11
+ const single_instance = @import("single_instance.zig");
12
+ const update_engine = @import("../updater/engine.zig");
7
13
  const sample_interval_ms = 500;
8
14
  const max_line_bytes = 96;
9
15
 
10
16
  pub fn start(name: []const u8, payload: []const u8, handle: effects.ChannelHandle) !void {
17
+ if (std.mem.eql(u8, name, single_instance.source_name)) return single_instance.attach(payload, handle);
18
+ if (std.mem.eql(u8, name, update_engine.channel_source)) return attachUpdates(payload, handle);
11
19
  if (!std.mem.eql(u8, name, "native-sdk.process.samples")) return error.UnknownChannelSource;
12
20
  if (payload.len != 0) return error.InvalidChannelSourceArguments;
13
21
  if (comptime builtin.single_threaded) return error.UnsupportedChannelSource;
14
22
  try startSamplerThread(handle);
15
23
  }
16
24
 
25
+ /// The open `native-sdk.updates` channels, each the engine's sink. A slot
26
+ /// frees itself when its channel answers `.closed`.
27
+ const UpdateChannel = struct {
28
+ handle: effects.ChannelHandle = undefined,
29
+ used: std.atomic.Value(bool) = .init(false),
30
+
31
+ fn post(context: *anyopaque, bytes: []const u8) update_engine.Posted {
32
+ const self: *UpdateChannel = @ptrCast(@alignCast(context));
33
+ return switch (self.handle.post(bytes)) {
34
+ .accepted => .delivered,
35
+ .dropped_full, .dropped_oversized => .dropped,
36
+ .closed => {
37
+ self.used.store(false, .release);
38
+ return .closed;
39
+ },
40
+ };
41
+ }
42
+ };
43
+ var update_channels: [4]UpdateChannel = .{UpdateChannel{}} ** 4;
44
+
45
+ comptime {
46
+ // Every status record fits one post; notes travel by request.
47
+ std.debug.assert(update_engine.max_status_bytes <= effects.max_effect_channel_bytes);
48
+ }
49
+
50
+ fn attachUpdates(payload: []const u8, handle: effects.ChannelHandle) !void {
51
+ if (payload.len != 0) return error.InvalidChannelSourceArguments;
52
+ for (&update_channels) |*channel| {
53
+ if (channel.used.cmpxchgStrong(false, true, .acq_rel, .acquire) == null) {
54
+ channel.handle = handle;
55
+ update_engine.attach(.{ .context = channel, .post = UpdateChannel.post }) catch {
56
+ channel.used.store(false, .release);
57
+ return error.UnsupportedChannelSource;
58
+ };
59
+ return;
60
+ }
61
+ }
62
+ return error.UnsupportedChannelSource;
63
+ }
64
+
17
65
  fn startSamplerThread(handle: effects.ChannelHandle) std.Thread.SpawnError!void {
18
66
  const thread = try std.Thread.spawn(.{}, samplerMain, .{handle});
19
67
  thread.detach();
@@ -30,6 +30,7 @@ const runtime_session_state = @import("session_state.zig");
30
30
  const session_journal = @import("session_journal.zig");
31
31
  const runtime_state = @import("state.zig");
32
32
  const runtime_system_services = @import("system_services.zig");
33
+ const runtime_dialogs = @import("dialogs.zig");
33
34
  const runtime_builtin_bridge = @import("builtin_bridge.zig");
34
35
  const runtime_view = @import("view.zig");
35
36
  const runtime_window_views = @import("window_views.zig");
@@ -110,6 +111,10 @@ const FocusTraversalDirection = runtime_state.FocusTraversalDirection;
110
111
  const copySourceInto = runtime_state.copySourceInto;
111
112
  const sourceWebViewUrl = runtime_state.sourceWebViewUrl;
112
113
  const AsyncBridgeResponseSlot = runtime_async_bridge.AsyncBridgeResponseSlot(Runtime);
114
+ pub const DialogCompletion = runtime_dialogs.Completion(Runtime);
115
+ pub const DialogRequestOptions = runtime_dialogs.DialogRequestOptions;
116
+ pub const max_dialog_requests = runtime_dialogs.max_dialog_requests;
117
+ pub const max_dialog_tag_bytes = runtime_dialogs.max_dialog_tag_bytes;
113
118
  const platformCursorFromCanvas = widget_bridge.platformCursorFromCanvas;
114
119
  const widgetRoleName = widget_bridge.widgetRoleName;
115
120
  const platformWidgetAccessibilityRole = widget_bridge.platformWidgetAccessibilityRole;
@@ -351,6 +356,8 @@ pub const Runtime = struct {
351
356
  /// copy bound, so `initAt` assigns it explicitly.
352
357
  frame_profile: runtime_frame_profile.FrameProfile = .{},
353
358
  async_bridge_responses: [max_async_bridge_responses]AsyncBridgeResponseSlot = [_]AsyncBridgeResponseSlot{.{}} ** max_async_bridge_responses,
359
+ /// Asynchronous dialogs in flight (see `dialogs.zig`).
360
+ dialog_broker: runtime_dialogs.Broker(Runtime) = .{},
354
361
  automation_windows: [automation.snapshot.max_windows]automation.snapshot.Window = undefined,
355
362
  /// Snapshot-side storage for the frame profile's per-stage stats
356
363
  /// (the snapshot Input only references runtime-owned memory).
@@ -562,6 +569,9 @@ pub const Runtime = struct {
562
569
  item.title = "";
563
570
  item.item_count = 0;
564
571
  }
572
+ // The dialog broker's result buffer exceeds the copy bound too;
573
+ // reset its bookkeeping and leave the buffer alone.
574
+ self.dialog_broker.reset();
565
575
  self.started_timestamp_ns = timestampToU64(nowNanoseconds());
566
576
  self.text_measure_provider = if (options.platform.services.measure_text_fn) |measure_fn|
567
577
  .{
@@ -600,6 +610,7 @@ pub const Runtime = struct {
600
610
  /// host) call it to return the storage.
601
611
  pub fn deinit(self: *Runtime) void {
602
612
  self.disarmMediaSurfaceWakes();
613
+ self.releaseDialogs();
603
614
  for (self.views[0..self.view_count]) |*view| {
604
615
  if (view.widget_text_bytes_heap_owned) self.owned_allocator.free(view.widget_text_bytes);
605
616
  if (view.canvas_widget_text_history_bytes_heap_owned) self.owned_allocator.free(view.canvas_widget_text_history_bytes);
@@ -832,6 +843,19 @@ pub const Runtime = struct {
832
843
  pub const showOpenDialog = SystemServiceMethods.showOpenDialog;
833
844
  pub const showSaveDialog = SystemServiceMethods.showSaveDialog;
834
845
  pub const showMessageDialog = SystemServiceMethods.showMessageDialog;
846
+
847
+ const DialogMethods = runtime_dialogs.RuntimeDialogs(Runtime);
848
+ pub const requestDialog = DialogMethods.requestDialog;
849
+ pub const requestOpenDialog = DialogMethods.requestOpenDialog;
850
+ pub const requestSaveDialog = DialogMethods.requestSaveDialog;
851
+ pub const requestMessageDialog = DialogMethods.requestMessageDialog;
852
+ pub const cancelDialog = DialogMethods.cancelDialog;
853
+ pub const adoptDialogResults = DialogMethods.adoptDialogResults;
854
+ pub const cancelDialogsForWindow = DialogMethods.cancelDialogsForWindow;
855
+ pub const abandonDialogs = DialogMethods.abandonDialogs;
856
+ pub const pendingDialogCount = DialogMethods.pendingDialogCount;
857
+ const releaseDialogs = DialogMethods.releaseDialogs;
858
+
835
859
  pub const showNotification = SystemServiceMethods.showNotification;
836
860
  pub const setCredential = SystemServiceMethods.setCredential;
837
861
  pub const getCredential = SystemServiceMethods.getCredential;
@@ -863,9 +887,6 @@ pub const Runtime = struct {
863
887
  const revealPathFromJson = SystemServiceMethods.revealPathFromJson;
864
888
  const addRecentDocumentFromJson = SystemServiceMethods.addRecentDocumentFromJson;
865
889
  const clearRecentDocumentsFromJson = SystemServiceMethods.clearRecentDocumentsFromJson;
866
- const openFileDialogFromJson = SystemServiceMethods.openFileDialogFromJson;
867
- const saveFileDialogFromJson = SystemServiceMethods.saveFileDialogFromJson;
868
- const showMessageDialogFromJson = SystemServiceMethods.showMessageDialogFromJson;
869
890
 
870
891
  /// Start (or replace) a platform timer. The platform delivers `.timer`
871
892
  /// events carrying `id` until `cancelTimer` (or after the first fire when