diff --git a/Cargo.lock b/Cargo.lock index b62eeef2ba..2c803cb39e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -293,6 +293,19 @@ dependencies = [ "cc", ] +[[package]] +name = "console" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "terminal_size", + "winapi", +] + [[package]] name = "core-foundation" version = "0.9.1" @@ -556,6 +569,12 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.28" @@ -928,6 +947,18 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" +dependencies = [ + "console", + "lazy_static", + "number_prefix", + "regex", +] + [[package]] name = "ipnet" version = "2.3.0" @@ -1205,6 +1236,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "once_cell" version = "1.7.2" @@ -1739,6 +1776,7 @@ dependencies = [ "flate2", "git-testament", "home", + "indicatif", "lazy_static", "libc", "num_cpus", @@ -2109,6 +2147,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "textwrap" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index ccb4328b10..0449573670 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ url = "2.1" wait-timeout = "0.2" xz2 = "0.1.3" zstd = "0.6" +indicatif = "0.16" [dependencies.retry] default-features = false diff --git a/src/cli/download_tracker.rs b/src/cli/download_tracker.rs index 2cb64665cf..11de3a787d 100644 --- a/src/cli/download_tracker.rs +++ b/src/cli/download_tracker.rs @@ -1,83 +1,79 @@ -use std::collections::VecDeque; use std::fmt; -use std::io::Write; -use std::time::{Duration, Instant}; +use std::time::Duration; -use term::Terminal; - -use super::term2; use crate::dist::Notification as In; use crate::utils::tty; -use crate::utils::units::{Size, Unit, UnitMode}; use crate::utils::Notification as Un; use crate::Notification; -/// Keep track of this many past download amounts -const DOWNLOAD_TRACK_COUNT: usize = 5; +fn new_progress_bar() -> indicatif::ProgressBar { + let progress_bar = indicatif::ProgressBar::hidden(); + progress_bar.set_style( + indicatif::ProgressStyle::default_bar() + .template("Total: {bytes} Speed: {bytes_per_sec} Elapsed: {elapsed}"), + ); + progress_bar +} /// Tracks download progress and displays information about it to a terminal. pub struct DownloadTracker { - /// Content-Length of the to-be downloaded object. - content_len: Option, - /// Total data downloaded in bytes. - total_downloaded: usize, - /// Data downloaded this second. - downloaded_this_sec: usize, - /// Keeps track of amount of data downloaded every last few secs. - /// Used for averaging the download speed. NB: This does not necessarily - /// represent adjacent seconds; thus it may not show the average at all. - downloaded_last_few_secs: VecDeque, - /// Time stamp of the last second - last_sec: Option, - /// Time stamp of the start of the download - start_sec: Option, - /// The terminal we write the information to. - /// XXX: Could be a term trait, but with #1818 on the horizon that - /// is a pointless change to make - better to let that transition - /// happen and take stock after that. - term: term2::StdoutTerminal, - /// Whether we displayed progress for the download or not. - /// - /// If the download is quick enough, we don't have time to - /// display the progress info. - /// In that case, we do not want to do some cleanup stuff we normally do. - /// - /// If we have displayed progress, this is the number of characters we - /// rendered, so we can erase it cleanly. - displayed_charcount: Option, - /// What units to show progress in - units: Vec, - /// Whether we display progress + progress_bar: indicatif::ProgressBar, display_progress: bool, } +// Note: Sometimes the [`DownloadTracker`]'s methods are called without setting +// the content length. impl DownloadTracker { /// Creates a new DownloadTracker. pub fn new() -> Self { Self { - content_len: None, - total_downloaded: 0, - downloaded_this_sec: 0, - downloaded_last_few_secs: VecDeque::with_capacity(DOWNLOAD_TRACK_COUNT), - start_sec: None, - last_sec: None, - term: term2::stdout(), - displayed_charcount: None, - units: vec![Unit::B], + progress_bar: new_progress_bar(), display_progress: true, } } - pub fn with_display_progress(mut self, display_progress: bool) -> Self { - self.display_progress = display_progress; + pub fn with_display_progress(mut self, display: bool) -> Self { + self.display_progress = display; self } + /// Notifies self that Content-Length information has been received. + pub fn content_length_received(&mut self, content_len: u64) { + if self.display_progress { + self.progress_bar = indicatif::ProgressBar::new(content_len); + self.progress_bar + .set_style(indicatif::ProgressStyle::default_bar().template( + " {bytes} / {total_bytes} ({percent:3.0}%) {bytes_per_sec} in {elapsed} ETA: {eta}", + )); + self.progress_bar + .set_draw_target(indicatif::ProgressDrawTarget::hidden()); + } + } + + /// Notifies self that data of size `len` has been received. + pub fn data_received(&mut self, len: usize) { + self.progress_bar.inc(len as u64); + if self.display_progress + && self.progress_bar.is_hidden() + && self.progress_bar.elapsed() >= Duration::from_secs(1) + { + self.progress_bar + .set_draw_target(indicatif::ProgressDrawTarget::stdout()); + } + } + + /// Notifies self that the download has finished. + pub fn download_finished(&mut self) { + if self.display_progress && self.progress_bar.elapsed() >= Duration::from_secs(1) { + self.progress_bar.finish(); + } + self.progress_bar = new_progress_bar(); + } + pub(crate) fn handle_notification(&mut self, n: &Notification<'_>) -> bool { match *n { Notification::Install(In::Utils(Un::DownloadContentLengthReceived(content_len))) => { self.content_length_received(content_len); - true } Notification::Install(In::Utils(Un::DownloadDataReceived(data))) => { @@ -90,142 +86,9 @@ impl DownloadTracker { self.download_finished(); true } - Notification::Install(In::Utils(Un::DownloadPushUnit(unit))) => { - self.push_unit(unit); - true - } - Notification::Install(In::Utils(Un::DownloadPopUnit)) => { - self.pop_unit(); - true - } - _ => false, } } - - /// Notifies self that Content-Length information has been received. - pub fn content_length_received(&mut self, content_len: u64) { - self.content_len = Some(content_len as usize); - } - - /// Notifies self that data of size `len` has been received. - pub fn data_received(&mut self, len: usize) { - self.total_downloaded += len; - self.downloaded_this_sec += len; - - let current_time = Instant::now(); - - match self.last_sec { - None => self.last_sec = Some(current_time), - Some(prev) => { - let elapsed = current_time.saturating_duration_since(prev); - if elapsed >= Duration::from_secs(1) { - if self.display_progress { - self.display(); - } - self.last_sec = Some(current_time); - if self.downloaded_last_few_secs.len() == DOWNLOAD_TRACK_COUNT { - self.downloaded_last_few_secs.pop_back(); - } - self.downloaded_last_few_secs - .push_front(self.downloaded_this_sec); - self.downloaded_this_sec = 0; - } - } - } - } - /// Notifies self that the download has finished. - pub fn download_finished(&mut self) { - if self.displayed_charcount.is_some() { - // Display the finished state - self.display(); - let _ = writeln!(self.term); - } - self.prepare_for_new_download(); - } - /// Resets the state to be ready for a new download. - fn prepare_for_new_download(&mut self) { - self.content_len = None; - self.total_downloaded = 0; - self.downloaded_this_sec = 0; - self.downloaded_last_few_secs.clear(); - self.start_sec = Some(Instant::now()); - self.last_sec = None; - self.displayed_charcount = None; - } - /// Display the tracked download information to the terminal. - fn display(&mut self) { - match self.start_sec { - // Maybe forgot to call `prepare_for_new_download` first - None => {} - Some(start_sec) => { - // Panic if someone pops the default bytes unit... - let unit = *self.units.last().unwrap(); - let total_h = Size::new(self.total_downloaded, unit, UnitMode::Norm); - let sum: usize = self.downloaded_last_few_secs.iter().sum(); - let len = self.downloaded_last_few_secs.len(); - let speed = if len > 0 { sum / len } else { 0 }; - let speed_h = Size::new(speed, unit, UnitMode::Rate); - let elapsed_h = Instant::now().saturating_duration_since(start_sec); - - // First, move to the start of the current line and clear it. - let _ = self.term.carriage_return(); - // We'd prefer to use delete_line() but on Windows it seems to - // sometimes do unusual things - // let _ = self.term.as_mut().unwrap().delete_line(); - // So instead we do: - if let Some(n) = self.displayed_charcount { - // This is not ideal as very narrow terminals might mess up, - // but it is more likely to succeed until term's windows console - // fixes whatever's up with delete_line(). - let _ = write!(self.term, "{}", " ".repeat(n)); - let _ = self.term.flush(); - let _ = self.term.carriage_return(); - } - - let output = match self.content_len { - Some(content_len) => { - let content_len_h = Size::new(content_len, unit, UnitMode::Norm); - let percent = (self.total_downloaded as f64 / content_len as f64) * 100.; - let remaining = content_len - self.total_downloaded; - let eta_h = Duration::from_secs(if speed == 0 { - std::u64::MAX - } else { - (remaining / speed) as u64 - }); - format!( - "{} / {} ({:3.0} %) {} in {} ETA: {}", - total_h, - content_len_h, - percent, - speed_h, - elapsed_h.display(), - eta_h.display(), - ) - } - None => format!( - "Total: {} Speed: {} Elapsed: {}", - total_h, - speed_h, - elapsed_h.display() - ), - }; - - let _ = write!(self.term, "{}", output); - // Since stdout is typically line-buffered and we don't print a newline, we manually flush. - let _ = self.term.flush(); - self.displayed_charcount = Some(output.chars().count()); - } - } - } - - pub(crate) fn push_unit(&mut self, new_unit: Unit) { - self.units.push(new_unit); - } - - pub fn pop_unit(&mut self) { - self.units.pop(); - } } trait DurationDisplay { diff --git a/src/diskio/threaded.rs b/src/diskio/threaded.rs index c7c8cc4386..44fe30d91f 100644 --- a/src/diskio/threaded.rs +++ b/src/diskio/threaded.rs @@ -15,7 +15,6 @@ use sharded_slab::pool::{OwnedRef, OwnedRefMut}; use super::{perform, CompletedIo, Executor, Item}; use crate::utils::notifications::Notification; -use crate::utils::units::Unit; #[derive(Copy, Clone, Debug, Enum)] pub(crate) enum Bucket { @@ -263,7 +262,6 @@ impl<'a> Executor for Threaded<'a> { let mut prev_files = self.n_files.load(Ordering::Relaxed); if let Some(handler) = self.notify_handler { handler(Notification::DownloadFinished); - handler(Notification::DownloadPushUnit(Unit::IO)); handler(Notification::DownloadContentLengthReceived( prev_files as u64, )); @@ -289,7 +287,6 @@ impl<'a> Executor for Threaded<'a> { self.pool.join(); if let Some(handler) = self.notify_handler { handler(Notification::DownloadFinished); - handler(Notification::DownloadPopUnit); } // close the feedback channel so that blocking reads on it can // complete. send is atomic, and we know the threads completed from the diff --git a/src/utils/notifications.rs b/src/utils/notifications.rs index 56ff44d6b3..936a9f2aa5 100644 --- a/src/utils/notifications.rs +++ b/src/utils/notifications.rs @@ -4,7 +4,7 @@ use std::path::Path; use url::Url; use crate::utils::notify::NotificationLevel; -use crate::utils::units::{self, Unit}; +use crate::utils::units; #[derive(Debug)] pub enum Notification<'a> { @@ -19,12 +19,6 @@ pub enum Notification<'a> { DownloadDataReceived(&'a [u8]), /// Download has finished. DownloadFinished, - /// The things we're tracking that are not counted in bytes. - /// Must be paired with a pop-units; our other calls are not - /// setup to guarantee this any better. - DownloadPushUnit(Unit), - /// finish using an unusual unit. - DownloadPopUnit, NoCanonicalPath(&'a Path), ResumingPartialDownload, /// This would make more sense as a crate::notifications::Notification @@ -54,8 +48,6 @@ impl<'a> Notification<'a> { | DownloadingFile(_, _) | DownloadContentLengthReceived(_) | DownloadDataReceived(_) - | DownloadPushUnit(_) - | DownloadPopUnit | DownloadFinished | ResumingPartialDownload | UsingCurl @@ -94,8 +86,6 @@ impl<'a> Display for Notification<'a> { DownloadingFile(url, _) => write!(f, "downloading file from: '{}'", url), DownloadContentLengthReceived(len) => write!(f, "download size is: '{}'", len), DownloadDataReceived(data) => write!(f, "received some data of size {}", data.len()), - DownloadPushUnit(_) => Ok(()), - DownloadPopUnit => Ok(()), DownloadFinished => write!(f, "download finished"), NoCanonicalPath(path) => write!(f, "could not canonicalize path: '{}'", path.display()), ResumingPartialDownload => write!(f, "resuming partial download"), diff --git a/src/utils/units.rs b/src/utils/units.rs index 39fddb1538..772ba6a82f 100644 --- a/src/utils/units.rs +++ b/src/utils/units.rs @@ -1,11 +1,13 @@ use std::fmt::{self, Display}; +#[allow(dead_code)] #[derive(Copy, Clone, Debug)] pub enum Unit { B, IO, } +#[allow(dead_code)] pub(crate) enum UnitMode { Norm, Rate,