1use super::*;
4use std::{
5 fmt::Write,
6 io,
7};
8#[derive(Debug)]
35pub struct Bar<T: ?Sized = DefaultOutputDevice>
36{
37 width: usize,
38 max_width: usize,
39 progress: f64,
40 buffer: String,
41 title: String,
42 #[cfg(feature="size")]
43 fit_to_term: bool,
44
45 output: AtomicRefCell<T>
47}
48
49pub const DEFAULT_SIZE: usize = 50;
52
53pub const DEFAULT_MAX_BORDER_SIZE: usize = 20;
56
57impl Default for Bar
69{
70 #[inline]
71 fn default() -> Self
72 {
73 Self::new_default(DEFAULT_SIZE)
74 }
75}
76
77
78impl Bar {
79 #[inline]
81 pub fn with_title_default(width: usize, title: impl AsRef<str>) -> Self
82 {
83 Self::with_title(create_default_output_device(), width, title)
84 }
85
86 #[cfg(feature="size")]
90 #[inline]
91 pub fn try_new_with_title_default(width: usize, title: impl AsRef<str>) -> Option<Self>
92 {
93 Self::try_new_with_title(create_default_output_device(), width, title)
94 }
95
96 #[inline]
103 pub fn new_default(width: usize) -> Self
104 {
105 Self::new(create_default_output_device(), width)
106 }
107
108 #[cfg(feature="size")]
112 #[inline]
113 pub fn try_new_default(width: usize) -> Option<Self>
114 {
115 Self::try_new(create_default_output_device(), width)
116 }
117
118 #[cfg(feature="size")]
122 #[inline]
123 pub fn try_new_default_size_default() -> Option<Self>
124 {
125 Self::try_new_default_size(create_default_output_device())
126 }
127
128 #[inline]
133 pub fn with_max_default(width: usize, max_width: usize) -> Self
134 {
135 Self::with_max(create_default_output_device(), width, max_width)
136 }
137}
138
139impl<T: io::Write + AsFd> Bar<T>
140{
141 pub fn with_title(output: impl Into<T> + AsFd, width: usize, title: impl AsRef<str>) -> Self
143 {
144 let mut this = Self::new(output, width);
145 this.add_title(title.as_ref());
146 this
147 }
148
149 #[inline(always)] fn add_title(&mut self, title: &str)
150 {
151 self.set_title(title);
152 self.update()
153 }
154
155 #[cfg(feature="size")]
159 pub fn try_new_with_title(output: impl Into<T> + AsFd, width: usize, title: impl AsRef<str>) -> Option<Self>
160 {
161 let (terminal_size::Width(tw), _) = terminal_size_of(&output)?;
162 let tw = usize::from(tw);
163 let mut o = Self::with_max(output.into(), if width < tw {width} else {tw}, tw);
164 o.set_title(title.as_ref());
165 o.fit_to_term = true;
166 o.update();
167 Some(o)
168 }
169
170 #[inline]
171 fn autofit(&mut self)
172 {
173 #[cfg(feature="size")]
174 self.fit();
175 }
176
177 #[cfg_attr(not(feature="size"), inline)]
184 pub fn new(output: impl Into<T> + AsFd, width: usize) -> Self
185 {
186 #[cfg(feature="size")]
187 return {
188 if let Some((terminal_size::Width(tw), _)) = terminal_size_of(&output) {
189 let tw = usize::from(tw);
190 let mut o = Self::with_max(output.into(), if width < tw {width} else {tw}, tw);
191 o.fit_to_term = true;
192 o
193 } else {
194 let mut o = Self::with_max(output.into(), width, width + DEFAULT_MAX_BORDER_SIZE);
195 o.fit_to_term = true;
196 o
197 }
198 };
199 #[cfg(not(feature="size"))]
200 return {
201 Self::with_max(output.into(), width, width +DEFAULT_MAX_BORDER_SIZE)
202 };
203 }
204
205 #[cfg(feature="size")]
209 pub fn try_new(output: impl Into<T> + AsFd, width: usize) -> Option<Self>
210 {
211 let (terminal_size::Width(tw), _) = terminal_size_of(&output)?;
212 let tw = usize::from(tw);
213 let mut o = Self::with_max(output.into(), if width < tw {width} else {tw}, tw);
214 o.fit_to_term = true;
215 Some(o)
216 }
217
218 #[cfg(feature="size")]
222 #[inline]
223 pub fn try_new_default_size(to: impl Into<T> + AsFd) -> Option<Self>
224 {
225 Self::try_new(to, DEFAULT_SIZE)
226 }
227
228 pub fn with_max(output: impl Into<T>, width: usize, max_width: usize) -> Self
233 {
234 let mut this = Self {
235 width,
236 max_width,
237 progress: 0.0,
238 buffer: String::with_capacity(width),
239 title: String::with_capacity(max_width - width),
240 #[cfg(feature="size")]
241 fit_to_term: false,
242 output: AtomicRefCell::new(output.into())
243 };
244 this.update();
245 this
246 }
247
248}
249
250impl<T: ?Sized + io::Write + AsFd> Bar<T> {
251 #[inline(always)]
252 #[cfg(feature="size")]
253 fn try_get_size(&self) -> Option<(terminal_size::Width, terminal_size::Height)>
254 {
255 let b = self.output.try_borrow().ok()?;
256 terminal_size::terminal_size_of::<&T>(&b)
257 }
258 pub fn fit(&mut self) -> bool
267 {
268 #[cfg(feature="size")] {
269 if let Some((terminal_size::Width(tw), _)) = terminal_size::terminal_size_of(self.output.get_mut()) {
270 let tw = usize::from(tw);
271 self.width = if self.width < tw {self.width} else {tw};
272 self.update_dimensions(tw);
273 return true;
274 }
275 }
276 false
277 }
278
279 #[inline] fn widths(&self) -> (usize, usize)
280 {
281 #[cfg(feature="size")]
282 if self.fit_to_term {
283 if let Some((terminal_size::Width(tw), _)) = self.try_get_size() {
284 let tw = usize::from(tw);
285 let width = if self.width < tw {self.width} else {tw};
286 return (width, tw);
287 }
288 };
289 (self.width, self.max_width)
290 }
291
292 pub fn update(&mut self)
294 {
295 self.buffer.clear();
296
297 let pct = (self.progress * (self.width as f64)) as usize;
298 for i in 0..self.width
299 {
300 if i >= pct {
301 write!(self.buffer, " ").unwrap();
302 } else {
303 write!(self.buffer, "=").unwrap();
304 }
305 }
306 }
307
308}
309impl<T: io::Write> Bar<T> {
310 pub fn complete(self) -> io::Result<()>
312 {
313 writeln!(&mut self.output.into_inner(), "")
314 }
315}
316
317fn ensure_eq(input: String, to: usize) -> String
318{
319 let chars = input.chars();
320 if chars.count() != to {
321 let mut chars = input.chars();
322 let mut output = String::with_capacity(to);
323 for _ in 0..to
324 {
325 if let Some(c) = chars.next() {
326 write!(output, "{}", c).unwrap();
327 } else {
328 write!(output, " ").unwrap();
329 }
330 }
331 output
332 } else {
333 input
334 }
335}
336
337
338fn ensure_lower(input: String, to: usize) -> String
339{
340 let chars = input.chars();
341 if chars.count() > to
342 {
343 let chars = input.chars();
344 let mut output = String::with_capacity(to);
345 for (i,c) in (0..).zip(chars)
346 {
347 write!(output, "{}", c).unwrap();
348 if to>3 && i == to-3 {
349 write!(output, "...").unwrap();
350 break;
351 } else if i==to {
352 break;
353 }
354 }
355
356 output
357 } else {
358 input
359 }
360}
361
362impl<T: ?Sized + io::Write + AsFd> Display for Bar<T>
363{
364 fn refresh(&self)
365 {
366 let (_, max_width) = self.widths();
367
368 let temp = format!("[{}]: {:.2}%", self.buffer, self.progress * 100.00);
369 let title = ensure_lower(format!(" {}", self.title), max_width - temp.chars().count());
370
371 let temp = ensure_eq(format!("{}{}", temp, title), max_width);
372
373 let Ok(mut out) = self.output.try_borrow_mut() else { return };
377
378 let _ = write!(out, "\x1B[0m\x1B[K{}", temp) .and_then(|_| write!(out, "\n\x1B[1A"))
381 .and_then(move |_| flush!(? out));
382 }
383
384 fn blank(&self)
385 {
386 let (_, max_width) = self.widths();
387
388 let Ok(mut out) = self.output.try_borrow_mut() else { return };
390
391 let _ = out.write_all(b"\r")
393 .and_then(|_| stackalloc::stackalloc(max_width, b' ',|spaces| out.write_all(spaces))) .and_then(|_| out.write_all(b"\r"))
395 .and_then(move |_| flush!(? out));
396 }
397
398 fn get_title(&self) -> &str
399 {
400 &self.title
401 }
402
403 fn set_title(&mut self, from: &str)
404 {
405 self.title = from.to_string();
406
407 let (_, max_width) = self.widths();
408
409 let out = self.output.get_mut();
411
412 let _ = out.write_all(b"\r")
414 .and_then(|_| stackalloc::stackalloc(max_width, b' ',|spaces| out.write_all(spaces))) .and_then(|_| out.write_all(b"\r"))
416 .and_then(move |_| flush!(? out));
417 }
418
419 fn update_dimensions(&mut self, to: usize)
420 {
421 self.max_width = to;
422
423 let out = self.output.get_mut();
425
426 let _ = out.write_all(b"\r")
428 .and_then(|_| stackalloc::stackalloc(to, b' ',|spaces| out.write_all(spaces))) .and_then(|_| out.write_all(b"\r"))
430 .and_then(move |_| flush!(? out));
431 }
432}
433
434impl<T: ?Sized + io::Write + AsFd> ProgressBar for Bar<T>
435{
436 fn get_progress(&self) -> f64
437 {
438 self.progress
439 }
440 fn set_progress(&mut self, value: f64)
441 {
442 if self.progress != value {
443 self.progress = value;
444 self.update();
445 }
446
447 let (_, max_width) = self.widths();
448
449 let out = self.output.get_mut();
451
452 let _ = out.write_all(b"\r")
454 .and_then(|_| stackalloc::stackalloc(max_width, b' ',|spaces| out.write_all(spaces))) .and_then(|_| out.write_all(b"\r"))
456 .and_then(move |_| flush!(? out));
457 }
458}
459
460impl<T: io::Write + AsFd> WithTitle for Bar<T>
461{
462 fn add_title(&mut self, string: impl AsRef<str>)
463 {
464 (*self).add_title(string.as_ref())
465 }
466 fn update(&mut self)
467 {
468 self.update();
469 }
470 fn complete(self)
471 {
472 let _ = self.complete();
474 }
475}
476
477const _:() = {
478 const fn declval<T>() -> Bar<T> {
479 unreachable!()
480 }
481 fn take_title(_: &(impl WithTitle + ?Sized)) {}
482 fn take_progress(_: &(impl ProgressBar + ?Sized)) {}
483 fn take_display(_: &(impl Display + ?Sized)) {}
484 fn test()
485 {
486 macro_rules! assert_is_bar {
487 ($ty:path) => {
488 take_title(&declval::<$ty>());
489 take_progress(&declval::<$ty>());
490 take_display(&declval::<$ty>());
491 }
492 }
493
494 assert_is_bar!(io::Stdout);
495 assert_is_bar!(std::fs::File);
496 }
497};
498
499#[cfg(test)]
500mod test
501{
502 use super::*;
503
504 #[test]
505 fn rendering_blanking()
506 {
507 let mut bar = {
508 #[cfg(feature="size")]
509 let Some(bar)= Bar::try_new_default_size_default() else { return };
510 #[cfg(not(feature="size"))]
511 let bar= Bar::new_default(50);
512 bar
513 };
514 bar.set_progress(0.5);
515 bar.blank();
516 bar.set_progress(0.7);
517 bar.set_title("70 percent.");
518 bar.refresh();
519 bar.set_progress(0.2);
521 bar.set_title("...");
522 bar.blank();
523 bar.complete().unwrap();
524 }
525
526 #[test]
527 fn creating_non_default_fd() {
528 #[cfg(feature="size")]
529 let Some(_): Option<Bar<std::io::Stderr>> = Bar::try_new(std::io::stderr(), super::DEFAULT_SIZE) else { return };
530 #[cfg(not(feature="size"))]
531 let _: Bar<std::io::Stderr> = Bar::new(std::io::stderr(), super::DEFAULT_SIZE);
532 }
533}