fast_rich/progress/
status.rs

1//! Status context for showing work in progress.
2
3use super::spinner::{Spinner, SpinnerStyle};
4use crate::style::Style;
5use std::io::{self, Write};
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8use std::thread;
9use std::time::Duration;
10
11/// A status indicator that shows a spinner while work is happening.
12///
13/// This is similar to Rich's `console.status()` context manager.
14pub struct Status {
15    /// The message text
16    message: Arc<Mutex<String>>,
17    /// Spinner style
18    spinner_style: SpinnerStyle,
19    /// Whether the status is currently running
20    running: Arc<AtomicBool>,
21    /// Handle to the spinner thread
22    thread_handle: Option<thread::JoinHandle<()>>,
23}
24
25impl Status {
26    /// Create a new status with a message.
27    pub fn new(message: &str) -> Self {
28        Status {
29            message: Arc::new(Mutex::new(message.to_string())),
30            spinner_style: SpinnerStyle::Dots,
31            running: Arc::new(AtomicBool::new(false)),
32            thread_handle: None,
33        }
34    }
35
36    /// Set the spinner style.
37    pub fn spinner_style(mut self, style: SpinnerStyle) -> Self {
38        self.spinner_style = style;
39        self
40    }
41
42    /// Set the spinner character style (no-op for simplified version).
43    pub fn style(self, _style: Style) -> Self {
44        self
45    }
46
47    /// Start the status display.
48    pub fn start(&mut self) {
49        if self.running.load(Ordering::SeqCst) {
50            return;
51        }
52
53        self.running.store(true, Ordering::SeqCst);
54
55        let running = self.running.clone();
56        let message = self.message.clone();
57        let spinner_style = self.spinner_style;
58
59        self.thread_handle = Some(thread::spawn(move || {
60            let spinner = Spinner::new("").style(spinner_style);
61            while running.load(Ordering::SeqCst) {
62                // Clear line and print spinner
63                let frame = spinner.current_frame();
64                let msg = message.lock().unwrap().clone();
65                print!("\r\x1B[K{} {}", frame, msg);
66                let _ = io::stdout().flush();
67
68                thread::sleep(Duration::from_millis(spinner_style.interval_ms()));
69            }
70
71            // Clear the status line when done
72            print!("\r\x1B[K");
73            let _ = io::stdout().flush();
74        }));
75    }
76
77    /// Stop the status display.
78    pub fn stop(&mut self) {
79        self.running.store(false, Ordering::SeqCst);
80
81        if let Some(handle) = self.thread_handle.take() {
82            let _ = handle.join();
83        }
84    }
85
86    /// Update the status message.
87    pub fn update(&mut self, message: &str) {
88        *self.message.lock().unwrap() = message.to_string();
89    }
90}
91
92impl Drop for Status {
93    fn drop(&mut self) {
94        self.stop();
95    }
96}
97
98/// A guard that displays a status while in scope.
99///
100/// This implements an RAII pattern similar to Python's context manager.
101pub struct StatusGuard {
102    status: Status,
103}
104
105impl StatusGuard {
106    /// Create and start a new status guard.
107    pub fn new(message: &str) -> Self {
108        let mut status = Status::new(message);
109        status.start();
110        StatusGuard { status }
111    }
112
113    /// Create with a custom spinner style.
114    #[allow(dead_code)]
115    pub fn with_style(message: &str, spinner_style: SpinnerStyle) -> Self {
116        let mut status = Status::new(message).spinner_style(spinner_style);
117        status.start();
118        StatusGuard { status }
119    }
120
121    /// Update the message.
122    #[allow(dead_code)]
123    pub fn update(&mut self, message: &str) {
124        self.status.update(message);
125    }
126}
127
128impl Drop for StatusGuard {
129    fn drop(&mut self) {
130        self.status.stop();
131    }
132}
133
134/// Convenience function to run work with a status spinner.
135///
136/// # Example
137///
138/// ```no_run
139/// use fast_rich::progress::with_status;
140///
141/// let result = with_status("Loading data...", || {
142///     // Do some work
143///     std::thread::sleep(std::time::Duration::from_secs(1));
144///     42
145/// });
146/// ```
147pub fn with_status<T, F: FnOnce() -> T>(message: &str, f: F) -> T {
148    let _guard = StatusGuard::new(message);
149    f()
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_status_new() {
158        let status = Status::new("Testing...");
159        assert!(!status.running.load(Ordering::SeqCst));
160    }
161
162    #[test]
163    fn test_status_guard_drop() {
164        // This mainly tests that drop doesn't panic
165        {
166            let _guard = StatusGuard::new("Test");
167            thread::sleep(Duration::from_millis(50));
168        }
169        // Guard should be dropped and cleaned up
170    }
171}