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