1use std::io::{IsTerminal, Write};
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10
11#[inline]
16pub fn is_tty() -> bool {
17 std::io::stderr().is_terminal()
18}
19
20#[inline]
22pub fn is_stdout_tty() -> bool {
23 std::io::stdout().is_terminal()
24}
25
26pub fn status(msg: &str) {
31 if is_tty() {
32 eprintln!("{}", msg);
33 }
34}
35
36#[macro_export]
38macro_rules! status {
39 ($($arg:tt)*) => {
40 if $crate::utils::progress::is_tty() {
41 eprintln!($($arg)*);
42 }
43 };
44}
45
46pub struct Spinner {
50 message: String,
51 frames: &'static [&'static str],
52 frame_idx: usize,
53 start: Instant,
54 is_tty: bool,
55 stopped: Arc<AtomicBool>,
56}
57
58impl Spinner {
59 const DEFAULT_FRAMES: &'static [&'static str] =
61 &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
62
63 const ASCII_FRAMES: &'static [&'static str] = &["|", "/", "-", "\\"];
65
66 pub fn new(message: impl Into<String>) -> Self {
68 Self {
69 message: message.into(),
70 frames: Self::DEFAULT_FRAMES,
71 frame_idx: 0,
72 start: Instant::now(),
73 is_tty: is_tty(),
74 stopped: Arc::new(AtomicBool::new(false)),
75 }
76 }
77
78 pub fn ascii(mut self) -> Self {
80 self.frames = Self::ASCII_FRAMES;
81 self
82 }
83
84 pub fn start(&self) {
86 if self.is_tty {
87 eprint!("\r{} {}...", self.frames[0], self.message);
88 let _ = std::io::stderr().flush();
89 }
90 }
91
92 pub fn tick(&mut self) {
94 if !self.is_tty || self.stopped.load(Ordering::Relaxed) {
95 return;
96 }
97
98 self.frame_idx = (self.frame_idx + 1) % self.frames.len();
99 eprint!("\r{} {}...", self.frames[self.frame_idx], self.message);
100 let _ = std::io::stderr().flush();
101 }
102
103 pub fn set_message(&mut self, message: impl Into<String>) {
105 self.message = message.into();
106 if self.is_tty {
107 eprint!("\r\x1b[K");
109 eprint!("{} {}...", self.frames[self.frame_idx], self.message);
110 let _ = std::io::stderr().flush();
111 }
112 }
113
114 pub fn success(self, message: &str) {
116 self.stopped.store(true, Ordering::Relaxed);
117 if self.is_tty {
118 eprintln!(
119 "\r\x1b[K✓ {} ({:.1}s)",
120 message,
121 self.start.elapsed().as_secs_f64()
122 );
123 }
124 }
125
126 pub fn error(self, message: &str) {
128 self.stopped.store(true, Ordering::Relaxed);
129 if self.is_tty {
130 eprintln!("\r\x1b[K✗ {}", message);
131 }
132 }
133
134 pub fn stop(self) {
136 self.stopped.store(true, Ordering::Relaxed);
137 if self.is_tty {
138 eprint!("\r\x1b[K");
139 let _ = std::io::stderr().flush();
140 }
141 }
142
143 pub fn elapsed(&self) -> Duration {
145 self.start.elapsed()
146 }
147}
148
149impl Drop for Spinner {
150 fn drop(&mut self) {
151 if self.is_tty && !self.stopped.load(Ordering::Relaxed) {
153 eprint!("\r\x1b[K");
154 let _ = std::io::stderr().flush();
155 }
156 }
157}
158
159pub struct ProgressBar {
161 total: u64,
162 current: u64,
163 message: String,
164 width: usize,
165 start: Instant,
166 is_tty: bool,
167 last_draw: Instant,
168}
169
170impl ProgressBar {
171 pub fn new(total: u64, message: impl Into<String>) -> Self {
173 Self {
174 total,
175 current: 0,
176 message: message.into(),
177 width: 30,
178 start: Instant::now(),
179 is_tty: is_tty(),
180 last_draw: Instant::now(),
181 }
182 }
183
184 pub fn with_width(mut self, width: usize) -> Self {
186 self.width = width;
187 self
188 }
189
190 pub fn set(&mut self, current: u64) {
192 self.current = current.min(self.total);
193 self.draw(false);
194 }
195
196 pub fn inc(&mut self) {
198 self.current = (self.current + 1).min(self.total);
199 self.draw(false);
200 }
201
202 pub fn inc_by(&mut self, amount: u64) {
204 self.current = (self.current + amount).min(self.total);
205 self.draw(false);
206 }
207
208 fn draw(&mut self, force: bool) {
210 if !self.is_tty {
211 return;
212 }
213
214 if !force && self.last_draw.elapsed() < Duration::from_millis(50) {
216 return;
217 }
218 self.last_draw = Instant::now();
219
220 let percent = if self.total > 0 {
221 (self.current as f64 / self.total as f64 * 100.0) as u64
222 } else {
223 0
224 };
225
226 let filled = if self.total > 0 {
227 (self.current as f64 / self.total as f64 * self.width as f64) as usize
228 } else {
229 0
230 };
231
232 let empty = self.width.saturating_sub(filled);
233 let bar = format!("{}{}", "█".repeat(filled), "░".repeat(empty));
234
235 let elapsed = self.start.elapsed().as_secs_f64();
236 let rate = if elapsed > 0.0 {
237 self.current as f64 / elapsed
238 } else {
239 0.0
240 };
241
242 let eta = if rate > 0.0 && self.current < self.total {
243 let remaining = self.total - self.current;
244 format!(" ETA: {:.0}s", remaining as f64 / rate)
245 } else {
246 String::new()
247 };
248
249 eprint!(
250 "\r{} [{}] {:>3}% ({}/{}){}",
251 self.message, bar, percent, self.current, self.total, eta
252 );
253 let _ = std::io::stderr().flush();
254 }
255
256 pub fn finish(mut self) {
258 self.current = self.total;
259 self.draw(true);
260 if self.is_tty {
261 eprintln!(); }
263 }
264
265 pub fn finish_with_message(self, message: &str) {
267 if self.is_tty {
268 eprintln!(
269 "\r\x1b[K{} ({:.1}s)",
270 message,
271 self.start.elapsed().as_secs_f64()
272 );
273 }
274 }
275}
276
277pub fn progress_message(quiet: bool, message: impl std::fmt::Display) {
281 if !quiet && is_tty() {
282 eprintln!("{}...", message);
283 }
284}
285
286#[macro_export]
288macro_rules! progress {
289 ($quiet:expr, $($arg:tt)*) => {
290 if !$quiet && $crate::utils::progress::is_tty() {
291 eprintln!($($arg)*);
292 }
293 };
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn test_is_tty() {
302 let _ = is_tty();
304 let _ = is_stdout_tty();
305 }
306
307 #[test]
308 fn test_spinner_creation() {
309 let spinner = Spinner::new("Loading");
310 assert!(!spinner.stopped.load(Ordering::Relaxed));
311 spinner.stop();
312 }
313
314 #[test]
315 fn test_spinner_ascii() {
316 let spinner = Spinner::new("Loading").ascii();
317 assert_eq!(spinner.frames, Spinner::ASCII_FRAMES);
318 spinner.stop();
319 }
320
321 #[test]
322 fn test_progress_bar_creation() {
323 let mut bar = ProgressBar::new(100, "Processing");
324 bar.set(50);
325 assert_eq!(bar.current, 50);
326 bar.inc();
327 assert_eq!(bar.current, 51);
328 bar.inc_by(10);
329 assert_eq!(bar.current, 61);
330 }
331
332 #[test]
333 fn test_progress_bar_overflow() {
334 let mut bar = ProgressBar::new(100, "Test");
335 bar.set(150); assert_eq!(bar.current, 100);
337 }
338
339 #[test]
340 fn test_progress_bar_zero_total() {
341 let bar = ProgressBar::new(0, "Empty");
342 assert_eq!(bar.total, 0);
343 }
344}