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}