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
10pub const NOTIFICATION_MESSAGE_TEMPLATE: &str = "notification_message_template";
12
13#[derive(Clone, Debug, Serialize, Default)]
15pub enum Urgency {
16 Low,
18 #[default]
20 Normal,
21 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#[derive(Clone, Debug, Default)]
46pub struct Notification {
47 pub id: u32,
49 pub app_name: String,
51 pub summary: String,
53 pub body: String,
55 pub expire_timeout: Option<Duration>,
57 pub urgency: Urgency,
59 pub is_read: bool,
61 pub timestamp: u64,
63}
64
65impl Notification {
66 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 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 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#[derive(Clone, Debug, Deserialize, Serialize)]
123pub struct NotificationFilter {
124 #[serde(with = "serde_regex", default)]
126 pub app_name: Option<Regex>,
127 #[serde(with = "serde_regex", default)]
129 pub summary: Option<Regex>,
130 #[serde(with = "serde_regex", default)]
132 pub body: Option<Regex>,
133}
134
135#[derive(Clone, Debug, Default, Serialize)]
137struct Context<'a> {
138 pub app_name: &'a str,
140 pub summary: &'a str,
142 pub body: &'a str,
144 #[serde(rename = "urgency")]
146 pub urgency_text: String,
147 pub unread_count: usize,
149 pub timestamp: u64,
151}
152
153#[derive(Debug)]
155pub enum Action {
156 Show(Notification),
158 ShowLast,
160 Close(Option<u32>),
162 CloseAll,
164}
165
166#[derive(Debug)]
168pub struct Manager {
169 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 pub fn init() -> Self {
184 Self {
185 inner: Arc::new(RwLock::new(Vec::new())),
186 }
187 }
188
189 pub fn count(&self) -> usize {
191 self.inner
192 .read()
193 .expect("failed to retrieve notifications")
194 .len()
195 }
196
197 pub fn add(&self, notification: Notification) {
199 self.inner
200 .write()
201 .expect("failed to retrieve notifications")
202 .push(notification);
203 }
204
205 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 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 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 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 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 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 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}