diff options
Diffstat (limited to 'cli')
-rw-r--r-- | cli/Cargo.toml | 2 | ||||
-rw-r--r-- | cli/http_util.rs | 41 | ||||
-rw-r--r-- | cli/lsp/language_server.rs | 3 | ||||
-rw-r--r-- | cli/npm/cache.rs | 32 | ||||
-rw-r--r-- | cli/npm/registry.rs | 43 | ||||
-rw-r--r-- | cli/proc_state.rs | 3 | ||||
-rw-r--r-- | cli/tools/upgrade.rs | 48 | ||||
-rw-r--r-- | cli/util/console.rs | 7 | ||||
-rw-r--r-- | cli/util/display.rs | 44 | ||||
-rw-r--r-- | cli/util/mod.rs | 1 | ||||
-rw-r--r-- | cli/util/progress_bar.rs | 143 | ||||
-rw-r--r-- | cli/util/progress_bar/draw_thread.rs | 218 | ||||
-rw-r--r-- | cli/util/progress_bar/mod.rs | 123 | ||||
-rw-r--r-- | cli/util/progress_bar/renderer.rs | 278 |
14 files changed, 767 insertions, 219 deletions
diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 41ff1d2b8..c6309e3dc 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -59,6 +59,7 @@ chrono = { version = "=0.4.22", default-features = false, features = ["clock"] } clap = "=3.1.12" clap_complete = "=3.1.2" clap_complete_fig = "=3.1.5" +console_static_text = "=0.3.3" data-url.workspace = true dissimilar = "=1.0.4" dprint-plugin-json = "=0.17.0" @@ -72,7 +73,6 @@ flate2.workspace = true http.workspace = true import_map = "=0.13.0" indexmap = "=1.9.2" -indicatif = "=0.17.1" jsonc-parser = { version = "=0.21.0", features = ["serde"] } libc.workspace = true log = { workspace = true, features = ["serde"] } diff --git a/cli/http_util.rs b/cli/http_util.rs index 827cc75f5..744493ceb 100644 --- a/cli/http_util.rs +++ b/cli/http_util.rs @@ -1,13 +1,16 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use crate::auth_tokens::AuthToken; +use crate::util::progress_bar::UpdateGuard; use crate::version::get_user_agent; use cache_control::Cachability; use cache_control::CacheControl; use chrono::DateTime; +use deno_core::anyhow::bail; use deno_core::error::custom_error; use deno_core::error::generic_error; use deno_core::error::AnyError; +use deno_core::futures::StreamExt; use deno_core::url::Url; use deno_runtime::deno_fetch::create_http_client; use deno_runtime::deno_fetch::reqwest; @@ -243,6 +246,44 @@ impl HttpClient { self.0.get(url) } + pub async fn download_with_progress<U: reqwest::IntoUrl>( + &self, + url: U, + progress_guard: &UpdateGuard, + ) -> Result<Option<Vec<u8>>, AnyError> { + let response = self.get(url).send().await?; + + if response.status() == 404 { + Ok(None) + } else if !response.status().is_success() { + let status = response.status(); + let maybe_response_text = response.text().await.ok(); + bail!( + "Bad response: {:?}{}", + status, + match maybe_response_text { + Some(text) => format!("\n\n{}", text), + None => String::new(), + } + ); + } else if let Some(total_size) = response.content_length() { + progress_guard.set_total_size(total_size); + let mut current_size = 0; + let mut data = Vec::with_capacity(total_size as usize); + let mut stream = response.bytes_stream(); + while let Some(item) = stream.next().await { + let bytes = item?; + current_size += bytes.len() as u64; + progress_guard.set_position(current_size); + data.extend(bytes.into_iter()); + } + Ok(Some(data)) + } else { + let bytes = response.bytes().await?; + Ok(Some(bytes.into())) + } + } + /// Asynchronously fetches the given HTTP URL one pass only. /// If no redirect is present and no error occurs, /// yields Code(ResultPayload). diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 11897af9d..61e367936 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -80,6 +80,7 @@ use crate::util::fs::remove_dir_all_if_exists; use crate::util::path::ensure_directory_specifier; use crate::util::path::specifier_to_file_path; use crate::util::progress_bar::ProgressBar; +use crate::util::progress_bar::ProgressBarStyle; #[derive(Debug, Clone)] pub struct LanguageServer(Arc<tokio::sync::Mutex<Inner>>); @@ -240,7 +241,7 @@ fn create_lsp_npm_resolver( http_client: HttpClient, ) -> NpmPackageResolver { let registry_url = RealNpmRegistryApi::default_url(); - let progress_bar = ProgressBar::default(); + let progress_bar = ProgressBar::new(ProgressBarStyle::TextOnly); let npm_cache = NpmCache::from_deno_dir( dir, // Use an "only" cache setting in order to make the diff --git a/cli/npm/cache.rs b/cli/npm/cache.rs index ad6ab9db2..952ee0285 100644 --- a/cli/npm/cache.rs +++ b/cli/npm/cache.rs @@ -409,26 +409,18 @@ impl NpmCache { ); } - let _guard = self.progress_bar.update(&dist.tarball); - let response = self.http_client.get(&dist.tarball).send().await?; - - if response.status() == 404 { - bail!("Could not find npm package tarball at: {}", dist.tarball); - } else if !response.status().is_success() { - let status = response.status(); - let maybe_response_text = response.text().await.ok(); - bail!( - "Bad response: {:?}{}", - status, - match maybe_response_text { - Some(text) => format!("\n\n{}", text), - None => String::new(), - } - ); - } else { - let bytes = response.bytes().await?; - - verify_and_extract_tarball(package, &bytes, dist, &package_folder) + let guard = self.progress_bar.update(&dist.tarball); + let maybe_bytes = self + .http_client + .download_with_progress(&dist.tarball, &guard) + .await?; + match maybe_bytes { + Some(bytes) => { + verify_and_extract_tarball(package, &bytes, dist, &package_folder) + } + None => { + bail!("Could not find npm package tarball at: {}", dist.tarball); + } } } diff --git a/cli/npm/registry.rs b/cli/npm/registry.rs index 749a047ab..97d2a0e4d 100644 --- a/cli/npm/registry.rs +++ b/cli/npm/registry.rs @@ -439,38 +439,19 @@ impl RealNpmRegistryApiInner { } let package_url = self.get_package_url(name); - let _guard = self.progress_bar.update(package_url.as_str()); - - let response = match self.http_client.get(package_url).send().await { - Ok(response) => response, - Err(err) => { - // attempt to use the local cache - if let Some(info) = self.load_file_cached_package_info(name) { - return Ok(Some(info)); - } else { - return Err(err.into()); - } + let guard = self.progress_bar.update(package_url.as_str()); + + let maybe_bytes = self + .http_client + .download_with_progress(package_url, &guard) + .await?; + match maybe_bytes { + Some(bytes) => { + let package_info = serde_json::from_slice(&bytes)?; + self.save_package_info_to_file_cache(name, &package_info); + Ok(Some(package_info)) } - }; - - if response.status() == 404 { - Ok(None) - } else if !response.status().is_success() { - let status = response.status(); - let maybe_response_text = response.text().await.ok(); - bail!( - "Bad response: {:?}{}", - status, - match maybe_response_text { - Some(text) => format!("\n\n{}", text), - None => String::new(), - } - ); - } else { - let bytes = response.bytes().await?; - let package_info = serde_json::from_slice(&bytes)?; - self.save_package_info_to_file_cache(name, &package_info); - Ok(Some(package_info)) + None => Ok(None), } } diff --git a/cli/proc_state.rs b/cli/proc_state.rs index 5be3ae62e..a238bd5d2 100644 --- a/cli/proc_state.rs +++ b/cli/proc_state.rs @@ -31,6 +31,7 @@ use crate::npm::RealNpmRegistryApi; use crate::resolver::CliResolver; use crate::tools::check; use crate::util::progress_bar::ProgressBar; +use crate::util::progress_bar::ProgressBarStyle; use deno_ast::MediaType; use deno_core::anyhow::anyhow; @@ -158,7 +159,7 @@ impl ProcState { let http_cache = HttpCache::new(&deps_cache_location); let root_cert_store = cli_options.resolve_root_cert_store()?; let cache_usage = cli_options.cache_setting(); - let progress_bar = ProgressBar::default(); + let progress_bar = ProgressBar::new(ProgressBarStyle::TextOnly); let http_client = HttpClient::new( Some(root_cert_store.clone()), cli_options.unsafely_ignore_certificate_errors().clone(), diff --git a/cli/tools/upgrade.rs b/cli/tools/upgrade.rs index cb155d716..82949410b 100644 --- a/cli/tools/upgrade.rs +++ b/cli/tools/upgrade.rs @@ -4,7 +4,9 @@ use crate::args::UpgradeFlags; use crate::colors; +use crate::util::display::human_download_size; use crate::util::progress_bar::ProgressBar; +use crate::util::progress_bar::ProgressBarStyle; use crate::version; use deno_core::anyhow::bail; @@ -348,7 +350,10 @@ pub async fn upgrade(upgrade_flags: UpgradeFlags) -> Result<(), AnyError> { fs::set_permissions(&new_exe_path, permissions)?; check_exe(&new_exe_path)?; - if !upgrade_flags.dry_run { + if upgrade_flags.dry_run { + fs::remove_file(&new_exe_path)?; + log::info!("Upgraded successfully (dry run)"); + } else { let output_exe_path = upgrade_flags.output.as_ref().unwrap_or(¤t_exe_path); let output_result = if *output_exe_path == current_exe_path { @@ -377,12 +382,9 @@ pub async fn upgrade(upgrade_flags: UpgradeFlags) -> Result<(), AnyError> { return Err(err.into()); } } - } else { - fs::remove_file(&new_exe_path)?; + log::info!("Upgraded successfully"); } - log::info!("Upgraded successfully"); - Ok(()) } @@ -436,35 +438,37 @@ async fn download_package( let res = client.get(download_url).send().await?; if res.status().is_success() { - let total_size = res.content_length().unwrap() as f64; - let mut current_size = 0.0; + let total_size = res.content_length().unwrap(); + let mut current_size = 0; let mut data = Vec::with_capacity(total_size as usize); let mut stream = res.bytes_stream(); let mut skip_print = 0; - const MEBIBYTE: f64 = 1024.0 * 1024.0; - let progress_bar = ProgressBar::default(); - let clear_guard = progress_bar.clear_guard(); + let progress_bar = ProgressBar::new(ProgressBarStyle::DownloadBars); + let progress = progress_bar.update(""); + progress.set_total_size(total_size); while let Some(item) = stream.next().await { let bytes = item?; - current_size += bytes.len() as f64; + current_size += bytes.len() as u64; data.extend_from_slice(&bytes); - if skip_print == 0 { - progress_bar.update(&format!( - "{:>4.1} MiB / {:.1} MiB ({:^5.1}%)", - current_size / MEBIBYTE, - total_size / MEBIBYTE, - (current_size / total_size) * 100.0, - )); + if progress_bar.is_enabled() { + progress.set_position(current_size); + } else if skip_print == 0 { + log::info!( + "{} / {} ({:^5.1}%)", + human_download_size(current_size, total_size), + human_download_size(total_size, total_size), + (current_size as f64 / total_size as f64) * 100.0, + ); skip_print = 10; } else { skip_print -= 1; } } - drop(clear_guard); + drop(progress); log::info!( - "{:.1} MiB / {:.1} MiB (100.0%)", - current_size / MEBIBYTE, - total_size / MEBIBYTE + "{} / {} (100.0%)", + human_download_size(current_size, total_size), + human_download_size(total_size, total_size) ); Ok(data) diff --git a/cli/util/console.rs b/cli/util/console.rs new file mode 100644 index 000000000..c36b274db --- /dev/null +++ b/cli/util/console.rs @@ -0,0 +1,7 @@ +use deno_runtime::ops::tty::ConsoleSize; + +/// Gets the console size. +pub fn console_size() -> Option<ConsoleSize> { + let stderr = &deno_runtime::ops::io::STDERR_HANDLE; + deno_runtime::ops::tty::console_size(stderr).ok() +} diff --git a/cli/util/display.rs b/cli/util/display.rs index f13965e28..16b301866 100644 --- a/cli/util/display.rs +++ b/cli/util/display.rs @@ -26,6 +26,25 @@ pub fn human_size(size: f64) -> String { format!("{}{}{}", negative, pretty_bytes, unit) } +const BYTES_TO_KIB: u64 = 2u64.pow(10); +const BYTES_TO_MIB: u64 = 2u64.pow(20); + +/// Gets the size used for downloading data. The total bytes is used to +/// determine the units to use. +pub fn human_download_size(byte_count: u64, total_bytes: u64) -> String { + return if total_bytes < BYTES_TO_MIB { + get_in_format(byte_count, BYTES_TO_KIB, "KiB") + } else { + get_in_format(byte_count, BYTES_TO_MIB, "MiB") + }; + + fn get_in_format(byte_count: u64, conversion: u64, suffix: &str) -> String { + let converted_value = byte_count / conversion; + let decimal = (byte_count % conversion) * 100 / conversion; + format!("{}.{:0>2}{}", converted_value, decimal, suffix) + } +} + /// A function that converts a milisecond elapsed time to a string that /// represents a human readable version of that time. pub fn human_elapsed(elapsed: u128) -> String { @@ -85,6 +104,31 @@ mod tests { } #[test] + fn test_human_download_size() { + assert_eq!( + human_download_size(BYTES_TO_KIB / 100 - 1, BYTES_TO_KIB), + "0.00KiB" + ); + assert_eq!( + human_download_size(BYTES_TO_KIB / 100 + 1, BYTES_TO_KIB), + "0.01KiB" + ); + assert_eq!( + human_download_size(BYTES_TO_KIB / 5, BYTES_TO_KIB), + "0.19KiB" + ); + assert_eq!( + human_download_size(BYTES_TO_MIB - 1, BYTES_TO_MIB - 1), + "1023.99KiB" + ); + assert_eq!(human_download_size(BYTES_TO_MIB, BYTES_TO_MIB), "1.00MiB"); + assert_eq!( + human_download_size(BYTES_TO_MIB * 9 - 1523, BYTES_TO_MIB), + "8.99MiB" + ); + } + + #[test] fn test_human_elapsed() { assert_eq!(human_elapsed(1), "1ms"); assert_eq!(human_elapsed(256), "256ms"); diff --git a/cli/util/mod.rs b/cli/util/mod.rs index 176991d32..ab311ee86 100644 --- a/cli/util/mod.rs +++ b/cli/util/mod.rs @@ -2,6 +2,7 @@ // Note: Only add code in this folder that has no application specific logic pub mod checksum; +pub mod console; pub mod diff; pub mod display; pub mod file_watcher; diff --git a/cli/util/progress_bar.rs b/cli/util/progress_bar.rs deleted file mode 100644 index 5b49fb279..000000000 --- a/cli/util/progress_bar.rs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. - -use crate::colors; -use deno_core::parking_lot::Mutex; -use indexmap::IndexSet; -use std::sync::Arc; -use std::time::Duration; - -#[derive(Clone, Debug, Default)] -pub struct ProgressBar(Arc<Mutex<ProgressBarInner>>); - -#[derive(Debug)] -struct ProgressBarInner { - pb: Option<indicatif::ProgressBar>, - is_tty: bool, - in_flight: IndexSet<String>, -} - -impl Default for ProgressBarInner { - fn default() -> Self { - Self { - pb: None, - is_tty: colors::is_tty(), - in_flight: IndexSet::default(), - } - } -} - -impl ProgressBarInner { - fn get_or_create_pb(&mut self) -> indicatif::ProgressBar { - if let Some(pb) = self.pb.as_ref() { - return pb.clone(); - } - - let pb = indicatif::ProgressBar::new_spinner(); - pb.enable_steady_tick(Duration::from_millis(120)); - pb.set_prefix("Download"); - pb.set_style( - indicatif::ProgressStyle::with_template( - "{prefix:.green} {spinner:.green} {msg}", - ) - .unwrap() - .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]), - ); - self.pb = Some(pb); - self.pb.as_ref().unwrap().clone() - } - - fn add_in_flight(&mut self, msg: &str) { - if self.in_flight.contains(msg) { - return; - } - - self.in_flight.insert(msg.to_string()); - } - - /// Returns if removed "in-flight" was last entry and progress - /// bar needs to be updated. - fn remove_in_flight(&mut self, msg: &str) -> bool { - if !self.in_flight.contains(msg) { - return false; - } - - let mut is_last = false; - if let Some(last) = self.in_flight.last() { - is_last = last == msg; - } - self.in_flight.remove(msg); - is_last - } - - fn update_progress_bar(&mut self) { - let pb = self.get_or_create_pb(); - if let Some(msg) = self.in_flight.last() { - pb.set_message(msg.clone()); - } - } -} - -pub struct UpdateGuard { - pb: ProgressBar, - msg: String, - noop: bool, -} - -impl Drop for UpdateGuard { - fn drop(&mut self) { - if self.noop { - return; - } - - let mut inner = self.pb.0.lock(); - if inner.remove_in_flight(&self.msg) { - inner.update_progress_bar(); - } - } -} - -impl ProgressBar { - pub fn update(&self, msg: &str) -> UpdateGuard { - let mut guard = UpdateGuard { - pb: self.clone(), - msg: msg.to_string(), - noop: false, - }; - let mut inner = self.0.lock(); - - // If we're not running in TTY we're just gonna fallback - // to using logger crate. - if !inner.is_tty { - log::log!(log::Level::Info, "{} {}", colors::green("Download"), msg); - guard.noop = true; - return guard; - } - - inner.add_in_flight(msg); - inner.update_progress_bar(); - guard - } - - pub fn clear(&self) { - let mut inner = self.0.lock(); - - if let Some(pb) = inner.pb.as_ref() { - pb.finish_and_clear(); - inner.pb = None; - } - } - - pub fn clear_guard(&self) -> ClearGuard { - ClearGuard { pb: self.clone() } - } -} - -pub struct ClearGuard { - pb: ProgressBar, -} - -impl Drop for ClearGuard { - fn drop(&mut self) { - self.pb.clear(); - } -} diff --git a/cli/util/progress_bar/draw_thread.rs b/cli/util/progress_bar/draw_thread.rs new file mode 100644 index 000000000..89e8ab53f --- /dev/null +++ b/cli/util/progress_bar/draw_thread.rs @@ -0,0 +1,218 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use console_static_text::ConsoleStaticText; +use deno_core::parking_lot::Mutex; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::time::Duration; +use std::time::SystemTime; + +use crate::util::console::console_size; + +use super::renderer::ProgressBarRenderer; +use super::renderer::ProgressData; +use super::renderer::ProgressDataDisplayEntry; + +#[derive(Clone, Debug)] +pub struct ProgressBarEntry { + id: usize, + pub message: String, + pos: Arc<AtomicU64>, + total_size: Arc<AtomicU64>, + draw_thread: DrawThread, +} + +impl ProgressBarEntry { + pub fn position(&self) -> u64 { + self.pos.load(Ordering::Relaxed) + } + + pub fn set_position(&self, new_pos: u64) { + self.pos.store(new_pos, Ordering::Relaxed); + } + + pub fn total_size(&self) -> u64 { + self.total_size.load(Ordering::Relaxed) + } + + pub fn set_total_size(&self, new_size: u64) { + self.total_size.store(new_size, Ordering::Relaxed); + } + + pub fn finish(&self) { + self.draw_thread.finish_entry(self.id); + } + + pub fn percent(&self) -> f64 { + let pos = self.pos.load(Ordering::Relaxed) as f64; + let total_size = self.total_size.load(Ordering::Relaxed) as f64; + if total_size == 0f64 { + 0f64 + } else { + pos / total_size + } + } +} + +#[derive(Debug)] +struct InternalState { + start_time: SystemTime, + // this ensures only one draw thread is running + drawer_id: usize, + keep_alive_count: usize, + has_draw_thread: bool, + total_entries: usize, + entries: Vec<ProgressBarEntry>, + static_text: ConsoleStaticText, + renderer: Box<dyn ProgressBarRenderer>, +} + +#[derive(Clone, Debug)] +pub struct DrawThread { + state: Arc<Mutex<InternalState>>, +} + +impl DrawThread { + pub fn new(renderer: Box<dyn ProgressBarRenderer>) -> Self { + Self { + state: Arc::new(Mutex::new(InternalState { + start_time: SystemTime::now(), + drawer_id: 0, + keep_alive_count: 0, + has_draw_thread: false, + total_entries: 0, + entries: Vec::new(), + static_text: ConsoleStaticText::new(|| { + let size = console_size().unwrap(); + console_static_text::ConsoleSize { + cols: Some(size.cols as u16), + rows: Some(size.rows as u16), + } + }), + renderer, + })), + } + } + + pub fn add_entry(&self, message: String) -> ProgressBarEntry { + let mut internal_state = self.state.lock(); + let id = internal_state.total_entries; + let entry = ProgressBarEntry { + id, + draw_thread: self.clone(), + message, + pos: Default::default(), + total_size: Default::default(), + }; + internal_state.entries.push(entry.clone()); + internal_state.total_entries += 1; + internal_state.keep_alive_count += 1; + + if !internal_state.has_draw_thread { + self.start_draw_thread(&mut internal_state); + } + + entry + } + + fn finish_entry(&self, entry_id: usize) { + let mut internal_state = self.state.lock(); + + if let Ok(index) = internal_state + .entries + .binary_search_by(|e| e.id.cmp(&entry_id)) + { + internal_state.entries.remove(index); + self.decrement_keep_alive(&mut internal_state); + } + } + + pub fn increment_clear(&self) { + let mut internal_state = self.state.lock(); + internal_state.keep_alive_count += 1; + } + + pub fn decrement_clear(&self) { + let mut internal_state = self.state.lock(); + self.decrement_keep_alive(&mut internal_state); + } + + fn decrement_keep_alive(&self, internal_state: &mut InternalState) { + internal_state.keep_alive_count -= 1; + + if internal_state.keep_alive_count == 0 { + internal_state.static_text.eprint_clear(); + // bump the drawer id to exit the draw thread + internal_state.drawer_id += 1; + internal_state.has_draw_thread = false; + } + } + + fn start_draw_thread(&self, internal_state: &mut InternalState) { + internal_state.drawer_id += 1; + internal_state.start_time = SystemTime::now(); + internal_state.has_draw_thread = true; + let drawer_id = internal_state.drawer_id; + let internal_state = self.state.clone(); + tokio::task::spawn_blocking(move || { + let mut previous_size = console_size().unwrap(); + loop { + let mut delay_ms = 120; + { + let mut internal_state = internal_state.lock(); + // exit if not the current draw thread + if internal_state.drawer_id != drawer_id { + break; + } + + let size = console_size().unwrap(); + if size != previous_size { + // means the user is actively resizing the console... + // wait a little bit until they stop resizing + previous_size = size; + delay_ms = 200; + } else if !internal_state.entries.is_empty() { + let preferred_entry = internal_state + .entries + .iter() + .find(|e| e.percent() > 0f64) + .or_else(|| internal_state.entries.iter().last()) + .unwrap(); + let text = internal_state.renderer.render(ProgressData { + duration: internal_state.start_time.elapsed().unwrap(), + terminal_width: size.cols, + pending_entries: internal_state.entries.len(), + total_entries: internal_state.total_entries, + display_entry: ProgressDataDisplayEntry { + message: preferred_entry.message.clone(), + position: preferred_entry.position(), + total_size: preferred_entry.total_size(), + }, + percent_done: { + let mut total_percent_sum = 0f64; + for entry in &internal_state.entries { + total_percent_sum += entry.percent(); + } + total_percent_sum += (internal_state.total_entries + - internal_state.entries.len()) + as f64; + total_percent_sum / (internal_state.total_entries as f64) + }, + }); + + internal_state.static_text.eprint_with_size( + &text, + console_static_text::ConsoleSize { + cols: Some(size.cols as u16), + rows: Some(size.rows as u16), + }, + ); + } + } + + std::thread::sleep(Duration::from_millis(delay_ms)); + } + }); + } +} diff --git a/cli/util/progress_bar/mod.rs b/cli/util/progress_bar/mod.rs new file mode 100644 index 000000000..122db7a59 --- /dev/null +++ b/cli/util/progress_bar/mod.rs @@ -0,0 +1,123 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use crate::colors; + +use self::draw_thread::DrawThread; +use self::draw_thread::ProgressBarEntry; + +use super::console::console_size; + +mod draw_thread; +mod renderer; + +// Inspired by Indicatif, but this custom implementation allows +// for more control over what's going on under the hood. + +pub struct UpdateGuard { + maybe_entry: Option<ProgressBarEntry>, +} + +impl Drop for UpdateGuard { + fn drop(&mut self) { + if let Some(entry) = &self.maybe_entry { + entry.finish(); + } + } +} + +impl UpdateGuard { + pub fn set_position(&self, value: u64) { + if let Some(entry) = &self.maybe_entry { + entry.set_position(value); + } + } + + pub fn set_total_size(&self, value: u64) { + if let Some(entry) = &self.maybe_entry { + entry.set_total_size(value); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProgressBarStyle { + DownloadBars, + TextOnly, +} + +#[derive(Clone, Debug)] +pub struct ProgressBar { + draw_thread: Option<DrawThread>, +} + +impl ProgressBar { + /// Checks if progress bars are supported + pub fn are_supported() -> bool { + atty::is(atty::Stream::Stderr) + && log::log_enabled!(log::Level::Info) + && console_size() + .map(|s| s.cols > 0 && s.rows > 0) + .unwrap_or(false) + } + + pub fn new(style: ProgressBarStyle) -> Self { + Self { + draw_thread: match Self::are_supported() { + true => Some(DrawThread::new(match style { + ProgressBarStyle::DownloadBars => { + Box::new(renderer::BarProgressBarRenderer) + } + ProgressBarStyle::TextOnly => { + Box::new(renderer::TextOnlyProgressBarRenderer) + } + })), + false => None, + }, + } + } + + pub fn is_enabled(&self) -> bool { + self.draw_thread.is_some() + } + + pub fn update(&self, msg: &str) -> UpdateGuard { + match &self.draw_thread { + Some(draw_thread) => { + let entry = draw_thread.add_entry(msg.to_string()); + UpdateGuard { + maybe_entry: Some(entry), + } + } + None => { + // if we're not running in TTY, fallback to using logger crate + if !msg.is_empty() { + log::log!(log::Level::Info, "{} {}", colors::green("Download"), msg); + } + UpdateGuard { maybe_entry: None } + } + } + } + + pub fn clear_guard(&self) -> ClearGuard { + if let Some(draw_thread) = &self.draw_thread { + draw_thread.increment_clear(); + } + ClearGuard { pb: self.clone() } + } + + fn decrement_clear(&self) { + if let Some(draw_thread) = &self.draw_thread { + draw_thread.decrement_clear(); + } + } +} + +pub struct ClearGuard { + pb: ProgressBar, +} + +impl Drop for ClearGuard { + fn drop(&mut self) { + self.pb.decrement_clear(); + } +} diff --git a/cli/util/progress_bar/renderer.rs b/cli/util/progress_bar/renderer.rs new file mode 100644 index 000000000..cb249ce36 --- /dev/null +++ b/cli/util/progress_bar/renderer.rs @@ -0,0 +1,278 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use std::time::Duration; + +use deno_runtime::colors; + +use crate::util::display::human_download_size; + +#[derive(Clone)] +pub struct ProgressDataDisplayEntry { + pub message: String, + pub position: u64, + pub total_size: u64, +} + +#[derive(Clone)] +pub struct ProgressData { + pub terminal_width: u32, + pub display_entry: ProgressDataDisplayEntry, + pub pending_entries: usize, + pub percent_done: f64, + pub total_entries: usize, + pub duration: Duration, +} + +pub trait ProgressBarRenderer: Send + std::fmt::Debug { + fn render(&self, data: ProgressData) -> String; +} + +/// Indicatif style progress bar. +#[derive(Debug)] +pub struct BarProgressBarRenderer; + +impl ProgressBarRenderer for BarProgressBarRenderer { + fn render(&self, data: ProgressData) -> String { + let (bytes_text, bytes_text_max_width) = { + let total_size = data.display_entry.total_size; + let pos = data.display_entry.position; + if total_size == 0 { + (String::new(), 0) + } else { + let total_size_str = human_download_size(total_size, total_size); + ( + format!( + " {}/{}", + human_download_size(pos, total_size), + total_size_str, + ), + 2 + total_size_str.len() * 2, + ) + } + }; + let (total_text, total_text_max_width) = if data.total_entries <= 1 { + (String::new(), 0) + } else { + let total_entries_str = data.total_entries.to_string(); + ( + format!( + " ({}/{})", + data.total_entries - data.pending_entries, + data.total_entries + ), + 4 + total_entries_str.len() * 2, + ) + }; + + let elapsed_text = get_elapsed_text(data.duration); + let mut text = String::new(); + if !data.display_entry.message.is_empty() { + text.push_str(&format!( + "{} {}{}\n", + colors::green("Download"), + data.display_entry.message, + bytes_text, + )); + } + text.push_str(&elapsed_text); + let max_width = + std::cmp::max(10, std::cmp::min(75, data.terminal_width as i32 - 5)) + as usize; + let same_line_text_width = + elapsed_text.len() + total_text_max_width + bytes_text_max_width + 3; // space, open and close brace + let total_bars = if same_line_text_width > max_width { + 1 + } else { + max_width - same_line_text_width + }; + let completed_bars = + (total_bars as f64 * data.percent_done).floor() as usize; + text.push_str(" ["); + if completed_bars != total_bars { + if completed_bars > 0 { + text.push_str(&format!( + "{}", + colors::cyan(format!("{}{}", "#".repeat(completed_bars - 1), ">")) + )) + } + text.push_str(&format!( + "{}", + colors::intense_blue("-".repeat(total_bars - completed_bars)) + )) + } else { + text.push_str(&format!("{}", colors::cyan("#".repeat(completed_bars)))) + } + text.push(']'); + + // suffix + if data.display_entry.message.is_empty() { + text.push_str(&colors::gray(bytes_text).to_string()); + } + text.push_str(&colors::gray(total_text).to_string()); + + text + } +} + +#[derive(Debug)] +pub struct TextOnlyProgressBarRenderer; + +impl ProgressBarRenderer for TextOnlyProgressBarRenderer { + fn render(&self, data: ProgressData) -> String { + let bytes_text = { + let total_size = data.display_entry.total_size; + let pos = data.display_entry.position; + if total_size == 0 { + String::new() + } else { + format!( + " {}/{}", + human_download_size(pos, total_size), + human_download_size(total_size, total_size) + ) + } + }; + let total_text = if data.total_entries <= 1 { + String::new() + } else { + format!( + " ({}/{})", + data.total_entries - data.pending_entries, + data.total_entries + ) + }; + + format!( + "{} {}{}{}", + colors::green("Download"), + data.display_entry.message, + colors::gray(bytes_text), + colors::gray(total_text), + ) + } +} + +fn get_elapsed_text(elapsed: Duration) -> String { + let elapsed_secs = elapsed.as_secs(); + let seconds = elapsed_secs % 60; + let minutes = elapsed_secs / 60; + format!("[{:0>2}:{:0>2}]", minutes, seconds) +} + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + use std::time::Duration; + + #[test] + fn should_get_elapsed_text() { + assert_eq!(get_elapsed_text(Duration::from_secs(1)), "[00:01]"); + assert_eq!(get_elapsed_text(Duration::from_secs(20)), "[00:20]"); + assert_eq!(get_elapsed_text(Duration::from_secs(59)), "[00:59]"); + assert_eq!(get_elapsed_text(Duration::from_secs(60)), "[01:00]"); + assert_eq!( + get_elapsed_text(Duration::from_secs(60 * 5 + 23)), + "[05:23]" + ); + assert_eq!( + get_elapsed_text(Duration::from_secs(60 * 59 + 59)), + "[59:59]" + ); + assert_eq!(get_elapsed_text(Duration::from_secs(60 * 60)), "[60:00]"); + assert_eq!( + get_elapsed_text(Duration::from_secs(60 * 60 * 3 + 20 * 60 + 2)), + "[200:02]" + ); + assert_eq!( + get_elapsed_text(Duration::from_secs(60 * 60 * 99)), + "[5940:00]" + ); + } + + const BYTES_TO_KIB: u64 = 2u64.pow(10); + + #[test] + fn should_render_bar_progress() { + let renderer = BarProgressBarRenderer; + let mut data = ProgressData { + display_entry: ProgressDataDisplayEntry { + message: "data".to_string(), + position: 0, + total_size: 10 * BYTES_TO_KIB, + }, + duration: Duration::from_secs(1), + pending_entries: 1, + total_entries: 1, + percent_done: 0f64, + terminal_width: 50, + }; + let text = renderer.render(data.clone()); + let text = test_util::strip_ansi_codes(&text); + assert_eq!( + text, + concat!( + "Download data 0.00KiB/10.00KiB\n", + "[00:01] [-----------------]", + ), + ); + + data.percent_done = 0.5f64; + data.display_entry.position = 5 * BYTES_TO_KIB; + data.display_entry.message = String::new(); + data.total_entries = 3; + let text = renderer.render(data.clone()); + let text = test_util::strip_ansi_codes(&text); + assert_eq!(text, "[00:01] [####>------] 5.00KiB/10.00KiB (2/3)",); + + // just ensure this doesn't panic + data.terminal_width = 0; + let text = renderer.render(data.clone()); + let text = test_util::strip_ansi_codes(&text); + assert_eq!(text, "[00:01] [-] 5.00KiB/10.00KiB (2/3)",); + + data.terminal_width = 50; + data.pending_entries = 0; + data.display_entry.position = 10 * BYTES_TO_KIB; + data.percent_done = 1.0f64; + let text = renderer.render(data.clone()); + let text = test_util::strip_ansi_codes(&text); + assert_eq!(text, "[00:01] [###########] 10.00KiB/10.00KiB (3/3)",); + + data.display_entry.position = 0; + data.display_entry.total_size = 0; + data.pending_entries = 0; + data.total_entries = 1; + let text = renderer.render(data); + let text = test_util::strip_ansi_codes(&text); + assert_eq!(text, "[00:01] [###################################]",); + } + + #[test] + fn should_render_text_only_progress() { + let renderer = TextOnlyProgressBarRenderer; + let mut data = ProgressData { + display_entry: ProgressDataDisplayEntry { + message: "data".to_string(), + position: 0, + total_size: 10 * BYTES_TO_KIB, + }, + duration: Duration::from_secs(1), + pending_entries: 1, + total_entries: 3, + percent_done: 0f64, + terminal_width: 50, + }; + let text = renderer.render(data.clone()); + let text = test_util::strip_ansi_codes(&text); + assert_eq!(text, "Download data 0.00KiB/10.00KiB (2/3)"); + + data.pending_entries = 0; + data.total_entries = 1; + data.display_entry.position = 0; + data.display_entry.total_size = 0; + let text = renderer.render(data); + let text = test_util::strip_ansi_codes(&text); + assert_eq!(text, "Download data"); + } +} |