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, parsed as console markup.
5//! Upstream drives it with its own `Live` loop; here a `Status` is a
6//! renderable whose spinner reads the console's clock
7//! ([`Console::get_time`]), so it animates when redrawn by a
8//! [`Live`](crate::live::Live) display. [`Status::renderable`] rendered at a
9//! point in time is the testable surface, and
10//! [`Status::update`] follows upstream: a new spinner name replaces the
11//! spinner (restarting its animation), anything else updates it in place.
12
13use crate::console::{Console, ConsoleOptions};
14use crate::measure::Measurement;
15use crate::protocol::Renderable;
16use crate::segment::Segment;
17use crate::spinner::Spinner;
18use crate::style::StyleType;
19
20/// A spinner + message status indicator. Mirrors `rich.status.Status`.
21pub struct Status {
22    status: String,
23    spinner_style: StyleType,
24    speed: f64,
25    spinner: Spinner,
26}
27
28impl Status {
29    /// A status showing `message` with the default `dots` spinner, styled
30    /// `status.spinner` (green in the default theme).
31    pub fn new(message: impl Into<String>) -> Self {
32        let status = message.into();
33        let spinner_style = StyleType::Name("status.spinner".to_string());
34        Status {
35            spinner: Spinner::new("dots")
36                .text(status.clone())
37                .style(spinner_style.clone()),
38            status,
39            spinner_style,
40            speed: 1.0,
41        }
42    }
43
44    fn rebuild(mut self, name: &str) -> Self {
45        self.spinner = Spinner::new(name)
46            .text(self.status.clone())
47            .style(self.spinner_style.clone())
48            .speed(self.speed);
49        self
50    }
51
52    /// Choose the spinner animation by name (default `dots`).
53    pub fn spinner(self, name: &str) -> Self {
54        self.rebuild(name)
55    }
56
57    /// Style applied to the spinner frame (default `status.spinner`).
58    pub fn spinner_style(mut self, style: impl Into<StyleType>) -> Self {
59        self.spinner_style = style.into();
60        self.spinner = self.spinner.style(self.spinner_style.clone());
61        self
62    }
63
64    /// Set the spinner animation speed multiplier (default 1.0).
65    pub fn speed(mut self, speed: f64) -> Self {
66        self.speed = speed;
67        self.spinner = self.spinner.speed(speed);
68        self
69    }
70
71    /// Port of `Status.update`. `None` (and, as upstream, a zero speed) leaves
72    /// a field unchanged. A new spinner name builds a fresh spinner; otherwise
73    /// the current one is updated in place, a speed change continuing from its
74    /// current frame.
75    pub fn update(
76        &mut self,
77        status: Option<&str>,
78        spinner: Option<&str>,
79        spinner_style: Option<StyleType>,
80        speed: Option<f64>,
81    ) {
82        if let Some(status) = status {
83            self.status = status.to_string();
84        }
85        if let Some(style) = spinner_style {
86            self.spinner_style = style;
87        }
88        if let Some(speed) = speed.filter(|s| *s != 0.0) {
89            self.speed = speed;
90        }
91        if let Some(name) = spinner {
92            self.spinner = Spinner::new(name)
93                .text(self.status.clone())
94                .style(self.spinner_style.clone())
95                .speed(self.speed);
96        } else {
97            self.spinner.update(
98                Some(&self.status),
99                Some(self.spinner_style.clone()),
100                Some(self.speed),
101            );
102        }
103    }
104
105    /// The underlying spinner. Mirrors upstream's `Status.renderable`.
106    pub fn renderable(&self) -> &Spinner {
107        &self.spinner
108    }
109}
110
111/// Upstream's `Status.__rich__` returns the spinner, so a status renders (and
112/// measures) exactly as its spinner: the frame for the console's clock.
113impl Renderable for Status {
114    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
115        self.spinner.rich_render(console, options)
116    }
117
118    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
119        self.spinner.measure(console, options)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::color::ColorSystem;
127
128    fn render(status: &Status) -> String {
129        Console::builder()
130            .force_terminal(true)
131            .color_system(Some(ColorSystem::Truecolor))
132            .width(30)
133            .no_color(false)
134            .build()
135            .render_to_string(status)
136    }
137
138    #[test]
139    fn default_status_frame() {
140        // Captured from real rich 15.0.0 (dots spinner, status.spinner=green, t=0).
141        assert_eq!(
142            render(&Status::new("Loading data")),
143            "\x1b[32m⠋\x1b[0m Loading data"
144        );
145    }
146
147    #[test]
148    fn custom_spinner() {
149        assert_eq!(
150            render(&Status::new("Building").spinner("line")),
151            "\x1b[32m-\x1b[0m Building"
152        );
153    }
154}