1use std::io::IsTerminal;
10use std::marker::PhantomData;
11use std::sync::Arc;
12use std::time::Duration;
13
14use indicatif::{ProgressBar as IndProgressBar, ProgressStyle};
15
16use super::Role;
17use super::renderer::{Renderer, Writer};
18use super::status_builder::StatusBuilder;
19
20pub(crate) fn stderr_is_terminal() -> bool {
21 std::io::stderr().is_terminal()
22}
23
24pub struct Spinner<'p> {
28 pub(crate) renderer: Arc<Renderer>,
29 pub(crate) sink: Arc<dyn Writer>,
30 pub(crate) depth: usize,
31 pub(crate) bar: IndProgressBar,
32 pub(crate) message: String,
33 pub(crate) finished: bool,
34 pub(crate) _phantom: PhantomData<&'p ()>,
35}
36
37impl<'p> Spinner<'p> {
38 pub fn set_message(&self, text: impl Into<String>) {
39 self.bar.set_message(text.into());
40 }
41
42 pub fn finish_ok(self, final_text: impl Into<String>) -> StatusBuilder<'p> {
43 self.finish_with(Role::Ok, final_text)
44 }
45 pub fn finish_warn(self, final_text: impl Into<String>) -> StatusBuilder<'p> {
46 self.finish_with(Role::Warn, final_text)
47 }
48 pub fn finish_fail(self, final_text: impl Into<String>) -> StatusBuilder<'p> {
49 self.finish_with(Role::Fail, final_text)
50 }
51 pub fn finish_skipped(self, final_text: impl Into<String>) -> StatusBuilder<'p> {
52 self.finish_with(Role::Skipped, final_text)
53 }
54
55 fn finish_with(mut self, role: Role, subject: impl Into<String>) -> StatusBuilder<'p> {
56 self.bar.finish_and_clear();
57 self.finished = true;
58 StatusBuilder::new(
64 self.renderer.clone(),
65 self.sink.clone(),
66 self.depth,
67 role,
68 subject,
69 )
70 }
71}
72
73impl Drop for Spinner<'_> {
74 fn drop(&mut self) {
75 if self.finished {
76 return;
77 }
78 self.bar.finish_and_clear();
79 let msg = std::mem::take(&mut self.message);
89 let sb = StatusBuilder::new(
90 self.renderer.clone(),
91 self.sink.clone(),
92 self.depth,
93 Role::Info,
94 msg,
95 );
96 drop(sb);
97 }
98}
99
100pub struct ProgressBar<'p> {
102 pub(crate) bar: IndProgressBar,
103 pub(crate) _phantom: PhantomData<&'p ()>,
104}
105
106impl<'p> ProgressBar<'p> {
107 pub fn inc(&self, delta: u64) {
108 self.bar.inc(delta);
109 }
110 pub fn set_position(&self, pos: u64) {
111 self.bar.set_position(pos);
112 }
113 pub fn set_message(&self, m: impl Into<String>) {
114 self.bar.set_message(m.into());
115 }
116 pub fn finish(self) {
117 self.bar.finish_and_clear();
118 }
119}
120
121pub(crate) fn make_spinner_bar(
126 multi: &indicatif::MultiProgress,
127 renderer: &Renderer,
128 verbosity: super::Verbosity,
129 message: &str,
130) -> IndProgressBar {
131 if verbosity == super::Verbosity::Quiet || !stderr_is_terminal() {
132 IndProgressBar::hidden()
133 } else {
134 build_spinner(multi, renderer, message)
135 }
136}
137
138pub(crate) fn make_progress_bar(
140 multi: &indicatif::MultiProgress,
141 total: u64,
142 verbosity: super::Verbosity,
143 message: &str,
144) -> IndProgressBar {
145 if verbosity == super::Verbosity::Quiet || !stderr_is_terminal() {
146 IndProgressBar::hidden()
147 } else {
148 build_progress_bar(multi, total, message)
149 }
150}
151
152pub(crate) fn build_spinner(
154 multi: &indicatif::MultiProgress,
155 renderer: &Renderer,
156 message: &str,
157) -> IndProgressBar {
158 let pb = multi.add(IndProgressBar::new_spinner());
159 let frames_raw = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
160 let styled: Vec<String> = frames_raw
161 .iter()
162 .map(|f| renderer.theme.info.apply_to(f).to_string())
163 .collect();
164 let mut tick_refs: Vec<&str> = styled.iter().map(|s| s.as_str()).collect();
165 tick_refs.push(" ");
166 pb.set_style(
167 ProgressStyle::with_template("{spinner} {msg}")
168 .unwrap_or_else(|_| ProgressStyle::default_spinner())
169 .tick_strings(&tick_refs),
170 );
171 pb.set_message(message.to_string());
172 pb.enable_steady_tick(Duration::from_millis(80));
173 pb
174}
175
176pub(crate) fn build_progress_bar(
177 multi: &indicatif::MultiProgress,
178 total: u64,
179 message: &str,
180) -> IndProgressBar {
181 let pb = multi.add(IndProgressBar::new(total));
182 pb.set_style(
183 ProgressStyle::with_template("{spinner:.cyan} [{bar:30.cyan/dim}] {pos}/{len} {msg}")
184 .unwrap_or_else(|_| ProgressStyle::default_bar())
185 .progress_chars("━╸─"),
186 );
187 pb.set_message(message.to_string());
188 pb
189}
190
191#[cfg(test)]
192mod tests {
193 use std::sync::{Arc, Mutex};
194
195 use super::super::renderer::{Renderer, StringSink};
196 use super::super::{Theme, Verbosity};
197 use super::*;
198 use crate::output::strip_ansi;
199
200 fn renderer() -> Arc<Renderer> {
201 Arc::new(Renderer::new(Theme::default(), Verbosity::Normal))
202 }
203
204 fn sink_for(buf: &Arc<Mutex<String>>) -> Arc<dyn Writer> {
205 Arc::new(StringSink(buf.clone()))
206 }
207
208 #[test]
209 fn finish_ok_emits_status_at_section_depth() {
210 let r = renderer();
211 let buf = Arc::new(Mutex::new(String::new()));
212 let sink = sink_for(&buf);
213 let sp = Spinner {
215 renderer: r.clone(),
216 sink: sink.clone(),
217 depth: 1,
218 bar: indicatif::ProgressBar::hidden(),
219 message: "doing work".into(),
220 finished: false,
221 _phantom: std::marker::PhantomData,
222 };
223 let _ = sp.finish_ok("done");
224 let out = strip_ansi(&buf.lock().unwrap());
226 assert!(out.contains(" ✓ done"), "got: {out:?}");
227 }
228
229 #[test]
230 fn drop_without_finish_emits_info_record() {
231 let r = renderer();
232 let buf = Arc::new(Mutex::new(String::new()));
233 let sink = sink_for(&buf);
234 {
235 let _sp = Spinner {
236 renderer: r.clone(),
237 sink: sink.clone(),
238 depth: 0,
239 bar: indicatif::ProgressBar::hidden(),
240 message: "abandoned".into(),
241 finished: false,
242 _phantom: std::marker::PhantomData,
243 };
244 }
245 let out = strip_ansi(&buf.lock().unwrap());
246 assert!(out.contains("abandoned"), "got: {out:?}");
248 }
249
250 #[test]
251 fn quiet_printer_returns_hidden_spinner() {
252 use super::super::printer::Printer;
253 let p = Printer::with_format(
254 super::super::Verbosity::Quiet,
255 None,
256 super::super::OutputFormat::Table,
257 );
258 let sp = p.spinner("x");
259 assert!(sp.bar.is_hidden(), "Quiet should yield a hidden bar");
260 }
261}