//! System notifications over the XDG notifications D-Bus interface, via //! `notify-rust`. use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use futures::StreamExt as _; use futures::channel::mpsc; use gpui::{ ForegroundExecutor, SharedString, SystemNotification, SystemNotificationResponse, Task, }; /// The XDG action key invoked when the user activates the notification body /// rather than a specific action button. const DEFAULT_ACTION: &str = "default"; type ResponseCallback = Rc>>>; pub(crate) struct SystemNotificationState { response_sender: mpsc::UnboundedSender, response_receiver: Option>, callback: ResponseCallback, _response_task: Option>, } impl SystemNotificationState { pub(crate) fn new() -> Self { let (response_sender, response_receiver) = mpsc::unbounded(); Self { response_sender, response_receiver: Some(response_receiver), callback: Rc::new(RefCell::new(None)), _response_task: None, } } pub(crate) fn show(&self, app_name: Option<&str>, notification: SystemNotification) { let mut builder = notify_rust::Notification::new(); if let Some(app_name) = app_name { builder.appname(app_name); } builder .summary(¬ification.title) .body(¬ification.body) .action(DEFAULT_ACTION, DEFAULT_ACTION); let mut action_ids = HashMap::new(); for (index, action) in notification.actions.iter().enumerate() { let transport_id = format!("gpui-action-{index}"); builder.action(&transport_id, &action.label); action_ids.insert(transport_id, action.id.clone()); } let built = builder.finalize(); let sender = self.response_sender.clone(); let tag = notification.tag.clone(); // `show` connects to the session bus and `wait_for_action` blocks // until the notification closes, so both run off the UI thread. let spawn_result = std::thread::Builder::new() .name("system-notification".to_string()) .spawn(move || match built.show() { Ok(handle) => handle.wait_for_action(|action| { let action_id = match action { // Sent by `notify-rust` when the notification closes // without being activated. "__closed" => return, transport_id => { let Some(action_id) = response_action_id(transport_id, &action_ids) else { log::warn!( "system notification returned unknown action {transport_id:?}" ); return; }; action_id } }; sender .unbounded_send(SystemNotificationResponse { tag, action_id }) .ok(); }), Err(error) => log::warn!("failed to show system notification: {error}"), }); if let Err(error) = spawn_result { log::warn!("failed to spawn system notification thread: {error}"); } } pub(crate) fn dismiss(&self, _tag: &str) { // The XDG notifications protocol only allows closing via the // server-assigned id on a live handle, which `wait_for_action` // consumes; stale notifications simply age out of the shade. } pub(crate) fn on_response( &mut self, executor: &ForegroundExecutor, callback: Box, ) { *self.callback.borrow_mut() = Some(callback); // Responses arrive from per-notification threads; this task hands // them to the registered callback on the main thread. if let Some(mut receiver) = self.response_receiver.take() { let callback = self.callback.clone(); self._response_task = Some(executor.spawn(async move { while let Some(response) = receiver.next().await { // Take the callback out for the call: it may re-enter the // platform or replace itself. let taken = callback.borrow_mut().take(); if let Some(mut taken) = taken { taken(response); callback.borrow_mut().get_or_insert(taken); } } })); } } } fn response_action_id( transport_id: &str, action_ids: &HashMap, ) -> Option> { if transport_id == DEFAULT_ACTION { Some(None) } else { action_ids.get(transport_id).cloned().map(Some) } } #[cfg(test)] mod tests { use super::*; #[test] fn caller_action_ids_do_not_collide_with_transport_action_ids() { let action_ids = HashMap::from([ ("gpui-action-0".to_string(), SharedString::from("default")), ("gpui-action-1".to_string(), SharedString::from("__closed")), ]); assert_eq!(response_action_id(DEFAULT_ACTION, &action_ids), Some(None)); assert_eq!( response_action_id("gpui-action-0", &action_ids), Some(Some("default".into())) ); assert_eq!( response_action_id("gpui-action-1", &action_ids), Some(Some("__closed".into())) ); assert_eq!(response_action_id("unknown", &action_ids), None); } }