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