@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.
- package/build/app.zig +50 -34
- package/package.json +9 -9
- package/packages/core/package-lock.json +2 -2
- package/packages/core/package.json +1 -1
- package/skill-data/core/references/bridge-security-native-capabilities.md +4 -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 +44 -0
- package/src/platform/macos/appkit_host.m +301 -67
- package/src/platform/macos/cef_host.mm +21 -0
- package/src/platform/macos/root.zig +117 -23
- package/src/platform/null_platform.zig +136 -0
- package/src/platform/root.zig +6 -0
- package/src/platform/types.zig +81 -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 +225 -39
- package/src/platform/windows/session_end_test.cpp +303 -0
- package/src/platform/windows/webview2_host.cpp +1064 -280
- package/src/root.zig +7 -0
- package/src/runtime/builtin_bridge.zig +108 -13
- 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 +71 -2
- package/src/runtime/effects_host_tests.zig +69 -1
- package/src/runtime/flow.zig +14 -6
- package/src/runtime/flow_reference.zig +14 -6
- package/src/runtime/platform_bridge_tests.zig +1 -0
- package/src/runtime/root.zig +5 -0
- package/src/runtime/system_services.zig +64 -82
- package/src/runtime/ui_app.zig +67 -0
- package/tools/native-sdk/main.zig +1 -1
package/src/runtime/flow.zig
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/runtime/root.zig
CHANGED
|
@@ -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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
|
package/src/runtime/ui_app.zig
CHANGED
|
@@ -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.
|
|
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();
|