1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
use anyhow::Result;
use crate::NotificationAction;
pub fn show_notification(title: &str, body: &str) -> Result<()> {
notify_rust::Notification::new()
.summary(title)
.body(body)
.show()
.map_err(|e| anyhow::anyhow!("Failed to show notification: {}", e))?;
Ok(())
}
/// Show a notification with action buttons using the `org.freedesktop.Notifications`
/// D-Bus `Notify` method's `actions` parameter.
///
/// The `callback` is invoked with the action `id` when the user clicks an action button.
/// Because `notify-rust`'s `wait_for_action` blocks, we spawn a background thread to
/// listen for the user's response and use a oneshot channel to deliver the action ID
/// back to a foreground task where the callback is invoked.
pub fn show_notification_with_actions(
title: &str,
body: &str,
actions: &[NotificationAction],
mut callback: Box<dyn FnMut(String)>,
foreground_executor: crate::ForegroundExecutor,
) -> Result<()> {
let mut notification = notify_rust::Notification::new();
notification.summary(title).body(body);
for action in actions {
notification.action(&action.id, &action.label);
}
let handle = notification
.show()
.map_err(|e| anyhow::anyhow!("Failed to show notification: {}", e))?;
// Set up a oneshot channel: the sender goes to the background thread,
// the receiver stays on the foreground where the callback lives.
let (tx, rx) = futures::channel::oneshot::channel::<String>();
// Spawn a foreground task that awaits the action ID and invokes the callback.
// This keeps the non-Send callback on the main thread.
foreground_executor
.spawn(async move {
if let Ok(action_id) = rx.await {
callback(action_id);
}
})
.detach();
// Spawn a background thread that blocks on `wait_for_action` and sends
// the action ID through the channel when the user clicks a button.
std::thread::spawn(move || {
let tx = std::sync::Mutex::new(Some(tx));
handle.wait_for_action(|action_id| {
// "__closed" is sent when the notification is dismissed without
// clicking an action button — skip it.
if action_id == "__closed" {
return;
}
if let Some(sender) = tx.lock().ok().and_then(|mut guard| guard.take()) {
let _ = sender.send(action_id.to_string());
}
});
});
Ok(())
}