Skip to main content

otter_support/
termprogress.rs

1// Copyright 2020-2021 Ian Jackson and contributors to Otter
2// SPDX-License-Identifier: AGPL-3.0-or-later There is NO WARRANTY.
3
4use crate::prelude::*;
5
6use unicode_width::UnicodeWidthChar;
7
8const FORCE_VAR: &str = "OTTER_TERMPROGRESS_FORCE";
9
10type Col = usize;
11
12pub trait Reporter {
13  fn report(&mut self, pi: &ProgressInfo<'_>);
14  fn clear(&mut self);
15}
16
17pub struct Null;
18impl Null {
19  pub fn reporter() -> Box<dyn Reporter> { Box::new(Null) }
20}
21
22#[allow(unused_variables)]
23impl Reporter for Null {
24  fn report(&mut self, pi: &ProgressInfo<'_>) { }
25  fn clear(&mut self) { }
26}
27
28pub fn reporter() -> Box<dyn Reporter> {
29  let term = console::Term::buffered_stderr();
30
31  let mut newlines = false;
32  let mut recheck_width = true;
33  let width = if let Ok(val) = env::var(FORCE_VAR) {
34    let mut val = &val[..];
35    if let Some(rhs) = val.strip_prefix('+') {
36        val = rhs;
37        newlines = true;
38    }
39    let width = val.parse()
40      .expect(&format!("bad {} syntax", FORCE_VAR));
41    recheck_width = false;
42    Some(width)
43  } else {
44    if_chain!{
45      if term.is_term();
46      if let Some((_, width)) = term.size_checked();
47      then { Some(width.into()) }
48      else { None }
49    }
50  };
51
52  if let Some(width) = width {
53    Box::new(TermReporter {
54      term, width, newlines, recheck_width,
55      needs_clear: None,
56      spinner: 0,
57    })
58  } else {
59    Box::new(Null)
60  }
61}
62
63pub struct TermReporter {
64  term: console::Term,
65  width: Col,
66  needs_clear: Option<()>,
67  spinner: usize,
68  newlines: bool,
69  recheck_width: bool,
70}
71
72const LHS_TARGET: Col = 25;
73const LHS_FRAC: f32 = (LHS_TARGET as f32) / 78.0;
74const SPINNER: &[char] = &['-', '\\', '/'];
75
76impl Reporter for TermReporter {
77  fn report(&mut self, pi: &ProgressInfo<'_>) {
78    if self.recheck_width {
79      if let Some((_, width)) = self.term.size_checked() {
80        self.width = width.into()
81      }
82    }
83
84    let mut out = String::new();
85    let w = self.width;
86    if let Some(w) = w.checked_sub(1) {
87      out.push(SPINNER[self.spinner]);
88      self.spinner += 1; self.spinner %= SPINNER.len();
89      if let Some(w) = w.checked_sub(1) {
90        let lhs = min(LHS_TARGET, ((w as f32) * LHS_FRAC) as Col);
91        self.bar(&mut out, lhs,     &pi.phase);
92        out.push('|');
93        self.bar(&mut out, w - lhs, &pi.item);
94      }
95    }
96    self.clear_line();
97    if out.len() > 0 {
98      if self.newlines {
99        writeln!(&mut self.term, "{}", out).unwrap_or(());
100      } else {
101        self.needs_clear = Some(());
102        self.term.write_str(&out).unwrap_or(());
103      }
104    }
105    self.term.flush().unwrap_or(());
106  }
107
108  fn clear(&mut self) {
109    self.clear_line();
110    self.term.flush().unwrap_or(());
111  }
112}
113
114impl TermReporter {
115  fn clear_line(&mut self) {
116    if let Some(()) = self.needs_clear.take() {
117      self.term.clear_line().unwrap_or(());
118    }
119  }
120
121  fn bar(&self, out: &mut String, fwidth: Col, info: &progress::Count) {
122    let desc = console::strip_ansi_codes(&info.desc);
123    let w_change = min(
124      (info.value.fraction() * (fwidth as f32)) as Col,
125      fwidth // just in case
126    );
127    let mut desc = desc
128      .chars()
129      .chain(iter::repeat(' '))
130      .peekable();
131    let mut w_sofar = 0;
132
133    let mut half = |stop_at|{
134      let mut accumulate = String::new();
135      loop {
136        let &c = desc.peek().unwrap();
137        if_let!{ Some(w) = c.width(); else continue; }
138        let w_next = w_sofar + w;
139        if w_next > stop_at { break }
140        accumulate.push(c);
141        w_sofar = w_next;
142        let _ = desc.next().unwrap();
143      }
144      accumulate
145    };
146
147    let lhs = half(w_change);
148    if lhs.len() > 0 {
149      let style = console::Style::new().for_stderr().reverse();
150      *out += &style.apply_to(lhs).to_string();
151    }
152
153    *out += &half(fwidth);
154    out.extend( iter::repeat(' ').take( fwidth - w_sofar ));
155  }
156}
157
158impl Drop for TermReporter {
159  fn drop(&mut self) {
160    self.clear();
161  }
162}
163
164pub struct Nest {
165  outer_total: f32,
166  outer_phase_base: f32,
167  outer_phase_size: f32,
168  desc_prefix: String,
169  actual_reporter: Box<dyn Reporter>,
170}
171
172impl Nest {
173  pub fn new(actual_reporter: Box<dyn Reporter>)
174             -> Self { Nest {
175    actual_reporter,
176    outer_total: 1.,
177    outer_phase_base: 0.,
178    outer_phase_size: 0.,
179    desc_prefix: default(),
180  } }
181
182  pub fn with_total(outer_total: f32, actual_reporter: Box<dyn Reporter>)
183             -> Self { Nest {
184    actual_reporter,
185    outer_total,
186    outer_phase_base: 0.,
187    outer_phase_size: 0.,
188    desc_prefix: default(),
189  } }
190
191  /// Starts an outer phase which is `frac` of the whole
192  ///
193  /// From now on, when reports are issued, the inner phases are each
194  /// mapped to the range "now" to "now" `frac`
195  pub fn start_phase(&mut self, frac: f32,
196                     phase_prefix: Option<String>,
197                     item_desc: Cow<'_,str>) {
198    self.outer_phase_base += self.outer_phase_size;
199    self.outer_phase_size = frac;
200
201    if let Some(p) = phase_prefix {
202      self.desc_prefix = p;
203    }
204
205    let f = self.outer_phase_base / self.outer_total;
206    let value = progress::Value::Fraction { f };
207
208    self.actual_reporter.report(&ProgressInfo {
209      phase: progress::Count { desc: (&*self.desc_prefix).into(), value },
210      item:  progress::Count { desc: item_desc,          value: default() },
211    });
212  }
213}
214
215impl Reporter for Nest {
216  fn report(&mut self, inner_pi: &ProgressInfo<'_>) {
217    let inner_frac = inner_pi.phase.value.fraction();
218    let outer_frac =
219      (self.outer_phase_size * inner_frac +
220       self.outer_phase_base) / self.outer_total;
221
222    let desc = if self.desc_prefix != "" {
223      format!("{} {}", &self.desc_prefix, inner_pi.phase.desc).into()
224    } else {
225      (*inner_pi.phase.desc).into()
226    };
227
228    let outer_value = progress::Value::Fraction { f: outer_frac };
229    let outer_phase = progress::Count {
230      desc,
231      value: outer_value,
232    };
233    let outer_pi = ProgressInfo {
234      phase: outer_phase,
235      item:  inner_pi.item.clone(),
236    };
237
238    self.actual_reporter.report(&outer_pi);
239  }
240
241  fn clear(&mut self) {
242    self.actual_reporter.clear();
243  }
244}