use serde::{Deserialize, Serialize};
use std::time::SystemTime;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum Urgency {
Low = 0,
Normal = 1,
Critical = 2,
}
impl From<u8> for Urgency {
fn from(val: u8) -> Self {
match val {
0 => Urgency::Low,
2 => Urgency::Critical,
_ => Urgency::Normal,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Notification {
pub(crate) id: u32,
pub(crate) app_name: String,
pub(crate) app_icon: String,
pub(crate) summary: String,
pub(crate) body: String,
pub(crate) actions: Vec<(String, String)>,
pub(crate) urgency: Urgency,
pub(crate) timeout_ms: i32,
pub(crate) timestamp: SystemTime,
pub(crate) read: bool,
pub(crate) desktop_entry: Option<String>,
}
pub(crate) fn clean_markup(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut in_tag = false;
for ch in text.chars() {
match ch {
'<' => in_tag = true,
'>' if in_tag => in_tag = false,
_ if !in_tag => result.push(ch),
_ => {}
}
}
result
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("'", "'")
.replace("'", "'")
}
pub(crate) fn parse_actions(flat: &[String]) -> Vec<(String, String)> {
flat.chunks(2)
.filter_map(|chunk| {
if chunk.len() == 2 {
Some((chunk[0].clone(), chunk[1].clone()))
} else {
None
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn urgency_from_u8() {
assert_eq!(Urgency::from(0), Urgency::Low);
assert_eq!(Urgency::from(1), Urgency::Normal);
assert_eq!(Urgency::from(2), Urgency::Critical);
assert_eq!(Urgency::from(255), Urgency::Normal);
}
#[test]
fn parse_actions_pairs() {
let flat = vec![
"reply".into(),
"Reply".into(),
"dismiss".into(),
"Dismiss".into(),
];
let actions = parse_actions(&flat);
assert_eq!(actions.len(), 2);
assert_eq!(actions[0], ("reply".into(), "Reply".into()));
}
#[test]
fn parse_actions_odd_length() {
let flat = vec!["only-one".into()];
let actions = parse_actions(&flat);
assert!(actions.is_empty());
}
#[test]
fn clean_markup_strips_tags() {
assert_eq!(clean_markup("<b>bold</b> text"), "bold text");
assert_eq!(
clean_markup(r#"<a href="http://example.com">link</a>"#),
"link"
);
assert_eq!(clean_markup("line1<br>line2"), "line1line2");
}
#[test]
fn clean_markup_decodes_entities() {
assert_eq!(clean_markup("a & b"), "a & b");
assert_eq!(clean_markup("<user@mail.com>"), "<user@mail.com>");
assert_eq!(clean_markup(""hello""), "\"hello\"");
assert_eq!(clean_markup("it's"), "it's");
}
#[test]
fn clean_markup_combined() {
assert_eq!(
clean_markup("From: <b>Alice</b> <alice@example.com>"),
"From: Alice <alice@example.com>"
);
}
#[test]
fn clean_markup_handles_nested_tags() {
assert_eq!(clean_markup("<b><i>foo</i></b>"), "foo");
assert_eq!(clean_markup("<b>x<i>y</i>z</b>"), "xyz");
}
#[test]
fn clean_markup_handles_interleaved_tags() {
assert_eq!(clean_markup("<b><i>foo</b></i>"), "foo");
assert_eq!(clean_markup("<a><b>x</a></b>tail"), "xtail");
}
#[test]
fn clean_markup_unmatched_lt_swallows_to_end() {
assert_eq!(clean_markup("hello < world"), "hello ");
assert_eq!(clean_markup("a < b > c"), "a c");
}
#[test]
fn clean_markup_passes_unsupported_numeric_entities_through() {
assert_eq!(clean_markup("a " b"), "a " b");
assert_eq!(clean_markup("a ' b"), "a ' b");
assert_eq!(clean_markup("a " b"), "a \" b");
assert_eq!(clean_markup("a ' b"), "a ' b");
}
}