Release Notes: - N/A or Added/Fixed/Improved ... --------- Co-authored-by: John Tur <john-tur@outlook.com>
155 lines
5.7 KiB
Rust
155 lines
5.7 KiB
Rust
//! 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);
|
|
}
|
|
}
|