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,
};
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();
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 {
"__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) {
}
pub(crate) fn on_response(
&mut self,
executor: &ForegroundExecutor,
callback: Box<dyn FnMut(SystemNotificationResponse)>,
) {
*self.callback.borrow_mut() = Some(callback);
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 {
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);
}
}