envelope_cli/tui/widgets/
notification.rs

1//! Toast notification widget
2//!
3//! Displays temporary notifications to the user.
4
5use ratatui::{
6    buffer::Buffer,
7    layout::Rect,
8    style::{Color, Modifier, Style},
9    widgets::{Block, Borders, Clear, Paragraph, Widget},
10};
11
12/// Type of notification
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum NotificationType {
15    /// Informational message
16    Info,
17    /// Success message
18    Success,
19    /// Warning message
20    Warning,
21    /// Error message
22    Error,
23}
24
25impl NotificationType {
26    /// Get the color for this notification type
27    pub fn color(&self) -> Color {
28        match self {
29            Self::Info => Color::Blue,
30            Self::Success => Color::Green,
31            Self::Warning => Color::Yellow,
32            Self::Error => Color::Red,
33        }
34    }
35
36    /// Get the icon/prefix for this notification type
37    pub fn icon(&self) -> &'static str {
38        match self {
39            Self::Info => "i",
40            Self::Success => "+",
41            Self::Warning => "!",
42            Self::Error => "x",
43        }
44    }
45
46    /// Get the title for this notification type
47    pub fn title(&self) -> &'static str {
48        match self {
49            Self::Info => "Info",
50            Self::Success => "Success",
51            Self::Warning => "Warning",
52            Self::Error => "Error",
53        }
54    }
55}
56
57/// A toast notification
58#[derive(Debug, Clone)]
59pub struct Notification {
60    /// The notification message
61    pub message: String,
62    /// Type of notification
63    pub notification_type: NotificationType,
64    /// Time when notification was created (for auto-dismiss)
65    pub created_at: std::time::Instant,
66    /// Duration to display (in seconds)
67    pub duration_secs: u64,
68}
69
70impl Notification {
71    /// Create a new notification
72    pub fn new(message: impl Into<String>, notification_type: NotificationType) -> Self {
73        Self {
74            message: message.into(),
75            notification_type,
76            created_at: std::time::Instant::now(),
77            duration_secs: 3,
78        }
79    }
80
81    /// Create an info notification
82    pub fn info(message: impl Into<String>) -> Self {
83        Self::new(message, NotificationType::Info)
84    }
85
86    /// Create a success notification
87    pub fn success(message: impl Into<String>) -> Self {
88        Self::new(message, NotificationType::Success)
89    }
90
91    /// Create a warning notification
92    pub fn warning(message: impl Into<String>) -> Self {
93        Self::new(message, NotificationType::Warning)
94    }
95
96    /// Create an error notification
97    pub fn error(message: impl Into<String>) -> Self {
98        Self::new(message, NotificationType::Error)
99    }
100
101    /// Set the duration for this notification
102    pub fn with_duration(mut self, seconds: u64) -> Self {
103        self.duration_secs = seconds;
104        self
105    }
106
107    /// Check if the notification has expired
108    pub fn is_expired(&self) -> bool {
109        self.created_at.elapsed().as_secs() >= self.duration_secs
110    }
111
112    /// Get remaining time as a fraction (0.0 to 1.0)
113    pub fn remaining_fraction(&self) -> f64 {
114        let elapsed = self.created_at.elapsed().as_secs_f64();
115        let total = self.duration_secs as f64;
116        (1.0 - elapsed / total).clamp(0.0, 1.0)
117    }
118}
119
120/// Widget for rendering a notification
121pub struct NotificationWidget<'a> {
122    notification: &'a Notification,
123}
124
125impl<'a> NotificationWidget<'a> {
126    /// Create a new notification widget
127    pub fn new(notification: &'a Notification) -> Self {
128        Self { notification }
129    }
130}
131
132impl<'a> Widget for NotificationWidget<'a> {
133    fn render(self, area: Rect, buf: &mut Buffer) {
134        let color = self.notification.notification_type.color();
135        let icon = self.notification.notification_type.icon();
136        let title = self.notification.notification_type.title();
137
138        // Clear the area first
139        Clear.render(area, buf);
140
141        let block = Block::default()
142            .borders(Borders::ALL)
143            .border_style(Style::default().fg(color))
144            .title(format!(" {} {} ", icon, title))
145            .title_style(Style::default().fg(color).add_modifier(Modifier::BOLD));
146
147        let paragraph = Paragraph::new(self.notification.message.as_str())
148            .style(Style::default().fg(Color::White))
149            .block(block);
150
151        paragraph.render(area, buf);
152    }
153}
154
155/// A queue of notifications to display
156#[derive(Debug, Default)]
157pub struct NotificationQueue {
158    notifications: Vec<Notification>,
159}
160
161impl NotificationQueue {
162    /// Create a new notification queue
163    pub fn new() -> Self {
164        Self::default()
165    }
166
167    /// Add a notification to the queue
168    pub fn push(&mut self, notification: Notification) {
169        self.notifications.push(notification);
170    }
171
172    /// Remove expired notifications
173    pub fn remove_expired(&mut self) {
174        self.notifications.retain(|n| !n.is_expired());
175    }
176
177    /// Get the current notification to display (if any)
178    pub fn current(&self) -> Option<&Notification> {
179        self.notifications.first()
180    }
181
182    /// Check if there are any notifications
183    pub fn is_empty(&self) -> bool {
184        self.notifications.is_empty()
185    }
186
187    /// Get the number of notifications
188    pub fn len(&self) -> usize {
189        self.notifications.len()
190    }
191
192    /// Clear all notifications
193    pub fn clear(&mut self) {
194        self.notifications.clear();
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_notification_creation() {
204        let n = Notification::info("Test message");
205        assert_eq!(n.message, "Test message");
206        assert_eq!(n.notification_type, NotificationType::Info);
207    }
208
209    #[test]
210    fn test_notification_types() {
211        assert_eq!(NotificationType::Info.color(), Color::Blue);
212        assert_eq!(NotificationType::Success.color(), Color::Green);
213        assert_eq!(NotificationType::Warning.color(), Color::Yellow);
214        assert_eq!(NotificationType::Error.color(), Color::Red);
215    }
216
217    #[test]
218    fn test_notification_queue() {
219        let mut queue = NotificationQueue::new();
220        assert!(queue.is_empty());
221
222        queue.push(Notification::info("First"));
223        queue.push(Notification::success("Second"));
224
225        assert_eq!(queue.len(), 2);
226        assert_eq!(queue.current().unwrap().message, "First");
227    }
228}