@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
@@ -143,8 +143,8 @@ extern fn native_sdk_windows_create(app_name: [*]const u8, app_name_len: usize,
143
143
  extern fn native_sdk_windows_destroy(host: *WindowsHost) void;
144
144
  extern fn native_sdk_windows_run(host: *WindowsHost, callback: WindowsCallback, context: ?*anyopaque) void;
145
145
  extern fn native_sdk_windows_stop(host: *WindowsHost) void;
146
- extern fn native_sdk_windows_wake(host: *WindowsHost) void;
147
- extern fn native_sdk_windows_request_frame(host: *WindowsHost) void;
146
+ extern fn native_sdk_windows_wake(host: *WindowsHost) c_int;
147
+ extern fn native_sdk_windows_request_frame(host: *WindowsHost) c_int;
148
148
  extern fn native_sdk_windows_decode_image(bytes: [*]const u8, bytes_len: usize, pixels: [*]u8, pixels_len: usize, max_pixels: usize, out_width: *usize, out_height: *usize) c_int;
149
149
  extern fn native_sdk_windows_load_webview(host: *WindowsHost, source: [*]const u8, source_len: usize, source_kind: c_int, asset_root: [*]const u8, asset_root_len: usize, asset_entry: [*]const u8, asset_entry_len: usize, asset_origin: [*]const u8, asset_origin_len: usize, spa_fallback: c_int) void;
150
150
  extern fn native_sdk_windows_load_window_webview(host: *WindowsHost, window_id: u64, source: [*]const u8, source_len: usize, source_kind: c_int, asset_root: [*]const u8, asset_root_len: usize, asset_entry: [*]const u8, asset_entry_len: usize, asset_origin: [*]const u8, asset_origin_len: usize, spa_fallback: c_int) c_int;
@@ -197,6 +197,9 @@ extern fn native_sdk_windows_reveal_path(host: *WindowsHost, path: [*]const u8,
197
197
  extern fn native_sdk_windows_show_open_dialog(host: *WindowsHost, opts: *const WindowsOpenDialogOpts, buffer: [*]u8, buffer_len: usize) WindowsOpenDialogResult;
198
198
  extern fn native_sdk_windows_show_save_dialog(host: *WindowsHost, opts: *const WindowsSaveDialogOpts, buffer: [*]u8, buffer_len: usize) usize;
199
199
  extern fn native_sdk_windows_show_message_dialog(host: *WindowsHost, opts: *const WindowsMessageDialogOpts) c_int;
200
+ extern fn native_sdk_windows_request_dialog(host: *WindowsHost, request: *const WindowsDialogRequest) c_int;
201
+ extern fn native_sdk_windows_cancel_dialog(host: *WindowsHost, id: u64) void;
202
+ extern fn native_sdk_windows_poll_dialog_result(host: *WindowsHost, buffer: [*]u8, buffer_len: usize, result: *WindowsDialogResult) c_int;
200
203
  extern fn native_sdk_windows_show_notification(host: *WindowsHost, title: [*]const u8, title_len: usize, subtitle: [*]const u8, subtitle_len: usize, body: [*]const u8, body_len: usize, notification_id: [*]const u8, notification_id_len: usize, action_label: [*]const u8, action_label_len: usize, action_command: [*]const u8, action_command_len: usize) c_int;
201
204
  extern fn native_sdk_windows_create_tray(host: *WindowsHost, icon_path: [*]const u8, icon_path_len: usize, tooltip: [*]const u8, tooltip_len: usize, 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) c_int;
202
205
  extern fn native_sdk_windows_update_tray_menu(host: *WindowsHost, 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) c_int;
@@ -277,6 +280,23 @@ const WindowsMessageDialogOpts = extern struct {
277
280
  tertiary_button_len: usize,
278
281
  };
279
282
 
283
+ const WindowsDialogRequest = extern struct {
284
+ id: u64,
285
+ parent_window_id: u64,
286
+ kind: c_int,
287
+ open_opts: ?*const WindowsOpenDialogOpts,
288
+ save_opts: ?*const WindowsSaveDialogOpts,
289
+ message_opts: ?*const WindowsMessageDialogOpts,
290
+ };
291
+
292
+ const WindowsDialogResult = extern struct {
293
+ id: u64,
294
+ outcome: c_int,
295
+ button: c_int,
296
+ count: usize,
297
+ bytes_written: usize,
298
+ };
299
+
280
300
  /// The startup-window twin of the runtime's create-time `.hide` gate:
281
301
  /// on windows, hide-on-close is honest only when the app declares a
282
302
  /// tray (status item) — SW_HIDE removes the taskbar entry and windows
@@ -477,6 +497,9 @@ pub const WindowsPlatform = struct {
477
497
  .show_open_dialog_fn = showOpenDialog,
478
498
  .show_save_dialog_fn = showSaveDialog,
479
499
  .show_message_dialog_fn = showMessageDialog,
500
+ .request_dialog_fn = if (self.web_engine == .system) requestDialog else null,
501
+ .cancel_dialog_fn = if (self.web_engine == .system) cancelDialog else null,
502
+ .poll_dialog_result_fn = if (self.web_engine == .system) pollDialogResult else null,
480
503
  .show_notification_fn = showNotification,
481
504
  .create_tray_fn = createTray,
482
505
  .update_tray_menu_fn = updateTrayMenu,
@@ -901,13 +924,14 @@ fn emitWindowEvent(context: ?*anyopaque, window_id: platform_mod.WindowId, name:
901
924
  native_sdk_windows_emit_window_event(self.host, window_id, name.ptr, name.len, detail_json.ptr, detail_json.len);
902
925
  }
903
926
 
904
- /// Thread-safe: `PostMessageW` into the existing message loop (the
905
- /// enqueue-only shape the wake contract requires — never the
927
+ /// Thread-safe: `PostMessageW` to the host's loop window (the
928
+ /// enqueue-only shape the wake contract requires — no lock, never the
906
929
  /// synchronous `SendMessage`), whose window procedure emits `.wake` on
907
- /// the loop thread.
930
+ /// the loop thread. A wake that could not be posted is an error, so the
931
+ /// caller re-arms instead of assuming the nudge happened.
908
932
  fn wake(context: ?*anyopaque) anyerror!void {
909
933
  const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
910
- native_sdk_windows_wake(self.host);
934
+ if (native_sdk_windows_wake(self.host) == 0) return error.WakeNotPosted;
911
935
  }
912
936
 
913
937
  /// Teardown abandoned an in-flight channel wake call: latch the flag
@@ -918,13 +942,14 @@ fn noteChannelWakeAbandoned(context: ?*anyopaque) void {
918
942
  self.channel_wake_abandoned.store(true, .seq_cst);
919
943
  }
920
944
 
921
- /// Thread-safe like `wake`: `PostMessage` into the message loop, whose
922
- /// window procedure emits one `.frame` event on the loop thread. The
923
- /// automation arrival watcher calls this when a command lands so
924
- /// consumption never depends on the host's own frame pump.
945
+ /// Thread-safe like `wake`: `PostMessage` to the loop window, whose
946
+ /// window procedure emits one `.frame` event on the loop thread;
947
+ /// requests coalesce while one is queued. The automation arrival
948
+ /// watcher calls this when a command lands so consumption never depends
949
+ /// on the host's own frame pump.
925
950
  fn requestFrame(context: ?*anyopaque) anyerror!void {
926
951
  const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
927
- native_sdk_windows_request_frame(self.host);
952
+ if (native_sdk_windows_request_frame(self.host) == 0) return error.FrameRequestNotPosted;
928
953
  }
929
954
 
930
955
  /// Headless image codec for session replay: the SAME WIC decode a live
@@ -1481,12 +1506,9 @@ fn closeWebView(context: ?*anyopaque, window_id: platform_mod.WindowId, label: [
1481
1506
  if (native_sdk_windows_close_webview(self.host, window_id, label.ptr, label.len) == 0) return error.WebViewNotFound;
1482
1507
  }
1483
1508
 
1484
- fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions, buffer: []u8) anyerror!platform_mod.OpenDialogResult {
1485
- const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
1486
- if (self.web_engine != .system) return error.UnsupportedService;
1487
- var ext_buf: [1024]u8 = undefined;
1488
- const ext_str = flattenFilters(options.filters, &ext_buf);
1489
- const opts = WindowsOpenDialogOpts{
1509
+ fn windowsOpenDialogOpts(options: platform_mod.OpenDialogOptions, ext_buf: []u8) WindowsOpenDialogOpts {
1510
+ const ext_str = flattenFilters(options.filters, ext_buf);
1511
+ return .{
1490
1512
  .title = options.title.ptr,
1491
1513
  .title_len = options.title.len,
1492
1514
  .default_path = options.default_path.ptr,
@@ -1496,17 +1518,11 @@ fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions,
1496
1518
  .allow_directories = if (options.allow_directories) 1 else 0,
1497
1519
  .allow_multiple = if (options.allow_multiple) 1 else 0,
1498
1520
  };
1499
- const result = native_sdk_windows_show_open_dialog(self.host, &opts, buffer.ptr, buffer.len);
1500
- if (result.bytes_written > buffer.len) return error.NoSpaceLeft;
1501
- return .{ .count = result.count, .paths = buffer[0..result.bytes_written] };
1502
1521
  }
1503
1522
 
1504
- fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions, buffer: []u8) anyerror!?[]const u8 {
1505
- const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
1506
- if (self.web_engine != .system) return error.UnsupportedService;
1507
- var ext_buf: [1024]u8 = undefined;
1508
- const ext_str = flattenFilters(options.filters, &ext_buf);
1509
- const opts = WindowsSaveDialogOpts{
1523
+ fn windowsSaveDialogOpts(options: platform_mod.SaveDialogOptions, ext_buf: []u8) WindowsSaveDialogOpts {
1524
+ const ext_str = flattenFilters(options.filters, ext_buf);
1525
+ return .{
1510
1526
  .title = options.title.ptr,
1511
1527
  .title_len = options.title.len,
1512
1528
  .default_path = options.default_path.ptr,
@@ -1516,16 +1532,10 @@ fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions,
1516
1532
  .extensions = ext_str.ptr,
1517
1533
  .extensions_len = ext_str.len,
1518
1534
  };
1519
- const written = native_sdk_windows_show_save_dialog(self.host, &opts, buffer.ptr, buffer.len);
1520
- if (written > buffer.len) return error.NoSpaceLeft;
1521
- if (written == 0) return null;
1522
- return buffer[0..written];
1523
1535
  }
1524
1536
 
1525
- fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOptions) anyerror!platform_mod.MessageDialogResult {
1526
- const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
1527
- if (self.web_engine != .system) return error.UnsupportedService;
1528
- const opts = WindowsMessageDialogOpts{
1537
+ fn windowsMessageDialogOpts(options: platform_mod.MessageDialogOptions) WindowsMessageDialogOpts {
1538
+ return .{
1529
1539
  .style = @intFromEnum(options.style),
1530
1540
  .title = options.title.ptr,
1531
1541
  .title_len = options.title.len,
@@ -1540,7 +1550,97 @@ fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOp
1540
1550
  .tertiary_button = options.tertiary_button.ptr,
1541
1551
  .tertiary_button_len = options.tertiary_button.len,
1542
1552
  };
1543
- return @enumFromInt(native_sdk_windows_show_message_dialog(self.host, &opts));
1553
+ }
1554
+
1555
+ fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions, buffer: []u8) anyerror!platform_mod.OpenDialogResult {
1556
+ const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
1557
+ if (self.web_engine != .system) return error.UnsupportedService;
1558
+ var ext_buf: [1024]u8 = undefined;
1559
+ const opts = windowsOpenDialogOpts(options, &ext_buf);
1560
+ const result = native_sdk_windows_show_open_dialog(self.host, &opts, buffer.ptr, buffer.len);
1561
+ if (result.bytes_written > buffer.len) return error.NoSpaceLeft;
1562
+ return .{ .count = result.count, .paths = buffer[0..result.bytes_written] };
1563
+ }
1564
+
1565
+ fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions, buffer: []u8) anyerror!?[]const u8 {
1566
+ const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
1567
+ if (self.web_engine != .system) return error.UnsupportedService;
1568
+ var ext_buf: [1024]u8 = undefined;
1569
+ const opts = windowsSaveDialogOpts(options, &ext_buf);
1570
+ const written = native_sdk_windows_show_save_dialog(self.host, &opts, buffer.ptr, buffer.len);
1571
+ if (written > buffer.len) return error.NoSpaceLeft;
1572
+ if (written == 0) return null;
1573
+ return buffer[0..written];
1574
+ }
1575
+
1576
+ fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOptions) anyerror!platform_mod.MessageDialogResult {
1577
+ const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
1578
+ if (self.web_engine != .system) return error.UnsupportedService;
1579
+ const opts = windowsMessageDialogOpts(options);
1580
+ // Negative: a session end left the dialog unanswered (the host
1581
+ // cancelled it to unwind this dispatch); no button stands in for it.
1582
+ const pressed = native_sdk_windows_show_message_dialog(self.host, &opts);
1583
+ if (pressed < 0) return error.SessionEnding;
1584
+ return @enumFromInt(pressed);
1585
+ }
1586
+
1587
+ // The host copies every option before returning, so they may live on this
1588
+ // stack; the result comes back through pollDialogResult on a later wake.
1589
+ fn requestDialog(context: ?*anyopaque, request: platform_mod.DialogRequest) anyerror!void {
1590
+ const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
1591
+ var ext_buf: [1024]u8 = undefined;
1592
+ var open_opts: WindowsOpenDialogOpts = undefined;
1593
+ var save_opts: WindowsSaveDialogOpts = undefined;
1594
+ var message_opts: WindowsMessageDialogOpts = undefined;
1595
+ var native_request = WindowsDialogRequest{
1596
+ .id = request.id,
1597
+ .parent_window_id = request.parent_window_id orelse 0,
1598
+ .kind = @intFromEnum(std.meta.activeTag(request.options)),
1599
+ .open_opts = null,
1600
+ .save_opts = null,
1601
+ .message_opts = null,
1602
+ };
1603
+ switch (request.options) {
1604
+ .open => |options| {
1605
+ open_opts = windowsOpenDialogOpts(options, &ext_buf);
1606
+ native_request.open_opts = &open_opts;
1607
+ },
1608
+ .save => |options| {
1609
+ save_opts = windowsSaveDialogOpts(options, &ext_buf);
1610
+ native_request.save_opts = &save_opts;
1611
+ },
1612
+ .message => |options| {
1613
+ message_opts = windowsMessageDialogOpts(options);
1614
+ native_request.message_opts = &message_opts;
1615
+ },
1616
+ }
1617
+ if (native_sdk_windows_request_dialog(self.host, &native_request) == 0) return error.DialogUnavailable;
1618
+ }
1619
+
1620
+ fn cancelDialog(context: ?*anyopaque, id: platform_mod.DialogRequestId) void {
1621
+ const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
1622
+ native_sdk_windows_cancel_dialog(self.host, id);
1623
+ }
1624
+
1625
+ fn pollDialogResult(context: ?*anyopaque, buffer: []u8) ?platform_mod.DialogResult {
1626
+ const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
1627
+ var result: WindowsDialogResult = undefined;
1628
+ if (native_sdk_windows_poll_dialog_result(self.host, buffer.ptr, buffer.len, &result) == 0) return null;
1629
+ return .{
1630
+ .id = result.id,
1631
+ .outcome = switch (result.outcome) {
1632
+ 0 => .selected,
1633
+ 1 => .cancelled,
1634
+ else => .failed,
1635
+ },
1636
+ .paths = buffer[0..@min(result.bytes_written, buffer.len)],
1637
+ .count = result.count,
1638
+ .button = switch (result.button) {
1639
+ 1 => .secondary,
1640
+ 2 => .tertiary,
1641
+ else => .primary,
1642
+ },
1643
+ };
1544
1644
  }
1545
1645
 
1546
1646
  fn showNotification(context: ?*anyopaque, options: platform_mod.NotificationOptions) anyerror!void {
@@ -2274,7 +2374,9 @@ test "windows tray carries lifecycle commands rich rows and key equivalents into
2274
2374
  try std.testing.expect(std.mem.indexOf(u8, host_source, "display_label += shortcutSuffix(item.key, item.modifiers);") != null);
2275
2375
  try std.testing.expect(std.mem.indexOf(u8, host_source, "const std::vector<TrayItem> displayed_items = host->tray_items;") != null);
2276
2376
  try std.testing.expect(std.mem.indexOf(u8, host_source, "emitTrayActionForCommandId(host, displayed_items, command_id);") != null);
2277
- try std.testing.expect(std.mem.indexOf(u8, host_source, "return emitTrayActionForCommandId(host, host->tray_items, item.command_id);") != null);
2377
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "out->kind = kTrayAction;") != null);
2378
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "out->tray_item_id = item.id;") != null);
2379
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "event.tray_item_id = match.tray_item_id;") != null);
2278
2380
  try std.testing.expect(std.mem.indexOf(u8, host_source, "tray_event == NIN_SELECT || tray_event == NIN_KEYSELECT || tray_event == WM_LBUTTONUP") != null);
