Skip to content

Commit 1a5f8ca

Browse files
feat(download_tracker): refactor in favor of indicatif
Co-authored-by: 0xPoe <[email protected]>
1 parent e6fea71 commit 1a5f8ca

File tree

3 files changed

+53
-318
lines changed

3 files changed

+53
-318
lines changed

src/cli/download_tracker.rs

Lines changed: 34 additions & 243 deletions
Original file line numberDiff line numberDiff line change
@@ -1,291 +1,82 @@
1-
use std::collections::VecDeque;
2-
use std::fmt;
3-
use std::io::Write;
4-
use std::time::{Duration, Instant};
1+
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
2+
use std::time::Duration;
53

64
use crate::dist::Notification as In;
75
use crate::notifications::Notification;
8-
use crate::process::{Process, terminalsource};
6+
use crate::process::Process;
97
use crate::utils::Notification as Un;
10-
use crate::utils::units::{Size, Unit, UnitMode};
11-
12-
/// Keep track of this many past download amounts
13-
const DOWNLOAD_TRACK_COUNT: usize = 5;
148

159
/// Tracks download progress and displays information about it to a terminal.
1610
///
1711
/// *not* safe for tracking concurrent downloads yet - it is basically undefined
1812
/// what will happen.
1913
pub(crate) struct DownloadTracker {
20-
/// Content-Length of the to-be downloaded object.
21-
content_len: Option<usize>,
22-
/// Total data downloaded in bytes.
23-
total_downloaded: usize,
24-
/// Data downloaded this second.
25-
downloaded_this_sec: usize,
26-
/// Keeps track of amount of data downloaded every last few secs.
27-
/// Used for averaging the download speed. NB: This does not necessarily
28-
/// represent adjacent seconds; thus it may not show the average at all.
29-
downloaded_last_few_secs: VecDeque<usize>,
30-
/// Time stamp of the last second
31-
last_sec: Option<Instant>,
32-
/// Time stamp of the start of the download
33-
start_sec: Option<Instant>,
34-
term: terminalsource::ColorableTerminal,
35-
/// Whether we displayed progress for the download or not.
36-
///
37-
/// If the download is quick enough, we don't have time to
38-
/// display the progress info.
39-
/// In that case, we do not want to do some cleanup stuff we normally do.
40-
///
41-
/// If we have displayed progress, this is the number of characters we
42-
/// rendered, so we can erase it cleanly.
43-
displayed_charcount: Option<usize>,
44-
/// What units to show progress in
45-
units: Vec<Unit>,
46-
/// Whether we display progress
47-
display_progress: bool,
48-
stdout_is_a_tty: bool,
14+
/// MultiProgress bar for the downloads.
15+
multi_progress_bars: MultiProgress,
16+
/// ProgressBar for the current download.
17+
progress_bar: ProgressBar,
4918
}
5019

