|
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; |
5 | 3 |
|
6 | 4 | use crate::dist::Notification as In;
|
7 | 5 | use crate::notifications::Notification;
|
8 |
| -use crate::process::{Process, terminalsource}; |
| 6 | +use crate::process::Process; |
9 | 7 | 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; |
14 | 8 |
|
15 | 9 | /// Tracks download progress and displays information about it to a terminal.
|
16 | 10 | ///
|
17 | 11 | /// *not* safe for tracking concurrent downloads yet - it is basically undefined
|
18 | 12 | /// what will happen.
|
19 | 13 | 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, |
49 | 18 | }
|
50 | 19 |
|
51 | 20 | impl DownloadTracker {
|
52 | 21 | /// Creates a new DownloadTracker.
|
53 | 22 | 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 | + |
54 | 29 | 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(), |
66 | 32 | }
|
67 | 33 | }
|
68 | 34 |
|
69 | 35 | pub(crate) fn handle_notification(&mut self, n: &Notification<'_>) -> bool {
|
70 | 36 | match *n {
|
71 | 37 | Notification::Install(In::Utils(Un::DownloadContentLengthReceived(content_len))) => {
|
72 | 38 | self.content_length_received(content_len);
|
73 |
| - |
74 | 39 | true
|
75 | 40 | }
|
76 | 41 | 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()); |
80 | 43 | true
|
81 | 44 | }
|
82 | 45 | Notification::Install(In::Utils(Un::DownloadFinished)) => {
|
83 | 46 | self.download_finished();
|
84 | 47 | true
|
85 | 48 | }
|
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, |
94 | 51 |
|
95 | 52 | _ => false,
|
96 | 53 | }
|
97 | 54 | }
|
98 | 55 |
|
99 |
| - /// Notifies self that Content-Length information has been received. |
| 56 | + /// Sets the length for a new ProgressBar and gives it a style. |
100 | 57 | 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 | + ); |
102 | 66 | }
|
103 | 67 |
|
104 | 68 | /// Notifies self that data of size `len` has been received.
|
105 | 69 | 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()); |
128 | 72 | }
|
| 73 | + self.progress_bar.inc(len as u64); |
129 | 74 | }
|
| 75 | + |
130 | 76 | /// Notifies self that the download has finished.
|
131 | 77 | 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(); |
290 | 81 | }
|
291 | 82 | }
|
0 commit comments