Skip to main content

sbom_tools/tui/viewmodel/
status.rs

1//! Status message management for TUI views.
2//!
3//! Provides a unified way to display temporary status messages
4//! across both diff and view TUI modes.
5
6use std::time::{Duration, Instant};
7
8/// Manages temporary status messages with optional auto-clear.
9///
10/// Status messages are displayed briefly to notify users of actions
11/// (e.g., "Exported to file.json", "Copied to clipboard").
12///
13/// # Example
14///
15/// ```ignore
16/// use crate::tui::viewmodel::StatusMessage;
17///
18/// let mut status = StatusMessage::new();
19///
20/// status.set("File exported successfully");
21/// assert!(status.message().is_some());
22///
23/// status.clear();
24/// assert!(status.message().is_none());
25/// ```
26#[derive(Debug, Clone, Default)]
27pub struct StatusMessage {
28    /// The current message (if any)
29    message: Option<String>,
30    /// When the message was set (for auto-clear)
31    set_at: Option<Instant>,
32    /// Auto-clear duration (None = no auto-clear)
33    auto_clear_after: Option<Duration>,
34}
35
36impl StatusMessage {
37    /// Create a new status message manager.
38    #[must_use] 
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Create a status message manager with auto-clear after duration.
44    #[must_use] 
45    pub const fn with_auto_clear(duration: Duration) -> Self {
46        Self {
47            message: None,
48            set_at: None,
49            auto_clear_after: Some(duration),
50        }
51    }
52
53    /// Set a status message.
54    pub fn set(&mut self, msg: impl Into<String>) {
55        self.message = Some(msg.into());
56        self.set_at = Some(Instant::now());
57    }
58
59    /// Clear the status message.
60    pub fn clear(&mut self) {
61        self.message = None;
62        self.set_at = None;
63    }
64
65    /// Get the current message (checking auto-clear if configured).
66    pub fn message(&mut self) -> Option<&str> {
67        // Check auto-clear
68        if let (Some(set_at), Some(duration)) = (self.set_at, self.auto_clear_after)
69            && set_at.elapsed() >= duration {
70                self.message = None;
71                self.set_at = None;
72            }
73        self.message.as_deref()
74    }
75
76    /// Get the current message without checking auto-clear.
77    ///
78    /// Use this when you don't want to mutate state.
79    #[must_use] 
80    pub fn peek(&self) -> Option<&str> {
81        self.message.as_deref()
82    }
83
84    /// Check if there's an active message.
85    #[must_use] 
86    pub const fn has_message(&self) -> bool {
87        self.message.is_some()
88    }
89
90    /// Get the message directly (for backwards compatibility).
91    ///
92    /// Returns a reference to the Option<String> without auto-clear logic.
93    #[must_use] 
94    pub const fn as_option(&self) -> &Option<String> {
95        &self.message
96    }
97
98    /// Take the message, clearing it.
99    pub const fn take(&mut self) -> Option<String> {
100        self.set_at = None;
101        self.message.take()
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use std::thread;
109
110    #[test]
111    fn test_status_message_set_clear() {
112        let mut status = StatusMessage::new();
113
114        assert!(!status.has_message());
115        assert!(status.peek().is_none());
116
117        status.set("Test message");
118        assert!(status.has_message());
119        assert_eq!(status.peek(), Some("Test message"));
120
121        status.clear();
122        assert!(!status.has_message());
123        assert!(status.peek().is_none());
124    }
125
126    #[test]
127    fn test_status_message_take() {
128        let mut status = StatusMessage::new();
129
130        status.set("Take me");
131        let taken = status.take();
132
133        assert_eq!(taken, Some("Take me".to_string()));
134        assert!(!status.has_message());
135    }
136
137    #[test]
138    fn test_status_message_auto_clear() {
139        let mut status = StatusMessage::with_auto_clear(Duration::from_millis(50));
140
141        status.set("Auto clear message");
142        assert!(status.message().is_some());
143
144        // Wait for auto-clear
145        thread::sleep(Duration::from_millis(60));
146        assert!(status.message().is_none());
147    }
148
149    #[test]
150    fn test_status_message_no_auto_clear_default() {
151        let mut status = StatusMessage::new();
152
153        status.set("No auto clear");
154        thread::sleep(Duration::from_millis(10));
155        assert!(status.message().is_some()); // Still there
156    }
157}