@zogien/native-sdk 0.11.0-fork.4 → 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 (35) hide show
  1. package/build/app.zig +50 -34
  2. package/package.json +9 -9
  3. package/packages/core/package-lock.json +2 -2
  4. package/packages/core/package.json +1 -1
  5. package/skill-data/core/references/bridge-security-native-capabilities.md +4 -0
  6. package/src/platform/linux/cef_host.cpp +22 -0
  7. package/src/platform/linux/gtk_host.c +317 -34
  8. package/src/platform/linux/gtk_host.h +37 -0
  9. package/src/platform/linux/root.zig +114 -20
  10. package/src/platform/macos/appkit_host.h +44 -0
  11. package/src/platform/macos/appkit_host.m +301 -67
  12. package/src/platform/macos/cef_host.mm +21 -0
  13. package/src/platform/macos/root.zig +117 -23
  14. package/src/platform/null_platform.zig +136 -0
  15. package/src/platform/root.zig +6 -0
  16. package/src/platform/types.zig +81 -0
  17. package/src/platform/windows/dialog_test.cpp +303 -0
  18. package/src/platform/windows/host_loop_test.cpp +169 -0
  19. package/src/platform/windows/root.zig +225 -39
  20. package/src/platform/windows/session_end_test.cpp +303 -0
  21. package/src/platform/windows/webview2_host.cpp +1064 -280
  22. package/src/root.zig +7 -0
  23. package/src/runtime/builtin_bridge.zig +108 -13
  24. package/src/runtime/core.zig +24 -3
  25. package/src/runtime/dialog_tests.zig +367 -0
  26. package/src/runtime/dialogs.zig +385 -0
  27. package/src/runtime/effects.zig +71 -2
  28. package/src/runtime/effects_host_tests.zig +69 -1
  29. package/src/runtime/flow.zig +14 -6
  30. package/src/runtime/flow_reference.zig +14 -6
  31. package/src/runtime/platform_bridge_tests.zig +1 -0
  32. package/src/runtime/root.zig +5 -0
  33. package/src/runtime/system_services.zig +64 -82
  34. package/src/runtime/ui_app.zig +67 -0
  35. package/tools/native-sdk/main.zig +1 -1
@@ -382,6 +382,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
382
382
  const state = event_value.window_frame_changed;
383
383
  if (host_flow.gate(policy, 1, @intFromBool(was_open), @intFromBool(state.open)) != 0) {
384
384
  const label = if (WindowViewMethods().findWindowIndexById(self, state.id)) |index| self.windows[index].info.label else state.label;
385
+ self.cancelDialogsForWindow(state.id);
385
386
  try dispatchEvent(self, app, .{ .window_closed = .{ .window_id = state.id, .label = label } });
386
387
  }
387
388
  },
@@ -449,6 +450,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
449
450
  emitFileDropEvent(self, drop) catch |err| log(self, "drop.files.emit_failed", @errorName(err), &.{trace.uint("window_id", drop.window_id)});
450
451
  },
451
452
  .lifecycle_stop => {
453
+ self.abandonDialogs();
452
454
  try dispatchEvent(self, app, .{ .lifecycle = .stop });
453
455
  },
454
456
  .extensions_stop => {
@@ -529,6 +531,10 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
529
531
  try dispatchEvent(self, app, .{ .video = event_value.video });
530
532
  },
531
533
  .event_wake => {
534
+ // Finished dialogs ride the same nudge the effects
535
+ // channel uses; their completions run before the app
536
+ // drains its own work.
537
+ self.adoptDialogResults();
532
538
  try dispatchEvent(self, app, .effects_wake);
533
539
  },
534
540
  .capability_gpu_surface_frame => {
@@ -1206,19 +1212,20 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
1206
1212
  self.invalidateFor(.command, null);
1207
1213
  return true;
1208
1214
  }
