Skip to main content

rich/
status.rs

1//! A status indicator with a spinner.
2//!
3//! Port of `rich/status.py` (the renderable surface). A [`Status`] shows a
4//! spinner animation followed by a status message. Upstream drives it with a
5//! `Live` loop; here the renderable shows the first frame (`t = 0`), and the
6//! live animation lands with the Live-loop work (see the Live/progress issue).
7
8use crate::console::{Console, ConsoleOptions};
9use crate::protocol::Renderable;
10use crate::segment::Segment;
11use crate::spinner::Spinner;
12use crate::style::Style;
13
14/// A spinner + message status indicator. Mirrors `rich.status.Status`.
15pub struct Status {
16    message: String,
17    spinner: String,
18    spinner_style: Style,
19    speed: f64,
20}
21
22impl Status {
23    /// A status showing `message` with the default `dots` spinner (green).
24    pub fn new(message: impl Into<String>) -> Self {
25        Status {
26            message: message.into(),
27            spinner: "dots".to_string(),
28            spinner_style: Style::parse("green").expect("valid built-in style"),
29            speed: 1.0,
30        }
31    }
32
33    /// Choose the spinner animation by name (default `dots`).
34    pub fn spinner(mut self, name: impl Into<String>) -> Self {
35        self.spinner = name.into();
36        self
37    }
38
39    /// Style applied to the spinner frame (default `status.spinner` = green).
40    pub fn spinner_style(mut self, style: Style) -> Self {
41        self.spinner_style = style;
42        self
43    }
44
45    /// Set the spinner animation speed multiplier (default 1.0).
46    pub fn speed(mut self, speed: f64) -> Self {
47        self.speed = speed;
48        self
49    }
50
51    /// The underlying spinner. Mirrors upstream's `Status.renderable`.
52    pub fn renderable(&self) -> Spinner {
53        Spinner::new(&self.spinner)
54            .text(&self.message)
55            .style(self.spinner_style.clone())
56            .speed(self.speed)
57    }
58}
59
60impl Renderable for Status {
61    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
62        // Static first frame; the live animation needs the Live loop.
63        self.renderable().render(0.0).rich_render(console, options)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::color::ColorSystem;
71
72    fn render(status: &Status) -> String {
73        Console::builder()
74            .force_terminal(true)
75            .color_system(Some(ColorSystem::Truecolor))
76            .width(30)
77            .no_color(false)
78            .build()
79            .render_to_string(status)
80    }
81
82    #[test]
83    fn default_status_frame() {
84        // Captured from real rich 15.0.0 (dots spinner, status.spinner=green, t=0).
85        assert_eq!(
86            render(&Status::new("Loading data")),
87            "\x1b[32m⠋\x1b[0m Loading data"
88        );
89    }
90
91    #[test]
92    fn custom_spinner() {
93        assert_eq!(
94            render(&Status::new("Building").spinner("line")),
95            "\x1b[32m-\x1b[0m Building"
96        );
97    }
98}