spinach/
spinner.rs

1use std::cell::RefCell;
2use std::sync::mpsc::{channel, Sender, TryRecvError};
3use std::thread::{sleep, spawn, JoinHandle};
4use std::time::Duration;
5
6use crate::state::{State, Update};
7use crate::term;
8
9/// A Spinach spinner
10///
11/// Represents a spinner that can be used to show progress or activity.
12///
13/// # Examples
14///
15/// ```
16/// use spinach::Spinner;
17///
18/// let spinner = Spinner::new("Loading...").start();
19/// // Perform some tasks
20/// spinner.text("gg!").success();
21/// ```
22#[derive(Debug, Default, Clone)]
23pub struct Spinner<S> {
24    update: RefCell<Update>,
25    state: S,
26}
27
28/// Represents the stopped state of a spinner.
29#[derive(Debug)]
30pub struct Stopped;
31
32/// Represents the running state of a spinner.
33#[derive(Debug)]
34pub struct Running {
35    sender: Sender<Update>,
36    handle: RefCell<Option<JoinHandle<()>>>,
37}
38
39impl<S> Spinner<S> {
40    /// Sets the color of the spinner.
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// use spinach::{Spinner, Color};
46    ///
47    /// let spinner = Spinner::new("workin'...").color(Color::Blue).start();
48    /// ```
49    pub fn color(&self, color: term::Color) -> &Self {
50        self.update.borrow_mut().color = Some(color);
51        self
52    }
53
54    /// Sets the text displayed alongside the spinner.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use spinach::Spinner;
60    ///
61    /// let spinner = Spinner::new("workin'...").start();
62    /// ```
63    pub fn text(&self, text: &str) -> &Self {
64        self.update.borrow_mut().text = Some(text.to_string());
65        self
66    }
67
68    /// Sets the symbols used for the spinner animation.
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use spinach::Spinner;
74    ///
75    /// let spinner = Spinner::new("workin'...").symbols(vec!["◐", "◓", "◑", "◒"]).start();
76    /// ```
77    pub fn symbols(&self, symbols: Vec<&'static str>) -> &Self {
78        self.update.borrow_mut().symbols = Some(symbols);
79        self
80    }
81
82    /// Sets a single symbol for the spinner animation.
83    /// This is useful when you want to set a final symbol, for example.
84    ///
85    /// # Examples
86    ///
87    /// ```
88    /// use spinach::Spinner;
89    ///
90    /// let spinner = Spinner::new("workin'...").start().text("done!").symbol("✔").stop();
91    /// ```
92    pub fn symbol(&self, symbol: &'static str) -> &Self {
93        self.update.borrow_mut().symbols = Some(vec![symbol]);
94        self
95    }
96
97    /// Sets the duration of each frame in the spinner animation.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use spinach::Spinner;
103    ///
104    /// let spinner = Spinner::new("workin'...").frames_duration(40).start();
105    /// ```
106    pub fn frames_duration(&self, ms: u64) -> &Self {
107        self.update.borrow_mut().frames_duration_ms = Some(ms);
108        self
109    }
110}
111
112impl Spinner<Stopped> {
113    /// Creates a new spinner.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use spinach::Spinner;
119    ///
120    /// let spinner = Spinner::new("let's go...").start();
121    /// ```
122    #[must_use]
123    pub fn new(text: &str) -> Self {
124        Spinner {
125            update: RefCell::new(Update::new(text)),
126            state: Stopped,
127        }
128    }
129
130    /// Starts the spinner.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use spinach::Spinner;
136    ///
137    /// let spinner = Spinner::new("let's go...").start();
138    /// ```
139    pub fn start(&self) -> Spinner<Running> {
140        term::hide_cursor();
141        let (sender, receiver) = channel::<Update>();
142        let mut state = State::default();
143        state.update(self.update.take());
144        let handle = RefCell::new(Some(spawn(move || {
145            let mut iteration = 0;
146            loop {
147                match receiver.try_recv() {
148                    Ok(update) if update.stop => {
149                        state.update(update);
150                        if iteration >= state.symbols.len() {
151                            iteration = 0;
152                        }
153                        state.render(iteration);
154                        break;
155                    }
156                    Ok(update) => state.update(update),
157                    Err(TryRecvError::Disconnected) => break,
158                    Err(TryRecvError::Empty) => (),
159                }
160                if iteration >= state.symbols.len() {
161                    iteration = 0;
162                }
163                state.render(iteration);
164                iteration += 1;
165                sleep(Duration::from_millis(state.frames_duration_ms));
166            }
167            term::new_line();
168            term::show_cursor();
169        })));
170
171        Spinner {
172            update: RefCell::new(Update::default()),
173            state: Running { sender, handle },
174        }
175    }
176}
177
178impl Spinner<Running> {
179    /// Joins the spinner thread, stopping it.
180    fn join(&self) {
181        if let Some(handle) = self.state.handle.borrow_mut().take() {
182            _ = handle.join();
183        }
184    }
185
186    /// Updates the spinner with the current update state.
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// use spinach::Spinner;
192    ///
193    /// let spinner = Spinner::new("Doing something...").start();
194    /// // Perform some tasks
195    /// spinner.text("Doing something else...").update();
196    /// ```
197    pub fn update(&self) -> &Self {
198        _ = self.state.sender.send(self.update.borrow().clone());
199        self
200    }
201
202    /// Stops the spinner.
203    ///
204    /// # Examples
205    ///
206    /// ```
207    /// use spinach::Spinner;
208    ///
209    /// let spinner = Spinner::new("Doing something...").start();
210    /// // Perform some tasks
211    /// spinner.text("done!").stop();
212    /// ```
213    pub fn stop(&self) {
214        self.update.borrow_mut().stop = true;
215        self.update();
216        self.join();
217    }
218
219    /// Stops the spinner with a pre-configured success indication.
220    /// Sets the symbol and color.
221    ///
222    /// # Examples
223    ///
224    /// ```
225    /// use spinach::Spinner;
226    ///
227    /// let spinner = Spinner::new("Doing something...").start();
228    /// // Perform some task that succeeds
229    /// spinner.text("done!").success();
230    /// ```
231    pub fn success(&self) {
232        self.update.borrow_mut().color = Some(term::Color::Green);
233        self.update.borrow_mut().symbols = Some(vec!["✔"]);
234        self.stop();
235    }
236
237    /// Stops the spinner with a pre-configured failure indication.
238    /// Sets the symbol and color.
239    ///
240    /// # Examples
241    ///
242    /// ```
243    /// use spinach::Spinner;
244    ///
245    /// let spinner = Spinner::new("Doing something...").start();
246    /// // Perform some task that fails
247    /// spinner.text("oops").failure();
248    /// ```
249    pub fn failure(&self) {
250        self.update.borrow_mut().color = Some(term::Color::Red);
251        self.update.borrow_mut().symbols = Some(vec!["✖"]);
252        self.stop();
253    }
254
255    /// Stops the spinner with a pre-configured warning indication.
256    /// Sets the symbol and color.
257    ///
258    /// # Examples
259    ///
260    /// ```
261    /// use spinach::Spinner;
262    ///
263    /// let spinner = Spinner::new("Doing something...").start();
264    /// // Perform some task with unexpected results
265    /// spinner.text("wait, what?").warn();
266    /// ```
267    pub fn warn(&self) {
268        self.update.borrow_mut().color = Some(term::Color::Yellow);
269        self.update.borrow_mut().symbols = Some(vec!["⚠"]);
270        self.stop();
271    }
272}