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)]
15pub enum Urgency {
16 Low,
18 Normal,
20 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#[derive(Clone, Debug, Default)]
51pub struct Notification {
52 pub id: u32,
54 pub app_name: String,
56 pub summary: String,
58 pub body: String,
60 pub expire_timeout: Option<Duration>,
62 pub urgency: Urgency,
64 pub is_read: bool,
66 pub timestamp: u64,
68}
69
70impl Notification {
71 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 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 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#[derive(Clone, Debug, Deserialize, Serialize)]
128pub struct NotificationFilter {
129 #[serde(with = "serde_regex", default)]
131 pub app_name: Option<Regex>,
132 #[serde(with = "serde_regex", default)]
134 pub summary: Option<Regex>,
135 #[serde(with = "serde_regex", default)]
137 pub body: Option<Regex>,
138}
139
140#[derive(Clone, Debug, Default, Serialize)]
142struct Context<'a> {
143 pub app_name: &'a str,
145 pub summary: &'a str,
147 pub body: &'a str,
149 #[serde(rename = "urgency")]
151 pub urgency_text: String,
152 pub unread_count: usize,
154 pub timestamp: u64,
156}
157
158#[derive(Debug)]
160pub enum Action {
161 Show(Notification),
163 ShowLast,
165 Close(Option<u32>),
167 CloseAll,
169}
170
171#[derive(Debug)]
173pub struct Manager {
174 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 pub fn init() -> Self {
189 Self {
190 inner: Arc::new(RwLock::new(Vec::new())),
191 }
192 }
193
194 pub fn count(&self) -> usize {
196 self.inner
197 .read()
198 .expect("failed to retrieve notifications")
199 .len()
200 }
201
202 pub fn add(&self, notification: Notification) {
204 self.inner
205 .write()
206 .expect("failed to retrieve notifications")
207 .push(notification);
208 }
209
210 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 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 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 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 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 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 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}