diff --git a/Cargo.lock b/Cargo.lock index 6c8532b1..6c99143a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,6 +226,31 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + [[package]] name = "async-io" version = "2.6.0" @@ -3948,6 +3973,7 @@ dependencies = [ name = "rio-notifier" version = "0.3.0" dependencies = [ + "block2 0.5.1", "objc-rs 0.3.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -6197,8 +6223,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" dependencies = [ "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", "async-process", "async-recursion", + "async-task", "async-trait", "blocking", "enumflags2", diff --git a/frontends/rioterm/src/application.rs b/frontends/rioterm/src/application.rs index 3f8ba365..bb61f1be 100644 --- a/frontends/rioterm/src/application.rs +++ b/frontends/rioterm/src/application.rs @@ -70,6 +70,8 @@ impl Application<'_> { event_loop.set_use_native_quit_dialog(config.window.macos_use_quit_dialog); } + rio_notifier::request_authorization(); + Application { config, event_proxy, diff --git a/rio-notifier/Cargo.toml b/rio-notifier/Cargo.toml index 18ef9ffa..388aab06 100644 --- a/rio-notifier/Cargo.toml +++ b/rio-notifier/Cargo.toml @@ -8,6 +8,7 @@ rust-version.workspace = true [target.'cfg(target_os = "macos")'.dependencies] objc = { workspace = true } +block2 = "0.5.1" objc2 = "0.5.2" objc2-foundation = { version = "0.2.2", features = ["NSString"] } objc2-user-notifications = { version = "0.2.2", features = [ diff --git a/rio-notifier/src/lib.rs b/rio-notifier/src/lib.rs index 7677890b..c4fd813d 100644 --- a/rio-notifier/src/lib.rs +++ b/rio-notifier/src/lib.rs @@ -1,3 +1,11 @@ +/// Request notification authorization from the OS. +/// On macOS this triggers the permission prompt on first call. +/// No-op on other platforms. +pub fn request_authorization() { + #[cfg(target_os = "macos")] + platform::request_authorization(); +} + /// Send a desktop notification using the platform's native API. /// /// - **macOS**: `UNUserNotificationCenter` (requires app bundle with identifier). @@ -20,12 +28,37 @@ pub fn send_notification(title: &str, body: &str) { #[cfg(target_os = "macos")] mod platform { + use block2::RcBlock; use objc::runtime::Object; use objc::{class, msg_send, sel, sel_impl}; - use objc2_foundation::NSString; + use objc2::runtime::Bool; + use objc2_foundation::{NSError, NSString}; use objc2_user_notifications::{ - UNMutableNotificationContent, UNNotificationRequest, UNUserNotificationCenter, + UNAuthorizationOptions, UNMutableNotificationContent, UNNotificationRequest, + UNUserNotificationCenter, }; + use std::sync::Once; + + pub(crate) fn request_authorization() { + static INIT: Once = Once::new(); + INIT.call_once(|| unsafe { + let bundle: *mut Object = msg_send![class!(NSBundle), mainBundle]; + if bundle.is_null() { + return; + } + let bundle_id: *mut Object = msg_send![bundle, bundleIdentifier]; + if bundle_id.is_null() { + return; + } + + let center = UNUserNotificationCenter::currentNotificationCenter(); + center.requestAuthorizationWithOptions_completionHandler( + UNAuthorizationOptions::UNAuthorizationOptionAlert + | UNAuthorizationOptions::UNAuthorizationOptionSound, + &RcBlock::new(|_ok: Bool, _err: *mut NSError| {}), + ); + }); + } pub fn notify(title: &str, body: &str) { unsafe {