2279
2381
  }
2280
2382
 
@@ -2336,6 +2438,87 @@ test "windows WM_CLOSE hide consults the live tray state and downgrades to a rea
2336
2438
  try std.testing.expect(std.mem.indexOf(u8, close_hook, "downgraded to a real close") != null);
2337
2439
  }
2338
2440
 
2441
+ test "windows session end runs the stop hook once and bypasses the close policy" {
2442
+ // C++ this suite cannot execute, pinned textually like the WM_CLOSE
2443
+ // hook above. A confirmed WM_ENDSESSION (logoff, system shutdown, a
2444
+ // Restart Manager close) never goes through the close-policy hide;
2445
+ // the query only says yes; the run loop's exit and the session end
2446
+ // share one exactly-once delivery.
2447
+ const host_source = @embedFile("webview2_host.cpp");
2448
+ const query_at = std.mem.indexOf(u8, host_source, "case WM_QUERYENDSESSION:") orelse return error.TestExpectedEqual;
2449
+ const end_at = std.mem.indexOfPos(u8, host_source, query_at, "case WM_ENDSESSION:") orelse return error.TestExpectedEqual;
2450
+ const close_at = std.mem.indexOfPos(u8, host_source, end_at, "case WM_CLOSE:") orelse return error.TestExpectedEqual;
2451
+ try std.testing.expect(std.mem.indexOf(u8, host_source[query_at..end_at], "return TRUE;") != null);
2452
+ try std.testing.expect(std.mem.indexOf(u8, host_source[end_at..close_at], "if (host && wparam) endSession(host);") != null);
2453
+ // While a session end unwinds a dispatch, closes are ignored before
2454
+ // the close policy is consulted.
2455
+ const ending_close_at = std.mem.indexOfPos(u8, host_source, close_at, "if (host && host->session_ending) return 0;") orelse return error.TestExpectedEqual;
2456
+ const hide_at = std.mem.indexOfPos(u8, host_source, close_at, "ShowWindow(hwnd, SW_HIDE);") orelse return error.TestExpectedEqual;
2457
+ try std.testing.expect(ending_close_at < hide_at);
2458
+
2459
+ // Outside a runtime dispatch the shutdown runs synchronously, then
2460
+ // the tray icon goes and the process ends. Inside one, the runtime
2461
+ // queues the shutdown behind it, so the host marks the session
2462
+ // ending, hands the shutdown over, stops the loop, and cancels the
2463
+ // dialogs holding the dispatch, without ending the process.
2464
+ const session_at = std.mem.indexOf(u8, host_source, "static void endSession(Host *host) {") orelse return error.TestExpectedEqual;
2465
+ const session_end = std.mem.indexOfPos(u8, host_source, session_at, "\n}\n") orelse return error.TestExpectedEqual;
2466
+ const session_fn = host_source[session_at..session_end];
2467
+ const order = [_][]const u8{
2468
+ "if (host->shutdown_delivered) return;",
2469
+ "if (host->runtime_dispatch_depth == 0) {",
2470
+ "deliverShutdown(host);",
2471
+ "removeNotificationIcon(host);",
2472
+ "TerminateProcess(GetCurrentProcess(), 0);",
2473
+ "host->session_ending = true;",
2474
+ "deliverShutdown(host);",
2475
+ "host->running = false;",
2476
+ "EnumThreadWindows(GetCurrentThreadId(), closeOwnedDialog,",
2477
+ };
2478
+ var cursor: usize = 0;
2479
+ for (order) |needle| cursor = (std.mem.indexOfPos(u8, session_fn, cursor, needle) orelse return error.TestExpectedEqual) + needle.len;
2480
+ try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, session_fn, "TerminateProcess("));
2481
+
2482
+ const deliver_fn_at = std.mem.indexOf(u8, host_source, "static void deliverShutdown(Host *host) {") orelse return error.TestExpectedEqual;
2483
+ const flag_at = std.mem.indexOfPos(u8, host_source, deliver_fn_at, "host->shutdown_delivered = true;") orelse return error.TestExpectedEqual;
2484
+ const callback_at = std.mem.indexOfPos(u8, host_source, deliver_fn_at, "host->callback(host->callback_context, &shutdown);") orelse return error.TestExpectedEqual;
2485
+ // The flag latches before the callback: a session end arriving while
2486
+ // the stop hook runs (a modal loop inside it) must not deliver again.
2487
+ try std.testing.expect(flag_at < callback_at);
2488
+ try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, host_source, "kind = kShutdown;"));
2489
+
2490
+ // The dispatch depth sees every call into the runtime: events through
2491
+ // the counting trampoline the run loop installs, Bridge messages
2492
+ // through their one delivery site.
2493
+ const run_at = std.mem.indexOf(u8, host_source, "void native_sdk_windows_run(") orelse return error.TestExpectedEqual;
2494
+ const run_end = std.mem.indexOfPos(u8, host_source, run_at, "\n}\n") orelse return error.TestExpectedEqual;
2495
+ const run_fn = host_source[run_at..run_end];
2496
+ try std.testing.expect(std.mem.indexOf(u8, run_fn, "host->callback = dispatchEventToRuntime;") != null);
2497
+ try std.testing.expect(std.mem.indexOf(u8, run_fn, "deliverShutdown(host);") != null);
2498
+ const trampoline_at = std.mem.indexOf(u8, host_source, "static void dispatchEventToRuntime(") orelse return error.TestExpectedEqual;
2499
+ const scope_at = std.mem.indexOfPos(u8, host_source, trampoline_at, "RuntimeDispatchScope scope(host);") orelse return error.TestExpectedEqual;
2500
+ const forward_at = std.mem.indexOfPos(u8, host_source, trampoline_at, "host->runtime_callback(host->runtime_context, event);") orelse return error.TestExpectedEqual;
2501
+ try std.testing.expect(scope_at < forward_at);
2502
+ // Nothing enters the runtime after the shutdown was handed over, on
2503
+ // every path: the stop hook's own events and a closing dialog's.
2504
+ const gate_at = std.mem.indexOfPos(u8, host_source, trampoline_at, "if (host->shutdown_delivered && event->kind != kShutdown) return;") orelse return error.TestExpectedEqual;
2505
+ try std.testing.expect(gate_at < forward_at);
2506
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "if (!host->bridge_callback || host->shutdown_delivered) return;") != null);
2507
+ try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, host_source, "host->bridge_callback("));
2508
+ const bridge_at = std.mem.indexOf(u8, host_source, "host->bridge_callback(") orelse return error.TestExpectedEqual;
2509
+ const bridge_scope_at = std.mem.lastIndexOf(u8, host_source[0..bridge_at], "RuntimeDispatchScope scope(host);") orelse return error.TestExpectedEqual;
2510
+ try std.testing.expect(bridge_at - bridge_scope_at < 128);
2511
+
2512
+ // Dialogs the unwinding cancels, through any chain of owners, report
2513
+ // no answer rather than a button, and new ones are not shown;
2514
+ // session_end_test.cpp drives this on a Windows host.
2515
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "for (HWND owner = GetWindow(hwnd, GW_OWNER); owner; owner = GetWindow(owner, GW_OWNER)) {") != null);
2516
+ const message_at = std.mem.indexOf(u8, host_source, "int native_sdk_windows_show_message_dialog(Host *host, const WindowsMessageDialogOpts *opts) {") orelse return error.TestExpectedEqual;
2517
+ const message_end = std.mem.indexOfPos(u8, host_source, message_at, "\n}\n") orelse return error.TestExpectedEqual;
2518
+ try std.testing.expectEqual(@as(usize, 3), std.mem.count(u8, host_source[message_at..message_end], "if (host->session_ending) return -1;"));
2519
+ try std.testing.expectEqual(@as(usize, 2), std.mem.count(u8, host_source, "|| buffer_len == 0 || host->session_ending) return"));
2520
+ }
2521
+
2339
2522
  test "windows passive show never foregrounds or focuses the window" {
2340
2523
  const host_source = @embedFile("webview2_host.cpp");
2341
2524
  const show_at = std.mem.indexOf(u8, host_source, "int native_sdk_windows_show_window(") orelse return error.TestExpectedEqual;
@@ -2720,11 +2903,51 @@ test "windows window focus includes focused native child views" {
2720
2903
  test "windows webview focus reports the focused child label" {
2721
2904
  // WebView2 owns the page HWND and its input; GotFocus is the
2722
2905
  // controller-level edge that mirrors keyboard ownership back into
2723
- // the runtime's per-view register.
2906
+ // the runtime's per-view register. The callback queues the focused
2907
+ // view, and the loop window reports it once the callback returns.
2724
2908
  const host_source = @embedFile("webview2_host.cpp");
2725
2909
  try std.testing.expect(std.mem.indexOf(u8, host_source, "add_GotFocus(Callback<ICoreWebView2FocusChangedEventHandler>") != null);
2726
- try std.testing.expect(std.mem.indexOf(u8, host_source, "event.kind = kViewFocused;") != null);
2727
- try std.testing.expect(std.mem.indexOf(u8, host_source, "event.view_label = focused->second.label.c_str();") != null);
2910
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "event.kind = DeferredWebViewEvent::Kind::Focus;") != null);
2911
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "event.label = focused->second.label;") != null);
2912
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "focused.kind = kViewFocused;") != null);
2913
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "focused.view_label = event.label.c_str();") != null);
2914
+ }
2915
+
2916
+ test "windows webview2 keeps its user data with the app's per-user data" {
2917
+ // WebView2's default user data folder sits beside the executable,
2918
+ // which is read-only under Program Files and is swapped away with
2919
+ // the program files on update. Every environment the host creates
2920
+ // names the folder beside the app_dirs roots instead, and an earlier
2921
+ // "<exe>.WebView2" folder moves there once.
2922
+ const host_source = @embedFile("webview2_host.cpp");
2923
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "factory(nullptr, webViewUserDataFolder(host), nullptr,") != null);
2924
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "factory(nullptr, nullptr, nullptr,") == null);
2925
+ const resolve_at = std.mem.indexOf(u8, host_source, "static const wchar_t *webViewUserDataFolder(Host *host) {") orelse return error.TestExpectedEqual;
2926
+ const resolve = host_source[resolve_at..];
2927
+ try std.testing.expect(std.mem.indexOf(u8, resolve, "GetEnvironmentVariableW(L\"LOCALAPPDATA\"") != null);
2928
+ try std.testing.expect(std.mem.indexOf(u8, resolve, "const std::wstring folder = app_root + L\"\\\\WebView2\";") != null);
2929
+ try std.testing.expect(std.mem.indexOf(u8, resolve, "MoveFileExW(legacy.c_str(), folder.c_str(), 0)") != null);
2930
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "return path + L\".WebView2\";") != null);
2931
+ }
2932
+
2933
+ test "windows webview assets resolve against the packaged resources beside bin" {
2934
+ // `native package` stages bin\<exe> and the app-relative asset tree
2935
+ // in resources\; a relative asset root names that tree when the app
2936
+ // runs from a package and the working directory otherwise.
2937
+ const host_source = @embedFile("webview2_host.cpp");
2938
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "std::string path = assetRootPath(webview.asset_root);") != null);
2939
+ const resources_at = std.mem.indexOf(u8, host_source, "static const std::wstring &packagedResourcesDirectory() {") orelse return error.TestExpectedEqual;
2940
+ const resources = host_source[resources_at..];
2941
+ try std.testing.expect(std.mem.indexOf(u8, resources, "CompareStringOrdinal(bin.c_str() + bin_slash + 1, -1, L\"bin\", -1, TRUE)") != null);
2942
+ try std.testing.expect(std.mem.indexOf(u8, resources, "return bin.substr(0, bin_slash) + L\"\\\\resources\";") != null);
2943
+ }
2944
+
2945
+ test "windows names the process by its bundle id for the taskbar" {
2946
+ // The installer's Start menu shortcut carries the bundle id as its
2947
+ // AppUserModelID; the process must claim the same id.
2948
+ const host_source = @embedFile("webview2_host.cpp");
2949
+ const create_at = std.mem.indexOf(u8, host_source, "host->bundle_id = slice(bundle_id, bundle_id_len);") orelse return error.TestExpectedEqual;
2950
+ try std.testing.expect(std.mem.indexOf(u8, host_source[create_at..], "SetCurrentProcessExplicitAppUserModelID(app_user_model_id.c_str());") != null);
2728
2951
  }
2729
2952
 
2730
2953
  test "windows audio event maps kinds and payload" {