summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock68
-rw-r--r--cli/Cargo.toml2
-rw-r--r--cli/http_util.rs41
-rw-r--r--cli/lsp/language_server.rs3
-rw-r--r--cli/npm/cache.rs32
-rw-r--r--cli/npm/registry.rs43
-rw-r--r--cli/proc_state.rs3
-rw-r--r--cli/tools/upgrade.rs48
-rw-r--r--cli/util/console.rs7
-rw-r--r--cli/util/display.rs44
-rw-r--r--cli/util/mod.rs1
-rw-r--r--cli/util/progress_bar.rs143
-rw-r--r--cli/util/progress_bar/draw_thread.rs218
-rw-r--r--cli/util/progress_bar/mod.rs123
-rw-r--r--cli/util/progress_bar/renderer.rs278
-rw-r--r--runtime/ops/io.rs12
-rw-r--r--runtime/ops/tty.rs94
17 files changed, 853 insertions, 307 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 91b9098a9..5e1629739 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -500,17 +500,13 @@ dependencies = [
]
[[package]]
-name = "console"
-version = "0.15.1"
+name = "console_static_text"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847"
+checksum = "3d749e1f5316d8a15ec592516a631ab9b8099cc6d085b69b905462fc071caedb"
dependencies = [
- "encode_unicode",
- "libc",
- "once_cell",
- "terminal_size",
"unicode-width",
- "winapi 0.3.9",
+ "vte",
]
[[package]]
@@ -778,6 +774,7 @@ dependencies = [
"clap",
"clap_complete",
"clap_complete_fig",
+ "console_static_text",
"data-url",
"deno_ast",
"deno_bench_util",
@@ -804,7 +801,6 @@ dependencies = [
"http",
"import_map",
"indexmap",
- "indicatif",
"jsonc-parser",
"junction",
"libc",
@@ -1559,12 +1555,6 @@ dependencies = [
]
[[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.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2324,17 +2314,6 @@ dependencies = [
]
[[package]]
-name = "indicatif"
-version = "0.17.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfddc9561e8baf264e0e45e197fd7696320026eb10a8180340debc27b18f535b"
-dependencies = [
- "console",
- "number_prefix",
- "unicode-width",
-]
-
-[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2998,12 +2977,6 @@ dependencies = [
]
[[package]]
-name = "number_prefix"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
-
-[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4711,16 +4684,6 @@ dependencies = [
]
[[package]]
-name = "terminal_size"
-version = "0.1.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
-dependencies = [
- "libc",
- "winapi 0.3.9",
-]
-
-[[package]]
name = "test_ffi"
version = "0.1.0"
dependencies = [
@@ -5417,6 +5380,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]]
+name = "vte"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aae21c12ad2ec2d168c236f369c38ff332bc1134f7246350dca641437365045"
+dependencies = [
+ "arrayvec",
+ "utf8parse",
+ "vte_generate_state_changes",
+]
+
+[[package]]
+name = "vte_generate_state_changes"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
+dependencies = [
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+]
+
+[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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(&current_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");
+ }
+}
diff --git a/runtime/ops/io.rs b/runtime/ops/io.rs
index 186052afe..d8a1af69e 100644
--- a/runtime/ops/io.rs
+++ b/runtime/ops/io.rs
@@ -44,33 +44,33 @@ use {
// alive for the duration of the application since the last handle/fd
// being dropped will close the corresponding pipe.
#[cfg(unix)]
-static STDIN_HANDLE: Lazy<StdFile> = Lazy::new(|| {
+pub static STDIN_HANDLE: Lazy<StdFile> = Lazy::new(|| {
// SAFETY: corresponds to OS stdin
unsafe { StdFile::from_raw_fd(0) }
});
#[cfg(unix)]
-static STDOUT_HANDLE: Lazy<StdFile> = Lazy::new(|| {
+pub static STDOUT_HANDLE: Lazy<StdFile> = Lazy::new(|| {
// SAFETY: corresponds to OS stdout
unsafe { StdFile::from_raw_fd(1) }
});
#[cfg(unix)]
-static STDERR_HANDLE: Lazy<StdFile> = Lazy::new(|| {
+pub static STDERR_HANDLE: Lazy<StdFile> = Lazy::new(|| {
// SAFETY: corresponds to OS stderr
unsafe { StdFile::from_raw_fd(2) }
});
#[cfg(windows)]
-static STDIN_HANDLE: Lazy<StdFile> = Lazy::new(|| {
+pub static STDIN_HANDLE: Lazy<StdFile> = Lazy::new(|| {
// SAFETY: corresponds to OS stdin
unsafe { StdFile::from_raw_handle(GetStdHandle(winbase::STD_INPUT_HANDLE)) }
});
#[cfg(windows)]
-static STDOUT_HANDLE: Lazy<StdFile> = Lazy::new(|| {
+pub static STDOUT_HANDLE: Lazy<StdFile> = Lazy::new(|| {
// SAFETY: corresponds to OS stdout
unsafe { StdFile::from_raw_handle(GetStdHandle(winbase::STD_OUTPUT_HANDLE)) }
});
#[cfg(windows)]
-static STDERR_HANDLE: Lazy<StdFile> = Lazy::new(|| {
+pub static STDERR_HANDLE: Lazy<StdFile> = Lazy::new(|| {
// SAFETY: corresponds to OS stderr
unsafe { StdFile::from_raw_handle(GetStdHandle(winbase::STD_ERROR_HANDLE)) }
});
diff --git a/runtime/ops/tty.rs b/runtime/ops/tty.rs
index f80788905..5f99d4574 100644
--- a/runtime/ops/tty.rs
+++ b/runtime/ops/tty.rs
@@ -201,46 +201,10 @@ fn op_console_size(
rid: u32,
) -> Result<(), AnyError> {
StdFileResource::with_file(state, rid, move |std_file| {
- #[cfg(windows)]
- {
- use std::os::windows::io::AsRawHandle;
- let handle = std_file.as_raw_handle();
-
- // SAFETY: winapi calls
- unsafe {
- let mut bufinfo: winapi::um::wincon::CONSOLE_SCREEN_BUFFER_INFO =
- std::mem::zeroed();
-
- if winapi::um::wincon::GetConsoleScreenBufferInfo(
- handle,
- &mut bufinfo,
- ) == 0
- {
- return Err(Error::last_os_error().into());
- }
- result[0] = bufinfo.dwSize.X as u32;
- result[1] = bufinfo.dwSize.Y as u32;
- Ok(())
- }
- }
-
- #[cfg(unix)]
- {
- use std::os::unix::io::AsRawFd;
-
- let fd = std_file.as_raw_fd();
- // TODO(bartlomieju):
- #[allow(clippy::undocumented_unsafe_blocks)]
- unsafe {
- let mut size: libc::winsize = std::mem::zeroed();
- if libc::ioctl(fd, libc::TIOCGWINSZ, &mut size as *mut _) != 0 {
- return Err(Error::last_os_error().into());
- }
- result[0] = size.ws_col as u32;
- result[1] = size.ws_row as u32;
- Ok(())
- }
- }
+ let size = console_size(std_file)?;
+ result[0] = size.cols;
+ result[1] = size.rows;
+ Ok(())
})
}
@@ -256,3 +220,53 @@ fn op_console_size(
last_result
}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct ConsoleSize {
+ pub cols: u32,
+ pub rows: u32,
+}
+
+pub fn console_size(
+ std_file: &std::fs::File,
+) -> Result<ConsoleSize, std::io::Error> {
+ #[cfg(windows)]
+ {
+ use std::os::windows::io::AsRawHandle;
+ let handle = std_file.as_raw_handle();
+
+ // SAFETY: winapi calls
+ unsafe {
+ let mut bufinfo: winapi::um::wincon::CONSOLE_SCREEN_BUFFER_INFO =
+ std::mem::zeroed();
+
+ if winapi::um::wincon::GetConsoleScreenBufferInfo(handle, &mut bufinfo)
+ == 0
+ {
+ return Err(Error::last_os_error());
+ }
+ Ok(ConsoleSize {
+ cols: bufinfo.dwSize.X as u32,
+ rows: bufinfo.dwSize.Y as u32,
+ })
+ }
+ }
+
+ #[cfg(unix)]
+ {
+ use std::os::unix::io::AsRawFd;
+
+ let fd = std_file.as_raw_fd();
+ // SAFETY: libc calls
+ unsafe {
+ let mut size: libc::winsize = std::mem::zeroed();
+ if libc::ioctl(fd, libc::TIOCGWINSZ, &mut size as *mut _) != 0 {
+ return Err(Error::last_os_error());
+ }
+ Ok(ConsoleSize {
+ cols: size.ws_col as u32,
+ rows: size.ws_row as u32,
+ })
+ }
+ }
+}