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        {
71            self.message = None;
72            self.set_at = None;
73        }
74        self.message.as_deref()
75    }
76
77    /// Get the current message without checking auto-clear.
78    ///
79    /// Use this when you don't want to mutate state.
80    #[must_use]
81    pub fn peek(&self) -> Option<&str> {
82        self.message.as_deref()
83    }
84
85    /// Check if there's an active message.
86    #[must_use]
87    pub const fn has_message(&self) -> bool {
88        self.message.is_some()
89    }
90
91    /// Get the message directly (for backwards compatibility).
92    ///
93    /// Returns a reference to the Option<String> without auto-clear logic.
94    #[must_use]
95    pub const fn as_option(&self) -> &Option<String> {
96        &self.message
97    }
98
99    /// Take the message, clearing it.
100    pub const fn take(&mut self) -> Option<String> {
101        self.set_at = None;
102        self.message.take()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use std::thread;
110
111    #[test]
112    fn test_status_message_set_clear() {
113        let mut status = StatusMessage::new();
114
115        assert!(!status.has_message());
116        assert!(status.peek().is_none());
117
118        status.set("Test message");
119        assert!(status.has_message());
120        assert_eq!(status.peek(), Some("Test message"));
121
122        status.clear();
123        assert!(!status.has_message());
124        assert!(status.peek().is_none());
125    }
126
127    #[test]
128    fn test_status_message_take() {
129        let mut status = StatusMessage::new();
130
131        status.set("Take me");
132        let taken = status.take();
133
134        assert_eq!(taken, Some("Take me".to_string()));
135        assert!(!status.has_message());
136    }
137
138    #[test]
139    fn test_status_message_auto_clear() {
140        let mut status = StatusMessage::with_auto_clear(Duration::from_millis(50));
141
142        status.set("Auto clear message");
143        assert!(status.message().is_some());
144
145        // Wait for auto-clear
146        thread::sleep(Duration::from_millis(60));
147        assert!(status.message().is_none());
148    }
149
150    #[test]
151    fn test_status_message_no_auto_clear_default() {
152        let mut status = StatusMessage::new();
153
154        status.set("No auto clear");
155        thread::sleep(Duration::from_millis(10));
156        assert!(status.message().is_some()); // Still there
157    }
158}