user_notify/platform_impl/xdg/
mod.rs

1//! Linux implementation
2
3mod category;
4
5use std::{
6    collections::HashMap,
7    sync::{Arc, OnceLock},
8};
9
10use async_trait::async_trait;
11use image::ImageReader;
12use notify_rust::{ActionResponse, CloseReason, Hint, Urgency, handle_action};
13use tokio::sync::RwLock;
14
15use crate::{NotificationBuilder, NotificationHandle, NotificationManager, NotificationResponse};
16
17/// Handle to linux notification
18#[derive(Debug, Clone)]
19pub struct NotificationHandleXdg {
20    id: String,
21    user_info: HashMap<String, String>,
22    handle: Arc<RwLock<Option<notify_rust::NotificationHandle>>>,
23}
24
25impl NotificationHandle for NotificationHandleXdg {
26    fn close(&self) -> Result<(), crate::Error> {
27        log::info!("called close notification handle {self:?}");
28        Ok(())
29    }
30
31    fn get_id(&self) -> String {
32        self.id.clone()
33    }
34
35    fn get_user_info(&self) -> &HashMap<String, String> {
36        &self.user_info
37    }
38}
39
40/// Linux implementation of NotificationManager
41#[derive(Default)]
42pub struct NotificationManagerXdg {
43    active_notifications: RwLock<Vec<NotificationHandleXdg>>,
44    #[allow(clippy::type_complexity)]
45    handler: OnceLock<Arc<Box<dyn Fn(crate::NotificationResponse) + Send + Sync + 'static>>>,
46}
47
48impl std::fmt::Debug for NotificationManagerXdg {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.debug_struct("NotificationManagerXdg")
51            .field("active_notifications", &self.active_notifications)
52            .field("handler", &self.handler.get().is_some().to_string())
53            .finish()
54    }
55}
56
57impl NotificationManagerXdg {
58    /// Create notification manager for linux
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    async fn add_notification(&self, notification: NotificationHandleXdg) {
64        self.active_notifications.write().await.push(notification);
65    }
66}
67
68#[async_trait]
69impl NotificationManager for NotificationManagerXdg {
70    async fn get_notification_permission_state(&self) -> Result<bool, crate::Error> {
71        log::info!(
72            "NotificationManagerXdg::get_notification_permission_state: not implemented yet"
73        );
74
75        Ok(true)
76    }
77
78    async fn first_time_ask_for_notification_permission(&self) -> Result<bool, crate::Error> {
79        log::info!(
80            "NotificationManagerXdg::first_time_ask_for_notification_permission: not implemented yet"
81        );
82        Ok(true)
83    }
84
85    fn register(
86        &self,
87        handler_callback: Box<dyn Fn(crate::NotificationResponse) + Send + Sync + 'static>,
88        categories: Vec<crate::NotificationCategory>,
89    ) -> Result<(), crate::Error> {
90        log::info!("NotificationManagerXdg::register {categories:?}");
91
92        let _ = self.handler.set(Arc::new(handler_callback));
93
94        // TODO categories? - though the rust notify library
95        // does not seem to implement replies anyways
96        // only buttons
97
98        Ok(())
99    }
100
101    fn remove_all_delivered_notifications(&self) -> Result<(), crate::Error> {
102        let mut active_notifications = self.active_notifications.try_write()?;
103        let removed_notifications = active_notifications.drain(..);
104
105        for notification in removed_notifications {
106            if let Some(handle) = notification.handle.try_write()?.take() {
107                handle.close();
108            } else {
109                log::error!("handle is not there anymore");
110            }
111        }
112
113        Ok(())
114    }
115
116    fn remove_delivered_notifications(&self, ids: Vec<&str>) -> Result<(), crate::Error> {
117        let mut active_notifications = self.active_notifications.try_write()?;
118        let all_notifications = active_notifications.drain(..);
119        let mut kept = Vec::new();
120        let mut removed = Vec::new();
121        for n in all_notifications {
122            if ids.contains(&n.id.as_str()) {
123                removed.push(n);
124            } else {
125                kept.push(n);
126            }
127        }
128        active_notifications.append(&mut kept);
129
130        for notification in removed {
131            if let Some(handle) = notification.handle.try_write()?.take() {
132                handle.close();
133            } else {
134                log::error!("handle is not there anymore");
135            }
136        }
137
138        Ok(())
139    }
140
141    async fn get_active_notifications(
142        &self,
143    ) -> Result<Vec<Box<dyn NotificationHandle>>, crate::Error> {
144        // can only get notification from active session for now
145        let active_notifications = self.active_notifications.read().await;
146        Ok(active_notifications
147            .clone()
148            .into_iter()
149            .map(|n| Box::new(n) as Box<dyn NotificationHandle>)
150            .collect())
151    }
152
153    async fn send_notification(
154        &self,
155        builder: NotificationBuilder,
156    ) -> Result<Box<dyn NotificationHandle>, crate::Error> {
157        log::info!("show notification {self:?}");
158        let id = uuid::Uuid::new_v4().to_string();
159
160        let mut notification = notify_rust::Notification::new();
161
162        // As said in the readme all notifications are persistent (TODO confirm it does what I expect on kde and gnome)
163        notification.hint(Hint::Urgency(notify_rust::Urgency::Normal));
164        notification.hint(Hint::Resident(true));
165        if let Some(xdg_app_name) = builder.xdg_app_name {
166            notification.appname(&xdg_app_name);
167        }
168
169        if let Some(body) = builder.body {
170            notification.body(&quick_xml::escape::escape(body));
171        }
172
173        if let Some(title) = builder.title {
174            notification.summary(&title);
175        }
176
177        // subtitles are not supported by xdg spec
178
179        if let Some(path) = builder.image {
180            match ImageReader::open(path) {
181                Err(error) => {
182                    log::error!("failed to load image: {error:?}");
183                }
184                Ok(img) => match img.decode() {
185                    Err(error) => {
186                        log::error!("failed to decode image: {error:?}");
187                    }
188                    Ok(img_data) => {
189                        let thumbnail = img_data.thumbnail(512, 512);
190                        match thumbnail.try_into() {
191                            Err(error) => log::error!("failed to convert image: {error:?}"),
192                            Ok(img) => {
193                                notification.hint(Hint::ImageData(img));
194                            }
195                        }
196                    }
197                },
198            }
199        }
200
201        if let Some(path) = builder.icon {
202            // untested
203            notification.icon(&format!("file://{}", path.display()));
204        } else {
205            notification.auto_icon();
206        }
207
208        if let Some(_thread_id) = builder.thread_id {
209            // not specified yet (as of first half of 2025, but it is planned)
210            // does not exist in xdg spec yet: https://github.com/flatpak/xdg-desktop-portal/discussions/1495
211        }
212
213        if let Some(_category_id) = builder.category_id {
214            // TODO add buttons acording to template
215            log::error!("buttons not implemented yet on linux");
216        }
217
218        if let Some(xdg_category) = builder.xdg_category {
219            notification.hint(Hint::Category(xdg_category.to_string()));
220        }
221
222        // if let Some(payload) = &builder.user_info {
223        //     // seems to not exist yet - TODO investigate
224        // }
225
226        notification
227            .urgency(Urgency::Normal)
228            .hint(Hint::Transient(false))
229            // default ation is needed otherwise the notification is not clickable
230            .action("default", "default");
231        //.action("open", "Open");
232
233        let notification_handle = notification.show_async().await?;
234
235        let user_info = builder.user_info.unwrap_or_default();
236
237        if let Some(handler) = self.handler.get() {
238            let handler_clone = handler.clone();
239            let notification_id = id.clone();
240            let cloned_user_info = user_info.clone();
241            // on_close and wait_for_action both consume notification_handle so we need to rely on this deprecated feature
242            handle_action(notification_handle.id(), move |action| {
243                let user_info = cloned_user_info.clone();
244                if let ActionResponse::Closed(reason) = action {
245                    match reason {
246                        CloseReason::Other(_) => {
247                            log::warn!("unhandles close reason {reason:?}")
248                        }
249                        CloseReason::Expired | CloseReason::CloseAction => { /* nothing */ }
250                        CloseReason::Dismissed => handler_clone(NotificationResponse {
251                            notification_id,
252                            action: crate::NotificationResponseAction::Dismiss,
253                            user_text: None,
254                            user_info,
255                        }),
256                    }
257                } else {
258                    handler_clone(NotificationResponse {
259                        notification_id,
260                        action: crate::NotificationResponseAction::Default,
261                        user_text: None,
262                        user_info,
263                    });
264                }
265            });
266        } else {
267            log::error!("no handler set");
268        }
269
270        let handle = NotificationHandleXdg {
271            id,
272            user_info,
273            handle: Arc::new(RwLock::new(Some(notification_handle))),
274        };
275
276        self.add_notification(handle.clone()).await;
277        Ok(Box::new(handle) as Box<dyn NotificationHandle>)
278    }
279}