otter_support/
termprogress.rs1use 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 );
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 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}