1209
- const response = switch (plan.family()) {
1215
+ const response: ?[]const u8 = switch (plan.family()) {
1210
1216
  .none => unreachable,
1211
1217
  .command => dispatchCommandBridgeCommand(self, app, request, message.window_id, message.webview_label, &result_buffer, &response_buffer),
1212
1218
  .window => dispatchWindowBridgeCommand(self, request, &result_buffer, &response_buffer),
1213
1219
  .view => dispatchViewBridgeCommand(self, request, message.window_id, &result_buffer, &response_buffer),
1214
1220
  .webview => dispatchWebViewBridgeCommand(self, request, message.window_id, &result_buffer, &response_buffer),
1215
1221
  .platform => dispatchPlatformBridgeCommand(self, request, &result_buffer, &response_buffer),
1216
- .dialog => dispatchDialogBridgeCommand(self, request, &result_buffer, &response_buffer),
1222
+ .dialog => dispatchDialogBridgeCommand(self, request, message, &result_buffer, &response_buffer),
1217
1223
  .os => dispatchOsBridgeCommand(self, request, message.origin, &result_buffer, &response_buffer),
1218
1224
  .clipboard => dispatchClipboardBridgeCommand(self, request, &result_buffer, &response_buffer),
1219
1225
  .credentials => dispatchCredentialBridgeCommand(self, request, &result_buffer, &response_buffer),
1220
1226
  };
1221
- try completeBridgeResponse(self, message.window_id, message.webview_label, response);
1227
+ // Dialogs answer later, when they finish.
1228
+ if (response) |bytes| try completeBridgeResponse(self, message.window_id, message.webview_label, bytes);
1222
1229
  self.invalidateFor(.command, null);
1223
1230
  return true;
1224
1231
  }
@@ -1271,7 +1278,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
1271
1278
  self.invalidateFor(.command, null);
1272
1279
  return true;
1273
1280
  }
1274
- const result = if (is_command)
1281
+ const result: ?[]const u8 = if (is_command)
1275
1282
  dispatchCommandBridgeCommand(self, app, request, message.window_id, message.webview_label, &result_buffer, &response_buffer)
1276
1283
  else if (is_window)
1277
1284
  dispatchWindowBridgeCommand(self, request, &result_buffer, &response_buffer)
@@ -1282,7 +1289,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
1282
1289
  else if (is_platform)
1283
1290
  dispatchPlatformBridgeCommand(self, request, &result_buffer, &response_buffer)
1284
1291
  else if (is_dialog)
1285
- dispatchDialogBridgeCommand(self, request, &result_buffer, &response_buffer)
1292
+ dispatchDialogBridgeCommand(self, request, message, &result_buffer, &response_buffer)
1286
1293
  else if (is_clipboard)
1287
1294
  dispatchClipboardBridgeCommand(self, request, &result_buffer, &response_buffer)
1288
1295
  else if (is_credentials)
@@ -1290,7 +1297,8 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
1290
1297
  else
1291
1298
  dispatchOsBridgeCommand(self, request, message.origin, &result_buffer, &response_buffer);
1292
1299
 
1293
- try completeBridgeResponse(self, message.window_id, message.webview_label, result);
1300
+ // Dialogs answer later, when they finish.
1301
+ if (result) |bytes| try completeBridgeResponse(self, message.window_id, message.webview_label, bytes);
1294
1302
  self.invalidateFor(.command, null);
1295
1303
  return true;
1296
1304
  }
@@ -369,6 +369,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
369
369
  // comes from runtime storage, which outlives the
370
370
  // event's transient slices.
371
371
  const label = if (WindowViewMethods().findWindowIndexById(self, state.id)) |index| self.windows[index].info.label else state.label;
372
+ self.cancelDialogsForWindow(state.id);
372
373
  try dispatchEvent(self, app, .{ .window_closed = .{ .window_id = state.id, .label = label } });
373
374
  }
374
375
  },
@@ -505,6 +506,10 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
505
506
  try dispatchEvent(self, app, .{ .video = video_event });
506
507
  },
507
508
  .wake => {
509
+ // Finished dialogs ride the same nudge the effects
510
+ // channel uses; their completions run before the app
511
+ // drains its own work.
512
+ self.adoptDialogResults();
508
513
  try dispatchEvent(self, app, .effects_wake);
509
514
  },