5120
impl DownloadTracker {
5221
/// Creates a new DownloadTracker.
5322
pub(crate) fn new_with_display_progress(display_progress: bool, process: &Process) -> Self {
23+
let multi_progress_bars = MultiProgress::with_draw_target(if display_progress {
24+
process.progress_draw_target()
25+
} else {
26+
ProgressDrawTarget::hidden()
27+
});
28+
5429
Self {
55-
content_len: None,
56-
total_downloaded: 0,
57-
downloaded_this_sec: 0,
58-
downloaded_last_few_secs: VecDeque::with_capacity(DOWNLOAD_TRACK_COUNT),
59-
start_sec: None,
60-
last_sec: None,
61-
term: process.stdout().terminal(process),
62-
displayed_charcount: None,
63-
units: vec![Unit::B],
64-
display_progress,
65-
stdout_is_a_tty: process.stdout().is_a_tty(process),
30+
multi_progress_bars,
31+
progress_bar: ProgressBar::hidden(),
6632
}
6733
}
6834

6935
pub(crate) fn handle_notification(&mut self, n: &Notification<'_>) -> bool {
7036
match *n {
7137
Notification::Install(In::Utils(Un::DownloadContentLengthReceived(content_len))) => {
7238
self.content_length_received(content_len);
73-
7439
true
7540
}
7641
Notification::Install(In::Utils(Un::DownloadDataReceived(data))) => {
77-
if self.stdout_is_a_tty {
78-
self.data_received(data.len());
79-
}
42+
self.data_received(data.len());
8043
true
8144
}
8245
Notification::Install(In::Utils(Un::DownloadFinished)) => {
8346
self.download_finished();
8447
true
8548
}
86-
Notification::Install(In::Utils(Un::DownloadPushUnit(unit))) => {
87-
self.push_unit(unit);
88-
true
89-
}
90-
Notification::Install(In::Utils(Un::DownloadPopUnit)) => {
91-
self.pop_unit();
92-
true
93-
}
49+
Notification::Install(In::Utils(Un::DownloadPushUnit(_))) => true,
50+
Notification::Install(In::Utils(Un::DownloadPopUnit)) => true,
9451

9552
_ => false,
9653
}
9754
}
9855

99-
/// Notifies self that Content-Length information has been received.
56+
/// Sets the length for a new ProgressBar and gives it a style.
10057
pub(crate) fn content_length_received(&mut self, content_len: u64) {
101-
self.content_len = Some(content_len as usize);
58+
self.progress_bar.set_length(content_len);
59+
self.progress_bar.set_style(
60+
ProgressStyle::with_template(
61+
"[{bar:40}] {bytes}/{total_bytes} ({bytes_per_sec}, ETA: {eta})",
62+
)
63+
.unwrap()
64+
.progress_chars("## "),
65+
);
10266
}
10367

10468
/// Notifies self that data of size `len` has been received.
10569
pub(crate) fn data_received(&mut self, len: usize) {
106-
self.total_downloaded += len;
107-
self.downloaded_this_sec += len;
108-
109-
let current_time = Instant::now();
110-
111-
match self.last_sec {
112-
None => self.last_sec = Some(current_time),
113-
Some(prev) => {
114-
let elapsed = current_time.saturating_duration_since(prev);
115-
if elapsed >= Duration::from_secs(1) {
116-
if self.display_progress {
117-
self.display();
118-
}
119-
self.last_sec = Some(current_time);
120-
if self.downloaded_last_few_secs.len() == DOWNLOAD_TRACK_COUNT {
121-
self.downloaded_last_few_secs.pop_back();
122-
}
123-
self.downloaded_last_few_secs
124-
.push_front(self.downloaded_this_sec);
125-
self.downloaded_this_sec = 0;
126-
}
127-
}
70+
if self.progress_bar.is_hidden() && self.progress_bar.elapsed() >= Duration::from_secs(1) {
71+
self.multi_progress_bars.add(self.progress_bar.clone());
12872
}
73+
self.progress_bar.inc(len as u64);
12974
}
75+
13076
/// Notifies self that the download has finished.
13177
pub(crate) fn download_finished(&mut self) {
132-
if self.displayed_charcount.is_some() {
133-
// Display the finished state
134-
self.display();
135-
let _ = writeln!(self.term.lock());
136-
}
137-
self.prepare_for_new_download();
138-
}
139-
/// Resets the state to be ready for a new download.
140-
fn prepare_for_new_download(&mut self) {
141-
self.content_len = None;
142-
self.total_downloaded = 0;
143-
self.downloaded_this_sec = 0;
144-
self.downloaded_last_few_secs.clear();
145-
self.start_sec = Some(Instant::now());
146-
self.last_sec = None;
147-
self.displayed_charcount = None;
148-
}
149-
/// Display the tracked download information to the terminal.
150-
fn display(&mut self) {
151-
match self.start_sec {
152-
// Maybe forgot to call `prepare_for_new_download` first
153-
None => {}
154-
Some(start_sec) => {
155-
// Panic if someone pops the default bytes unit...
156-
let unit = *self.units.last().unwrap();
157-
let total_h = Size::new(self.total_downloaded, unit, UnitMode::Norm);
158-
let sum: usize = self.downloaded_last_few_secs.iter().sum();
159-
let len = self.downloaded_last_few_secs.len();
160-
let speed = if len > 0 { sum / len } else { 0 };
161-
let speed_h = Size::new(speed, unit, UnitMode::Rate);
162-
let elapsed_h = Instant::now().saturating_duration_since(start_sec);
163-
164-
// First, move to the start of the current line and clear it.
165-
let _ = self.term.carriage_return();
166-
// We'd prefer to use delete_line() but on Windows it seems to
167-
// sometimes do unusual things
168-
// let _ = self.term.as_mut().unwrap().delete_line();
169-
// So instead we do:
170-
if let Some(n) = self.displayed_charcount {
171-
// This is not ideal as very narrow terminals might mess up,
172-
// but it is more likely to succeed until term's windows console
173-
// fixes whatever's up with delete_line().
174-
let _ = write!(self.term.lock(), "{}", " ".repeat(n));
175-
let _ = self.term.lock().flush();
176-
let _ = self.term.carriage_return();
177-
}
178-
179-
let output = match self.content_len {
180-
Some(content_len) => {
181-
let content_len_h = Size::new(content_len, unit, UnitMode::Norm);
182-
let percent = (self.total_downloaded as f64 / content_len as f64) * 100.;
183-
let remaining = content_len - self.total_downloaded;
184-
let eta_h = Duration::from_secs(if speed == 0 {
185-
u64::MAX
186-
} else {
187-
(remaining / speed) as u64
188-
});
189-
format!(
190-
"{} / {} ({:3.0} %) {} in {}{}",
191-
total_h,
192-
content_len_h,
193-
percent,
194-
speed_h,
195-
elapsed_h.display(),
196-
Eta(eta_h),
197-
)
198-
}
199-
None => format!(
200-
"Total: {} Speed: {} Elapsed: {}",
201-
total_h,
202-
speed_h,
203-
elapsed_h.display()
204-
),
205-
};
206-
207-
let _ = write!(self.term.lock(), "{output}");
208-
// Since stdout is typically line-buffered and we don't print a newline, we manually flush.
209-
let _ = self.term.lock().flush();
210-
self.displayed_charcount = Some(output.chars().count());
211-
}
212-
}
213-
}
214-
215-
pub(crate) fn push_unit(&mut self, new_unit: Unit) {
216-
self.units.push(new_unit);
217-
}
218-
219-
pub(crate) fn pop_unit(&mut self) {
220-
self.units.pop();
221-
}
222-
}
223-
224-
struct Eta(Duration);
225-
226-
impl fmt::Display for Eta {
227-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228-
match self.0 {
229-
Duration::ZERO => Ok(()),
230-
_ => write!(f, " ETA: {}", self.0.display()),
231-
}
232-
}
233-
}
234-
235-
trait DurationDisplay {
236-
fn display(self) -> Display;
237-
}
238-
239-
impl DurationDisplay for Duration {
240-
fn display(self) -> Display {
241-
Display(self)
242-
}
243-
}
244-
245-
/// Human readable representation of a `Duration`.
246-
struct Display(Duration);
247-
248-
impl fmt::Display for Display {
249-
#[allow(clippy::many_single_char_names)]
250-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251-
const SECS_PER_YEAR: u64 = 60 * 60 * 24 * 365;
252-
let secs = self.0.as_secs();
253-
if secs > SECS_PER_YEAR {
254-
return f.write_str("Unknown");
255-
}
256-
match format_dhms(secs) {
257-
(0, 0, 0, s) => write!(f, "{s:2.0}s"),
258-
(0, 0, m, s) => write!(f, "{m:2.0}m {s:2.0}s"),
259-
(0, h, m, s) => write!(f, "{h:2.0}h {m:2.0}m {s:2.0}s"),
260-
(d, h, m, s) => write!(f, "{d:3.0}d {h:2.0}h {m:2.0}m {s:2.0}s"),
261-
}
262-
}
263-
}
264-
265-
// we're doing modular arithmetic, treat as integer
266-
fn format_dhms(sec: u64) -> (u64, u8, u8, u8) {
267-
let (mins, sec) = (sec / 60, (sec % 60) as u8);
268-
let (hours, mins) = (mins / 60, (mins % 60) as u8);
269-
let (days, hours) = (hours / 24, (hours % 24) as u8);
270-
(days, hours, mins, sec)
271-
}
272-
273-
#[cfg(test)]
274-
mod tests {
275-
use super::format_dhms;
276-
277-
#[test]
278-
fn download_tracker_format_dhms_test() {
279-
assert_eq!(format_dhms(2), (0, 0, 0, 2));
280-
281-
assert_eq!(format_dhms(60), (0, 0, 1, 0));
282-
283-
assert_eq!(format_dhms(3_600), (0, 1, 0, 0));
284-
285-
assert_eq!(format_dhms(3_600 * 24), (1, 0, 0, 0));
286-
287-
assert_eq!(format_dhms(52_292), (0, 14, 31, 32));
288-
289-
assert_eq!(format_dhms(222_292), (2, 13, 44, 52));
78+
self.progress_bar.finish_and_clear();
79+
self.multi_progress_bars.remove(&self.progress_bar);
80+
self.progress_bar = ProgressBar::hidden();
29081
}
29182
}

src/utils/notifications.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ impl Display for Notification<'_> {
8989
SetDefaultBufferSize(size) => write!(
9090
f,
9191
"using up to {} of RAM to unpack components",
92-
units::Size::new(*size, units::Unit::B, units::UnitMode::Norm)
92+
units::Size::new(*size, units::Unit::B)
9393
),
9494
DownloadingFile(url, _) => write!(f, "downloading file from: '{url}'"),
9595
DownloadContentLengthReceived(len) => write!(f, "download size is: '{len}'"),

0 commit comments

Comments
 (0)