termprogress/
progress.rs

1///! Progress bar that has a size and also a max size.
2
3use super::*;
4use std::{
5    fmt::Write,
6    io,
7};
8/// A progress bar with a size and optionally title. It implements the `ProgressBar` trait, and is the default progress bar.
9///
10/// The bar has a max size, that is usually derived from the size of your terminal (if it can be detected) or can be set yourself, to stop the title line going over the side.
11/// It also has a configurable width, which is defaulted to 50.
12///
13/// # Usage
14///
15/// To create a new `progress::Bar` with a max size tuned to your terminal (or `Width+20`, if it cannot be detected), and of the default size, `Bar` implements the `Default` trait:
16/// ```rust
17/// # use termprogress::prelude::*;
18/// let mut bar = Bar::default(); //Creates a bar of width 50 by default.
19/// ```
20///
21/// You can configure sizes and initial title with `new()`, `with_title()`, and `with_max()` functions.
22/// # How it looks
23/// It renders in the terminal like:
24/// `[=========================                         ]: 50% this is a title that may get cut if it reaches max le...`
25///
26/// # Thread `Sync`safety
27/// This type is safely `Sync` (where `T` is), the behaviour is defined to prevent overlapping writes to `T`.
28/// Though it is *advised* to not render a `Bar` from more than a single thread, you still safely can.
29///
30/// Rendering functions should not be called on multiple threads at the same time, though it is safe to do so.
31/// A thread-sync'd rendering operation will safetly (and silently) give up before writing if another thread is already engaging in one.
32///
33/// A display operation on one thread will cause any other threads attempting on to silently and safely abort their display attempt before anything is written to output.
34#[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    // Allowing `Bar` to manage the sync will ensure that the bar is not interrupted by another bar-related write, and so any accidental inter-thread corrupting writes will not be drawn (unlike if we relied on `T`'s sync, since we have multiple `write()` calls when rendering and blanking.) *NOTE*: using `AtomicRefCell` i think is actually still be preferable for those reasons. If `T` can be shared and written to with internal sync (like stdout/err,) then non-`Bar` writes are not affected, but `Bar` writes are better contained.
46    output: AtomicRefCell<T>
47}
48
49/// The default size of the terminal bar when the programmer does not provide her own.
50/// Or if `size` is not used.
51pub const DEFAULT_SIZE: usize = 50;
52
53/// The default size of the max render bar when the programmer does not provide her own.
54/// Or if `size` is not used.
55pub const DEFAULT_MAX_BORDER_SIZE: usize = 20;
56
57/*
58impl<T: Default + io::Write> Default for Bar<T>
59{
60#[inline] 
61fn default() -> Self
62{
63Self::new(T::default(), DEFAULT_SIZE)
64    }
65}
66 */
67
68impl Default for Bar
69{
70    #[inline]
71    fn default() -> Self
72    {
73	Self::new_default(DEFAULT_SIZE)
74    }
75}
76
77
78impl Bar {
79    /// Create a new bar `width` long with a title using the default output descriptor `stdout`.
80    #[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    /// Attempt to create a new bar with max display width of our terminal and a title.
87    ///
88    /// If `stdout` is not a terminal, then `None` is returned.
89    #[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    /// Create a new bar with max display width of our terminal
97    ///
98    /// # Notes
99    /// Without feature `size`, will be the same as `Self::with_max(width, width +20)`
100    ///
101    /// To try to create one that always adheres to `size`, use the `try_new()` family of functions.
102    #[inline]
103    pub fn new_default(width: usize) -> Self
104    {
105	Self::new(create_default_output_device(), width)
106    }
107    
108    /// Attempt to create a new bar with max display width of our terminal.
109    ///
110    /// If `stdout` is not a terminal, then `None` is returned.
111    #[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    /// Attempt to create a new bar with max display width of our terminal.
119    ///
120    /// If `stdout` is not a terminal, then `None` is returned.
121    #[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    /// Create a bar with a max display width
129    ///
130    /// # Panics
131    /// If `width` is larger than or equal to `max_width`.
132    #[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    /// Create a new bar `width` long with a title.
142    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    /// Attempt to create a new bar with max display width of our terminal and a title.
156    ///
157    /// If `output` is not a terminal, then `None` is returned.
158    #[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    /// Create a new bar with max display width of our terminal
178    ///
179    /// # Notes
180    /// Without feature `size`, will be the same as `Self::with_max(width, width +20)`
181    ///
182    /// To try to create one that always adheres to `size`, use the `try_new()` family of functions.
183    #[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    /// Attempt to create a new bar with max display width of our terminal.
206    ///
207    /// If `output` is not a terminal, then `None` is returned.
208    #[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    /// Attempt to create a new bar with max display width of our terminal.
219    ///
220    /// If `output` is not a terminal, then `None` is returned.
221    #[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    /// Create a bar with a max display width
229    ///
230    /// # Panics
231    /// If `width` is larger than or equal to `max_width`.
232    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    /// Fit to terminal's width if possible.
259    ///
260    /// # Notes
261    /// Available with feature `size` only.
262    ///
263    /// # Returns
264    /// If the re-fit succeeded.
265    /// A `fit()` will also fail if another thread is already engaging in a display operation.
266    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    /// Update the buffer.
293    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    /// Consume the bar and complete it, regardless of progress.
311    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	// If another thread is writing, just abort (XXX: Is this the best way to handle it?)
374	//
375	// We acquire the lock after work allocation and computation to keep it for the shortest amount of time, this is an acceptible tradeoff since multiple threads shouldn't be calling this at once anyway.
376	let Ok(mut out) = self.output.try_borrow_mut() else { return };
377	
378	//TODO: What to do about I/O errors?
379	let _ = write!(out, "\x1B[0m\x1B[K{}", temp) // XXX: For now, just abort if one fails.
380	    .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	// If another thread is writing, just abort (XXX: Is this the best way to handle it?)
389	let Ok(mut out) = self.output.try_borrow_mut() else { return };
390	
391	//TODO: What to do about I/O errors?
392	let _ = out.write_all(b"\r")
393	    .and_then(|_| stackalloc::stackalloc(max_width, b' ',|spaces| out.write_all(spaces))) // Write `max_width` spaces (TODO: Is there a better way to do this? With no allocation? With a repeating iterator maybe?)
394	    .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	// self.refresh(), with exclusive access. (XXX: Maybe move this to a non-pub `&mut self` helper function)
410	let out = self.output.get_mut();
411	
412	//TODO: What to do about I/O errors?
413	let _ = out.write_all(b"\r")
414	    .and_then(|_| stackalloc::stackalloc(max_width, b' ',|spaces| out.write_all(spaces))) // Write `max_width` spaces (TODO: Is there a better way to do this? With no allocation? With a repeating iterator maybe?)
415	    .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	// self.refresh(), with exclusive access. (XXX: Maybe move this to a non-pub `&mut self` helper function)
424	let out = self.output.get_mut();
425	
426	//TODO: What to do about I/O errors?
427	let _ = out.write_all(b"\r")
428	    .and_then(|_| stackalloc::stackalloc(to, b' ',|spaces| out.write_all(spaces))) // Write `max_width` spaces (TODO: Is there a better way to do this? With no allocation? With a repeating iterator maybe?)
429	    .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	// self.refresh(), with exclusive access. (XXX: Maybe move this to a non-pub `&mut self` helper function)
450	let out = self.output.get_mut();
451	
452	//TODO: What to do about I/O errors?
453	let _ = out.write_all(b"\r")
454	    .and_then(|_| stackalloc::stackalloc(max_width, b' ',|spaces| out.write_all(spaces))) // Write `max_width` spaces (TODO: Is there a better way to do this? With no allocation? With a repeating iterator maybe?)
455	    .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	//TODO: What to do about I/O errors?
473	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	//println!();
520	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}