Skip to main content

vfa_tui/models/
notification.rs

1//! Notification model — toast messages displayed in the TUI overlay.
2
3use std::time::{Duration, Instant};
4
5/// A transient in-application notification rendered as a toast overlay.
6///
7/// Notifications auto-dismiss after their [`ttl`](Notification::ttl) has elapsed.
8/// `created_at` uses [`Instant`] rather than a wall-clock timestamp so the
9/// auto-dismiss logic can compare elapsed time without clock skew.
10#[derive(Debug, Clone)]
11pub struct Notification {
12    /// Human-readable message to display.
13    pub message: String,
14    /// How urgently this notification should be presented.
15    pub severity: NotificationSeverity,
16    /// Monotonic timestamp of when the notification was created.
17    pub created_at: Instant,
18    /// Duration after `created_at` at which the notification should be dismissed.
19    pub ttl: Duration,
20}
21
22impl Notification {
23    /// Create a notification that will auto-dismiss after `ttl`.
24    pub fn new(message: String, severity: NotificationSeverity, ttl: Duration) -> Self {
25        Self {
26            message,
27            severity,
28            created_at: Instant::now(),
29            ttl,
30        }
31    }
32
33    /// Returns `true` if the notification has lived past its TTL.
34    pub fn is_expired(&self) -> bool {
35        self.created_at.elapsed() >= self.ttl
36    }
37}
38
39/// Severity classification for [`Notification`] messages.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum NotificationSeverity {
42    Info,
43    Warning,
44    Error,
45    Success,
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn notification_severity_equality() {
54        assert_eq!(NotificationSeverity::Info, NotificationSeverity::Info);
55        assert_ne!(NotificationSeverity::Info, NotificationSeverity::Error);
56        assert_eq!(NotificationSeverity::Success, NotificationSeverity::Success);
57    }
58
59    #[test]
60    fn notification_new_sets_fields() {
61        let msg = "Catalog reloaded".to_string();
62        let ttl = Duration::from_secs(5);
63        let n = Notification::new(msg.clone(), NotificationSeverity::Success, ttl);
64
65        assert_eq!(n.message, msg);
66        assert_eq!(n.severity, NotificationSeverity::Success);
67        assert_eq!(n.ttl, ttl);
68        // Should not be expired immediately after construction.
69        assert!(!n.is_expired());
70    }
71
72    #[test]
73    fn notification_is_expired_with_zero_ttl() {
74        // A zero-TTL notification should be considered expired right away
75        // (elapsed >= 0 is always true).
76        let n = Notification::new(
77            "gone".to_string(),
78            NotificationSeverity::Info,
79            Duration::ZERO,
80        );
81        assert!(n.is_expired());
82    }
83
84    #[test]
85    fn notification_clone_preserves_severity() {
86        let n = Notification::new(
87            "test".to_string(),
88            NotificationSeverity::Warning,
89            Duration::from_secs(3),
90        );
91        let cloned = n.clone();
92        assert_eq!(cloned.severity, NotificationSeverity::Warning);
93        assert_eq!(cloned.message, "test");
94    }
95
96    #[test]
97    fn notification_debug_does_not_panic() {
98        let n = Notification::new(
99            "debug test".to_string(),
100            NotificationSeverity::Error,
101            Duration::from_secs(10),
102        );
103        let _ = format!("{:?}", n);
104    }
105}