@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
@@ -817,6 +817,26 @@ static int NativeSdkCredentialStatus(OSStatus status, int missingCode) {
817
817
  @property(nonatomic, strong) NSString *openCommand;
818
818
  @end
819
819
 
820
+ /* One asynchronous dialog (native_sdk_appkit_request_dialog): what is
821
+ * showing, where, and once it finishes, the result poll hands back. */
822
+ @interface NativeSdkDialog : NSObject
823
+ @property(nonatomic, assign) uint64_t dialogId;
824
+ @property(nonatomic, assign) uint64_t parentWindowId;
825
+ @property(nonatomic, strong) NSSavePanel *panel;
826
+ @property(nonatomic, strong) NSAlert *alert;
827
+ /* The sheet's window, when the dialog is attached to one. */
828
+ @property(nonatomic, weak) NSWindow *sheetParent;
829
+ /* A parentless alert runs modally from the next loop turn. */
830
+ @property(nonatomic, assign) BOOL modalRunning;
831
+ @property(nonatomic, assign) BOOL cancelRequested;
832
+ @property(nonatomic, assign) int outcome;
833
+ @property(nonatomic, assign) int button;
834
+ @property(nonatomic, strong) NSArray<NSString *> *paths;
835
+ @end
836
+
837
+ @implementation NativeSdkDialog
838
+ @end
839
+
820
840
  @interface NativeSdkAppKitHost : NSObject <WKNavigationDelegate, NSMenuDelegate, NSUserNotificationCenterDelegate>
821
841
  @property(nonatomic, strong) NSWindow *window;
822
842
  @property(nonatomic, strong) WKWebView *webView;
@@ -881,6 +901,10 @@ static int NativeSdkCredentialStatus(OSStatus status, int missingCode) {
881
901
  * block starts still gets its own turn. */
882
902
  @property(atomic, assign) BOOL crossThreadFramePending;
883
903
  @property(nonatomic, strong) NSMutableDictionary<NSNumber *, NSTimer *> *appTimers;
904
+ /* Asynchronous dialogs by id while presented, and finished ones in the
905
+ * order they finished, awaiting poll. */
906
+ @property(nonatomic, strong) NSMutableDictionary<NSNumber *, NativeSdkDialog *> *dialogs;
907
+ @property(nonatomic, strong) NSMutableArray<NativeSdkDialog *> *finishedDialogs;
884
908
  /* Notification userInfo persists with the delivered notification, so it
885
909
  * carries only a per-process token. Commands remain in this live host;
886
910
  * stale notifications from an earlier execution cannot dispatch into a
@@ -1242,6 +1266,25 @@ static int NativeSdkCredentialStatus(OSStatus status, int missingCode) {
1242
1266
  - (void)emitShortcutWithId:(NSString *)identifier key:(NSString *)key modifiers:(uint32_t)modifiers event:(NSEvent *)event;
1243
1267
  @end
1244
1268
 
1269
+ static void NativeSdkCancelDialogsForWindow(NativeSdkAppKitHost *host, uint64_t windowId);
1270
+ static void NativeSdkCancelAllDialogs(NativeSdkAppKitHost *host);
1271
+
1272
+ /* [NSApp stop:] and stopModal take effect only after the loop handles its
1273
+ * next event; post an empty one so a stop requested from a timer or block
1274
+ * ends the loop now. */
1275
+ static void NativeSdkPostEmptyEvent(void) {
1276
+ NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
1277
+ location:NSZeroPoint
1278
+ modifierFlags:0
1279
+ timestamp:0
1280
+ windowNumber:0
1281
+ context:nil
1282
+ subtype:0
1283
+ data1:0
1284
+ data2:0];
1285
+ [NSApp postEvent:event atStart:NO];
1286
+ }
1287
+
1245
1288
  // Recursively re-emit the gpu-surface resize event for every metal
1246
1289
  // surface under `view` (the tall-titlebar chrome re-query path).
1247
1290
  static void NativeSdkEmitGpuSurfaceResizes(NSView *view) {
@@ -1423,6 +1466,9 @@ static NSPoint NativeSdkViewLocalYDownPoint(NSView *view, NSPoint point) {
1423
1466
 
1424
1467
  - (void)windowWillClose:(NSNotification *)notification {
1425
1468
  (void)notification;
1469
+ // End this window's sheets while it can still take them down; a
1470
+ // sheet on a closed window never calls its completion handler.
1471
+ NativeSdkCancelDialogsForWindow(self.host, self.windowId);
1426
1472
  if (self.observesContentLayout) {
1427
1473
  NSWindow *window = self.host.windows[@(self.windowId)];
1428
1474
  [window removeObserver:self forKeyPath:@"contentLayoutRect"];
@@ -8182,6 +8228,8 @@ static float NativeSdkCaptureReadRemixedSample(const AudioBufferList *buffers, c
8182
8228
  self.nativeViewExplicitTextKeys = [[NSMutableSet alloc] init];
8183
8229
  self.bridgeEnabledChildWebViewKeys = [[NSMutableSet alloc] init];
8184
8230
  self.appTimers = [[NSMutableDictionary alloc] init];
8231
+ self.dialogs = [[NSMutableDictionary alloc] init];
8232
+ self.finishedDialogs = [[NSMutableArray alloc] init];
8185
8233
  self.notificationActionCommands = [[NSMutableDictionary alloc] init];
8186
8234
  self.notificationActionTokensByIdentifier = [[NSMutableDictionary alloc] init];
8187
8235
  self.statusItems = [[NSMutableDictionary alloc] init];
@@ -8497,6 +8545,9 @@ static float NativeSdkCaptureReadRemixedSample(const AudioBufferList *buffers, c
8497
8545
  - (void)closeWindowWithId:(uint64_t)windowId {
8498
8546
  NSWindow *window = self.windows[@(windowId)];
8499
8547
  if (!window) return;
8548
+ // performClose: refuses a window with a sheet attached, so end this
8549
+ // window's dialogs first (Electron's NativeWindowMac::Close does too).
8550
+ NativeSdkCancelDialogsForWindow(self, windowId);
8500
8551
  // A runtime-initiated close is the app DECIDING to close: it must
8501
8552
  // not bounce off the window's own .hide close policy, so it skips
8502
8553
  // performClose: (which runs windowShouldClose:) and closes
@@ -10766,17 +10817,9 @@ static NSString *NativeSdkSigningTeamIdentifier(NSString *bundlePath) {
10766
10817
  }
10767
10818
  [self stopAppearanceObservers];
10768
10819
  [self stopApplicationActivationObservers];
10820
+ NativeSdkCancelAllDialogs(self);
10769
10821
  [NSApp stop:nil];
10770
- NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
10771
- location:NSZeroPoint
10772
- modifierFlags:0
10773
- timestamp:0
10774
- windowNumber:0
10775
- context:nil
10776
- subtype:0
10777
- data1:0
10778
- data2:0];
10779
- [NSApp postEvent:event atStart:NO];
10822
+ NativeSdkPostEmptyEvent();
10780
10823
  }
10781
10824
 
10782
10825
  /* Drain the quit verb a pre-run dispatch parked (see
@@ -13271,6 +13314,372 @@ int native_sdk_appkit_check_for_updates(native_sdk_appkit_host_t *host, int user
13271
13314
  return 1;
13272
13315
  }
13273
13316
 
13317
+ // ---------------------------------------------------------------------------
13318
+ // App-driven updates (`updates.mode = "app"`): the platform half of
13319
+ // src/updater/engine.zig, reached through src/updater/macos.zig. Unlike the
13320
+ // host flow above, nothing here shows UI; the engine calls these from its
13321
+ // worker threads and from the app loop. The release stages beside the app,
13322
+ // `<parent>/.<App.app>.native-update/`, so installing it is a rename on the
13323
+ // same volume, and a helper installs it once this process exits.
13324
+
13325
+ enum {
13326
+ NativeSdkUpdateOk = 0,
13327
+ NativeSdkUpdateNetwork = 1,
13328
+ NativeSdkUpdateHttpStatus = 2,
13329
+ NativeSdkUpdateTooLarge = 3,
13330
+ NativeSdkUpdateCancelled = 4,
13331
+ NativeSdkUpdateDisk = 5,
13332
+ };
13333
+
13334
+ static NSString *NativeSdkUpdateString(const char *bytes, size_t length) {
13335
+ if (!bytes || length == 0) return @"";
13336
+ return [[NSString alloc] initWithBytes:bytes length:length encoding:NSUTF8StringEncoding] ?: @"";
13337
+ }
13338
+
13339
+ static size_t NativeSdkUpdateCopy(NSString *value, char *output, size_t capacity) {
13340
+ const char *bytes = value.UTF8String;
13341
+ const size_t length = bytes ? strlen(bytes) : 0;
13342
+ if (length == 0 || length > capacity) return 0;
13343
+ memcpy(output, bytes, length);
13344
+ return length;
13345
+ }
13346
+
13347
+ static NSString *NativeSdkUpdateBundlePath(void) {
13348
+ return NSBundle.mainBundle.bundlePath.stringByStandardizingPath;
13349
+ }
13350
+
13351
+ static NSString *NativeSdkUpdateSiblingPath(NSString *suffix) {
13352
+ NSString *bundlePath = NativeSdkUpdateBundlePath();
13353
+ return [bundlePath.stringByDeletingLastPathComponent stringByAppendingPathComponent:[NSString stringWithFormat:@".%@.%@", bundlePath.lastPathComponent, suffix]];
13354
+ }
13355
+
13356
+ static NSString *NativeSdkUpdateResultPath(NSString *bundleIdentifier) {
13357
+ NSURL *support = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask].firstObject;
13358
+ if (!support || bundleIdentifier.length == 0) return nil;
13359
+ return [[support.path stringByAppendingPathComponent:bundleIdentifier] stringByAppendingPathComponent:@".native-update-result"];
13360
+ }
13361
+
13362
+ static BOOL NativeSdkUpdateURLAllowed(NSURL *url, BOOL allowLoopbackHttp) {
13363
+ NSString *scheme = url.scheme.lowercaseString;
13364
+ if ([scheme isEqualToString:@"https"]) return url.host.length > 0;
13365
+ if (!allowLoopbackHttp || ![scheme isEqualToString:@"http"]) return NO;
13366
+ NSString *host = url.host.lowercaseString;
13367
+ return [host isEqualToString:@"127.0.0.1"] || [host isEqualToString:@"::1"] || [host isEqualToString:@"localhost"];
13368
+ }
13369
+
13370
+ /// The one app bundle a staged release unpacked to, when it is this app at
13371
+ /// `version`; Info.plist is read directly because NSBundle caches bundles
13372
+ /// by path and the staging path is reused.
13373
+ static NSString *NativeSdkUpdateStagedBundle(NSString *stagePath, NSString *bundleIdentifier, NSString *version) {
13374
+ NSFileManager *manager = [NSFileManager defaultManager];
13375
+ NSArray<NSString *> *entries = [manager contentsOfDirectoryAtPath:stagePath error:nil];
13376
+ NSString *bundleName = nil;
13377
+ for (NSString *entry in entries ?: @[]) {
13378
+ if ([entry isEqualToString:@".native-staged"]) continue;
13379
+ if (bundleName || ![[entry pathExtension].lowercaseString isEqualToString:@"app"]) return nil;
13380
+ bundleName = entry;
13381
+ }
13382
+ if (!bundleName) return nil;
13383
+ NSString *candidate = [stagePath stringByAppendingPathComponent:bundleName];
13384
+ NSNumber *isDirectory = nil;
13385
+ NSNumber *isSymlink = nil;
13386
+ [[NSURL fileURLWithPath:candidate] getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil];
13387
+ [[NSURL fileURLWithPath:candidate] getResourceValue:&isSymlink forKey:NSURLIsSymbolicLinkKey error:nil];
13388
+ if (!isDirectory.boolValue || isSymlink.boolValue) return nil;
13389
+ NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:[candidate stringByAppendingPathComponent:@"Contents/Info.plist"]];
13390
+ id identifier = info[@"CFBundleIdentifier"];
13391
+ id bundleVersion = info[@"CFBundleVersion"];
13392
+ if (![identifier isKindOfClass:[NSString class]] || ![identifier isEqualToString:bundleIdentifier]) return nil;
13393
+ if (![bundleVersion isKindOfClass:[NSString class]] || ![bundleVersion isEqualToString:version]) return nil;
13394
+ return candidate;
13395
+ }
13396
+
13397
+ /// A Developer ID app accepts only a release signed by the same team, as
13398
+ /// the host flow does; unsigned and ad-hoc installs rely on the feed's
13399
+ /// Ed25519 signature alone.
13400
+ static BOOL NativeSdkUpdateSignatureMatches(NSString *candidate, NSString *bundleIdentifier) {
13401
+ NSString *teamIdentifier = NativeSdkSigningTeamIdentifier(NativeSdkUpdateBundlePath());
13402
+ if (teamIdentifier.length == 0) return YES;
13403
+ if ([teamIdentifier rangeOfCharacterFromSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]].location != NSNotFound ||
13404
+ [bundleIdentifier rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\\\""]].location != NSNotFound) return NO;
13405
+ NSString *requirement = [NSString stringWithFormat:@"anchor apple generic and certificate leaf[subject.OU] = \"%@\" and identifier \"%@\"", teamIdentifier, bundleIdentifier];
13406
+ return NativeSdkRunTask(@"/usr/bin/codesign", @[ @"--verify", @"--deep", @"--strict", [@"-R=" stringByAppendingString:requirement], candidate ], NULL);
13407
+ }
13408
+
13409
+ typedef void (*NativeSdkUpdateProgressFn)(void *context, uint64_t received);
13410
+ typedef int (*NativeSdkUpdateCancelledFn)(void *context);
13411
+
13412
+ @interface NativeSdkUpdateTransfer : NSObject <NSURLSessionDataDelegate>
13413
+ @property(nonatomic) BOOL allowLoopbackHttp;
13414
+ @property(nonatomic) uint64_t limit;
13415
+ @property(nonatomic) uint64_t received;
13416
+ @property(atomic) int result;
13417
+ @property(nonatomic, strong) NSMutableData *data;
13418
+ @property(nonatomic, strong) NSFileHandle *file;
13419
+ @property(nonatomic) void *context;
13420
+ @property(nonatomic) NativeSdkUpdateProgressFn progress;
13421
+ @property(nonatomic, strong) dispatch_semaphore_t done;
13422
+ @end
13423
+
13424
+ @implementation NativeSdkUpdateTransfer
13425
+
13426
+ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler {
13427
+ (void)session;
13428
+ (void)response;
13429
+ if (NativeSdkUpdateURLAllowed(request.URL, self.allowLoopbackHttp)) {
13430
+ completionHandler(request);
13431
+ return;
13432
+ }
13433
+ self.result = NativeSdkUpdateNetwork;
13434
+ completionHandler(nil);
13435
+ [task cancel];
13436
+ }
13437
+
13438
+ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
13439
+ (void)session;
13440
+ (void)dataTask;
13441
+ NSHTTPURLResponse *http = [response isKindOfClass:[NSHTTPURLResponse class]] ? (NSHTTPURLResponse *)response : nil;
13442
+ if (!http || !NativeSdkUpdateURLAllowed(response.URL, self.allowLoopbackHttp)) {
13443
+ self.result = NativeSdkUpdateNetwork;
13444
+ } else if (http.statusCode < 200 || http.statusCode >= 300) {
13445
+ self.result = NativeSdkUpdateHttpStatus;
13446
+ } else if (response.expectedContentLength > 0 && (uint64_t)response.expectedContentLength > self.limit) {
13447
+ self.result = NativeSdkUpdateTooLarge;
13448
+ }
13449
+ completionHandler(self.result == NativeSdkUpdateOk ? NSURLSessionResponseAllow : NSURLSessionResponseCancel);
13450
+ }
13451
+
13452
+ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
13453
+ (void)session;
13454
+ if (self.result != NativeSdkUpdateOk) return;
13455
+ if (data.length > self.limit - self.received) {
13456
+ self.result = NativeSdkUpdateTooLarge;
13457
+ [dataTask cancel];
13458
+ return;
13459
+ }
13460
+ if (self.file) {
13461
+ NSError *error = nil;
13462
+ if (![self.file writeData:data error:&error]) {
13463
+ self.result = NativeSdkUpdateDisk;
13464
+ [dataTask cancel];
13465
+ return;
13466
+ }
13467
+ } else {
13468
+ [self.data appendData:data];
13469
+ }
13470
+ self.received += data.length;
13471
+ if (self.progress) self.progress(self.context, self.received);
13472
+ }
13473
+
13474
+ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
13475
+ (void)session;
13476
+ (void)task;
13477
+ if (error && self.result == NativeSdkUpdateOk) self.result = NativeSdkUpdateNetwork;
13478
+ dispatch_semaphore_signal(self.done);
13479
+ }
13480
+
13481
+ @end
13482
+
13483
+ /// GETs `url` through the user's network settings, waiting on the calling
13484
+ /// (worker) thread and honoring `cancelled` while the transfer stalls.
13485
+ static int NativeSdkUpdateTransferRun(NSURL *url, NativeSdkUpdateTransfer *transfer, void *context, NativeSdkUpdateCancelledFn cancelled) {
13486
+ NSURLSessionConfiguration *configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration;
13487
+ configuration.timeoutIntervalForRequest = 30;
13488
+ configuration.timeoutIntervalForResource = 60 * 60;
13489
+ configuration.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
13490
+ NSOperationQueue *queue = [[NSOperationQueue alloc] init];
13491
+ queue.maxConcurrentOperationCount = 1;
13492
+ NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:transfer delegateQueue:queue];
13493
+ transfer.done = dispatch_semaphore_create(0);
13494
+ NSURLSessionDataTask *task = [session dataTaskWithURL:url];
13495
+ [task resume];
13496
+ while (dispatch_semaphore_wait(transfer.done, dispatch_time(DISPATCH_TIME_NOW, 200 * NSEC_PER_MSEC)) != 0) {
13497
+ if (cancelled && cancelled(context) && transfer.result == NativeSdkUpdateOk) {
13498
+ transfer.result = NativeSdkUpdateCancelled;
13499
+ [task cancel];
13500
+ }
13501
+ }
13502
+ [session finishTasksAndInvalidate];
13503
+ return transfer.result;
13504
+ }
13505
+
13506
+ int native_sdk_macos_updates_eligibility(void) {
13507
+ @autoreleasepool {
13508
+ NSString *bundlePath = NativeSdkUpdateBundlePath();
13509
+ if (![bundlePath.pathExtension.lowercaseString isEqualToString:@"app"]) return 1;
13510
+ // A quarantined app Gatekeeper runs from a randomized read-only
13511
+ // mount cannot replace itself until the user moves it.
13512
+ if ([bundlePath rangeOfString:@"/AppTranslocation/"].location != NSNotFound) return 2;
13513
+ if (![[NSFileManager defaultManager] isWritableFileAtPath:bundlePath.stringByDeletingLastPathComponent]) return 2;
13514
+ return 0;
13515
+ }
13516
+ }
13517
+
13518
+ size_t native_sdk_macos_updates_staged_version(const char *bundle_id, size_t bundle_id_len, char *output, size_t capacity) {
13519
+ @autoreleasepool {
13520
+ NSString *stagePath = NativeSdkUpdateSiblingPath(@"native-update");
13521
+ NSString *version = [NSString stringWithContentsOfFile:[stagePath stringByAppendingPathComponent:@".native-staged"] encoding:NSUTF8StringEncoding error:nil];
13522
+ if (version.length == 0 || !NativeSdkUpdateStagedBundle(stagePath, NativeSdkUpdateString(bundle_id, bundle_id_len), version)) return 0;
13523
+ return NativeSdkUpdateCopy(version, output, capacity);
13524
+ }
13525
+ }
13526
+
13527
+ size_t native_sdk_macos_updates_take_last(const char *bundle_id, size_t bundle_id_len, char *output, size_t capacity) {
13528
+ @autoreleasepool {
13529
+ NSString *resultPath = NativeSdkUpdateResultPath(NativeSdkUpdateString(bundle_id, bundle_id_len));
13530
+ if (!resultPath) return 0;
13531
+ NSString *record = [NSString stringWithContentsOfFile:resultPath encoding:NSUTF8StringEncoding error:nil];
13532
+ if (!record) return 0;
13533
+ [[NSFileManager defaultManager] removeItemAtPath:resultPath error:nil];
13534
+ NSString *line = [record stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
13535
+ if (![line hasPrefix:@"updated "]) return 0;
13536
+ return NativeSdkUpdateCopy([line substringFromIndex:8], output, capacity);
13537
+ }
13538
+ }
13539
+
13540
+ int native_sdk_macos_updates_fetch(const char *url, size_t url_len, int allow_loopback_http, uint8_t *output, size_t capacity, size_t *output_len) {
13541
+ @autoreleasepool {
13542
+ NSURL *target = [NSURL URLWithString:NativeSdkUpdateString(url, url_len)];
13543
+ if (!target || !NativeSdkUpdateURLAllowed(target, allow_loopback_http != 0)) return NativeSdkUpdateNetwork;
13544
+ NativeSdkUpdateTransfer *transfer = [[NativeSdkUpdateTransfer alloc] init];
13545
+ transfer.allowLoopbackHttp = allow_loopback_http != 0;
13546
+ transfer.limit = capacity;
13547
+ transfer.data = [NSMutableData data];
13548
+ const int result = NativeSdkUpdateTransferRun(target, transfer, NULL, NULL);
13549
+ if (result != NativeSdkUpdateOk) return result;
13550
+ memcpy(output, transfer.data.bytes, transfer.data.length);
13551
+ *output_len = transfer.data.length;
13552
+ return NativeSdkUpdateOk;
13553
+ }
13554
+ }
13555
+
13556
+ int native_sdk_macos_updates_download(const char *url, size_t url_len, int allow_loopback_http, uint64_t limit, char *path_output, size_t path_capacity, size_t *path_len, void *context, NativeSdkUpdateProgressFn progress, NativeSdkUpdateCancelledFn cancelled) {
13557
+ @autoreleasepool {
13558
+ NSURL *target = [NSURL URLWithString:NativeSdkUpdateString(url, url_len)];
13559
+ if (!target || !NativeSdkUpdateURLAllowed(target, allow_loopback_http != 0)) return NativeSdkUpdateNetwork;
13560
+ NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"native-update-%@.zip", NSUUID.UUID.UUIDString]];
13561
+ NSFileManager *manager = [NSFileManager defaultManager];
13562
+ if (![manager createFileAtPath:path contents:nil attributes:@{ NSFilePosixPermissions: @0600 }]) return NativeSdkUpdateDisk;
13563
+ NSFileHandle *file = [NSFileHandle fileHandleForWritingAtPath:path];
13564
+ if (!file) {
13565
+ [manager removeItemAtPath:path error:nil];
13566
+ return NativeSdkUpdateDisk;
13567
+ }
13568
+ NativeSdkUpdateTransfer *transfer = [[NativeSdkUpdateTransfer alloc] init];
13569
+ transfer.allowLoopbackHttp = allow_loopback_http != 0;
13570
+ transfer.limit = limit;
13571
+ transfer.file = file;
13572
+ transfer.context = context;
13573
+ transfer.progress = progress;
13574
+ int result = NativeSdkUpdateTransferRun(target, transfer, context, cancelled);
13575
+ if (![file closeAndReturnError:nil] && result == NativeSdkUpdateOk) result = NativeSdkUpdateDisk;
13576
+ const size_t length = NativeSdkUpdateCopy(path, path_output, path_capacity);
13577
+ if (result == NativeSdkUpdateOk && length == 0) result = NativeSdkUpdateDisk;
13578
+ if (result != NativeSdkUpdateOk) {
13579
+ [manager removeItemAtPath:path error:nil];
13580
+ return result;
13581
+ }
13582
+ *path_len = length;
13583
+ return NativeSdkUpdateOk;
13584
+ }
13585
+ }
13586
+
13587
+ int native_sdk_macos_updates_stage(const char *archive_path, size_t archive_path_len, const char *bundle_id, size_t bundle_id_len, const char *version, size_t version_len) {
13588
+ @autoreleasepool {
13589
+ NSFileManager *manager = [NSFileManager defaultManager];
13590
+ NSString *bundleIdentifier = NativeSdkUpdateString(bundle_id, bundle_id_len);
13591
+ NSString *expectedVersion = NativeSdkUpdateString(version, version_len);
13592
+ NSString *stagePath = NativeSdkUpdateSiblingPath(@"native-update");
13593
+ [manager removeItemAtPath:stagePath error:nil];
13594
+ if (![manager createDirectoryAtPath:stagePath withIntermediateDirectories:NO attributes:nil error:nil]) return 2;
13595
+ NSString *candidate = nil;
13596
+ if (NativeSdkRunTask(@"/usr/bin/ditto", @[ @"-x", @"-k", NativeSdkUpdateString(archive_path, archive_path_len), stagePath ], NULL)) {
13597
+ candidate = NativeSdkUpdateStagedBundle(stagePath, bundleIdentifier, expectedVersion);
13598
+ }
13599
+ if (!candidate || !NativeSdkUpdateSignatureMatches(candidate, bundleIdentifier)) {
13600
+ [manager removeItemAtPath:stagePath error:nil];
13601
+ return 1;
13602
+ }
13603
+ if (![expectedVersion writeToFile:[stagePath stringByAppendingPathComponent:@".native-staged"] atomically:YES encoding:NSUTF8StringEncoding error:nil]) {
13604
+ [manager removeItemAtPath:stagePath error:nil];
13605
+ return 2;
13606
+ }
13607
+ return 0;
13608
+ }
13609
+ }
13610
+
13611
+ void native_sdk_macos_updates_discard(void) {
13612
+ @autoreleasepool {
13613
+ [[NSFileManager defaultManager] removeItemAtPath:NativeSdkUpdateSiblingPath(@"native-update") error:nil];
13614
+ }
13615
+ }
13616
+
13617
+ /// Starts the helper that waits for this process to exit, replaces the app
13618
+ /// with the staged release (the old one returns if that fails), records
13619
+ /// the switch for the new version to report, and with `relaunch` opens it.
13620
+ int native_sdk_macos_updates_install(const char *bundle_id, size_t bundle_id_len, const char *from_version, size_t from_version_len, int relaunch) {
13621
+ @autoreleasepool {
13622
+ NSFileManager *manager = [NSFileManager defaultManager];
13623
+ NSString *bundleIdentifier = NativeSdkUpdateString(bundle_id, bundle_id_len);
13624
+ NSString *stagePath = NativeSdkUpdateSiblingPath(@"native-update");
13625
+ NSString *version = [NSString stringWithContentsOfFile:[stagePath stringByAppendingPathComponent:@".native-staged"] encoding:NSUTF8StringEncoding error:nil];
13626
+ NSString *candidate = version.length > 0 ? NativeSdkUpdateStagedBundle(stagePath, bundleIdentifier, version) : nil;
13627
+ if (!candidate) return 0;
13628
+ NSString *scriptPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"native-update-install-%@.sh", NSUUID.UUID.UUIDString]];
13629
+ NSString *script = @"#!/bin/sh\n"
13630
+ "set -u\n"
13631
+ "pid=\"$1\"; current=\"$2\"; candidate=\"$3\"; backup=\"$4\"; stage=\"$5\"; relaunch=\"$6\"; result=\"$7\"; from=\"$8\"; to=\"$9\"\n"
13632
+ "count=0\n"
13633
+ "while /bin/kill -0 \"$pid\" 2>/dev/null; do\n"
13634
+ " count=$((count + 1))\n"
13635
+ " if [ \"$count\" -ge 600 ]; then /bin/rm -f \"$0\"; exit 1; fi\n"
13636
+ " /bin/sleep 0.1\n"
13637
+ "done\n"
13638
+ "/bin/rm -rf \"$backup\"\n"
13639
+ "if /bin/mv \"$current\" \"$backup\"; then\n"
13640
+ " if /bin/mv \"$candidate\" \"$current\"; then\n"
13641
+ " if [ -n \"$result\" ]; then /bin/mkdir -p \"${result%/*}\" && /usr/bin/printf 'updated %s %s\\n' \"$from\" \"$to\" > \"$result\"; fi\n"
13642
+ " if [ \"$relaunch\" != 1 ] || /usr/bin/open \"$current\"; then\n"
13643
+ " /bin/rm -rf \"$backup\" \"$stage\"\n"
13644
+ " /bin/rm -f \"$0\"\n"
13645
+ " exit 0\n"
13646
+ " fi\n"
13647
+ " if [ -n \"$result\" ]; then /bin/rm -f \"$result\"; fi\n"
13648
+ " /bin/rm -rf \"$current\"\n"
13649
+ " fi\n"
13650
+ " /bin/mv \"$backup\" \"$current\"\n"
13651
+ "fi\n"
13652
+ "/bin/rm -rf \"$stage\"\n"
13653
+ "if [ \"$relaunch\" = 1 ]; then /usr/bin/open \"$current\" >/dev/null 2>&1 || true; fi\n"
13654
+ "/bin/rm -f \"$0\"\n"
13655
+ "exit 1\n";
13656
+ if (![script writeToFile:scriptPath atomically:YES encoding:NSUTF8StringEncoding error:nil] ||
13657
+ ![manager setAttributes:@{ NSFilePosixPermissions: @0700 } ofItemAtPath:scriptPath error:nil]) {
13658
+ [manager removeItemAtPath:scriptPath error:nil];
13659
+ return 0;
13660
+ }
13661
+ NSTask *installer = [[NSTask alloc] init];
13662
+ installer.executableURL = [NSURL fileURLWithPath:@"/bin/sh"];
13663
+ installer.arguments = @[
13664
+ scriptPath,
13665
+ [NSString stringWithFormat:@"%d", getpid()],
13666
+ NativeSdkUpdateBundlePath(),
13667
+ candidate,
13668
+ NativeSdkUpdateSiblingPath(@"native-update-backup"),
13669
+ stagePath,
13670
+ relaunch ? @"1" : @"0",
13671
+ NativeSdkUpdateResultPath(bundleIdentifier) ?: @"",
13672
+ NativeSdkUpdateString(from_version, from_version_len),
13673
+ version,
13674
+ ];
13675
+ if (![installer launchAndReturnError:nil]) {
13676
+ [manager removeItemAtPath:scriptPath error:nil];
13677
+ return 0;
13678
+ }
13679
+ return 1;
13680
+ }
13681
+ }
13682
+
13274
13683
  void native_sdk_appkit_run(native_sdk_appkit_host_t *host, native_sdk_appkit_event_callback_t callback, void *context) {
13275
13684
  NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host;
13276
13685
  [object runWithCallback:callback context:context];
@@ -13992,23 +14401,71 @@ static void NativeSdkConfigurePanelExtensions(NSSavePanel *panel, NSArray<NSStri
13992
14401
  }
13993
14402
  }
13994
14403
 
14404
+ static NSString *NativeSdkDialogString(const char *bytes, size_t len) {
14405
+ if (!bytes || len == 0) return nil;
14406
+ return [[NSString alloc] initWithBytes:bytes length:len encoding:NSUTF8StringEncoding];
14407
+ }
14408
+
14409
+ static NSOpenPanel *NativeSdkMakeOpenPanel(const native_sdk_appkit_open_dialog_opts_t *opts) {
14410
+ NSOpenPanel *panel = [NSOpenPanel openPanel];
14411
+ NSString *title = NativeSdkDialogString(opts->title, opts->title_len);
14412
+ if (title) panel.title = title;
14413
+ NSString *path = NativeSdkDialogString(opts->default_path, opts->default_path_len);
14414
+ if (path) panel.directoryURL = [NSURL fileURLWithPath:path];
14415
+ panel.canChooseFiles = opts->allow_directories == 0;
14416
+ panel.canChooseDirectories = opts->allow_directories != 0;
14417
+ panel.allowsMultipleSelection = opts->allow_multiple != 0;
14418
+ NativeSdkConfigurePanelExtensions(panel, NativeSdkParseExtensions(opts->extensions, opts->extensions_len));
14419
+ return panel;
14420
+ }
14421
+
14422
+ static NSSavePanel *NativeSdkMakeSavePanel(const native_sdk_appkit_save_dialog_opts_t *opts) {
14423
+ NSSavePanel *panel = [NSSavePanel savePanel];
14424
+ NSString *title = NativeSdkDialogString(opts->title, opts->title_len);
14425
+ if (title) panel.title = title;
14426
+ NSString *path = NativeSdkDialogString(opts->default_path, opts->default_path_len);
14427
+ if (path) panel.directoryURL = [NSURL fileURLWithPath:path];
14428
+ NSString *name = NativeSdkDialogString(opts->default_name, opts->default_name_len);
14429
+ if (name) panel.nameFieldStringValue = name;
14430
+ NativeSdkConfigurePanelExtensions(panel, NativeSdkParseExtensions(opts->extensions, opts->extensions_len));
14431
+ return panel;
14432
+ }
14433
+
14434
+ static NSAlert *NativeSdkMakeAlert(const native_sdk_appkit_message_dialog_opts_t *opts) {
14435
+ NSAlert *alert = [[NSAlert alloc] init];
14436
+ switch (opts->style) {
14437
+ case 1: alert.alertStyle = NSAlertStyleWarning; break;
14438
+ case 2: alert.alertStyle = NSAlertStyleCritical; break;
14439
+ default: alert.alertStyle = NSAlertStyleInformational; break;
14440
+ }
14441
+ NSString *title = NativeSdkDialogString(opts->title, opts->title_len);
14442
+ NSString *message = NativeSdkDialogString(opts->message, opts->message_len);
14443
+ NSString *informative = NativeSdkDialogString(opts->informative_text, opts->informative_text_len);
14444
+ if (message.length > 0) {
14445
+ alert.messageText = message;
14446
+ } else if (title.length > 0) {
14447
+ alert.messageText = title;
14448
+ }
14449
+ if (informative.length > 0) {
14450
+ alert.informativeText = informative;
14451
+ }
14452
+ if (opts->message && opts->message_len > 0) {
14453
+ alert.window.title = title.length > 0 ? title : @"";
14454
+ }
14455
+ NSString *primary = NativeSdkDialogString(opts->primary_button, opts->primary_button_len);
14456
+ [alert addButtonWithTitle:primary ?: @"OK"];
14457
+ NSString *secondary = NativeSdkDialogString(opts->secondary_button, opts->secondary_button_len);
14458
+ if (secondary) [alert addButtonWithTitle:secondary];
14459
+ NSString *tertiary = NativeSdkDialogString(opts->tertiary_button, opts->tertiary_button_len);
14460
+ if (tertiary) [alert addButtonWithTitle:tertiary];
14461
+ return alert;
14462
+ }
14463
+
13995
14464
  native_sdk_appkit_open_dialog_result_t native_sdk_appkit_show_open_dialog(native_sdk_appkit_host_t *host, const native_sdk_appkit_open_dialog_opts_t *opts, char *buffer, size_t buffer_len) {
13996
14465
  (void)host;
13997
14466
  native_sdk_appkit_open_dialog_result_t result = { .count = 0, .bytes_written = 0 };
13998
14467
  @autoreleasepool {
13999
- NSOpenPanel *panel = [NSOpenPanel openPanel];
14000
- if (opts->title && opts->title_len > 0) {
14001
- panel.title = [[NSString alloc] initWithBytes:opts->title length:opts->title_len encoding:NSUTF8StringEncoding];
14002
- }
14003
- if (opts->default_path && opts->default_path_len > 0) {
14004
- NSString *path = [[NSString alloc] initWithBytes:opts->default_path length:opts->default_path_len encoding:NSUTF8StringEncoding];
14005
- panel.directoryURL = [NSURL fileURLWithPath:path];
14006
- }
14007
- panel.canChooseFiles = opts->allow_directories == 0;
14008
- panel.canChooseDirectories = opts->allow_directories != 0;
14009
- panel.allowsMultipleSelection = opts->allow_multiple != 0;
14010
- NativeSdkConfigurePanelExtensions(panel, NativeSdkParseExtensions(opts->extensions, opts->extensions_len));
14011
-
14468
+ NSOpenPanel *panel = NativeSdkMakeOpenPanel(opts);
14012
14469
  if ([panel runModal] != NSModalResponseOK) return result;
14013
14470
 
14014
14471
  size_t offset = 0;
@@ -14035,19 +14492,7 @@ native_sdk_appkit_open_dialog_result_t native_sdk_appkit_show_open_dialog(native
14035
14492
  size_t native_sdk_appkit_show_save_dialog(native_sdk_appkit_host_t *host, const native_sdk_appkit_save_dialog_opts_t *opts, char *buffer, size_t buffer_len) {
14036
14493
  (void)host;
14037
14494
  @autoreleasepool {
14038
- NSSavePanel *panel = [NSSavePanel savePanel];
14039
- if (opts->title && opts->title_len > 0) {
14040
- panel.title = [[NSString alloc] initWithBytes:opts->title length:opts->title_len encoding:NSUTF8StringEncoding];
14041
- }
14042
- if (opts->default_path && opts->default_path_len > 0) {
14043
- NSString *path = [[NSString alloc] initWithBytes:opts->default_path length:opts->default_path_len encoding:NSUTF8StringEncoding];
14044
- panel.directoryURL = [NSURL fileURLWithPath:path];
14045
- }
14046
- if (opts->default_name && opts->default_name_len > 0) {
14047
- panel.nameFieldStringValue = [[NSString alloc] initWithBytes:opts->default_name length:opts->default_name_len encoding:NSUTF8StringEncoding];
14048
- }
14049
- NativeSdkConfigurePanelExtensions(panel, NativeSdkParseExtensions(opts->extensions, opts->extensions_len));
14050
-
14495
+ NSSavePanel *panel = NativeSdkMakeSavePanel(opts);
14051
14496
  if ([panel runModal] != NSModalResponseOK) return 0;
14052
14497
 
14053
14498
  NSString *path = panel.URL.path;
@@ -14063,45 +14508,200 @@ size_t native_sdk_appkit_show_save_dialog(native_sdk_appkit_host_t *host, const
14063
14508
  int native_sdk_appkit_show_message_dialog(native_sdk_appkit_host_t *host, const native_sdk_appkit_message_dialog_opts_t *opts) {
14064
14509
  (void)host;
14065
14510
  @autoreleasepool {
14066
- NSAlert *alert = [[NSAlert alloc] init];
14067
- switch (opts->style) {
14068
- case 1: alert.alertStyle = NSAlertStyleWarning; break;
14069
- case 2: alert.alertStyle = NSAlertStyleCritical; break;
14070
- default: alert.alertStyle = NSAlertStyleInformational; break;
14071
- }
14072
- NSString *title = opts->title && opts->title_len > 0 ? [[NSString alloc] initWithBytes:opts->title length:opts->title_len encoding:NSUTF8StringEncoding] : nil;
14073
- NSString *message = opts->message && opts->message_len > 0 ? [[NSString alloc] initWithBytes:opts->message length:opts->message_len encoding:NSUTF8StringEncoding] : nil;
14074
- NSString *informative = opts->informative_text && opts->informative_text_len > 0 ? [[NSString alloc] initWithBytes:opts->informative_text length:opts->informative_text_len encoding:NSUTF8StringEncoding] : nil;
14075
- if (message.length > 0) {
14076
- alert.messageText = message;
14077
- } else if (title.length > 0) {
14078
- alert.messageText = title;
14079
- }
14080
- if (informative.length > 0) {
14081
- alert.informativeText = informative;
14082
- }
14083
- if (opts->message && opts->message_len > 0) {
14084
- alert.window.title = title.length > 0 ? title : @"";
14085
- }
14086
- if (opts->primary_button && opts->primary_button_len > 0) {
14087
- [alert addButtonWithTitle:[[NSString alloc] initWithBytes:opts->primary_button length:opts->primary_button_len encoding:NSUTF8StringEncoding]];
14511
+ NSAlert *alert = NativeSdkMakeAlert(opts);
14512
+ NSModalResponse response = [alert runModal];
14513
+ if (response == NSAlertFirstButtonReturn) return 0;
14514
+ if (response == NSAlertSecondButtonReturn) return 1;
14515
+ return 2;
14516
+ }
14517
+ }
14518
+
14519
+ /* Record a dialog's result and wake the loop; the runtime collects it with
14520
+ * poll. Never runs app code: AppKit calls completion handlers inside its
14521
+ * own transaction (electron#26884), so the result waits for the wake. */
14522
+ static void NativeSdkFinishDialog(NativeSdkAppKitHost *host, NativeSdkDialog *dialog, NSModalResponse response) {
14523
+ if (!host) return;
14524
+ NSNumber *key = @(dialog.dialogId);
14525
+ if (host.dialogs[key] != dialog) return;
14526
+ [host.dialogs removeObjectForKey:key];
14527
+ dialog.outcome = NATIVE_SDK_APPKIT_DIALOG_CANCELLED;
14528
+ if (dialog.alert) {
14529
+ if (response >= NSAlertFirstButtonReturn && response <= NSAlertThirdButtonReturn) {
14530
+ dialog.outcome = NATIVE_SDK_APPKIT_DIALOG_SELECTED;
14531
+ dialog.button = (int)(response - NSAlertFirstButtonReturn);
14532
+ }
14533
+ } else if (response == NSModalResponseOK) {
14534
+ NSMutableArray<NSString *> *paths = [NSMutableArray array];
14535
+ if ([dialog.panel isKindOfClass:[NSOpenPanel class]]) {
14536
+ for (NSURL *url in ((NSOpenPanel *)dialog.panel).URLs) {
14537
+ if (url.path) [paths addObject:url.path];
14538
+ }
14539
+ } else if (dialog.panel.URL.path) {
14540
+ [paths addObject:dialog.panel.URL.path];
14541
+ }
14542
+ dialog.paths = paths;
14543
+ if (paths.count > 0) dialog.outcome = NATIVE_SDK_APPKIT_DIALOG_SELECTED;
14544
+ }
14545
+ dialog.panel = nil;
14546
+ dialog.alert = nil;
14547
+ if (host.didShutdown) return;
14548
+ [host.finishedDialogs addObject:dialog];
14549
+ [host wakeFromAnyThread];
14550
+ }
14551
+
14552
+ static void NativeSdkPresentDialog(NativeSdkAppKitHost *host, NativeSdkDialog *dialog, NSWindow *parent) {
14553
+ __weak NativeSdkAppKitHost *weakHost = host;
14554
+ if (dialog.panel) {
14555
+ void (^done)(NSModalResponse) = ^(NSModalResponse response) {
14556
+ NativeSdkFinishDialog(weakHost, dialog, response);
14557
+ };
14558
+ if (parent) {
14559
+ dialog.sheetParent = parent;
14560
+ [dialog.panel beginSheetModalForWindow:parent completionHandler:done];
14088
14561
  } else {
14089
- [alert addButtonWithTitle:@"OK"];
14562
+ [dialog.panel beginWithCompletionHandler:done];
14090
14563
  }
14091
- if (opts->secondary_button && opts->secondary_button_len > 0) {
14092
- [alert addButtonWithTitle:[[NSString alloc] initWithBytes:opts->secondary_button length:opts->secondary_button_len encoding:NSUTF8StringEncoding]];
14564
+ return;
14565
+ }
14566
+ if (parent) {
14567
+ dialog.sheetParent = parent;
14568
+ [dialog.alert beginSheetModalForWindow:parent completionHandler:^(NSModalResponse response) {
14569
+ NativeSdkFinishDialog(weakHost, dialog, response);
14570
+ }];
14571
+ return;
14572
+ }
14573
+ // NSAlert has no modeless form. With no window to attach a sheet to,
14574
+ // run it modally from a fresh loop turn, outside any runtime
14575
+ // dispatch, so events keep reaching the app meanwhile (Electron runs
14576
+ // a parentless message box modally too). A run loop block, not
14577
+ // dispatch_async: a modal loop inside a main-queue block keeps the
14578
+ // serial main queue from draining, and wakes and quits ride it.
14579
+ CFRunLoopRef mainLoop = CFRunLoopGetMain();
14580
+ CFRunLoopPerformBlock(mainLoop, kCFRunLoopCommonModes, ^{
14581
+ NativeSdkAppKitHost *strongHost = weakHost;
14582
+ if (!strongHost || dialog.cancelRequested || strongHost.dialogs[@(dialog.dialogId)] != dialog) {
14583
+ NativeSdkFinishDialog(strongHost, dialog, NSModalResponseCancel);
14584
+ return;
14093
14585
  }
14094
- if (opts->tertiary_button && opts->tertiary_button_len > 0) {
14095
- [alert addButtonWithTitle:[[NSString alloc] initWithBytes:opts->tertiary_button length:opts->tertiary_button_len encoding:NSUTF8StringEncoding]];
14586
+ dialog.modalRunning = YES;
14587
+ const NSModalResponse response = [dialog.alert runModal];
14588
+ dialog.modalRunning = NO;
14589
+ NativeSdkFinishDialog(strongHost, dialog, response);
14590
+ // A stop during the alert ended only the modal loop; [NSApp stop:]
14591
+ // inside a modal session never reaches the main one.
14592
+ if (strongHost.didShutdown) {
14593
+ [NSApp stop:nil];
14594
+ NativeSdkPostEmptyEvent();
14096
14595
  }
14596
+ });
14597
+ CFRunLoopWakeUp(mainLoop);
14598
+ }
14097
14599
 
14098
- NSModalResponse response = [alert runModal];
14099
- if (response == NSAlertFirstButtonReturn) return 0;
14100
- if (response == NSAlertSecondButtonReturn) return 1;
14101
- return 2;
14600
+ static void NativeSdkCancelDialog(NativeSdkDialog *dialog) {
14601
+ NSWindow *sheet = dialog.panel ?: dialog.alert.window;
14602
+ if (dialog.sheetParent && sheet) {
14603
+ // Ending the sheet on its window detaches it at once, so a close
14604
+ // right after is not refused for a sheet still attached.
14605
+ [dialog.sheetParent endSheet:sheet returnCode:NSModalResponseCancel];
14606
+ return;
14607
+ }
14608
+ if (dialog.panel) {
14609
+ [dialog.panel cancel:nil];
14610
+ return;
14611
+ }
14612
+ if (!dialog.alert) return;
14613
+ if (dialog.modalRunning) {
14614
+ [NSApp stopModalWithCode:NSModalResponseCancel];
14615
+ NativeSdkPostEmptyEvent();
14616
+ } else {
14617
+ // Not started yet: the deferred presentation finishes it instead.
14618
+ dialog.cancelRequested = YES;
14619
+ }
14620
+ }
14621
+
14622
+ static void NativeSdkCancelDialogsForWindow(NativeSdkAppKitHost *host, uint64_t windowId) {
14623
+ for (NativeSdkDialog *dialog in [host.dialogs.allValues copy]) {
14624
+ if (dialog.parentWindowId == windowId) NativeSdkCancelDialog(dialog);
14102
14625
  }
14103
14626
  }
14104
14627
 
14628
+ static void NativeSdkCancelAllDialogs(NativeSdkAppKitHost *host) {
14629
+ for (NativeSdkDialog *dialog in [host.dialogs.allValues copy]) NativeSdkCancelDialog(dialog);
14630
+ [host.finishedDialogs removeAllObjects];
14631
+ }
14632
+
14633
+ int native_sdk_appkit_request_dialog(native_sdk_appkit_host_t *host, const native_sdk_appkit_dialog_request_t *request) {
14634
+ NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host;
14635
+ if (!object || !request || object.didShutdown || request->id == 0) return 0;
14636
+ @autoreleasepool {
14637
+ NativeSdkDialog *dialog = [[NativeSdkDialog alloc] init];
14638
+ dialog.dialogId = request->id;
14639
+ dialog.parentWindowId = request->parent_window_id;
14640
+ switch (request->kind) {
14641
+ case NATIVE_SDK_APPKIT_DIALOG_OPEN:
14642
+ if (!request->open_opts) return 0;
14643
+ dialog.panel = NativeSdkMakeOpenPanel(request->open_opts);
14644
+ break;
14645
+ case NATIVE_SDK_APPKIT_DIALOG_SAVE:
14646
+ if (!request->save_opts) return 0;
14647
+ dialog.panel = NativeSdkMakeSavePanel(request->save_opts);
14648
+ break;
14649
+ case NATIVE_SDK_APPKIT_DIALOG_MESSAGE:
14650
+ if (!request->message_opts) return 0;
14651
+ dialog.alert = NativeSdkMakeAlert(request->message_opts);
14652
+ break;
14653
+ default:
14654
+ return 0;
14655
+ }
14656
+ // A parent that cannot host a sheet right now (hidden, minimized,
14657
+ // already showing one) gets a modeless panel instead, as Electron
14658
+ // does for invisible parents (electron#22671).
14659
+ NSWindow *parent = request->parent_window_id != 0 ? object.windows[@(request->parent_window_id)] : nil;
14660
+ if (parent && (!parent.isVisible || parent.isMiniaturized || parent.attachedSheet)) parent = nil;
14661
+ if (!parent) dialog.parentWindowId = 0;
14662
+ object.dialogs[@(dialog.dialogId)] = dialog;
14663
+ NativeSdkPresentDialog(object, dialog, parent);
14664
+ return 1;
14665
+ }
14666
+ }
14667
+
14668
+ void native_sdk_appkit_cancel_dialog(native_sdk_appkit_host_t *host, uint64_t id) {
14669
+ NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host;
14670
+ if (!object) return;
14671
+ NativeSdkDialog *dialog = object.dialogs[@(id)];
14672
+ if (dialog) NativeSdkCancelDialog(dialog);
14673
+ }
14674
+
14675
+ int native_sdk_appkit_poll_dialog_result(native_sdk_appkit_host_t *host, char *buffer, size_t buffer_len, native_sdk_appkit_dialog_result_t *result) {
14676
+ NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host;
14677
+ if (!object || !result || object.finishedDialogs.count == 0) return 0;
14678
+ NativeSdkDialog *dialog = object.finishedDialogs.firstObject;
14679
+ [object.finishedDialogs removeObjectAtIndex:0];
14680
+ *result = (native_sdk_appkit_dialog_result_t){ .id = dialog.dialogId, .outcome = dialog.outcome, .button = dialog.button };
14681
+ size_t offset = 0;
14682
+ size_t count = 0;
14683
+ for (NSString *path in dialog.paths) {
14684
+ NSData *data = [path dataUsingEncoding:NSUTF8StringEncoding];
14685
+ if (!data) continue;
14686
+ const size_t needed = data.length + (count > 0 ? 1 : 0);
14687
+ if (!buffer || needed > buffer_len - offset) {
14688
+ // More paths than the runtime can hold: report failure rather
14689
+ // than a silently shortened selection.
14690
+ result->outcome = NATIVE_SDK_APPKIT_DIALOG_FAILED;
14691
+ offset = 0;
14692
+ count = 0;
14693
+ break;
14694
+ }
14695
+ if (count > 0) buffer[offset++] = '\n';
14696
+ memcpy(buffer + offset, data.bytes, data.length);
14697
+ offset += data.length;
14698
+ count++;
14699
+ }
14700
+ result->count = count;
14701
+ result->bytes_written = offset;
14702
+ return 1;
14703
+ }
14704
+
14105
14705
  static NSImage *NativeSdkTrayImageWithOpacity(NSImage *source, double opacity) {
14106
14706
  if (!source) return nil;
14107
14707
  const CGFloat fraction = MAX(0.0, MIN(1.0, opacity));