//! 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<RefCell<Option<Box<dyn FnMut(SystemNotificationResponse)>>>>; pub(crate) struct SystemNotificationState { response_sender: mpsc::UnboundedSender<SystemNotificationResponse>, response_receiver: Option<mpsc::UnboundedReceiver<SystemNotificationResponse>>, callback: ResponseCallback, _response_task: Option<Task<()>>,} 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<dyn FnMut(SystemNotificationResponse)>, ) { *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<String, SharedString>,) -> Option<Option<SharedString>> { 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); }}
//! 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<RefCell<Option<Box<dyn FnMut(SystemNotificationResponse)>>>>;
pub(crate) struct SystemNotificationState {
response_sender: mpsc::UnboundedSender<SystemNotificationResponse>,
response_receiver: Option<mpsc::UnboundedReceiver<SystemNotificationResponse>>,
callback: ResponseCallback,
_response_task: Option<Task<()>>,
}
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<dyn FnMut(SystemNotificationResponse)>,
) {
*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<String, SharedString>,
) -> Option<Option<SharedString>> {
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);
}
}