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