use ratatui::style::Color;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NotificationType {
#[default]
Info,
Warning,
Error,
}
impl NotificationType {
fn duration(self) -> Option<Duration> {
match self {
NotificationType::Info => Some(Duration::from_millis(1500)),
NotificationType::Warning => Some(Duration::from_secs(10)),
NotificationType::Error => None, }
}
fn style(self) -> NotificationStyle {
match self {
NotificationType::Info => NotificationStyle {
fg: Color::White,
bg: Color::DarkGray,
border: Color::Gray,
},
NotificationType::Warning => NotificationStyle {
fg: Color::Black,
bg: Color::Yellow,
border: Color::Yellow,
},
NotificationType::Error => NotificationStyle {
fg: Color::White,
bg: Color::Red,
border: Color::LightRed,
},
}
}
}
#[derive(Debug, Clone)]
pub struct NotificationStyle {
pub fg: Color,
pub bg: Color,
pub border: Color,
}
impl Default for NotificationStyle {
fn default() -> Self {
NotificationType::Info.style()
}
}
#[derive(Debug, Clone)]
pub struct Notification {
pub message: String,
pub style: NotificationStyle,
#[allow(dead_code)]
pub notification_type: NotificationType,
pub created_at: Instant,
pub duration: Option<Duration>, }
impl Notification {
pub fn new(message: &str) -> Self {
Self::with_type(message, NotificationType::Info)
}
pub fn with_type(message: &str, notification_type: NotificationType) -> Self {
Self {
message: message.to_string(),
style: notification_type.style(),
notification_type,
created_at: Instant::now(),
duration: notification_type.duration(),
}
}
pub fn is_expired(&self) -> bool {
match self.duration {
Some(d) => self.created_at.elapsed() > d,
None => false, }
}
}
#[derive(Debug, Default)]
pub struct NotificationState {
pub current: Option<Notification>,
}
impl NotificationState {
pub fn new() -> Self {
Self::default()
}
pub fn show(&mut self, message: &str) {
self.current = Some(Notification::new(message));
}
pub fn show_with_type(&mut self, message: &str, notification_type: NotificationType) {
self.current = Some(Notification::with_type(message, notification_type));
}
pub fn show_warning(&mut self, message: &str) {
self.show_with_type(message, NotificationType::Warning);
}
#[allow(dead_code)]
pub fn show_error(&mut self, message: &str) {
self.show_with_type(message, NotificationType::Error);
}
#[allow(dead_code)]
pub fn dismiss(&mut self) {
self.current = None;
}
pub fn clear_if_expired(&mut self) -> bool {
if let Some(ref notif) = self.current
&& notif.is_expired()
{
self.current = None;
return true;
}
false
}
pub fn current(&self) -> Option<&Notification> {
self.current.as_ref()
}
#[cfg(test)]
pub fn current_message(&self) -> Option<&str> {
self.current.as_ref().map(|n| n.message.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_info_notification() {
let notif = Notification::new("Test message");
assert_eq!(notif.message, "Test message");
assert_eq!(notif.notification_type, NotificationType::Info);
assert_eq!(notif.duration, Some(Duration::from_millis(1500)));
assert_eq!(notif.style.fg, Color::White);
assert_eq!(notif.style.bg, Color::DarkGray);
assert!(!notif.is_expired());
}
#[test]
fn test_warning_notification() {
let notif = Notification::with_type("Warning!", NotificationType::Warning);
assert_eq!(notif.message, "Warning!");
assert_eq!(notif.notification_type, NotificationType::Warning);
assert_eq!(notif.duration, Some(Duration::from_secs(10)));
assert_eq!(notif.style.fg, Color::Black);
assert_eq!(notif.style.bg, Color::Yellow);
}
#[test]
fn test_error_notification() {
let notif = Notification::with_type("Error!", NotificationType::Error);
assert_eq!(notif.message, "Error!");
assert_eq!(notif.notification_type, NotificationType::Error);
assert_eq!(notif.duration, None); assert_eq!(notif.style.fg, Color::White);
assert_eq!(notif.style.bg, Color::Red);
}
#[test]
fn test_notification_expiration() {
let mut notif = Notification::new("Expiring");
notif.duration = Some(Duration::from_millis(10));
assert!(!notif.is_expired());
thread::sleep(Duration::from_millis(20));
assert!(notif.is_expired());
}
#[test]
fn test_notification_state_show() {
let mut state = NotificationState::new();
assert!(state.current().is_none());
state.show("Hello");
assert!(state.current().is_some());
assert_eq!(state.current_message(), Some("Hello"));
}
#[test]
fn test_notification_state_show_warning() {
let mut state = NotificationState::new();
state.show_warning("Config invalid");
let notif = state.current().unwrap();
assert_eq!(notif.message, "Config invalid");
assert_eq!(notif.notification_type, NotificationType::Warning);
assert_eq!(notif.duration, Some(Duration::from_secs(10)));
}
#[test]
fn test_notification_replacement() {
let mut state = NotificationState::new();
state.show("First");
assert_eq!(state.current_message(), Some("First"));
state.show("Second");
assert_eq!(state.current_message(), Some("Second"));
}
#[test]
fn test_clear_if_expired() {
let mut state = NotificationState::new();
state.show("Test");
if let Some(ref mut notif) = state.current {
notif.duration = Some(Duration::from_millis(10));
}
assert!(!state.clear_if_expired()); thread::sleep(Duration::from_millis(20));
assert!(state.clear_if_expired()); assert!(state.current().is_none());
}
#[test]
fn test_error_notification_never_expires() {
let mut state = NotificationState::new();
state.show_error("Critical error");
let notif = state.current().unwrap();
assert_eq!(notif.notification_type, NotificationType::Error);
assert_eq!(notif.duration, None);
assert!(!notif.is_expired()); assert!(!state.clear_if_expired()); assert!(state.current().is_some()); }
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_notification_replacement(messages in prop::collection::vec("[a-zA-Z0-9 ]{1,50}", 1..10)) {
let mut state = NotificationState::new();
for msg in &messages {
state.show(msg);
}
let last_message = messages.last().unwrap();
prop_assert_eq!(state.current_message(), Some(last_message.as_str()));
}
}
}