runst/
notification.rs

1use crate::error::{Error, Result};
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use std::error::Error as StdError;
5use std::fmt::Display;
6use std::sync::{Arc, RwLock};
7use std::time::Duration;
8use tera::{Context as TeraContext, Tera};
9
10/// Name of the template for rendering the notification message.
11pub const NOTIFICATION_MESSAGE_TEMPLATE: &str = "notification_message_template";
12
13/// Possible urgency levels for the notification.
14#[derive(Clone, Debug, Serialize, Default)]
15pub enum Urgency {
16    /// Low urgency.
17    Low,
18    /// Normal urgency (default).
19    #[default]
20    Normal,
21    /// Critical urgency.
22    Critical,
23}
24
25impl Display for Urgency {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(f, "{}", format!("{self:?}").to_lowercase())
28    }
29}
30
31impl From<u64> for Urgency {
32    fn from(value: u64) -> Self {
33        match value {
34            0 => Self::Low,
35            1 => Self::Normal,
36            2 => Self::Critical,
37            _ => Self::default(),
38        }
39    }
40}
41
42/// Representation of a notification.
43///
44/// See [D-Bus Notify Parameters](https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html)
45#[derive(Clone, Debug, Default)]
46pub struct Notification {
47    /// The optional notification ID.
48    pub id: u32,
49    /// Name of the application that sends the notification.
50    pub app_name: String,
51    /// Summary text.
52    pub summary: String,
53    /// Body.
54    pub body: String,
55    /// The timeout time in milliseconds.
56    pub expire_timeout: Option<Duration>,
57    /// Urgency.
58    pub urgency: Urgency,
59    /// Whether if the notification is read.
60    pub is_read: bool,
61    /// Timestamp that the notification is created.
62    pub timestamp: u64,
63}
64
65impl Notification {
66    /// Converts [`Notification`] into [`TeraContext`].
67    pub fn into_context(&self, urgency_text: String, unread_count: usize) -> Result<TeraContext> {
68        Ok(TeraContext::from_serialize(Context {
69            app_name: &self.app_name,
70            summary: &self.summary,
71            body: &self.body,
72            urgency_text,
73            unread_count,
74            timestamp: self.timestamp,
75        })?)
76    }
77
78    /// Renders the notification message using the given template.
79    pub fn render_message(
80        &self,
81        template: &Tera,
82        urgency_text: Option<String>,
83        unread_count: usize,
84    ) -> Result<String> {
85        match template.render(
86            NOTIFICATION_MESSAGE_TEMPLATE,
87            &self.into_context(
88                urgency_text.unwrap_or_else(|| self.urgency.to_string()),
89                unread_count,
90            )?,
91        ) {
92            Ok(v) => Ok::<String, Error>(v),
93            Err(e) => {
94                if let Some(error_source) = e.source() {
95                    Err(Error::TemplateRender(error_source.to_string()))
96                } else {
97                    Err(Error::Template(e))
98                }
99            }
100        }
101    }
102
103    /// Returns true if the given filter matches the notification message.
104    pub fn matches_filter(&self, filter: &NotificationFilter) -> bool {
105        macro_rules! check_filter {
106            ($field: ident) => {
107                if let Some($field) = &filter.$field {
108                    if !$field.is_match(&self.$field) {
109                        return false;
110                    }
111                }
112            };
113        }
114        check_filter!(app_name);
115        check_filter!(summary);
116        check_filter!(body);
117        true
118    }
119}
120
121/// Notification message filter.
122#[derive(Clone, Debug, Deserialize, Serialize)]
123pub struct NotificationFilter {
124    /// Name of the application.
125    #[serde(with = "serde_regex", default)]
126    pub app_name: Option<Regex>,
127    /// Summary text.
128    #[serde(with = "serde_regex", default)]
129    pub summary: Option<Regex>,
130    /// Body.
131    #[serde(with = "serde_regex", default)]
132    pub body: Option<Regex>,
133}
134
135/// Template context for the notification.
136#[derive(Clone, Debug, Default, Serialize)]
137struct Context<'a> {
138    /// Name of the application that sends the notification.
139    pub app_name: &'a str,
140    /// Summary text.
141    pub summary: &'a str,
142    /// Body.
143    pub body: &'a str,
144    /// Urgency.
145    #[serde(rename = "urgency")]
146    pub urgency_text: String,
147    /// Count of unread notifications.
148    pub unread_count: usize,
149    /// Timestamp of the notification.
150    pub timestamp: u64,
151}
152
153/// Possible actions for a notification.
154#[derive(Debug)]
155pub enum Action {
156    /// Show a notification.
157    Show(Notification),
158    /// Show the last notification.
159    ShowLast,
160    /// Close a notification.
161    Close(Option<u32>),
162    /// Close all the notifications.
163    CloseAll,
164}
165
166/// Notification manager.
167#[derive(Debug)]
168pub struct Manager {
169    /// Inner type that holds the notifications in thread-safe way.
170    inner: Arc<RwLock<Vec<Notification>>>,
171}
172
173impl Clone for Manager {
174    fn clone(&self) -> Self {
175        Self {
176            inner: Arc::clone(&self.inner),
177        }
178    }
179}
180
181impl Manager {
182    /// Initializes the notification manager.
183    pub fn init() -> Self {
184        Self {
185            inner: Arc::new(RwLock::new(Vec::new())),
186        }
187    }
188
189    /// Returns the number of notifications.
190    pub fn count(&self) -> usize {
191        self.inner
192            .read()
193            .expect("failed to retrieve notifications")
194            .len()
195    }
196
197    /// Adds a new notifications to manage.
198    pub fn add(&self, notification: Notification) {
199        self.inner
200            .write()
201            .expect("failed to retrieve notifications")
202            .push(notification);
203    }
204
205    /// Returns the last unread notification.
206    pub fn get_last_unread(&self) -> Notification {
207        let notifications = self.inner.read().expect("failed to retrieve notifications");
208        let notifications = notifications
209            .iter()
210            .filter(|v| !v.is_read)
211            .collect::<Vec<&Notification>>();
212        notifications[notifications.len() - 1].clone()
213    }
214
215    /// Marks the last notification as read.
216    pub fn mark_last_as_read(&self) {
217        let mut notifications = self
218            .inner
219            .write()
220            .expect("failed to retrieve notifications");
221        if let Some(notification) = notifications.iter_mut().filter(|v| !v.is_read).last() {
222            notification.is_read = true;
223        }
224    }
225
226    /// Marks the next notification as unread starting from the first one.
227    ///
228    /// Returns true if there is an unread notification remaining.
229    pub fn mark_next_as_unread(&self) -> bool {
230        let mut notifications = self
231            .inner
232            .write()
233            .expect("failed to retrieve notifications");
234        let last_unread_index = notifications.iter_mut().position(|v| !v.is_read);
235        if last_unread_index.is_none() {
236            let len = notifications.len();
237            notifications[len - 1].is_read = false;
238        }
239        if let Some(index) = last_unread_index {
240            notifications[index].is_read = true;
241            if index > 0 {
242                notifications[index - 1].is_read = false;
243            } else {
244                return false;
245            }
246        }
247        true
248    }
249
250    /// Marks the given notification as read.
251    pub fn mark_as_read(&self, id: u32) {
252        let mut notifications = self
253            .inner
254            .write()
255            .expect("failed to retrieve notifications");
256        if let Some(notification) = notifications
257            .iter_mut()
258            .find(|notification| notification.id == id)
259        {
260            notification.is_read = true;
261        }
262    }
263
264    /// Marks all the notifications as read.
265    pub fn mark_all_as_read(&self) {
266        let mut notifications = self
267            .inner
268            .write()
269            .expect("failed to retrieve notifications");
270        notifications.iter_mut().for_each(|v| v.is_read = true);
271    }
272
273    /// Returns the number of unread notifications.
274    pub fn get_unread_count(&self) -> usize {
275        let notifications = self.inner.read().expect("failed to retrieve notifications");
276        notifications.iter().filter(|v| !v.is_read).count()
277    }
278
279    /// Returns true if the notification is unread.
280    pub fn is_unread(&self, id: u32) -> bool {
281        let notifications = self.inner.read().expect("failed to retrieve notifications");
282        notifications
283            .iter()
284            .find(|notification| notification.id == id)
285            .map(|v| !v.is_read)
286            .unwrap_or_default()
287    }
288}
289#[cfg(test)]
290mod tests {
291    use super::*;
292    #[test]
293    fn test_notification_filter() {
294        let notification = Notification {
295            app_name: String::from("app"),
296            summary: String::from("test"),
297            body: String::from("this is a test notification"),
298            ..Default::default()
299        };
300        assert!(notification.matches_filter(&NotificationFilter {
301            app_name: Regex::new("app").ok(),
302            summary: None,
303            body: None,
304        }));
305        assert!(notification.matches_filter(&NotificationFilter {
306            app_name: None,
307            summary: Regex::new("tes*").ok(),
308            body: None,
309        }));
310        assert!(notification.matches_filter(&NotificationFilter {
311            app_name: None,
312            summary: None,
313            body: Regex::new("notification").ok(),
314        }));
315        assert!(notification.matches_filter(&NotificationFilter {
316            app_name: Regex::new("app").ok(),
317            summary: Regex::new("test").ok(),
318            body: Regex::new("notification").ok(),
319        }));
320        assert!(notification.matches_filter(&NotificationFilter {
321            app_name: None,
322            summary: None,
323            body: None,
324        }));
325        assert!(!notification.matches_filter(&NotificationFilter {
326            app_name: Regex::new("xxx").ok(),
327            summary: None,
328            body: Regex::new("yyy").ok(),
329        }));
330        assert!(!notification.matches_filter(&NotificationFilter {
331            app_name: Regex::new("xxx").ok(),
332            summary: Regex::new("aaa").ok(),
333            body: Regex::new("yyy").ok(),
334        }));
335        assert!(!notification.matches_filter(&NotificationFilter {
336            app_name: Regex::new("app").ok(),
337            summary: Regex::new("invalid").ok(),
338            body: Regex::new("regex").ok(),
339        }));
340    }
341}