510
515
  .files_dropped => |drop| {
@@ -523,6 +528,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
523
528
  self.invalidateFor(.command, null);
524
529
  },
525
530
  .app_shutdown => {
531
+ self.abandonDialogs();
526
532
  try dispatchEvent(self, app, .{ .lifecycle = .stop });
527
533
  if (self.options.extensions) |registry| try registry.stopAll(extensionContext(self));
528
534
  // A release the app staged but did not restart into
@@ -1214,19 +1220,20 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
1214
1220
  self.invalidateFor(.command, null);
1215
1221
  return true;
1216
1222
  }
1217
- const response = switch (plan.family()) {
1223
+ const response: ?[]const u8 = switch (plan.family()) {
1218
1224
  .none => unreachable,
1219
1225
  .command => dispatchCommandBridgeCommand(self, app, request, message.window_id, message.webview_label, &result_buffer, &response_buffer),
1220
1226
  .window => dispatchWindowBridgeCommand(self, request, &result_buffer, &response_buffer),
1221
1227
  .view => dispatchViewBridgeCommand(self, request, message.window_id, &result_buffer, &response_buffer),
1222
1228
  .webview => dispatchWebViewBridgeCommand(self, request, message.window_id, &result_buffer, &response_buffer),
1223
1229
  .platform => dispatchPlatformBridgeCommand(self, request, &result_buffer, &response_buffer),
1224
- .dialog => dispatchDialogBridgeCommand(self, request, &result_buffer, &response_buffer),
1230
+ .dialog => dispatchDialogBridgeCommand(self, request, message, &result_buffer, &response_buffer),
1225
1231
  .os => dispatchOsBridgeCommand(self, request, message.origin, &result_buffer, &response_buffer),
1226
1232
  .clipboard => dispatchClipboardBridgeCommand(self, request, &result_buffer, &response_buffer),
1227
1233
  .credentials => dispatchCredentialBridgeCommand(self, request, &result_buffer, &response_buffer),
1228
1234
  };
1229
- try completeBridgeResponse(self, message.window_id, message.webview_label, response);
1235
+ // Dialogs answer later, when they finish.
1236
+ if (response) |bytes| try completeBridgeResponse(self, message.window_id, message.webview_label, bytes);
1230
1237
  self.invalidateFor(.command, null);
1231
1238
  return true;
1232
1239
  }
@@ -1279,7 +1286,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
1279
1286
  self.invalidateFor(.command, null);
1280
1287
  return true;
1281
1288
  }
1282
- const result = if (is_command)
1289
+ const result: ?[]const u8 = if (is_command)
1283
1290
  dispatchCommandBridgeCommand(self, app, request, message.window_id, message.webview_label, &result_buffer, &response_buffer)
1284
1291
  else if (is_window)
1285
1292
  dispatchWindowBridgeCommand(self, request, &result_buffer, &response_buffer)
@@ -1290,7 +1297,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
1290
1297
  else if (is_platform)
1291
1298
  dispatchPlatformBridgeCommand(self, request, &result_buffer, &response_buffer)
1292
1299
  else if (is_dialog)
1293
- dispatchDialogBridgeCommand(self, request, &result_buffer, &response_buffer)
1300
+ dispatchDialogBridgeCommand(self, request, message, &result_buffer, &response_buffer)
1294
1301
  else if (is_clipboard)
1295
1302
  dispatchClipboardBridgeCommand(self, request, &result_buffer, &response_buffer)
1296
1303
  else if (is_credentials)
@@ -1298,7 +1305,8 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
1298
1305
  else
1299
1306
  dispatchOsBridgeCommand(self, request, message.origin, &result_buffer, &response_buffer);
1300
1307
 
1301
- try completeBridgeResponse(self, message.window_id, message.webview_label, result);
1308
+ // Dialogs answer later, when they finish.
1309
+ if (result) |bytes| try completeBridgeResponse(self, message.window_id, message.webview_label, bytes);
1302
1310
  self.invalidateFor(.command, null);
1303
1311
  return true;
1304
1312
  }
