1use std::io::{IsTerminal, Write};
2use std::mem::ManuallyDrop;
3use std::sync::{Arc, Mutex};
4use std::{fmt, io, thread, time};
5
6use crossbeam_channel as chan;
7
8use radicle_signals as signals;
9use signals::Signal;
10
11use crate::io::{ERROR_PREFIX, WARNING_PREFIX};
12use crate::Paint;
13
14pub const DEFAULT_TICK: time::Duration = time::Duration::from_millis(99);
16pub const DEFAULT_STYLE: [Paint<&'static str>; 4] = [
18 Paint::magenta("◢"),
19 Paint::cyan("◣"),
20 Paint::magenta("◤"),
21 Paint::blue("◥"),
22];
23
24struct Progress {
25 state: State,
26 message: Paint<String>,
27}
28
29impl Progress {
30 fn new(message: Paint<String>) -> Self {
31 Self {
32 state: State::Running { cursor: 0 },
33 message,
34 }
35 }
36}
37
38enum State {
39 Running { cursor: usize },
40 Canceled,
41 Done,
42 Warn,
43 Error,
44}
45
46pub struct Spinner {
48 progress: Arc<Mutex<Progress>>,
49 handle: ManuallyDrop<thread::JoinHandle<()>>,
50}
51
52impl Drop for Spinner {
53 fn drop(&mut self) {
54 if let Ok(mut progress) = self.progress.lock() {
55 if let State::Running { .. } = progress.state {
56 progress.state = State::Canceled;
57 }
58 }
59 unsafe { ManuallyDrop::take(&mut self.handle) }
60 .join()
61 .unwrap();
62 }
63}
64
65impl Spinner {
66 pub fn finish(self) {
68 if let Ok(mut progress) = self.progress.lock() {
69 progress.state = State::Done;
70 }
71 }
72
73 pub fn failed(self) {
75 if let Ok(mut progress) = self.progress.lock() {
76 progress.state = State::Error;
77 }
78 }
79
80 pub fn error(self, msg: impl fmt::Display) {
82 if let Ok(mut progress) = self.progress.lock() {
83 progress.state = State::Error;
84 progress.message = Paint::new(format!(
85 "{} {} {}",
86 progress.message,
87 Paint::red("error:"),
88 msg
89 ));
90 }
91 }
92
93 pub fn warn(self) {
95 if let Ok(mut progress) = self.progress.lock() {
96 progress.state = State::Warn;
97 }
98 }
99
100 pub fn message(&mut self, msg: impl fmt::Display) {
102 let msg = msg.to_string();
103
104 if let Ok(mut progress) = self.progress.lock() {
105 progress.message = Paint::new(msg);
106 }
107 }
108}
109
110pub fn spinner(message: impl ToString) -> Spinner {
114 let (stdout, stderr) = (io::stdout(), io::stderr());
115 if stderr.is_terminal() {
116 spinner_to(message, stdout, stderr)
117 } else {
118 spinner_to(message, stdout, io::sink())
119 }
120}
121
122pub fn spinner_to(
130 message: impl ToString,
131 mut completion: impl io::Write + Send + 'static,
132 animation: impl io::Write + Send + 'static,
133) -> Spinner {
134 let message = message.to_string();
135 let progress = Arc::new(Mutex::new(Progress::new(Paint::new(message))));
136 let (sig_tx, sig_rx) = chan::unbounded();
137 let sig_result = signals::install(sig_tx);
138 let handle = thread::Builder::new()
139 .name(String::from("spinner"))
140 .spawn({
141 let progress = progress.clone();
142
143 move || {
144 let mut animation = termion::cursor::HideCursor::from(animation);
145
146 loop {
147 let Ok(mut progress) = progress.lock() else {
148 break;
149 };
150 if sig_result.is_ok() {
152 match sig_rx.try_recv() {
153 Ok(sig) if sig == Signal::Interrupt || sig == Signal::Terminate => {
154 write!(animation, "\r{}", termion::clear::UntilNewline).ok();
155 writeln!(
156 completion,
157 "{ERROR_PREFIX} {} {}",
158 &progress.message,
159 Paint::red("<canceled>")
160 )
161 .ok();
162 drop(animation);
163 std::process::exit(-1);
164 }
165 Ok(_) => {}
166 Err(_) => {}
167 }
168 }
169 match &mut *progress {
170 Progress {
171 state: State::Running { cursor },
172 message,
173 } => {
174 let spinner = DEFAULT_STYLE[*cursor];
175
176 write!(
177 animation,
178 "\r{}{spinner} {message}",
179 termion::clear::UntilNewline,
180 )
181 .ok();
182
183 *cursor += 1;
184 *cursor %= DEFAULT_STYLE.len();
185 }
186 Progress {
187 state: State::Done,
188 message,
189 } => {
190 write!(animation, "\r{}", termion::clear::UntilNewline).ok();
191 writeln!(completion, "{} {message}", Paint::green("✓")).ok();
192 break;
193 }
194 Progress {
195 state: State::Canceled,
196 message,
197 } => {
198 write!(animation, "\r{}", termion::clear::UntilNewline).ok();
199 writeln!(
200 completion,
201 "{ERROR_PREFIX} {message} {}",
202 Paint::red("<canceled>")
203 )
204 .ok();
205 break;
206 }
207 Progress {
208 state: State::Warn,
209 message,
210 } => {
211 write!(animation, "\r{}", termion::clear::UntilNewline).ok();
212 writeln!(completion, "{WARNING_PREFIX} {message}").ok();
213 break;
214 }
215 Progress {
216 state: State::Error,
217 message,
218 } => {
219 write!(animation, "\r{}", termion::clear::UntilNewline).ok();
220 writeln!(completion, "{ERROR_PREFIX} {message}").ok();
221 break;
222 }
223 }
224 drop(progress);
225 thread::sleep(DEFAULT_TICK);
226 }
227 if sig_result.is_ok() {
228 let _ = signals::uninstall();
229 }
230 }
231 })
232 .unwrap();
234
235 Spinner {
236 progress,
237 handle: ManuallyDrop::new(handle),
238 }
239}