kyuri/
lib.rs

1//! A simple progress display library.
2//!
3//! Kyuri is a simple progress display library. Different from [indicatif](https://github.com/console-rs/indicatif), it:
4//! - Depends on std only when terminal support is unnecessary.
5//!   - Custom features `console_width` and `unicode` are available for ANSI mode terminal width detection and Unicode width calculation.
6//! - The `Manager` (like `MultiProgress` in indicatif) manages all progress bar management and rendering.
7//! - Friendly to writing to files.
8//! - Predictable about when it would draw.
9//! - Custom integrations with other libraries (an example: examples/tracing.rs)
10//!
11//! ## Examples
12//!
13//! ```
14//! use kyuri::Manager;
15//!
16//! const TEMPLATE: &str = "{msg}: {bar} ({pos}/{len})";
17//! let manager = Manager::new(std::time::Duration::from_secs(1));
18//!
19//! let bar = manager.create_bar(100, "Processing", TEMPLATE, true);
20//! for i in 0..=100 {
21//!     bar.set_pos(i);
22//!     std::thread::sleep(std::time::Duration::from_millis(1));
23//! }
24//! bar.finish_and_drop();
25//! ```
26//!
27//! ## Template
28//!
29//! The template in Kyuri looks like the one in indicatif. However, only a very small subset is implemented, and some have different meanings.
30//!
31//! Tags in template looks like `{something}`. Supported tags:
32//! - `{msg}`, `{message}`: The message of the bar.
33//! - `{elapsed}`, `{elapsed_precise}`: The elapsed time (H:MM:SS).
34//! - `{bytes}`: The current position in bytes (power-of-two, `KiB`, `MiB`, ...).
35//! - `{pos}`: The current position.
36//! - `{total_bytes}`: The total length in bytes (power-of-two, `KiB`, `MiB`, ...).
37//! - `{total}`, `{len}`: The total length.
38//! - `{bytes_per_sec}`, `{bytes_per_second}`: The current speed in bytes per second.
39//! - `{eta}`: The estimated time of arrival (H:MM:SS).
40//! - `{bar}`, `{barNUM}`: The progress bar. The `NUM` is the size of the bar, default is 20.
41//! - `{state_emoji}`: The state emoji of the bar. ✅ for finished, 🆕 for new, 💥 for overflowed, ⏳ for in progress.
42//!
43//! Doubled `{` and `}` would not be interpreted as tags.
44
45#![warn(missing_docs)]
46
47use std::{
48    collections::BTreeMap,
49    sync::{
50        atomic::{AtomicBool, AtomicUsize},
51        Arc, Mutex, Weak,
52    },
53};
54
55mod template;
56mod ticker;
57pub mod writer;
58use template::{Template, TemplatePart};
59use termsize::get_width;
60use ticker::Ticker;
61mod termsize;
62
63const CLEAR_ANSI: &str = "\r\x1b[K";
64const UP_ANSI: &str = "\x1b[F";
65
66pub(crate) struct BarState {
67    len: u64,
68    pos: u64,
69    message: String,
70    template: Template,
71    created_at: std::time::Instant,
72    visible: bool,
73    /// Note that need_redraw for individual bars would only be respected when output is not a terminal.
74    need_redraw: bool,
75}
76
77fn duration_to_human(duration: std::time::Duration) -> String {
78    let elapsed = duration.as_secs();
79    let hours = elapsed / 3600;
80    let minutes = (elapsed % 3600) / 60;
81    let seconds = elapsed % 60;
82    format!("{}:{:02}:{:02}", hours, minutes, seconds)
83}
84
85fn bytes_to_human(bytes: u64) -> String {
86    const KB: u64 = 1024;
87    const MB: u64 = KB * 1024;
88    const GB: u64 = MB * 1024;
89    const TB: u64 = GB * 1024;
90
91    if bytes < KB {
92        format!("{} B", bytes)
93    } else if bytes < MB {
94        format!("{:.2} KiB", bytes as f64 / KB as f64)
95    } else if bytes < GB {
96        format!("{:.2} MiB", bytes as f64 / MB as f64)
97    } else if bytes < TB {
98        format!("{:.2} GiB", bytes as f64 / GB as f64)
99    } else {
100        format!("{:.2} TiB", bytes as f64 / TB as f64)
101    }
102}
103
104fn string_width(s: &str) -> usize {
105    #[cfg(feature = "unicode")]
106    {
107        unicode_width::UnicodeWidthStr::width(s)
108    }
109
110    #[cfg(not(feature = "unicode"))]
111    {
112        s.chars().count()
113    }
114}
115
116impl BarState {
117    pub fn render(&self) -> String {
118        let mut result = String::new();
119        let elapsed = std::time::Instant::now() - self.created_at;
120        let bytes_per_second = self.pos as f64 / elapsed.as_secs_f64();
121        for part in self.template.parts.iter() {
122            match part {
123                TemplatePart::Text(text) => {
124                    result.push_str(text);
125                }
126                TemplatePart::Newline => {
127                    result.push('\n');
128                }
129                TemplatePart::Message => {
130                    result.push_str(&self.message);
131                }
132                TemplatePart::Elapsed => {
133                    result.push_str(&duration_to_human(elapsed));
134                }
135                TemplatePart::Bytes => {
136                    result.push_str(&bytes_to_human(self.pos));
137                }
138                TemplatePart::Pos => {
139                    result.push_str(&self.pos.to_string());
140                }
141                TemplatePart::TotalBytes => {
142                    result.push_str(&bytes_to_human(self.len));
143                }
144                TemplatePart::Total => {
145                    result.push_str(&self.len.to_string());
146                }
147                TemplatePart::BytesPerSecond => {
148                    result.push_str(&format!("{}/s", bytes_to_human(bytes_per_second as u64)));
149                }
150                TemplatePart::Eta => {
151                    if self.pos == 0 {
152                        result.push_str("Unknown");
153                    } else {
154                        let eta = (self.len - self.pos) as f64 / bytes_per_second;
155                        result.push_str(&duration_to_human(std::time::Duration::from_secs(
156                            eta as u64,
157                        )));
158                    }
159                }
160                TemplatePart::Bar(size) => {
161                    let filled = (self.pos as f64 / self.len as f64 * *size as f64) as usize;
162                    if *size >= filled {
163                        let empty = *size - filled;
164                        result.push('[');
165                        for _ in 0..filled {
166                            result.push('=');
167                        }
168                        for _ in 0..empty {
169                            result.push(' ');
170                        }
171                        result.push(']');
172                    } else {
173                        let overflowed = filled - *size;
174                        result.push('[');
175                        for _ in 0..*size {
176                            result.push('=');
177                        }
178                        for _ in 0..overflowed {
179                            result.push('!');
180                        }
181                    }
182                }
183                TemplatePart::StateEmoji => {
184                    if self.pos == self.len {
185                        result.push('✅');
186                    } else if self.pos == 0 {
187                        result.push('🆕');
188                    } else if self.pos > self.len {
189                        result.push('💥');
190                    } else {
191                        // 0 < self.pos < self.len
192                        result.push('⏳');
193                    }
194                }
195            }
196        }
197        result
198    }
199}
200
201/// A handle for users to control a progress bar created by `Manager`.
202pub struct Bar {
203    id: usize,
204    manager: Weak<ManagerInner>,
205}
206
207/// Lock order:
208/// - last_draw
209/// - out
210/// - states
211pub(crate) struct ManagerInner {
212    states: Mutex<BTreeMap<usize, Arc<Mutex<BarState>>>>,
213    ansi: Mutex<Option<bool>>,
214    interval: std::time::Duration,
215    pub(crate) out: Arc<Mutex<Box<dyn Out>>>,
216    ticker: Mutex<Option<Ticker>>,
217    force_when_finished: AtomicBool,
218
219    // interval states
220    next_id: AtomicUsize,
221    last_draw: Mutex<std::time::Instant>,
222    last_lines: AtomicUsize,
223    need_redraw: AtomicBool,
224}
225
226impl ManagerInner {
227    pub(crate) fn is_ticker_enabled(&self) -> bool {
228        self.ticker.lock().unwrap().is_some()
229    }
230
231    /// This is expected to be called only when it's ANSI mode.
232    pub(crate) fn clear_existing(&self, out: &mut Box<dyn Out>) {
233        for _ in 0..self.last_lines.load(std::sync::atomic::Ordering::Acquire) {
234            let _ = out.write_all(format!("{}{}", UP_ANSI, CLEAR_ANSI).as_bytes());
235        }
236    }
237
238    pub(crate) fn is_terminal(&self, out: &mut Box<dyn Out>) -> bool {
239        let ansi = self.ansi.lock().unwrap();
240        match *ansi {
241            None => out.is_terminal(),
242            Some(force) => force,
243        }
244    }
245
246    pub(crate) fn draw_inner(
247        &self,
248        states: &BTreeMap<usize, Arc<Mutex<BarState>>>,
249        out: &mut Box<dyn Out>,
250        is_terminal: bool,
251    ) {
252        let mut newlines = 0;
253        for state in states.values() {
254            let mut state = state.lock().unwrap();
255            if !state.visible {
256                continue;
257            }
258            if !is_terminal && !state.need_redraw {
259                continue;
260            }
261            let outstr = format!("{}\n", state.render());
262            if is_terminal {
263                let splits = outstr.split('\n');
264                let term_col = get_width(out.as_ref()) as usize;
265                for i in splits {
266                    let width = string_width(i);
267                    newlines += width / term_col;
268                    if width % term_col != 0 {
269                        newlines += 1;
270                    }
271                }
272            }
273            let _ = out.write_all(outstr.as_bytes());
274            state.need_redraw = false;
275        }
276        if is_terminal {
277            self.last_lines
278                .store(newlines, std::sync::atomic::Ordering::Release);
279        }
280    }
281
282    pub(crate) fn mark_redraw(&self) {
283        self.need_redraw
284            .store(true, std::sync::atomic::Ordering::Release);
285    }
286
287    pub(crate) fn draw(&self, force: bool) {
288        if !force && self.is_ticker_enabled() {
289            return;
290        }
291        let now = std::time::Instant::now();
292        let mut last_draw = self.last_draw.lock().unwrap();
293        if !force && now - *last_draw < self.interval {
294            return;
295        }
296
297        if !self
298            .need_redraw
299            .swap(false, std::sync::atomic::Ordering::AcqRel)
300        {
301            return;
302        }
303        let mut out = self.out.lock().unwrap();
304        let states = self.states.lock().unwrap();
305        let is_terminal = self.is_terminal(&mut out);
306        if is_terminal && states.len() > 0 {
307            // Don't clean output when no bars are present
308            self.clear_existing(&mut out);
309        }
310
311        self.draw_inner(&states, &mut out, is_terminal);
312
313        *last_draw = now;
314    }
315
316    pub(crate) fn suspend<F: FnOnce(&mut Box<dyn Out>) -> R, R>(&self, f: F) -> R {
317        let mut out = self.out.lock().unwrap();
318        let is_terminal = self.is_terminal(&mut out);
319        if is_terminal {
320            self.clear_existing(&mut out);
321        }
322        let result = f(&mut out);
323        if is_terminal {
324            let states = self.states.lock().unwrap();
325            self.draw_inner(&states, &mut out, is_terminal);
326        }
327        result
328    }
329}
330
331/// Trait for progress output streams, requires Unix file descriptor support.
332/// `std::io::stdout`, `std::io::stderr` and `std::fs::File` implement this trait.
333#[cfg(all(unix, feature = "console_width"))]
334pub trait Out: std::io::Write + std::io::IsTerminal + std::os::fd::AsRawFd + Send + Sync {}
335#[cfg(all(unix, feature = "console_width"))]
336impl<T: std::io::Write + std::io::IsTerminal + std::os::fd::AsRawFd + Send + Sync> Out for T {}
337
338/// Trait for progress output streams, requires Windows HANDLE support.
339/// `std::io::stdout`, `std::io::stderr` and `std::fs::File` implement this trait.
340#[cfg(all(windows, feature = "console_width"))]
341pub trait Out:
342    std::io::Write + std::io::IsTerminal + std::os::windows::io::AsRawHandle + Send + Sync
343{
344}
345#[cfg(all(windows, feature = "console_width"))]
346impl<T: std::io::Write + std::io::IsTerminal + std::os::windows::io::AsRawHandle + Send + Sync> Out
347    for T
348{
349}
350
351/// Trait for progress output streams.
352/// `std::io::stdout`, `std::io::stderr` and `std::fs::File` implement this trait.
353#[cfg(not(any(
354    all(windows, feature = "console_width"),
355    all(unix, feature = "console_width")
356)))]
357pub trait Out: std::io::Write + std::io::IsTerminal + Send + Sync {}
358#[cfg(not(any(
359    all(windows, feature = "console_width"),
360    all(unix, feature = "console_width")
361)))]
362impl<T: std::io::Write + std::io::IsTerminal + Send + Sync> Out for T {}
363
364/// The manager for progress bars. It's expected for users to create a `Manager`, create progress bars from it,
365/// and drop it when all work has been done.
366///
367/// When manager is dropped, it would force a draw. After that bars would not be able to be interacted with.
368pub struct Manager {
369    inner: Arc<ManagerInner>,
370}
371
372impl Manager {
373    /// Create a new `Manager` to stdout.
374    ///
375    /// The `interval` parameter specifies the minimum interval between two unforced draws.
376    pub fn new(interval: std::time::Duration) -> Self {
377        Manager {
378            inner: Arc::new(ManagerInner {
379                states: Mutex::new(BTreeMap::new()),
380                next_id: AtomicUsize::new(0),
381                interval,
382                out: Arc::new(Mutex::new(Box::new(std::io::stdout()))),
383                last_draw: Mutex::new(std::time::Instant::now() - interval),
384                last_lines: AtomicUsize::new(0),
385                ansi: Mutex::new(None),
386                need_redraw: AtomicBool::new(false),
387                ticker: Mutex::new(None),
388                force_when_finished: AtomicBool::new(true),
389            }),
390        }
391    }
392
393    fn mark_redraw(&self) {
394        self.inner.mark_redraw();
395    }
396
397    /// Set the `Manager` to write to stdout.
398    pub fn with_stdout(self) -> Self {
399        *self.inner.out.lock().unwrap() = Box::new(std::io::stdout());
400        self.mark_redraw();
401        self
402    }
403
404    /// Set the `Manager` to write to stderr.
405    pub fn with_stderr(self) -> Self {
406        *self.inner.out.lock().unwrap() = Box::new(std::io::stderr());
407        self.mark_redraw();
408        self
409    }
410
411    /// Set the `Manager` to write to a file.
412    pub fn with_file(self, file: std::fs::File) -> Self {
413        *self.inner.out.lock().unwrap() = Box::new(file);
414        self.mark_redraw();
415        self
416    }
417
418    /// Let `Manager` automatically detect whether it's writing to a terminal and use ANSI or not.
419    pub fn auto_ansi(self) -> Self {
420        *self.inner.ansi.lock().unwrap() = None;
421        self.mark_redraw();
422        self
423    }
424
425    /// Force `Manager` to use ANSI escape codes or not.
426    pub fn force_ansi(self, force: bool) -> Self {
427        *self.inner.ansi.lock().unwrap() = Some(force);
428        self.mark_redraw();
429        self
430    }
431
432    /// Ticker enables a background thread to draw progress bars at a fixed interval.
433    ///
434    /// When ticker is enabled, unforced draw would be ignored.
435    pub fn set_ticker(&self, set_ticker: bool) {
436        let mut ticker = self.inner.ticker.lock().unwrap();
437        if set_ticker && ticker.is_none() {
438            *ticker = Some(Ticker::new(self.inner.clone()));
439        } else if !set_ticker && ticker.is_some() {
440            *ticker = None;
441        }
442    }
443
444    /// If manager shall forcely draw when pos == len without explicitly calling finish().
445    ///
446    /// Default is true.
447    pub fn force_draw_when_finished(&self, force: bool) {
448        self.inner
449            .force_when_finished
450            .store(force, std::sync::atomic::Ordering::Release);
451    }
452
453    /// Create a new progress bar.
454    ///
455    /// - `len`: The total length of the progress bar.
456    /// - `message`: The message of the bar. Use `{msg}` in the template to refer to this.
457    /// - `template`: The template of the bar.
458    /// - `visible`: Whether the bar is visible.
459    ///
460    /// This makes a forced draw when visible is true.
461    pub fn create_bar(&self, len: u64, message: &str, template: &str, visible: bool) -> Bar {
462        let id = self
463            .inner
464            .next_id
465            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
466        let bar_state = Arc::new(Mutex::new(BarState {
467            len,
468            pos: 0,
469            message: message.to_string(),
470            template: Template::new(template),
471            created_at: std::time::Instant::now(),
472            visible,
473            need_redraw: true,
474        }));
475
476        self.inner
477            .states
478            .lock()
479            .unwrap()
480            .insert(id, bar_state.clone());
481
482        if visible {
483            self.mark_redraw();
484            self.draw(true);
485        }
486
487        Bar {
488            manager: Arc::downgrade(&self.inner),
489            id,
490        }
491    }
492
493    /// Draw all progress bars. In most cases it's not necessary to call this manually.
494    ///
495    /// If nothing changed, it would not draw no matter what.
496    ///
497    /// If ticker is enabled, unforced draw would be ignored. Otherwise, it would only draw when the interval has passed.
498    ///
499    /// Progress bars would be drawn by the order of `Bar` creation. In ANSI mode, it would clear the previous output.
500    ///
501    /// Finally, when output is not a terminal, bars would be drawn only when it needs to be redrawn.
502    pub fn draw(&self, force: bool) {
503        self.inner.draw(force);
504    }
505
506    /// Hide all progress bars, run the closure, and show them again like indicatif::MultiProgress::suspend.
507    ///
508    /// This method is used for implementing integrations with other libraries that may print to the terminal.
509    ///
510    /// When output is not a terminal, the closure would still be run but nothing would be done to the progress bars.
511    pub fn suspend<F: FnOnce(&mut Box<dyn Out>) -> R, R>(&self, f: F) -> R {
512        self.inner.suspend(f)
513    }
514
515    /// Create a writer for integration with other libraries.
516    pub fn create_writer(&self) -> writer::KyuriWriter {
517        writer::KyuriWriter::new(self.inner.clone())
518    }
519}
520
521impl Drop for ManagerInner {
522    /// Force a draw when the `ManagerInner` is dropped.
523    fn drop(&mut self) {
524        self.draw(true);
525    }
526}
527
528impl Bar {
529    fn get_manager_and_state(&self) -> Option<(Arc<ManagerInner>, Arc<Mutex<BarState>>)> {
530        let manager = self.manager.upgrade()?;
531        let state = manager.states.lock().unwrap().get(&self.id)?.clone();
532        Some((manager, state))
533    }
534
535    fn check_if_force_draw(&self, manager: Arc<ManagerInner>, pos: u64, len: u64) {
536        if pos == len && manager
537            .force_when_finished
538            .load(std::sync::atomic::Ordering::Acquire)
539        {
540            manager.draw(true);
541        } else {
542            manager.draw(false);
543        }
544    }
545
546    /// Increment the progress bar by `n`. This makes an unforced draw.
547    pub fn inc(&self, n: u64) {
548        if let Some((manager, state)) = self.get_manager_and_state() {
549            let mut state = state.lock().unwrap();
550            state.pos += n;
551            state.need_redraw = true;
552            let pos = state.pos;
553            let len = state.len;
554            // Drop state before drawing, deadlock otherwise!
555            std::mem::drop(state);
556            manager.mark_redraw();
557            self.check_if_force_draw(manager, pos, len);
558        }
559    }
560
561    /// Set the position of the progress bar. This makes an unforced draw.
562    pub fn set_pos(&self, pos: u64) {
563        if let Some((manager, state)) = self.get_manager_and_state() {
564            let mut state = state.lock().unwrap();
565            state.pos = pos;
566            state.need_redraw = true;
567            let pos = state.pos;
568            let len = state.len;
569            // Drop state before drawing, deadlock otherwise!
570            std::mem::drop(state);
571            manager.mark_redraw();
572            self.check_if_force_draw(manager, pos, len);
573        }
574    }
575
576    /// Set the total length of the progress bar. This makes an unforced draw.
577    pub fn set_len(&self, len: u64) {
578        if let Some((manager, state)) = self.get_manager_and_state() {
579            let mut state = state.lock().unwrap();
580            state.len = len;
581            state.need_redraw = true;
582            let pos = state.pos;
583            let len = state.len;
584            // Drop state before drawing, deadlock otherwise!
585            std::mem::drop(state);
586            manager.mark_redraw();
587            self.check_if_force_draw(manager, pos, len);
588        }
589    }
590
591    /// Get the position of the progress bar.
592    ///
593    /// When manager is dropped, this would return 0
594    pub fn get_pos(&self) -> u64 {
595        self.get_manager_and_state()
596            .map_or(0, |(_, state)| state.lock().unwrap().pos)
597    }
598
599    /// Get the total length of the progress bar.
600    ///
601    /// When manager is dropped, this would return 0
602    pub fn get_len(&self) -> u64 {
603        self.get_manager_and_state()
604            .map_or(0, |(_, state)| state.lock().unwrap().len)
605    }
606
607    /// Set the progress bar to the end, and force a draw.
608    pub fn finish(&self) {
609        if let Some((manager, state)) = self.get_manager_and_state() {
610            let state = state.lock().unwrap();
611            let pos = state.pos;
612            let len = state.len;
613            if pos != len {
614                self.set_pos(len);
615            }
616            std::mem::drop(state);
617            manager.draw(true);
618        }
619    }
620
621    /// Set the progress bar to the end, force a draw, and remove the progress bar from the manager.
622    pub fn finish_and_drop(self) {
623        self.finish();
624        // Automatically drop
625    }
626
627    /// Set the visibility of the progress bar. This makes an forced draw when visible actually changes.
628    pub fn set_visible(&self, visible: bool) {
629        if let Some((manager, state)) = self.get_manager_and_state() {
630            let mut state = state.lock().unwrap();
631            if state.visible != visible {
632                state.visible = visible;
633                state.need_redraw = true;
634                // Drop state before drawing, deadlock otherwise!
635                std::mem::drop(state);
636                manager.mark_redraw();
637                manager.draw(true);
638            }
639        }
640    }
641
642    /// Get the visibility of the progress bar.
643    ///
644    /// When manager is dropped, this would return false
645    pub fn is_visible(&self) -> bool {
646        self.get_manager_and_state()
647            .map_or(false, |(_, state)| state.lock().unwrap().visible)
648    }
649
650    /// Set the message of the progress bar. This makes an unforced draw.
651    pub fn set_message(&self, message: &str) {
652        if let Some((manager, state)) = self.get_manager_and_state() {
653            let mut state = state.lock().unwrap();
654            state.message = message.to_string();
655            state.need_redraw = true;
656            // Drop state before drawing, deadlock otherwise!
657            std::mem::drop(state);
658            manager.mark_redraw();
659            manager.draw(false);
660        }
661    }
662
663    /// Set the template of the progress bar. This makes an unforced draw.
664    pub fn set_template(&self, template: &str) {
665        if let Some((manager, state)) = self.get_manager_and_state() {
666            let mut state = state.lock().unwrap();
667            state.template = Template::new(template);
668            state.need_redraw = true;
669            // Drop state before drawing, deadlock otherwise!
670            std::mem::drop(state);
671            manager.mark_redraw();
672            manager.draw(false);
673        }
674    }
675
676    /// Return whether the progress bar (the manager) is still alive.
677    ///
678    /// When the manager is dropped, the progress bar would not be able to be interacted with.
679    pub fn alive(&self) -> bool {
680        self.get_manager_and_state().is_some()
681    }
682}
683
684impl Drop for Bar {
685    /// Drop the progress bar. This removes the progress bar from the manager and forces a draw.
686    fn drop(&mut self) {
687        if let Some((manager, _)) = self.get_manager_and_state() {
688            manager.states.lock().unwrap().remove(&self.id);
689            manager.mark_redraw();
690            manager.draw(true);
691        }
692    }
693}
694
695#[cfg(test)]
696mod tests {
697    use std::io::{Read, Seek};
698
699    use super::*;
700
701    #[test]
702    fn basic_test() {
703        let manager = Manager::new(std::time::Duration::from_secs(1));
704        let bar_1 = manager.create_bar(
705            100,
706            "Downloading",
707            "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
708            true,
709        );
710        let bar_2 = manager.create_bar(
711            100,
712            "Uploading",
713            "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
714            true,
715        );
716
717        bar_1.set_pos(50);
718        bar_2.set_pos(25);
719
720        std::mem::drop(bar_1);
721        std::mem::drop(bar_2);
722    }
723
724    #[test]
725    fn dont_crash_when_zero() {
726        let manager = Manager::new(std::time::Duration::from_secs(1));
727        let bar = manager.create_bar(
728            0,
729            "Downloading",
730            "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
731            true,
732        );
733
734        bar.set_pos(0);
735        manager.draw(true);
736    }
737
738    #[test]
739    fn inc() {
740        let manager = Manager::new(std::time::Duration::from_secs(1));
741        let bar = manager.create_bar(
742            100,
743            "Downloading",
744            "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
745            true,
746        );
747
748        bar.inc(10);
749        bar.inc(10);
750        bar.inc(10);
751        bar.inc(10);
752        bar.inc(10);
753
754        assert_eq!(bar.get_pos(), 50);
755
756        std::mem::drop(bar);
757    }
758
759    #[test]
760    fn visible() {
761        let manager = Manager::new(std::time::Duration::from_secs(1));
762        let bar = manager.create_bar(
763            100,
764            "Downloading",
765            "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
766            true,
767        );
768
769        assert!(bar.is_visible());
770
771        bar.set_visible(false);
772        assert!(!bar.is_visible());
773
774        std::mem::drop(bar);
775    }
776
777    #[test]
778    fn ticker() {
779        let manager = Manager::new(std::time::Duration::from_secs(1));
780        manager.set_ticker(true);
781        let bar = manager.create_bar(
782            100,
783            "Downloading",
784            "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
785            true,
786        );
787
788        std::thread::sleep(std::time::Duration::from_secs(2));
789        std::mem::drop(bar);
790    }
791
792    #[test]
793    fn alive() {
794        let manager = Manager::new(std::time::Duration::from_secs(1));
795        let bar = manager.create_bar(
796            100,
797            "Downloading",
798            "{msg}\n[{elapsed}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
799            true,
800        );
801
802        assert!(bar.alive());
803
804        std::mem::drop(manager);
805        assert!(!bar.alive());
806    }
807
808    #[cfg(target_os = "linux")]
809    #[test]
810    fn test_pb_to_file() {
811        const TEMPLATE_SIMPLE: &str = "{msg}\n{bytes}/{total_bytes}";
812        let memfd_name = std::ffi::CString::new("test_pb_to_file").unwrap();
813        let memfd_fd =
814            nix::sys::memfd::memfd_create(&memfd_name, nix::sys::memfd::MemFdCreateFlag::empty())
815                .unwrap();
816        let memfd_writer: std::fs::File = memfd_fd.into();
817        let mut memfd_writer_clone = memfd_writer.try_clone().unwrap();
818        let progressbar_manager =
819            Manager::new(std::time::Duration::from_secs(1)).with_file(memfd_writer);
820        let pb1 = progressbar_manager.create_bar(
821            10,
822            "Downloading http://d1.example.com/",
823            TEMPLATE_SIMPLE,
824            true,
825        );
826        let pb2 = progressbar_manager.create_bar(
827            10,
828            "Downloading http://d2.example.com/",
829            TEMPLATE_SIMPLE,
830            true,
831        );
832
833        pb1.set_pos(2);
834        pb2.set_pos(3);
835        progressbar_manager.draw(true);
836        pb1.set_pos(5);
837        pb2.set_pos(7);
838
839        std::mem::drop(progressbar_manager);
840        memfd_writer_clone
841            .seek(std::io::SeekFrom::Start(0))
842            .unwrap();
843        let mut output = String::new();
844        memfd_writer_clone.read_to_string(&mut output).unwrap();
845        assert_eq!(
846            output,
847            r#"Downloading http://d1.example.com/
8480 B/10 B
849Downloading http://d2.example.com/
8500 B/10 B
851Downloading http://d1.example.com/
8522 B/10 B
853Downloading http://d2.example.com/
8543 B/10 B
855Downloading http://d1.example.com/
8565 B/10 B
857Downloading http://d2.example.com/
8587 B/10 B
859"#
860        );
861    }
862}