@@ -6,4 +6,5 @@ test {
6
6
  _ = @import("webview_view_bridge_tests.zig");
7
7
  _ = @import("web_layer_gate_tests.zig");
8
8
  _ = @import("bridge_permission_tests.zig");
9
+ _ = @import("dialog_tests.zig");
9
10
  }
@@ -40,6 +40,10 @@ pub const Event = core.Event;
40
40
  pub const App = core.App;
41
41
  pub const Options = core.Options;
42
42
  pub const Runtime = core.Runtime;
43
+ pub const DialogCompletion = core.DialogCompletion;
44
+ pub const DialogRequestOptions = core.DialogRequestOptions;
45
+ pub const max_dialog_requests = core.max_dialog_requests;
46
+ pub const max_dialog_tag_bytes = core.max_dialog_tag_bytes;
43
47
  pub const TestHarness = core.TestHarness;
44
48
  pub const DispatchError = core.DispatchError;
45
49
  pub const max_dispatch_errors = core.max_dispatch_errors;
@@ -165,6 +169,7 @@ pub const max_effect_pty_chunk_bytes = runtime_effects.max_effect_pty_chunk_byte
165
169
  pub const max_effect_pty_write_bytes = runtime_effects.max_effect_pty_write_bytes;
166
170
  pub const max_effect_pty_term_bytes = runtime_effects.max_effect_pty_term_bytes;
167
171
  pub const EffectHostResult = runtime_effects.EffectHostResult;
172
+ pub const DesktopCapabilityCompletion = runtime_effects.DesktopCapabilityCompletion;
168
173
  pub const HostCallBinding = runtime_effects.HostCallBinding;
169
174
  pub const HostCallCompletion = runtime_effects.HostCallCompletion;
170
175
  pub const ServiceHost = @import("service_host.zig").ServiceHost;
@@ -404,95 +404,77 @@ pub fn RuntimeSystemServices(comptime Runtime: type) type {
404
404
  return writeTrueJson(output);
405
405
  }
406
406
 
