use crate::error::{Error, Result};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::error::Error as StdError;
use std::fmt::Display;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use tera::{Context as TeraContext, Tera};
pub const NOTIFICATION_MESSAGE_TEMPLATE: &str = "notification_message_template";
#[derive(Clone, Debug, Serialize)]
pub enum Urgency {
Low,
Normal,
Critical,
}
impl Display for Urgency {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", format!("{self:?}").to_lowercase())
}
}
impl From<u64> for Urgency {
fn from(value: u64) -> Self {
match value {
0 => Self::Low,
1 => Self::Normal,
2 => Self::Critical,
_ => Self::default(),
}
}
}
impl Default for Urgency {
fn default() -> Self {
Self::Normal
}
}
#[derive(Clone, Debug, Default)]
pub struct Notification {
pub id: u32,
pub app_name: String,
pub summary: String,
pub body: String,
pub expire_timeout: Option<Duration>,
pub urgency: Urgency,
pub is_read: bool,
pub timestamp: u64,
}
impl Notification {
pub fn into_context(&self, urgency_text: String, unread_count: usize) -> Result<TeraContext> {
Ok(TeraContext::from_serialize(Context {
app_name: &self.app_name,
summary: &self.summary,
body: &self.body,
urgency_text,
unread_count,
timestamp: self.timestamp,
})?)
}
pub fn render_message(
&self,
template: &Tera,
urgency_text: Option<String>,
unread_count: usize,
) -> Result<String> {
match template.render(
NOTIFICATION_MESSAGE_TEMPLATE,
&self.into_context(
urgency_text.unwrap_or_else(|| self.urgency.to_string()),
unread_count,
)?,
) {
Ok(v) => Ok(v),
Err(e) => {
if let Some(error_source) = e.source() {
Err(Error::TemplateRender(error_source.to_string()))
} else {
Err(Error::Template(e))
}
}
}
}
pub fn matches_filter(&self, filter: &NotificationFilter) -> bool {
macro_rules! check_filter {
($field: ident) => {
if let Some($field) = &filter.$field {
if !$field.is_match(&self.$field) {
return false;
}
}
};
}
check_filter!(app_name);
check_filter!(summary);
check_filter!(body);
true
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct NotificationFilter {
#[serde(with = "serde_regex", default)]
pub app_name: Option<Regex>,
#[serde(with = "serde_regex", default)]
pub summary: Option<Regex>,
#[serde(with = "serde_regex", default)]
pub body: Option<Regex>,
}
#[derive(Clone, Debug, Default, Serialize)]
struct Context<'a> {
pub app_name: &'a str,
pub summary: &'a str,
pub body: &'a str,
#[serde(rename = "urgency")]
pub urgency_text: String,
pub unread_count: usize,
pub timestamp: u64,
}
#[derive(Debug)]
pub enum Action {
Show(Notification),
ShowLast,
Close(Option<u32>),
CloseAll,
}
#[derive(Debug)]
pub struct Manager {
inner: Arc<RwLock<Vec<Notification>>>,
}
impl Clone for Manager {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl Manager {
pub fn init() -> Self {
Self {
inner: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn count(&self) -> usize {
self.inner
.read()
.expect("failed to retrieve notifications")
.len()
}
pub fn counts(&self) -> (usize, usize) {
let normal = self.all_unread().len();
let notifications = self.inner.read().expect("failed to retrieve notifications");
let urgent = notifications
.iter()
.filter(|v| !v.is_read && matches!(v.urgency, Urgency::Critical))
.count();
(normal, urgent)
}
pub fn add(&self, notification: Notification) {
self.inner
.write()
.expect("failed to retrieve notifications")
.push(notification);
}
pub fn all_unread(&self) -> Vec<Notification> {
let notifications = self.inner.read().expect("failed to retrieve notifications");
let notifications = notifications
.iter()
.filter(|v| !v.is_read)
.cloned()
.collect::<Vec<Notification>>();
notifications
}
pub fn get_last_unread(&self) -> Notification {
let notifications = self.inner.read().expect("failed to retrieve notifications");
let notifications = notifications
.iter()
.filter(|v| !v.is_read)
.collect::<Vec<&Notification>>();
notifications[notifications.len() - 1].clone()
}
pub fn mark_last_as_read(&self) {
let mut notifications = self
.inner
.write()
.expect("failed to retrieve notifications");
if let Some(notification) = notifications.iter_mut().filter(|v| !v.is_read).last() {
notification.is_read = true;
}
}
pub fn mark_next_as_unread(&self) -> bool {
let mut notifications = self
.inner
.write()
.expect("failed to retrieve notifications");
let last_unread_index = notifications.iter_mut().position(|v| !v.is_read);
if last_unread_index.is_none() {
let len = notifications.len();
notifications[len - 1].is_read = false;
}
if let Some(index) = last_unread_index {
notifications[index].is_read = true;
if index > 0 {
notifications[index - 1].is_read = false;
} else {
return false;
}
}
true
}
pub fn mark_as_read(&self, id: u32) {
let mut notifications = self
.inner
.write()
.expect("failed to retrieve notifications");
if let Some(notification) = notifications
.iter_mut()
.find(|notification| notification.id == id)
{
notification.is_read = true;
}
}
pub fn mark_all_as_read(&self) {
let mut notifications = self
.inner
.write()
.expect("failed to retrieve notifications");
notifications.iter_mut().for_each(|v| v.is_read = true);
}
pub fn get_unread_count(&self) -> usize {
let notifications = self.inner.read().expect("failed to retrieve notifications");
notifications.iter().filter(|v| !v.is_read).count()
}
pub fn is_unread(&self, id: u32) -> bool {
let notifications = self.inner.read().expect("failed to retrieve notifications");
notifications
.iter()
.find(|notification| notification.id == id)
.map(|v| !v.is_read)
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_notification_filter() {
let notification = Notification {
app_name: String::from("app"),
summary: String::from("test"),
body: String::from("this is a test notification"),
..Default::default()
};
assert!(notification.matches_filter(&NotificationFilter {
app_name: Regex::new("app").ok(),
summary: None,
body: None,
}));
assert!(notification.matches_filter(&NotificationFilter {
app_name: None,
summary: Regex::new("te*").ok(),
body: None,
}));
assert!(notification.matches_filter(&NotificationFilter {
app_name: None,
summary: None,
body: Regex::new("notification").ok(),
}));
assert!(notification.matches_filter(&NotificationFilter {
app_name: Regex::new("app").ok(),
summary: Regex::new("test").ok(),
body: Regex::new("notification").ok(),
}));
assert!(notification.matches_filter(&NotificationFilter {
app_name: None,
summary: None,
body: None,
}));
assert!(!notification.matches_filter(&NotificationFilter {
app_name: Regex::new("xxx").ok(),
summary: None,
body: Regex::new("yyy").ok(),
}));
assert!(!notification.matches_filter(&NotificationFilter {
app_name: Regex::new("xxx").ok(),
summary: Regex::new("aaa").ok(),
body: Regex::new("yyy").ok(),
}));
assert!(!notification.matches_filter(&NotificationFilter {
app_name: Regex::new("app").ok(),
summary: Regex::new("invalid").ok(),
body: Regex::new("regex").ok(),
}));
}
}