@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
@@ -396,6 +396,26 @@ typedef struct native_sdk_gtk_notification_action {
396
396
 
397
397
  static const char native_sdk_notification_action_name[] = "native-sdk-notification-action";
398
398
 
399
+ /* One asynchronous dialog (native_sdk_gtk_request_dialog), from request
400
+ * until poll takes its result. GTK holds it as the finish callback's
401
+ * data; once native_sdk_gtk_destroy detaches it (host NULL) the callback
402
+ * only frees it. */
403
+ typedef struct native_sdk_gtk_dialog {
404
+ struct native_sdk_gtk_dialog *next;
405
+ native_sdk_gtk_host_t *host;
406
+ uint64_t id;
407
+ uint64_t parent_window_id;
408
+ int kind;
409
+ int folders;
410
+ GObject *dialog;
411
+ GCancellable *cancellable;
412
+ int finished;
413
+ uint64_t finish_serial;
414
+ int outcome;
415
+ int button;
416
+ GPtrArray *paths;
417
+ } native_sdk_gtk_dialog_t;
418
+
399
419
  struct native_sdk_gtk_host {
400
420
  GtkApplication *app;
401
421
  /* Display providers are global and the display retains them until
@@ -476,6 +496,14 @@ struct native_sdk_gtk_host {
476
496
  GHashTable *notification_actions;
477
497
  GHashTable *notification_action_tokens_by_id;
478
498
  native_sdk_gtk_audio_t audio;
499
+ /* Asynchronous dialogs not yet polled, newest first; finished ones
500
+ * are handed back in finish order. */
501
+ native_sdk_gtk_dialog_t *dialogs;
502
+ uint64_t dialog_finish_serial;
503
+ /* The queued wake that hands finished dialogs to the runtime, tracked
504
+ * like stop_idle_source: finishes coalesce into it, and destroy
505
+ * removes it so it never fires into a freed host. */
506
+ guint dialog_wake_source;
479
507
  };
480
508
 
481
509
  static void native_sdk_emit(native_sdk_gtk_host_t *host, native_sdk_gtk_event_t event);
@@ -483,6 +511,8 @@ static void native_sdk_context_menu_free(native_sdk_gtk_context_menu_t *menu);
483
511
  static gboolean native_sdk_nudge_chrome_requery(gpointer data);
484
512
  static gboolean native_sdk_on_file_drop(GtkDropTarget *target, const GValue *value, double x, double y, gpointer data);
485
513
  static GtkWindow *native_sdk_parent_window(native_sdk_gtk_host_t *host);
514
+ static void native_sdk_cancel_dialogs_for_window(native_sdk_gtk_host_t *host, uint64_t window_id);
515
+ static void native_sdk_detach_dialogs(native_sdk_gtk_host_t *host);
486
516
  static const char *native_sdk_shortcut_key_for_keyval(guint keyval, char *buffer, size_t buffer_len, int *uses_implicit_shift);
487
517
  static void native_sdk_audio_release(native_sdk_gtk_host_t *host, int cancel_download);
488
518
 
@@ -2781,6 +2811,7 @@ static gboolean on_close_request(GtkWindow *window, gpointer data) {
2781
2811
  (void)window;
2782
2812
  native_sdk_gtk_window_t *win = data;
2783
2813
  native_sdk_gtk_host_t *host = win->host;
2814
+ native_sdk_cancel_dialogs_for_window(host, win->id);
2784
2815
  int closed_index = -1;
2785
2816
  for (int i = 0; i < host->window_count; i++) {
2786
2817
  if (&host->windows[i] == win) {
@@ -3479,6 +3510,8 @@ void native_sdk_gtk_destroy(native_sdk_gtk_host_t *host) {
3479
3510
  /* Retire the audio pipeline (and cancel its cache download) before
3480
3511
  * anything else goes away; everything runs on this thread. */
3481
3512
  native_sdk_audio_release(host, 1);
3513
+ native_sdk_detach_dialogs(host);
3514
+ if (host->dialog_wake_source) g_source_remove(host->dialog_wake_source);
3482
3515
  if (host->frame_timer) g_source_remove(host->frame_timer);
3483
3516
  /* A stop turn still queued at teardown (the loop quit before the
3484
3517
  * idle ran) must not fire into freed memory. */
@@ -6642,6 +6675,53 @@ static char *native_sdk_bytes_to_string(const char *bytes, size_t len) {
6642
6675
  return bytes && len > 0 ? native_sdk_strndup(bytes, len) : NULL;
6643
6676
  }
6644
6677
 
6678
+ /* The dialog builders hand back plain GObjects, keeping the GTK 4.10
6679
+ * dialog types out of file-scope signatures: against an older GTK this
6680
+ * file then fails only with the GTK-age diagnostics the GLib 2.72 receipt
6681
+ * allows (.github/scripts/glib272_error_allowlist.py). */
6682
+ static GObject *native_sdk_new_open_dialog(const native_sdk_gtk_open_dialog_opts_t *opts) {
6683
+ GtkFileDialog *dialog = gtk_file_dialog_new();
6684
+ char *title = native_sdk_bytes_to_string(opts->title, opts->title_len);
6685
+ if (title) gtk_file_dialog_set_title(dialog, title);
6686
+ free(title);
6687
+ return G_OBJECT(dialog);
6688
+ }
6689
+
6690
+ static GObject *native_sdk_new_save_dialog(const native_sdk_gtk_save_dialog_opts_t *opts) {
6691
+ GtkFileDialog *dialog = gtk_file_dialog_new();
6692
+ char *title = native_sdk_bytes_to_string(opts->title, opts->title_len);
6693
+ char *default_name = native_sdk_bytes_to_string(opts->default_name, opts->default_name_len);
6694
+ if (title) gtk_file_dialog_set_title(dialog, title);
6695
+ if (default_name) gtk_file_dialog_set_initial_name(dialog, default_name);
6696
+ free(title);
6697
+ free(default_name);
6698
+ return G_OBJECT(dialog);
6699
+ }
6700
+
6701
+ static GObject *native_sdk_new_alert_dialog(const native_sdk_gtk_message_dialog_opts_t *opts) {
6702
+ /* The constructor requires a non-NULL printf-style format string. */
6703
+ GtkAlertDialog *dialog = gtk_alert_dialog_new("");
6704
+ char *title = native_sdk_bytes_to_string(opts->title, opts->title_len);
6705
+ char *message = native_sdk_bytes_to_string(opts->message, opts->message_len);
6706
+ char *informative = native_sdk_bytes_to_string(opts->informative_text, opts->informative_text_len);
6707
+ char *primary = native_sdk_bytes_to_string(opts->primary_button, opts->primary_button_len);
6708
+ char *secondary = native_sdk_bytes_to_string(opts->secondary_button, opts->secondary_button_len);
6709
+ char *tertiary = native_sdk_bytes_to_string(opts->tertiary_button, opts->tertiary_button_len);
6710
+ gtk_alert_dialog_set_message(dialog, title ? title : (message ? message : ""));
6711
+ if (informative || (title && message)) gtk_alert_dialog_set_detail(dialog, informative ? informative : message);
6712
+ const char *buttons[4] = { primary ? primary : "OK", NULL, NULL, NULL };
6713
+ if (secondary) buttons[1] = secondary;
6714
+ if (tertiary) buttons[2] = tertiary;
6715
+ gtk_alert_dialog_set_buttons(dialog, buttons);
6716
+ free(title);
6717
+ free(message);
6718
+ free(informative);
6719
+ free(primary);
6720
+ free(secondary);
6721
+ free(tertiary);
6722
+ return G_OBJECT(dialog);
6723
+ }
6724
+
6645
6725
  static void native_sdk_open_dialog_done(GObject *source, GAsyncResult *result, gpointer data) {
6646
6726
  native_sdk_file_dialog_state_t *state = data;
6647
6727
  GError *error = NULL;
@@ -6668,13 +6748,10 @@ static void native_sdk_save_dialog_done(GObject *source, GAsyncResult *result, g
6668
6748
 
6669
6749
  native_sdk_gtk_open_dialog_result_t native_sdk_gtk_show_open_dialog(native_sdk_gtk_host_t *host, const native_sdk_gtk_open_dialog_opts_t *opts, char *buffer, size_t buffer_len) {
6670
6750
  native_sdk_gtk_open_dialog_result_t result = {0};
6671
- GtkFileDialog *dialog = gtk_file_dialog_new();
6672
- char *title = native_sdk_bytes_to_string(opts->title, opts->title_len);
6673
- if (title) gtk_file_dialog_set_title(dialog, title);
6751
+ GtkFileDialog *dialog = GTK_FILE_DIALOG(native_sdk_new_open_dialog(opts));
6674
6752
  GtkWindow *parent = native_sdk_parent_window(host);
6675
6753
  native_sdk_file_dialog_state_t state = { .loop = g_main_loop_new(NULL, FALSE) };
6676
6754
  if (!state.loop) {
6677
- if (title) free(title);
6678
6755
  g_object_unref(dialog);
6679
6756
  return result;
6680
6757
  }
@@ -6711,22 +6788,15 @@ native_sdk_gtk_open_dialog_result_t native_sdk_gtk_show_open_dialog(native_sdk_g
6711
6788
  result.bytes_written = overflow ? native_sdk_overflow_size(buffer_len) : offset;
6712
6789
  g_object_unref(state.files);
6713
6790
  }
6714
- if (title) free(title);
6715
6791
  g_object_unref(dialog);
6716
6792
  return result;
6717
6793
  }
6718
6794
 
6719
6795
  size_t native_sdk_gtk_show_save_dialog(native_sdk_gtk_host_t *host, const native_sdk_gtk_save_dialog_opts_t *opts, char *buffer, size_t buffer_len) {
6720
- GtkFileDialog *dialog = gtk_file_dialog_new();
6721
- char *title = native_sdk_bytes_to_string(opts->title, opts->title_len);
6722
- char *default_name = native_sdk_bytes_to_string(opts->default_name, opts->default_name_len);
6723
- if (title) gtk_file_dialog_set_title(dialog, title);
6724
- if (default_name) gtk_file_dialog_set_initial_name(dialog, default_name);
6796
+ GtkFileDialog *dialog = GTK_FILE_DIALOG(native_sdk_new_save_dialog(opts));
6725
6797
  GtkWindow *parent = native_sdk_parent_window(host);
6726
6798
  native_sdk_file_dialog_state_t state = { .loop = g_main_loop_new(NULL, FALSE) };
6727
6799
  if (!state.loop) {
6728
- if (title) free(title);
6729
- if (default_name) free(default_name);
6730
6800
  g_object_unref(dialog);
6731
6801
  return 0;
6732
6802
  }
@@ -6748,8 +6818,6 @@ size_t native_sdk_gtk_show_save_dialog(native_sdk_gtk_host_t *host, const native
6748
6818
  }
6749
6819
  g_object_unref(state.file);
6750
6820
  }
6751
- if (title) free(title);
6752
- if (default_name) free(default_name);
6753
6821
  g_object_unref(dialog);
6754
6822
  return written;
6755
6823
  }
@@ -6771,34 +6839,249 @@ static void native_sdk_alert_done(GObject *source, GAsyncResult *result, gpointe
6771
6839
  }
6772
6840
 
6773
6841
  int native_sdk_gtk_show_message_dialog(native_sdk_gtk_host_t *host, const native_sdk_gtk_message_dialog_opts_t *opts) {
6774
- /* The constructor requires a non-NULL printf-style format string. */
6775
- GtkAlertDialog *dialog = gtk_alert_dialog_new("");
6776
- char *title = native_sdk_bytes_to_string(opts->title, opts->title_len);
6777
- char *message = native_sdk_bytes_to_string(opts->message, opts->message_len);
6778
- char *informative = native_sdk_bytes_to_string(opts->informative_text, opts->informative_text_len);
6779
- char *primary = native_sdk_bytes_to_string(opts->primary_button, opts->primary_button_len);
6780
- char *secondary = native_sdk_bytes_to_string(opts->secondary_button, opts->secondary_button_len);
6781
- char *tertiary = native_sdk_bytes_to_string(opts->tertiary_button, opts->tertiary_button_len);
6782
- gtk_alert_dialog_set_message(dialog, title ? title : (message ? message : ""));
6783
- if (informative || (title && message)) gtk_alert_dialog_set_detail(dialog, informative ? informative : message);
6784
- const char *buttons[4] = { primary ? primary : "OK", NULL, NULL, NULL };
6785
- if (secondary) buttons[1] = secondary;
6786
- if (tertiary) buttons[2] = tertiary;
6787
- gtk_alert_dialog_set_buttons(dialog, buttons);
6842
+ GtkAlertDialog *dialog = GTK_ALERT_DIALOG(native_sdk_new_alert_dialog(opts));
6788
6843
  native_sdk_alert_state_t state = { .loop = g_main_loop_new(NULL, FALSE), .response = 0 };
6789
6844
  if (state.loop) {
6790
6845
  gtk_alert_dialog_choose(dialog, native_sdk_parent_window(host), NULL, native_sdk_alert_done, &state);
6791
6846
  g_main_loop_run(state.loop);
6792
6847
  g_main_loop_unref(state.loop);
6793
6848
  }
6794
- if (title) free(title);
6795
- if (message) free(message);
6796
- if (informative) free(informative);
6797
- if (primary) free(primary);
6798
- if (secondary) free(secondary);
6799
- if (tertiary) free(tertiary);
6800
6849
  g_object_unref(dialog);
6801
6850
  if (state.response <= 0) return 0;
6802
6851
  if (state.response == 1) return 1;
6803
6852
  return 2;
6804
6853
  }
6854
+
6855
+ /* GTK's dialogs are asynchronous already; the blocking calls above only
6856
+ * wrap them in a nested loop. The asynchronous ones drop that loop: the
6857
+ * finish callback records the result and schedules a wake, and the
6858
+ * runtime collects it with poll. The callback never emits, because GTask
6859
+ * may run it inside the dispatch that cancelled the dialog. */
6860
+
6861
+ static void native_sdk_dialog_free(native_sdk_gtk_dialog_t *entry) {
6862
+ if (entry->dialog) g_object_unref(entry->dialog);
6863
+ if (entry->cancellable) g_object_unref(entry->cancellable);
6864
+ if (entry->paths) g_ptr_array_unref(entry->paths);
6865
+ free(entry);
6866
+ }
6867
+
6868
+ static void native_sdk_dialog_unlink(native_sdk_gtk_host_t *host, native_sdk_gtk_dialog_t *entry) {
6869
+ for (native_sdk_gtk_dialog_t **link = &host->dialogs; *link; link = &(*link)->next) {
6870
+ if (*link == entry) {
6871
+ *link = entry->next;
6872
+ entry->next = NULL;
6873
+ return;
6874
+ }
6875
+ }
6876
+ }
6877
+
6878
+ static native_sdk_gtk_dialog_t *native_sdk_dialog_find(native_sdk_gtk_host_t *host, uint64_t id) {
6879
+ for (native_sdk_gtk_dialog_t *entry = host->dialogs; entry; entry = entry->next) {
6880
+ if (entry->id == id) return entry;
6881
+ }
6882
+ return NULL;
6883
+ }
6884
+
6885
+ /* A dialog the app or a window close cancelled reports cancelled, like
6886
+ * one the user dismissed; anything else that went wrong is a failure. */
6887
+ static int native_sdk_dialog_error_outcome(const GError *error) {
6888
+ if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return NATIVE_SDK_GTK_DIALOG_CANCELLED;
6889
+ #if GTK_CHECK_VERSION(4, 10, 0)
6890
+ if (g_error_matches(error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED) ||
6891
+ g_error_matches(error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_CANCELLED)) {
6892
+ return NATIVE_SDK_GTK_DIALOG_CANCELLED;
6893
+ }
6894
+ #endif
6895
+ return NATIVE_SDK_GTK_DIALOG_FAILED;
6896
+ }
6897
+
6898
+ static gboolean native_sdk_dialog_wake_idle(gpointer data) {
6899
+ native_sdk_gtk_host_t *host = data;
6900
+ host->dialog_wake_source = 0;
6901
+ if (!host->did_shutdown) native_sdk_emit(host, (native_sdk_gtk_event_t){ .kind = NATIVE_SDK_GTK_EVENT_WAKE });
6902
+ return G_SOURCE_REMOVE;
6903
+ }
6904
+
6905
+ static void native_sdk_dialog_finish(native_sdk_gtk_dialog_t *entry, GError *error) {
6906
+ if (error) {
6907
+ entry->outcome = native_sdk_dialog_error_outcome(error);
6908
+ g_error_free(error);
6909
+ }
6910
+ native_sdk_gtk_host_t *host = entry->host;
6911
+ if (!host) {
6912
+ /* The host went first (native_sdk_gtk_destroy detached this). */
6913
+ native_sdk_dialog_free(entry);
6914
+ return;
6915
+ }
6916
+ entry->finished = 1;
6917
+ entry->finish_serial = ++host->dialog_finish_serial;
6918
+ if (!host->dialog_wake_source) host->dialog_wake_source = g_idle_add(native_sdk_dialog_wake_idle, host);
6919
+ }
6920
+
6921
+ static void native_sdk_dialog_add_file(native_sdk_gtk_dialog_t *entry, GFile *file) {
6922
+ char *path = file ? g_file_get_path(file) : NULL;
6923
+ if (path) g_ptr_array_add(entry->paths, path);
6924
+ }
6925
+
6926
+ static void native_sdk_dialog_files_done(GObject *source, GAsyncResult *result, gpointer data) {
6927
+ native_sdk_gtk_dialog_t *entry = data;
6928
+ GError *error = NULL;
6929
+ GListModel *files = NULL;
6930
+ if (entry->folders) {
6931
+ files = gtk_file_dialog_select_multiple_folders_finish(GTK_FILE_DIALOG(source), result, &error);
6932
+ } else {
6933
+ files = gtk_file_dialog_open_multiple_finish(GTK_FILE_DIALOG(source), result, &error);
6934
+ }
6935
+ if (files) {
6936
+ guint count = g_list_model_get_n_items(files);
6937
+ for (guint i = 0; i < count; i++) {
6938
+ GFile *file = G_FILE(g_list_model_get_item(files, i));
6939
+ native_sdk_dialog_add_file(entry, file);
6940
+ g_object_unref(file);
6941
+ }
6942
+ g_object_unref(files);
6943
+ }
6944
+ if (!error) entry->outcome = entry->paths->len > 0 ? NATIVE_SDK_GTK_DIALOG_SELECTED : NATIVE_SDK_GTK_DIALOG_CANCELLED;
6945
+ native_sdk_dialog_finish(entry, error);
6946
+ }
6947
+
6948
+ static void native_sdk_dialog_file_done(GObject *source, GAsyncResult *result, gpointer data) {
6949
+ native_sdk_gtk_dialog_t *entry = data;
6950
+ GError *error = NULL;
6951
+ /* Assigned rather than initialized, like `files` above: against an
6952
+ * older GTK that keeps the diagnostic to the receipt's
6953
+ * assignment-from-int shape (see the dialog builders). */
6954
+ GFile *file = NULL;
6955
+ file = gtk_file_dialog_save_finish(GTK_FILE_DIALOG(source), result, &error);
6956
+ if (file) {
6957
+ native_sdk_dialog_add_file(entry, file);
6958
+ g_object_unref(file);
6959
+ }
6960
+ if (!error) entry->outcome = entry->paths->len > 0 ? NATIVE_SDK_GTK_DIALOG_SELECTED : NATIVE_SDK_GTK_DIALOG_CANCELLED;
6961
+ native_sdk_dialog_finish(entry, error);
6962
+ }
6963
+
6964
+ static void native_sdk_dialog_alert_done(GObject *source, GAsyncResult *result, gpointer data) {
6965
+ native_sdk_gtk_dialog_t *entry = data;
6966
+ GError *error = NULL;
6967
+ const int response = gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(source), result, &error);
6968
+ if (!error) {
6969
+ entry->outcome = NATIVE_SDK_GTK_DIALOG_SELECTED;
6970
+ entry->button = response > 2 ? 2 : (response < 0 ? 0 : response);
6971
+ }
6972
+ native_sdk_dialog_finish(entry, error);
6973
+ }
6974
+
6975
+ /* A closing window takes its dialogs down with it. */
6976
+ static void native_sdk_cancel_dialogs_for_window(native_sdk_gtk_host_t *host, uint64_t window_id) {
6977
+ for (native_sdk_gtk_dialog_t *entry = host->dialogs; entry; entry = entry->next) {
6978
+ if (!entry->finished && entry->parent_window_id == window_id) g_cancellable_cancel(entry->cancellable);
6979
+ }
6980
+ }
6981
+
6982
+ /* Host teardown: finished results are dropped, and open dialogs are
6983
+ * cancelled and left to free themselves when GTK reports them. */
6984
+ static void native_sdk_detach_dialogs(native_sdk_gtk_host_t *host) {
6985
+ while (host->dialogs) {
6986
+ native_sdk_gtk_dialog_t *entry = host->dialogs;
6987
+ host->dialogs = entry->next;
6988
+ entry->next = NULL;
6989
+ if (entry->finished) {
6990
+ native_sdk_dialog_free(entry);
6991
+ continue;
6992
+ }
6993
+ entry->host = NULL;
6994
+ /* Last touch: the callback may run, and free entry, right here. */
6995
+ g_cancellable_cancel(entry->cancellable);
6996
+ }
6997
+ }
6998
+
6999
+ int native_sdk_gtk_request_dialog(native_sdk_gtk_host_t *host, const native_sdk_gtk_dialog_request_t *request) {
7000
+ if (!host || !request || request->id == 0 || host->did_shutdown) return 0;
7001
+ if (native_sdk_dialog_find(host, request->id)) return 0;
7002
+ if ((request->kind == NATIVE_SDK_GTK_DIALOG_OPEN && !request->open_opts) ||
7003
+ (request->kind == NATIVE_SDK_GTK_DIALOG_SAVE && !request->save_opts) ||
7004
+ (request->kind == NATIVE_SDK_GTK_DIALOG_MESSAGE && !request->message_opts) ||
7005
+ request->kind < NATIVE_SDK_GTK_DIALOG_OPEN || request->kind > NATIVE_SDK_GTK_DIALOG_MESSAGE) {
7006
+ return 0;
7007
+ }
7008
+ native_sdk_gtk_dialog_t *entry = calloc(1, sizeof(*entry));
7009
+ if (!entry) return 0;
7010
+ entry->host = host;
7011
+ entry->id = request->id;
7012
+ entry->kind = request->kind;
7013
+ entry->outcome = NATIVE_SDK_GTK_DIALOG_CANCELLED;
7014
+ entry->cancellable = g_cancellable_new();
7015
+ entry->paths = g_ptr_array_new_with_free_func(g_free);
7016
+ native_sdk_gtk_window_t *win = request->parent_window_id != 0 ? native_sdk_find_window(host, request->parent_window_id) : NULL;
7017
+ GtkWindow *parent = win ? win->gtk_window : NULL;
7018
+ if (parent) entry->parent_window_id = request->parent_window_id;
7019
+ entry->next = host->dialogs;
7020
+ host->dialogs = entry;
7021
+ switch (request->kind) {
7022
+ case NATIVE_SDK_GTK_DIALOG_OPEN: {
7023
+ GtkFileDialog *dialog = GTK_FILE_DIALOG(native_sdk_new_open_dialog(request->open_opts));
7024
+ entry->dialog = G_OBJECT(dialog);
7025
+ entry->folders = request->open_opts->allow_directories != 0;
7026
+ if (entry->folders) {
7027
+ gtk_file_dialog_select_multiple_folders(dialog, parent, entry->cancellable, native_sdk_dialog_files_done, entry);
7028
+ } else {
7029
+ gtk_file_dialog_open_multiple(dialog, parent, entry->cancellable, native_sdk_dialog_files_done, entry);
7030
+ }
7031
+ break;
7032
+ }
7033
+ case NATIVE_SDK_GTK_DIALOG_SAVE: {
7034
+ GtkFileDialog *dialog = GTK_FILE_DIALOG(native_sdk_new_save_dialog(request->save_opts));
7035
+ entry->dialog = G_OBJECT(dialog);
7036
+ gtk_file_dialog_save(dialog, parent, entry->cancellable, native_sdk_dialog_file_done, entry);
7037
+ break;
7038
+ }
7039
+ default: {
7040
+ GtkAlertDialog *dialog = GTK_ALERT_DIALOG(native_sdk_new_alert_dialog(request->message_opts));
7041
+ entry->dialog = G_OBJECT(dialog);
7042
+ gtk_alert_dialog_choose(dialog, parent, entry->cancellable, native_sdk_dialog_alert_done, entry);
7043
+ break;
7044
+ }
7045
+ }
7046
+ return 1;
7047
+ }
7048
+
7049
+ void native_sdk_gtk_cancel_dialog(native_sdk_gtk_host_t *host, uint64_t id) {
7050
+ if (!host) return;
7051
+ native_sdk_gtk_dialog_t *entry = native_sdk_dialog_find(host, id);
7052
+ if (entry && !entry->finished) g_cancellable_cancel(entry->cancellable);
7053
+ }
7054
+
7055
+ int native_sdk_gtk_poll_dialog_result(native_sdk_gtk_host_t *host, char *buffer, size_t buffer_len, native_sdk_gtk_dialog_result_t *result) {
7056
+ if (!host || !result) return 0;
7057
+ native_sdk_gtk_dialog_t *oldest = NULL;
7058
+ for (native_sdk_gtk_dialog_t *entry = host->dialogs; entry; entry = entry->next) {
7059
+ if (entry->finished && (!oldest || entry->finish_serial < oldest->finish_serial)) oldest = entry;
7060
+ }
7061
+ if (!oldest) return 0;
7062
+ native_sdk_dialog_unlink(host, oldest);
7063
+ *result = (native_sdk_gtk_dialog_result_t){ .id = oldest->id, .outcome = oldest->outcome, .button = oldest->button };
7064
+ size_t offset = 0;
7065
+ size_t count = 0;
7066
+ for (guint i = 0; i < oldest->paths->len; i++) {
7067
+ const char *path = g_ptr_array_index(oldest->paths, i);
7068
+ const size_t len = strlen(path);
7069
+ const size_t needed = len + (count > 0 ? 1 : 0);
7070
+ if (!buffer || needed > buffer_len - offset) {
7071
+ /* More paths than the runtime holds: a failure, not a
7072
+ * silently shortened selection. */
7073
+ result->outcome = NATIVE_SDK_GTK_DIALOG_FAILED;
7074
+ offset = 0;
7075
+ count = 0;
7076
+ break;
7077
+ }
7078
+ if (count > 0) buffer[offset++] = '\n';
7079
+ memcpy(buffer + offset, path, len);
7080
+ offset += len;
7081
+ count++;
7082
+ }
7083
+ result->count = count;
7084
+ result->bytes_written = offset;
7085
+ native_sdk_dialog_free(oldest);
7086
+ return 1;
7087
+ }
@@ -156,6 +156,38 @@ typedef struct {
156
156
  size_t tertiary_button_len;
157
157
  } native_sdk_gtk_message_dialog_opts_t;
158
158
 
159
+ enum {
160
+ NATIVE_SDK_GTK_DIALOG_OPEN = 0,
161
+ NATIVE_SDK_GTK_DIALOG_SAVE = 1,
162
+ NATIVE_SDK_GTK_DIALOG_MESSAGE = 2,
163
+ };
164
+
165
+ enum {
166
+ NATIVE_SDK_GTK_DIALOG_SELECTED = 0,
167
+ NATIVE_SDK_GTK_DIALOG_CANCELLED = 1,
168
+ NATIVE_SDK_GTK_DIALOG_FAILED = 2,
169
+ };
170
+
171
+ /* An asynchronous dialog request (native_sdk_gtk_request_dialog): kind
172
+ * picks which of the option pointers is set. */
173
+ typedef struct {
174
+ uint64_t id;
175
+ uint64_t parent_window_id;
176
+ int kind;
177
+ const native_sdk_gtk_open_dialog_opts_t *open_opts;
178
+ const native_sdk_gtk_save_dialog_opts_t *save_opts;
179
+ const native_sdk_gtk_message_dialog_opts_t *message_opts;
180
+ } native_sdk_gtk_dialog_request_t;
181
+
182
+ /* A finished asynchronous dialog; paths are newline-joined. */
183
+ typedef struct {
184
+ uint64_t id;
185
+ int outcome;
186
+ int button;
187
+ size_t count;
188
+ size_t bytes_written;
189
+ } native_sdk_gtk_dialog_result_t;
190
+
159
191
  /* resizable/titlebar_style/min_width/min_height mirror the AppKit host's
160
192
  * window options: titlebar_style 0 = standard decorations, 1 =
161
193
  * hidden_inset, 2 = hidden_inset_tall (the hidden styles create the
@@ -287,6 +319,11 @@ int native_sdk_gtk_clipboard_write_data(native_sdk_gtk_host_t *host, const char
287
319
  native_sdk_gtk_open_dialog_result_t native_sdk_gtk_show_open_dialog(native_sdk_gtk_host_t *host, const native_sdk_gtk_open_dialog_opts_t *opts, char *buffer, size_t buffer_len);
288
320
  size_t native_sdk_gtk_show_save_dialog(native_sdk_gtk_host_t *host, const native_sdk_gtk_save_dialog_opts_t *opts, char *buffer, size_t buffer_len);
289
321
  int native_sdk_gtk_show_message_dialog(native_sdk_gtk_host_t *host, const native_sdk_gtk_message_dialog_opts_t *opts);
322
+ /* Show a dialog without a nested loop: it returns at once, and the
323
+ * result waits for poll after a wake. Returns 0 when nothing was shown. */
324
+ int native_sdk_gtk_request_dialog(native_sdk_gtk_host_t *host, const native_sdk_gtk_dialog_request_t *request);
325
+ void native_sdk_gtk_cancel_dialog(native_sdk_gtk_host_t *host, uint64_t id);
326
+ int native_sdk_gtk_poll_dialog_result(native_sdk_gtk_host_t *host, char *buffer, size_t buffer_len, native_sdk_gtk_dialog_result_t *result);
290
327
 
291
328
  #ifdef __cplusplus
292
329
  }
@@ -234,9 +234,29 @@ const GtkMessageDialogOpts = extern struct {
234
234
  tertiary_button_len: usize,
235
235
  };
236
236
 
237
+ const GtkDialogRequest = extern struct {
238
+ id: u64,
239
+ parent_window_id: u64,
240
+ kind: c_int,
241
+ open_opts: ?*const GtkOpenDialogOpts,
242
+ save_opts: ?*const GtkSaveDialogOpts,
243
+ message_opts: ?*const GtkMessageDialogOpts,
244
+ };
245
+
246
+ const GtkDialogResult = extern struct {
247
+ id: u64,
248
+ outcome: c_int,
249
+ button: c_int,
250
+ count: usize,
251
+ bytes_written: usize,
252
+ };
253
+
237
254
  extern fn native_sdk_gtk_show_open_dialog(host: *GtkHost, opts: *const GtkOpenDialogOpts, buffer: [*]u8, buffer_len: usize) GtkOpenDialogResult;
238
255
  extern fn native_sdk_gtk_show_save_dialog(host: *GtkHost, opts: *const GtkSaveDialogOpts, buffer: [*]u8, buffer_len: usize) usize;
239
256
  extern fn native_sdk_gtk_show_message_dialog(host: *GtkHost, opts: *const GtkMessageDialogOpts) c_int;
257
+ extern fn native_sdk_gtk_request_dialog(host: *GtkHost, request: *const GtkDialogRequest) c_int;
258
+ extern fn native_sdk_gtk_cancel_dialog(host: *GtkHost, id: u64) void;
259
+ extern fn native_sdk_gtk_poll_dialog_result(host: *GtkHost, buffer: [*]u8, buffer_len: usize, result: *GtkDialogResult) c_int;
240
260
 
241
261
  /// The startup-window twin of the runtime's create-time `.hide` gate:
242
262
  /// GTK reports `window_hide_on_close` false (no status item exists to
@@ -390,6 +410,9 @@ pub const LinuxPlatform = struct {
390
410
  .show_open_dialog_fn = showOpenDialog,
391
411
  .show_save_dialog_fn = showSaveDialog,
392
412
  .show_message_dialog_fn = showMessageDialog,
413
+ .request_dialog_fn = if (self.web_engine == .system) requestDialog else null,
414
+ .cancel_dialog_fn = if (self.web_engine == .system) cancelDialog else null,
415
+ .poll_dialog_result_fn = if (self.web_engine == .system) pollDialogResult else null,
393
416
  .open_external_url_fn = openExternalUrl,
394
417
  .reveal_path_fn = revealPath,
395
418
  .show_notification_fn = showNotification,
@@ -1271,11 +1294,9 @@ fn closeWebView(context: ?*anyopaque, window_id: platform_mod.WindowId, label: [
1271
1294
  if (native_sdk_gtk_close_webview(self.host, window_id, label.ptr, label.len) == 0) return error.WebViewNotFound;
1272
1295
  }
1273
1296
 
1274
- fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions, buffer: []u8) anyerror!platform_mod.OpenDialogResult {
1275
- const self: *LinuxPlatform = @ptrCast(@alignCast(context.?));
1276
- var ext_buf: [1024]u8 = undefined;
1277
- const ext_str = flattenFilters(options.filters, &ext_buf);
1278
- const opts = GtkOpenDialogOpts{
1297
+ fn gtkOpenDialogOpts(options: platform_mod.OpenDialogOptions, ext_buf: []u8) GtkOpenDialogOpts {
1298
+ const ext_str = flattenFilters(options.filters, ext_buf);
1299
+ return .{
1279
1300
  .title = options.title.ptr,
1280
1301
  .title_len = options.title.len,
1281
1302
  .default_path = options.default_path.ptr,
@@ -1285,16 +1306,11 @@ fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions,
1285
1306
  .allow_directories = if (options.allow_directories) 1 else 0,
1286
1307
  .allow_multiple = if (options.allow_multiple) 1 else 0,
1287
1308
  };
1288
- const result = native_sdk_gtk_show_open_dialog(self.host, &opts, buffer.ptr, buffer.len);
1289
- if (result.bytes_written > buffer.len) return error.NoSpaceLeft;
1290
- return .{ .count = result.count, .paths = buffer[0..result.bytes_written] };
1291
1309
  }
1292
1310
 
1293
- fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions, buffer: []u8) anyerror!?[]const u8 {
1294
- const self: *LinuxPlatform = @ptrCast(@alignCast(context.?));
1295
- var ext_buf: [1024]u8 = undefined;
1296
- const ext_str = flattenFilters(options.filters, &ext_buf);
1297
- const opts = GtkSaveDialogOpts{
1311
+ fn gtkSaveDialogOpts(options: platform_mod.SaveDialogOptions, ext_buf: []u8) GtkSaveDialogOpts {
1312
+ const ext_str = flattenFilters(options.filters, ext_buf);
1313
+ return .{
1298
1314
  .title = options.title.ptr,
1299
1315
  .title_len = options.title.len,
1300
1316
  .default_path = options.default_path.ptr,
@@ -1304,15 +1320,10 @@ fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions,
1304
1320
  .extensions = ext_str.ptr,
1305
1321
  .extensions_len = ext_str.len,
1306
1322
  };
1307
- const written = native_sdk_gtk_show_save_dialog(self.host, &opts, buffer.ptr, buffer.len);
1308
- if (written > buffer.len) return error.NoSpaceLeft;
1309
- if (written == 0) return null;
1310
- return buffer[0..written];
1311
1323
  }
1312
1324
 
1313
- fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOptions) anyerror!platform_mod.MessageDialogResult {
1314
- const self: *LinuxPlatform = @ptrCast(@alignCast(context.?));
1315
- const opts = GtkMessageDialogOpts{
1325
+ fn gtkMessageDialogOpts(options: platform_mod.MessageDialogOptions) GtkMessageDialogOpts {
1326
+ return .{
1316
1327
  .style = @intFromEnum(options.style),
1317
1328
  .title = options.title.ptr,
1318
1329
  .title_len = options.title.len,
@@ -1327,9 +1338,92 @@ fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOp
1327
1338
  .tertiary_button = options.tertiary_button.ptr,
1328
1339
  .tertiary_button_len = options.tertiary_button.len,
1329
1340
  };
1341
+ }
1342
+
1343
+ fn showOpenDialog(context: ?*anyopaque, options: platform_mod.OpenDialogOptions, buffer: []u8) anyerror!platform_mod.OpenDialogResult {
1344
+ const self: *LinuxPlatform = @ptrCast(@alignCast(context.?));
1345
+ var ext_buf: [1024]u8 = undefined;
1346
+ const opts = gtkOpenDialogOpts(options, &ext_buf);
1347
+ const result = native_sdk_gtk_show_open_dialog(self.host, &opts, buffer.ptr, buffer.len);
1348
+ if (result.bytes_written > buffer.len) return error.NoSpaceLeft;
1349
+ return .{ .count = result.count, .paths = buffer[0..result.bytes_written] };
1350
+ }
1351
+
1352
+ fn showSaveDialog(context: ?*anyopaque, options: platform_mod.SaveDialogOptions, buffer: []u8) anyerror!?[]const u8 {
1353
+ const self: *LinuxPlatform = @ptrCast(@alignCast(context.?));
1354
+ var ext_buf: [1024]u8 = undefined;
1355
+ const opts = gtkSaveDialogOpts(options, &ext_buf);
1356
+ const written = native_sdk_gtk_show_save_dialog(self.host, &opts, buffer.ptr, buffer.len);
1357
+ if (written > buffer.len) return error.NoSpaceLeft;
1358
+ if (written == 0) return null;
1359
+ return buffer[0..written];
1360
+ }
1361
+
1362
+ fn showMessageDialog(context: ?*anyopaque, options: platform_mod.MessageDialogOptions) anyerror!platform_mod.MessageDialogResult {
1363
+ const self: *LinuxPlatform = @ptrCast(@alignCast(context.?));
1364
+ const opts = gtkMessageDialogOpts(options);
1330
1365
  return @enumFromInt(native_sdk_gtk_show_message_dialog(self.host, &opts));
1331
1366
  }
1332
1367
 
1368
+ // The host copies every option before returning, so they may live on this
1369
+ // stack; the result comes back through pollDialogResult on a later wake.
1370
+ fn requestDialog(context: ?*anyopaque, request: platform_mod.DialogRequest) anyerror!void {
1371
+ const self: *LinuxPlatform = @ptrCast(@alignCast(context.?));
1372
+ var ext_buf: [1024]u8 = undefined;
1373
+ var open_opts: GtkOpenDialogOpts = undefined;
1374
+ var save_opts: GtkSaveDialogOpts = undefined;
1375
+ var message_opts: GtkMessageDialogOpts = undefined;
1376
+ var native_request = GtkDialogRequest{
1377
+ .id = request.id,
1378
+ .parent_window_id = request.parent_window_id orelse 0,
1379
+ .kind = @intFromEnum(std.meta.activeTag(request.options)),
1380
+ .open_opts = null,
1381
+ .save_opts = null,
1382
+ .message_opts = null,
1383
+ };
1384
+ switch (request.options) {
1385
+ .open => |options| {
1386
+ open_opts = gtkOpenDialogOpts(options, &ext_buf);
1387
+ native_request.open_opts = &open_opts;
1388
+ },
1389
+ .save => |options| {
1390
+ save_opts = gtkSaveDialogOpts(options, &ext_buf);
1391
+ native_request.save_opts = &save_opts;
1392
+ },
1393
+ .message => |options| {
1394
+ message_opts = gtkMessageDialogOpts(options);
1395
+ native_request.message_opts = &message_opts;
1396
+ },
1397
+ }
1398
+ if (native_sdk_gtk_request_dialog(self.host, &native_request) == 0) return error.DialogUnavailable;
1399
+ }
1400
+
1401
+ fn cancelDialog(context: ?*anyopaque, id: platform_mod.DialogRequestId) void {
1402
+ const self: *LinuxPlatform = @ptrCast(@alignCast(context.?));
1403
+ native_sdk_gtk_cancel_dialog(self.host, id);
1404
+ }
1405
+
1406
+ fn pollDialogResult(context: ?*anyopaque, buffer: []u8) ?platform_mod.DialogResult {
1407
+ const self: *LinuxPlatform = @ptrCast(@alignCast(context.?));
1408
+ var result: GtkDialogResult = undefined;
1409
+ if (native_sdk_gtk_poll_dialog_result(self.host, buffer.ptr, buffer.len, &result) == 0) return null;
1410
+ return .{
1411
+ .id = result.id,
1412
+ .outcome = switch (result.outcome) {
1413
+ 0 => .selected,
1414
+ 1 => .cancelled,
1415
+ else => .failed,
1416
+ },
1417
+ .paths = buffer[0..@min(result.bytes_written, buffer.len)],
1418
+ .count = result.count,
1419
+ .button = switch (result.button) {
1420
+ 1 => .secondary,
1421
+ 2 => .tertiary,
1422
+ else => .primary,
1423
+ },
1424
+ };
1425
+ }
1426
+
1333
1427
  fn openExternalUrl(context: ?*anyopaque, url: []const u8) anyerror!void {
1334
1428
  const self: *LinuxPlatform = @ptrCast(@alignCast(context.?));
1335
1429
  if (self.web_engine != .system) return error.UnsupportedService;