407
- pub fn openFileDialogFromJson(self: *Runtime, payload: []const u8, output: []u8) ![]const u8 {
408
- var storage = json.StringStorage.init(output);
409
- const title = jsonStringField(payload, "title", &storage) orelse "";
410
- const default_path = jsonStringField(payload, "defaultPath", &storage) orelse "";
411
- const allow_dirs = jsonBoolField(payload, "allowDirectories") orelse false;
412
- const allow_multi = jsonBoolField(payload, "allowMultiple") orelse false;
413
- var dialog_buffer: [platform.max_dialog_paths_bytes]u8 = undefined;
414
- const result = try self.showOpenDialog(.{
415
- .title = title,
416
- .default_path = default_path,
417
- .allow_directories = allow_dirs,
418
- .allow_multiple = allow_multi,
419
- }, &dialog_buffer);
420
-
421
- var writer = std.Io.Writer.fixed(output);
422
- if (result.count == 0) {
423
- try writer.writeAll("null");
424
- } else {
425
- try writer.writeByte('[');
426
- var start: usize = 0;
427
- var i: usize = 0;
428
- for (result.paths, 0..) |ch, pos| {
429
- if (ch == '\n') {
430
- if (i > 0) try writer.writeByte(',');
431
- try json.writeString(&writer, result.paths[start..pos]);
432
- start = pos + 1;
433
- i += 1;
434
- }
435
- }
436
- if (start < result.paths.len) {
437
- if (i > 0) try writer.writeByte(',');
438
- try json.writeString(&writer, result.paths[start..]);
439
- }
440
- try writer.writeByte(']');
407
+ /// Read a `native-sdk.dialog.*` bridge payload. Strings land in
408
+ /// `storage` and borrow it.
409
+ pub fn dialogOptionsFromJson(kind: platform.DialogKind, payload: []const u8, storage: *json.StringStorage) !platform.DialogOptions {
410
+ switch (kind) {
411
+ .open => return .{ .open = .{
412
+ .title = jsonStringField(payload, "title", storage) orelse "",
413
+ .default_path = jsonStringField(payload, "defaultPath", storage) orelse "",
414
+ .allow_directories = jsonBoolField(payload, "allowDirectories") orelse false,
415
+ .allow_multiple = jsonBoolField(payload, "allowMultiple") orelse false,
416
+ } },
417
+ .save => return .{ .save = .{
418
+ .title = jsonStringField(payload, "title", storage) orelse "",
419
+ .default_path = jsonStringField(payload, "defaultPath", storage) orelse "",
420
+ .default_name = jsonStringField(payload, "defaultName", storage) orelse "",
421
+ } },
422
+ .message => {
423
+ const title = jsonStringField(payload, "title", storage) orelse "";
424
+ const message = jsonStringField(payload, "message", storage) orelse "";
425
+ const informative = jsonStringField(payload, "informativeText", storage) orelse "";
426
+ const primary = jsonStringField(payload, "primaryButton", storage) orelse "OK";
427
+ const secondary = jsonStringField(payload, "secondaryButton", storage) orelse "";
428
+ const tertiary = jsonStringField(payload, "tertiaryButton", storage) orelse "";
429
+ const style_str = jsonStringField(payload, "style", storage) orelse "info";
430
+ const style: platform.MessageDialogStyle = if (std.mem.eql(u8, style_str, "warning"))
431
+ .warning
432
+ else if (std.mem.eql(u8, style_str, "critical"))
433
+ .critical
434
+ else
435
+ .info;
436
+ return .{ .message = .{
437
+ .style = style,
438
+ .title = title,
439
+ .message = message,
440
+ .informative_text = informative,
441
+ .primary_button = primary,
442
+ .secondary_button = secondary,
443
+ .tertiary_button = tertiary,
444
+ } };
445
+ },
441
446
  }
442
- return writer.buffered();
443
447
  }
444
448
 
445
- pub fn saveFileDialogFromJson(self: *Runtime, payload: []const u8, output: []u8) ![]const u8 {
446
- var storage = json.StringStorage.init(output);
447
- const title = jsonStringField(payload, "title", &storage) orelse "";
448
- const default_path = jsonStringField(payload, "defaultPath", &storage) orelse "";
449
- const default_name = jsonStringField(payload, "defaultName", &storage) orelse "";
450
- var dialog_buffer: [platform.max_dialog_path_bytes]u8 = undefined;
451
- const path = try self.showSaveDialog(.{
452
- .title = title,
453
- .default_path = default_path,
454
- .default_name = default_name,
455
- }, &dialog_buffer);
456
-
449
+ /// The bridge result for a finished dialog: open dialogs answer an
450
+ /// array of paths, save dialogs a path, message dialogs the
451
+ /// pressed button's name. A dialog closed without a choice answers
452
+ /// `null` (open, save) or `"dismissed"` (message).
453
+ pub fn writeDialogResultJson(kind: platform.DialogKind, result: platform.DialogResult, output: []u8) ![]const u8 {
457
454
  var writer = std.Io.Writer.fixed(output);
458
- if (path) |p| {
459
- try json.writeString(&writer, p);
460
- } else {
461
- try writer.writeAll("null");
455
+ const chosen = result.outcome == .selected;
456
+ switch (kind) {
457
+ .open => {
458
+ if (!chosen or result.count == 0) {
459
+ try writer.writeAll("null");
460
+ return writer.buffered();
461
+ }
462
+ try writer.writeByte('[');
463
+ var paths = std.mem.splitScalar(u8, result.paths, '\n');
464
+ var first = true;
465
+ while (paths.next()) |path| {
466
+ if (path.len == 0) continue;
467
+ if (!first) try writer.writeByte(',');
468
+ first = false;
469
+ try json.writeString(&writer, path);
470
+ }
471
+ try writer.writeByte(']');
472
+ },
473
+ .save => if (chosen and result.paths.len > 0) try json.writeString(&writer, result.paths) else try writer.writeAll("null"),
474
+ .message => try json.writeString(&writer, if (chosen) @tagName(result.button) else "dismissed"),
462
475
  }
463
476
  return writer.buffered();
464
477
  }
465
-
466
- pub fn showMessageDialogFromJson(self: *Runtime, payload: []const u8, output: []u8) ![]const u8 {
467
- var storage = json.StringStorage.init(output);
468
- const title = jsonStringField(payload, "title", &storage) orelse "";
469
- const message = jsonStringField(payload, "message", &storage) orelse "";
470
- const informative = jsonStringField(payload, "informativeText", &storage) orelse "";
471
- const primary = jsonStringField(payload, "primaryButton", &storage) orelse "OK";
472
- const secondary = jsonStringField(payload, "secondaryButton", &storage) orelse "";
473
- const tertiary = jsonStringField(payload, "tertiaryButton", &storage) orelse "";
474
- const style_str = jsonStringField(payload, "style", &storage) orelse "info";
475
- const style: platform.MessageDialogStyle = if (std.mem.eql(u8, style_str, "warning"))
476
- .warning
477
- else if (std.mem.eql(u8, style_str, "critical"))
478
- .critical
479
- else
480
- .info;
481
-
482
- const result = try self.showMessageDialog(.{
483
- .style = style,
484
- .title = title,
485
- .message = message,
486
- .informative_text = informative,
487
- .primary_button = primary,
488
- .secondary_button = secondary,
489
- .tertiary_button = tertiary,
490
- });
491
-
492
- var writer = std.Io.Writer.fixed(output);
493
- try json.writeString(&writer, @tagName(result));
494
- return writer.buffered();
495
- }
496
478
  };
497
479
  }
498
480
 
@@ -47,6 +47,7 @@ const component_policy = @import("component_policy.zig");
47
47
  const hover_policy = @import("canvas_hover_policy.zig");
48
48
  const launch_timing = @import("launch_timing.zig");
49
49
  const runtime_effects = @import("effects.zig");
50
+ const runtime_dialogs = @import("dialogs.zig");
50
51
  const terminal_session = @import("terminal_session.zig");
51
52
  const ui_app_provenance = @import("ui_app_provenance.zig");
52
53
  const markup_development = @import("markup_development_policy.zig");
@@ -1684,6 +1685,8 @@ pub fn UiAppWithFeatures(comptime ModelT: type, comptime MsgT: type, comptime fe
1684
1685
  .reveal_path_fn = effectsRevealPath,
1685
1686
  .format_local_time_fn = effectsFormatLocalTime,
1686
1687
  .execute_capability_fn = effectsExecuteCapability,
1688
+ .start_capability_fn = effectsStartCapability,
1689
+ .cancel_capability_fn = effectsCancelCapability,
1687
1690
  .replay_window_result_fn = effectsRestoreWindowResult,
1688
1691
  });
1689
1692
  if (runtime.options.session_recorder) |recorder| {
@@ -7025,6 +7028,70 @@ fn effectsRestoreWindowResult(context: *anyopaque, label: []const u8, closed: bo
7025
7028
  runtime.restoreWindowCapabilityResult(label, closed);
7026
7029
  }
7027
7030
 
7031
+ /// What a directory picker needs to answer its capability request, kept in
7032
+ /// the dialog's completion tag until the picker finishes.
7033
+ const DirectoryPickerRequest = struct {
7034
+ completion: runtime_effects.DesktopCapabilityCompletion,
7035
+ /// The request's correlation prefix, echoed in the reply header.
7036
+ prefix: [36]u8,
7037
+ prefix_len: u8,
7038
+ };
7039
+
7040
+ comptime {
7041
+ std.debug.assert(@sizeOf(DirectoryPickerRequest) <= runtime_dialogs.max_dialog_tag_bytes);
7042
+ }
7043
+
7044
+ /// `native-sdk.dialog.openDirectory` opens a native folder picker without
7045
+ /// blocking the event loop; the reply follows when the picker finishes.
7046
+ /// Other capabilities, and the picker on a platform without asynchronous
7047
+ /// dialogs, keep the synchronous `effectsExecuteCapability` and its
7048
+ /// replies.
7049
+ fn effectsStartCapability(context: *anyopaque, name: []const u8, payload: []const u8, completion: runtime_effects.DesktopCapabilityCompletion) anyerror!?u64 {
7050
+ if (!std.mem.eql(u8, name, "native-sdk.dialog.openDirectory")) return null;
7051
+ const runtime: *Runtime = @ptrCast(@alignCast(context));
7052
+ if (!runtime.options.platform.services.supportsAsyncDialogs()) return null;
7053
+ const desktop_files = @import("desktop_files.zig");
7054
+ if (!authorization.hasPermission(runtime.authorization_policy, runtime.options.security.permissions, security.permission_dialog)) return error.PermissionDenied;
7055
+ var reader = try desktop_files.request(payload);
7056
+ const prefix_len = reader.at;
7057
+ const title = try reader.field();
7058
+ const path = try reader.field();
7059
+ try reader.finish();
7060
+ if (title.len > 255 or path.len > desktop_files.max_path_bytes or std.mem.indexOfScalar(u8, path, 0) != null) return error.InvalidRequest;
7061
+ var request: DirectoryPickerRequest = .{ .completion = completion, .prefix = undefined, .prefix_len = @intCast(prefix_len) };
7062
+ @memcpy(request.prefix[0..prefix_len], payload[0..prefix_len]);
7063
+ return try runtime.requestOpenDialog(.{ .title = title, .default_path = path, .allow_directories = true, .allow_multiple = false }, null, .{
7064
+ .context = completion.context,
7065
+ .tag = std.mem.asBytes(&request),
7066
+ .on_result = directoryPickerFinished,
7067
+ });
7068
+ }
7069
+
7070
+ fn effectsCancelCapability(context: *anyopaque, handle: u64) void {
7071
+ const runtime: *Runtime = @ptrCast(@alignCast(context));
7072
+ runtime.cancelDialog(handle);
7073
+ }
7074
+
7075
+ fn directoryPickerFinished(context: *anyopaque, runtime: *Runtime, tag: []const u8, result: platform.DialogResult) anyerror!void {
7076
+ _ = .{ context, runtime };
7077
+ const desktop_files = @import("desktop_files.zig");
7078
+ var request: DirectoryPickerRequest = undefined;
7079
+ @memcpy(std.mem.asBytes(&request), tag[0..@sizeOf(DirectoryPickerRequest)]);
7080
+ const prefix = request.prefix[0..request.prefix_len];
7081
+ var output: [64 + 2 + platform.max_dialog_path_bytes]u8 = undefined;
7082
+ if (result.outcome == .failed) {
7083
+ request.completion.complete(true, output[0..try desktop_files.replyHeaderFor(&output, prefix, "DialogFailed")]);
7084
+ return;
7085
+ }
7086
+ // A cancelled picker answers an empty selection, as it always has.
7087
+ const selected = if (result.outcome == .selected and result.count > 0) result.paths else "";
7088
+ const header = try desktop_files.replyHeaderFor(&output, prefix, "");
7089
+ if (selected.len > 65535 or output.len - header < 2 or selected.len > output.len - header - 2) return error.OverBound;
7090
+ std.mem.writeInt(u16, output[header..][0..2], @intCast(selected.len), .little);
7091
+ @memcpy(output[header + 2 ..][0..selected.len], selected);
7092
+ request.completion.complete(true, output[0 .. header + 2 + selected.len]);
7093
+ }
7094
+
7028
7095
  fn effectsExecuteCapability(context: *anyopaque, name: []const u8, payload: []const u8, output: []u8) anyerror![]const u8 {
7029
7096
  const runtime: *Runtime = @ptrCast(@alignCast(context));
7030
7097
  const desktop_files = @import("desktop_files.zig");
@@ -6,7 +6,7 @@ const tooling = @import("tooling");
6
6
  const automation_protocol = @import("automation_protocol");
7
7
  const cli_build_info = @import("cli_build_info");
8
8
 
9
- const version = "0.11.0-fork.4";
9
+ const version = "0.11.0-fork.5";
10
10
 
11
11
  pub fn main(init: std.process.Init) !void {
12
12
  const allocator = init.arena.allocator();