howudoin/consumers/
term_line.rs

1use crate::*;
2use indicatif::*;
3use report::*;
4
5/// A terminal line consumer.
6///
7/// Backended by [`indicatif`], this consumer will create a progress bars for each available report.
8/// It provides a simple line interface.
9///
10/// ![Screenshot from 2023-01-10 15-36-43](https://user-images.githubusercontent.com/13831379/211470141-2879f70a-42b5-49ad-894a-3a0bb9c57bac.png)
11///
12/// To see an example using `TermLine`, `cargo run --all-features --example term-line` can be run
13/// in the repository.
14///
15/// [`indicatif`]: https://github.com/console-rs/indicatif
16pub struct TermLine {
17    debounce: Duration,
18    bars: flat_tree::FlatTree<Id, ProgressBar>,
19    mp: MultiProgress,
20}
21
22impl Consume for TermLine {
23    fn debounce(&self) -> Duration {
24        self.debounce
25    }
26
27    fn rpt(&mut self, rpt: &report::Report, id: Id, parent: Option<Id>, _: &Controller) {
28        match self.bars.get(&id) {
29            Some(x) => update_bar(x, rpt),
30            None => update_bar(&self.add_bar(id, parent), rpt),
31        };
32    }
33
34    fn closed(&mut self, id: Id) {
35        if let Some(bar) = self.bars.remove(&id) {
36            bar.finish_and_clear();
37            self.mp.remove(&bar);
38        }
39    }
40}
41
42impl TermLine {
43    /// Create a new, default, `TermLine`.
44    pub fn new() -> Self {
45        Self {
46            debounce: Duration::from_millis(50),
47            mp: MultiProgress::new(),
48            bars: Default::default(),
49        }
50    }
51
52    /// Create a new `TermLine` with the debounce duration.
53    pub fn with_debounce(debounce: Duration) -> Self {
54        Self {
55            debounce,
56            ..Self::new()
57        }
58    }
59
60    fn add_bar(&mut self, id: Id, parent: Option<Id>) -> ProgressBar {
61        match parent.and_then(|x| self.bars.get(&x)).cloned() {
62            None => {
63                let bar = self.mp.add(pb());
64                self.bars.insert_root(id, bar.clone());
65                bar
66            }
67            Some(parent) => {
68                let bar = self.mp.insert_after(&parent, pb());
69                self.bars.insert(id, bar.clone());
70                bar
71            }
72        }
73    }
74}
75
76impl Default for TermLine {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82fn update_bar(pb: &ProgressBar, rpt: &Report) {
83    let Report {
84        label,
85        desc,
86        state,
87        accums,
88    } = rpt;
89
90    pb.set_prefix(label.clone());
91    pb.set_message(desc.clone());
92
93    match state {
94        State::InProgress {
95            len,
96            pos,
97            bytes,
98            remaining: _,
99        } => {
100            pb.set_length(len.unwrap_or(!0));
101            pb.set_position(*pos);
102            match len.is_some() {
103                true => pb.set_style(bar_style(*bytes)),
104                false => pb.set_style(spinner_style(*bytes)),
105            }
106        }
107
108        State::Completed { duration } => {
109            pb.finish_with_message(format!(
110                "finished in {}",
111                HumanDuration(Duration::try_from_secs_f32(*duration).unwrap_or_default())
112            ));
113        }
114
115        State::Cancelled => {
116            pb.abandon_with_message("cancelled");
117        }
118    }
119
120    for Message { severity, msg } in accums {
121        pb.println(format!("{severity}: {msg}"));
122    }
123}
124
125fn pb() -> ProgressBar {
126    let pb = ProgressBar::hidden().with_style(spinner_style(false));
127    pb.enable_steady_tick(std::time::Duration::from_millis(250));
128    pb
129}
130
131fn spinner_style(fmt_bytes: bool) -> ProgressStyle {
132    let tmp = if fmt_bytes {
133        format!(
134            " {} {}: {} {} {}",
135            SPINNER, PREFIX, BYTES, BYTES_PER_SEC, MSG
136        )
137    } else {
138        format!(" {} {}: {} {}", SPINNER, PREFIX, POS, MSG)
139    };
140    ProgressStyle::default_bar()
141        .template(&tmp)
142        .expect("template should be fine")
143        .progress_chars("=> ")
144        .tick_chars(r#"|/-\|"#)
145}
146
147fn bar_style(fmt_bytes: bool) -> ProgressStyle {
148    let tmp = if fmt_bytes {
149        format!(
150            " {} {} {} {}
151 {} {} ({}/{}) {}",
152            SPINNER, PREFIX, BYTES_PER_SEC, ETA, BAR, PCT, BYTES, BYTES_TOTAL, MSG
153        )
154    } else {
155        format!(
156            " {} {} {}
157 {} {} ({}/{}) {}",
158            SPINNER, PREFIX, ETA, BAR, PCT, POS, LEN, MSG
159        )
160    };
161
162    ProgressStyle::default_bar()
163        .template(&tmp)
164        .expect("template should be fine")
165        .progress_chars("=> ")
166        .tick_chars(r#"|/-\|"#)
167}
168
169const SPINNER: &str = "{spinner:.red.bold}";
170const PREFIX: &str = "{prefix:.cyan.bold}";
171const BYTES: &str = "{bytes}";
172const BYTES_TOTAL: &str = "{total_bytes}";
173const BYTES_PER_SEC: &str = "<{binary_bytes_per_sec:.yellow.bold}>";
174const POS: &str = "{pos}";
175const LEN: &str = "{len}";
176const ETA: &str = "({eta:.green.bold.italic})";
177const BAR: &str = "[{bar:30}]";
178const PCT: &str = "{percent:>03}%";
179const MSG: &str = "{wide_msg:.cyan}";