1use std::{
2 borrow::Cow,
3 thread::JoinHandle,
4 time::{Duration, Instant},
5};
6
7use terminal_size::Width;
8
9use crate::{
10 internal::{FillItem, Item},
11 Error, DEFAULT_DRAW_DELAY, DEFAULT_DRAW_INTERVAL, MIN_ETA_ELAPSED, MIN_SPEED_ELAPSED,
12};
13
14pub struct State {
27 pos: u64,
28 total: Option<u64>,
29 percent: Option<f64>,
30 pre_inc: bool,
31 thousands_separator: String,
32 message: Cow<'static, str>,
33
34 start_time: Instant,
35 speed: Option<f64>,
36 eta_instant: Option<Instant>,
37
38 items: Vec<Item>,
39
40 prev_draw: Option<Instant>,
41 next_draw: Option<Instant>,
42 is_finished: bool,
43}
44
45impl State {
46 pub fn eta(&self) -> Option<Duration> {
61 if self.is_finished {
62 Some(Duration::ZERO)
63 } else if let Some(eta) = self.eta_instant {
64 eta.checked_duration_since(Instant::now())
65 } else {
66 None
67 }
68 }
69
70 pub fn percent(&self) -> Option<f64> {
89 self.percent
90 }
91
92 pub fn pos(&self) -> u64 {
105 self.pos
106 }
107
108 pub fn speed(&self) -> Option<f64> {
120 self.speed
121 }
122
123 pub fn thousands_separator(&self) -> &str {
140 &self.thousands_separator
141 }
142
143 pub fn total(&self) -> Option<u64> {
155 self.total
156 }
157}
158
159impl State {
163 pub(crate) fn finish(&mut self, drawer: &JoinHandle<()>) {
164 if !self.is_finished {
165 if let Some(total) = self.total {
166 self.pos = total;
167 } else {
168 self.total = Some(self.pos);
169 }
170 self.percent = Some(100.0);
171 self.eta_instant = None;
172 self.is_finished = true;
173 drawer.thread().unpark();
174
175 self.draw();
176 if terminal_size::terminal_size().is_some() {
177 eprintln!();
178 }
179 }
180 }
181
182 pub(crate) fn finish_and_clear(&mut self, drawer: &JoinHandle<()>) {
183 if !self.is_finished {
184 self.is_finished = true;
185 drawer.thread().unpark();
186
187 if let Some((Width(width), _)) = terminal_size::terminal_size() {
188 let width = width as usize;
189 eprint!("\r{:width$.width$}\r", "");
190 }
191 }
192 }
193
194 pub(crate) fn finish_at_current_pos(&mut self, drawer: &JoinHandle<()>) {
195 if !self.is_finished {
196 self.is_finished = true;
197 drawer.thread().unpark();
198
199 self.draw();
200 if terminal_size::terminal_size().is_some() {
201 eprintln!();
202 }
203 }
204 }
205
206 pub(crate) fn finish_quietly(&mut self, drawer: &JoinHandle<()>) {
211 if !self.is_finished {
212 self.is_finished = true;
213 drawer.thread().unpark();
214 }
215 }
216
217 pub(crate) fn is_finished(&self) -> bool {
218 self.is_finished
219 }
220
221 pub(crate) fn inc(&mut self, steps: u64, drawer: &JoinHandle<()>) {
222 let now = Instant::now();
223 let elapsed = now - self.start_time;
224
225 self.pos += steps;
226
227 let completed = if self.pre_inc {
228 self.pos.saturating_sub(1)
229 } else {
230 self.pos
231 };
232
233 if elapsed >= MIN_SPEED_ELAPSED && completed > 0 {
234 self.speed = Some(completed as f64 / elapsed.as_secs_f64());
235 }
236
237 if let Some(total) = self.total {
238 self.percent = Some(completed as f64 / total as f64 * 100.0);
239
240 if completed > total {
241 self.eta_instant = None;
242 } else if elapsed >= MIN_ETA_ELAPSED && completed > 0 {
243 let duration = elapsed.mul_f64(total as f64 / completed as f64);
244 self.eta_instant = Some(self.start_time + duration);
245 }
246 }
247
248 self.queue_draw(now, drawer);
249 }
250
251 pub(crate) fn message(
252 &mut self,
253 message: impl Into<Cow<'static, str>>,
254 drawer: &JoinHandle<()>,
255 ) {
256 self.message = message.into();
257 self.queue_draw(Instant::now(), drawer);
258 }
259
260 pub(crate) fn new(
261 total: Option<u64>,
262 pre_inc: bool,
263 thousands_separator: String,
264 items: Vec<Item>,
265 ) -> Result<Self, Error> {
266 let mut fill_item_count = 0;
267 for item in &items {
268 if let Item::Fill(_) = item {
269 fill_item_count += 1;
270 }
271 }
272
273 if fill_item_count > 1 {
274 Err(Error::MultipleFillItems)
275 } else {
276 let now = Instant::now();
277
278 Ok(Self {
279 pos: 0,
280 total,
281 percent: if total.is_none() { None } else { Some(0.0) },
282 pre_inc,
283 thousands_separator,
284 message: Cow::Borrowed(""),
285
286 start_time: now,
287 speed: None,
288 eta_instant: None,
289
290 items,
291
292 prev_draw: None,
293 next_draw: Some(now + DEFAULT_DRAW_DELAY),
294 is_finished: false,
295 })
296 }
297 }
298
299 pub(crate) fn try_draw(&mut self) -> Result<(), Option<Duration>> {
304 assert!(!self.is_finished);
305
306 if let Some(next_draw) = self.next_draw {
307 let now = Instant::now();
308 if next_draw > now {
309 Err(Some(next_draw - now))
310 } else {
311 self.draw();
312 self.prev_draw = Some(now);
313 self.next_draw = None;
314 Ok(())
315 }
316 } else {
317 Err(None)
318 }
319 }
320}
321
322impl State {
326 fn draw(&mut self) {
327 if let Some((Width(width), _)) = terminal_size::terminal_size() {
328 let width = width as usize;
329
330 let mut pre_fill = String::with_capacity(width);
331 let mut fill = None;
332 let mut post_fill = String::with_capacity(width);
333
334 for item in &self.items {
335 let active = if fill.is_none() {
336 &mut pre_fill
337 } else {
338 &mut post_fill
339 };
340
341 match item {
342 Item::Fill(item) => fill = Some(item),
343 Item::Fn(f) => active.push_str(&f(self)),
344 Item::Literal(s) => active.push_str(s),
345 }
346 }
347
348 let fill_width =
349 width.saturating_sub(pre_fill.chars().count() + post_fill.chars().count());
350
351 let mut line = String::with_capacity(width);
352 line.push_str(&pre_fill);
353 match fill {
354 Some(&FillItem::Bar) => {
355 if let Some(percent) = self.percent {
356 let done_width =
357 ((fill_width as f64 * percent / 100.0) as usize).min(fill_width);
358 line.push_str(&"#".repeat(done_width));
359 line.push_str(&"-".repeat(fill_width - done_width));
360 } else {
361 line.push_str(&" ".repeat(fill_width));
362 }
363 }
364
365 Some(FillItem::Message) => {
366 line.push_str(&format!("{:fill_width$.fill_width$}", self.message))
367 }
368
369 None => (),
370 }
371 line.push_str(&post_fill);
372
373 eprint!("\r{:width$.width$}", line);
374 }
375 }
376
377 fn queue_draw(&mut self, now: Instant, drawer: &JoinHandle<()>) {
378 if !self.is_finished && self.next_draw.is_none() {
379 let mut next_draw = now + DEFAULT_DRAW_DELAY;
380 if let Some(prev_draw) = self.prev_draw {
381 next_draw = next_draw.max(prev_draw + DEFAULT_DRAW_INTERVAL);
382 }
383 self.next_draw = Some(next_draw);
384
385 drawer.thread().unpark();
386 }
387 }
388}