1#![allow(missing_docs)]
2use crate::styles::{paint, DIM, WHITE};
20use std::io::Write;
21use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
22use std::sync::{Arc, Mutex};
23use std::thread::{self, JoinHandle};
24use std::time::{Duration, Instant};
25
26const FRAMES: &[&str] = &["◒", "◐", "◓", "◑"];
27const FRAME_DELAY: Duration = Duration::from_millis(80);
28
29const STEP_SUBMIT: &str = "◇";
30const STEP_CANCEL: &str = "■";
31const STEP_ERROR: &str = "▲";
32
33#[derive(Clone, Copy, PartialEq, Eq)]
34pub enum Indicator {
35 Dots,
36 Timer,
37}
38
39struct Inner {
40 message: Mutex<String>,
41 running: AtomicBool,
42 cancelled: AtomicBool,
43 indicator: AtomicU8, }
45
46pub struct Spinner {
48 inner: Arc<Inner>,
49 handle: Mutex<Option<JoinHandle<()>>>,
50 started: AtomicBool,
51}
52
53pub fn spinner() -> Spinner {
55 Spinner {
56 inner: Arc::new(Inner {
57 message: Mutex::new(String::new()),
58 running: AtomicBool::new(false),
59 cancelled: AtomicBool::new(false),
60 indicator: AtomicU8::new(0),
61 }),
62 handle: Mutex::new(None),
63 started: AtomicBool::new(false),
64 }
65}
66
67impl Spinner {
68 pub fn with_timer(self) -> Self {
70 self.inner.indicator.store(1, Ordering::Relaxed);
71 self
72 }
73
74 pub fn is_cancelled(&self) -> bool {
75 self.inner.cancelled.load(Ordering::Relaxed)
76 }
77
78 pub fn start(&self, msg: impl Into<String>) {
80 if self.started.swap(true, Ordering::SeqCst) {
81 return;
82 }
83 *self.inner.message.lock().unwrap() = trim_dots(msg.into());
84 self.inner.running.store(true, Ordering::SeqCst);
85 let inner = self.inner.clone();
86 let handle = thread::spawn(move || run_loop(inner));
87 *self.handle.lock().unwrap() = Some(handle);
88 }
89
90 pub fn message(&self, msg: impl Into<String>) {
92 *self.inner.message.lock().unwrap() = trim_dots(msg.into());
93 }
94
95 pub fn stop(&self, msg: impl Into<String>) {
97 self.terminate(msg.into(), Status::Submit);
98 }
99
100 pub fn cancel(&self, msg: impl Into<String>) {
102 self.inner.cancelled.store(true, Ordering::SeqCst);
103 self.terminate(msg.into(), Status::Cancel);
104 }
105
106 pub fn error(&self, msg: impl Into<String>) {
108 self.terminate(msg.into(), Status::Error);
109 }
110
111 pub fn clear(&self) {
113 self.inner.running.store(false, Ordering::SeqCst);
114 if let Some(h) = self.handle.lock().unwrap().take() {
115 let _ = h.join();
116 }
117 let mut out = std::io::stderr();
118 let _ = write!(out, "\r\x1b[2K");
119 let _ = out.flush();
120 }
121
122 fn terminate(&self, msg: String, status: Status) {
123 self.inner.running.store(false, Ordering::SeqCst);
124 if let Some(h) = self.handle.lock().unwrap().take() {
125 let _ = h.join();
126 }
127 let final_msg = if msg.is_empty() {
128 self.inner.message.lock().unwrap().clone()
129 } else {
130 msg
131 };
132 let c = super::settings::colors();
133 let glyph = match status {
134 Status::Submit => paint(c.success, STEP_SUBMIT),
135 Status::Cancel => paint(c.cancel, STEP_CANCEL),
136 Status::Error => paint(c.error, STEP_ERROR),
137 };
138 let mut out = std::io::stderr();
139 let _ = write!(out, "\r\x1b[2K{} {}\n", glyph, paint(WHITE, &final_msg));
140 let _ = out.flush();
141 }
142}
143
144impl Drop for Spinner {
145 fn drop(&mut self) {
146 if self.inner.running.load(Ordering::Relaxed) {
147 self.clear();
148 }
149 }
150}
151
152enum Status {
153 Submit,
154 Cancel,
155 Error,
156}
157
158fn trim_dots(mut s: String) -> String {
159 while s.ends_with('.') {
160 s.pop();
161 }
162 s
163}
164
165fn run_loop(inner: Arc<Inner>) {
166 let mut idx = 0usize;
167 let mut dot_phase = 0u8;
168 let origin = Instant::now();
169 let mut out = std::io::stderr();
170 while inner.running.load(Ordering::Relaxed) {
171 let frame = FRAMES[idx % FRAMES.len()];
172 let msg = inner.message.lock().unwrap().clone();
173 let indicator = inner.indicator.load(Ordering::Relaxed);
174 let suffix = if indicator == 1 {
175 format!(" {}", paint(DIM, &format_timer(origin.elapsed())))
176 } else {
177 let n = (dot_phase / 8) as usize;
178 ".".repeat(n.min(3))
179 };
180 let _ = write!(
181 out,
182 "\r\x1b[2K{} {}{}",
183 paint(super::settings::colors().accent, frame),
184 paint(WHITE, &msg),
185 suffix
186 );
187 let _ = out.flush();
188 idx = idx.wrapping_add(1);
189 dot_phase = dot_phase.wrapping_add(1);
190 thread::sleep(FRAME_DELAY);
191 }
192}
193
194fn format_timer(d: Duration) -> String {
195 let secs = d.as_secs();
196 let m = secs / 60;
197 let s = secs % 60;
198 if m > 0 {
199 format!("[{m}m {s}s]")
200 } else {
201 format!("[{s}s]")
202 }
203}