ao_plugin_notifier_desktop/
lib.rs1use ao_core::{
31 notifier::{NotificationPayload, Notifier, NotifierError},
32 reactions::EventPriority,
33};
34use async_trait::async_trait;
35use notify_rust::Notification;
36
37pub struct DesktopNotifier;
39
40impl DesktopNotifier {
41 pub fn new() -> Self {
42 Self
43 }
44}
45
46impl Default for DesktopNotifier {
47 fn default() -> Self {
48 Self::new()
49 }
50}
51
52#[cfg(not(target_os = "macos"))]
56fn urgency(priority: EventPriority, escalated: bool) -> notify_rust::Urgency {
57 use notify_rust::Urgency;
58 if escalated {
59 return Urgency::Critical;
60 }
61 match priority {
62 EventPriority::Urgent | EventPriority::Action => Urgency::Critical,
63 EventPriority::Warning => Urgency::Normal,
64 EventPriority::Info => Urgency::Low,
65 }
66}
67
68fn build_notification(
70 title: &str,
71 body: &str,
72 priority: EventPriority,
73 escalated: bool,
74) -> Notification {
75 let mut n = Notification::new();
76 n.summary(title).body(body);
77
78 #[cfg(not(target_os = "macos"))]
80 {
81 n.urgency(urgency(priority, escalated));
82 }
83
84 #[cfg(target_os = "macos")]
86 {
87 let _ = (priority, escalated);
88 }
89
90 n
91}
92
93#[async_trait]
94impl Notifier for DesktopNotifier {
95 fn name(&self) -> &str {
96 "desktop"
97 }
98
99 async fn send(&self, payload: &NotificationPayload) -> Result<(), NotifierError> {
100 let title = if payload.escalated {
101 format!("[ESCALATED] {}", payload.title)
102 } else {
103 payload.title.clone()
104 };
105
106 let notification =
107 build_notification(&title, &payload.body, payload.priority, payload.escalated);
108
109 #[cfg(not(target_os = "macos"))]
112 notification
113 .show_async()
114 .await
115 .map_err(|e| NotifierError::Unavailable(format!("desktop notification failed: {e}")))?;
116
117 #[cfg(target_os = "macos")]
118 notification
119 .show()
120 .map_err(|e| NotifierError::Unavailable(format!("desktop notification failed: {e}")))?;
121
122 tracing::debug!("desktop notification sent");
123 Ok(())
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn name_is_desktop() {
133 assert_eq!(DesktopNotifier::new().name(), "desktop");
134 }
135
136 #[test]
137 fn default_impl_works() {
138 let n: DesktopNotifier = Default::default();
139 assert_eq!(n.name(), "desktop");
140 }
141
142 #[cfg(not(target_os = "macos"))]
143 #[test]
144 fn urgency_mapping_covers_all_variants() {
145 use notify_rust::Urgency;
146 assert_eq!(urgency(EventPriority::Urgent, false), Urgency::Critical);
147 assert_eq!(urgency(EventPriority::Action, false), Urgency::Critical);
148 assert_eq!(urgency(EventPriority::Warning, false), Urgency::Normal);
149 assert_eq!(urgency(EventPriority::Info, false), Urgency::Low);
150 }
151
152 #[cfg(not(target_os = "macos"))]
153 #[test]
154 fn escalated_always_critical() {
155 use notify_rust::Urgency;
156 assert_eq!(urgency(EventPriority::Urgent, true), Urgency::Critical);
157 assert_eq!(urgency(EventPriority::Action, true), Urgency::Critical);
158 assert_eq!(urgency(EventPriority::Warning, true), Urgency::Critical);
159 assert_eq!(urgency(EventPriority::Info, true), Urgency::Critical);
160 }
161
162 #[test]
163 fn build_notification_sets_title_and_body() {
164 let n = build_notification("Test Title", "Test Body", EventPriority::Info, false);
165 assert_eq!(n.summary, "Test Title");
166 assert_eq!(n.body, "Test Body");
167 }
168}