diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index a064f44..20b58aa 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -1,5 +1,5 @@
- 0.3.8
+ 0.3.9
diff --git a/src/Rustino.Native/Cargo.toml b/src/Rustino.Native/Cargo.toml
index 46857c1..de25696 100644
--- a/src/Rustino.Native/Cargo.toml
+++ b/src/Rustino.Native/Cargo.toml
@@ -31,8 +31,8 @@ windows = { version = "0.61", features = [
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
-objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSDockTile"] }
-objc2-foundation = { version = "0.3", features = ["NSString"] }
+objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSDockTile", "NSWorkspace"] }
+objc2-foundation = { version = "0.3", features = ["NSString", "NSBundle"] }
[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18"
diff --git a/src/Rustino.Native/src/lib.rs b/src/Rustino.Native/src/lib.rs
index bd20e59..eb61e8d 100644
--- a/src/Rustino.Native/src/lib.rs
+++ b/src/Rustino.Native/src/lib.rs
@@ -98,13 +98,74 @@ pub unsafe extern "C" fn rustino_show_notification(
if let Some(id) = unsafe { util::cstr_to_string(app_id) } {
n.app_id(&id);
}
- #[cfg(not(target_os = "windows"))]
+ #[cfg(target_os = "macos")]
+ {
+ let requested = unsafe { util::cstr_to_string(app_id) };
+ macos_notification::ensure_application_set(requested.as_deref());
+ }
+ #[cfg(not(any(target_os = "windows", target_os = "macos")))]
let _ = app_id;
if n.show().is_ok() { 1 } else { 0 }
})
.unwrap_or(0)
}
+// notify-rust's macOS backend lazily calls `set_application("use_default")` on the
+// first notification if nobody has set one, which makes macOS pop a "Where is
+// use_default?" app-picker dialog (see Ivy-Tendril#1682). We must call
+// `notify_rust::set_application` ourselves before that happens — and since its
+// internal `Once` latches even on failure, it can only ever be called once, with
+// an id we already know is valid.
+#[cfg(target_os = "macos")]
+mod macos_notification {
+ use objc2_app_kit::NSWorkspace;
+ use objc2_foundation::{NSBundle, NSString};
+ use std::sync::Once;
+
+ static ENSURE_APPLICATION_SET: Once = Once::new();
+
+ /// Picks a LaunchServices-registered bundle id and calls `notify_rust::set_application`
+ /// with it exactly once. Must be called before the first `Notification::show()`.
+ pub fn ensure_application_set(requested_app_id: Option<&str>) {
+ ENSURE_APPLICATION_SET.call_once(|| {
+ let bundle_id = pick_bundle_id(requested_app_id);
+ let _ = notify_rust::set_application(&bundle_id);
+ });
+ }
+
+ /// Returns the first candidate that is actually registered with LaunchServices,
+ /// falling back to "com.apple.Finder" (guaranteed installed; notify-rust's own
+ /// eventual default) if nothing else resolves.
+ fn pick_bundle_id(requested_app_id: Option<&str>) -> String {
+ let main_bundle_id = main_bundle_identifier();
+ let candidates = [
+ requested_app_id,
+ main_bundle_id.as_deref(),
+ Some("com.apple.Terminal"),
+ ];
+ candidates
+ .into_iter()
+ .flatten()
+ .find(|id| !id.is_empty() && is_registered_bundle_id(id))
+ .map(str::to_string)
+ .unwrap_or_else(|| "com.apple.Finder".to_string())
+ }
+
+ /// The running app's own bundle identifier, when running inside a real .app bundle.
+ fn main_bundle_identifier() -> Option {
+ NSBundle::mainBundle().bundleIdentifier().map(|s| s.to_string())
+ }
+
+ /// LaunchServices only accepts bundle ids it knows about; `setApplication` silently
+ /// rejects anything else, so we pre-validate via the same lookup it uses internally
+ /// (`LSCopyApplicationURLsForBundleIdentifier`, exposed here as `URLForApplication...`).
+ fn is_registered_bundle_id(bundle_id: &str) -> bool {
+ let workspace = NSWorkspace::sharedWorkspace();
+ let id = NSString::from_str(bundle_id);
+ workspace.URLForApplicationWithBundleIdentifier(&id).is_some()
+ }
+}
+
// ---------------------------------------------------------------------------
// Dual-mode setters (pre-run: modify config, post-run: send command)
// ---------------------------------------------------------------------------