|
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