From e773709e31d2baae709515d38126396c7ad383bf Mon Sep 17 00:00:00 2001 From: Benjamin Saunders Date: Sun, 23 Oct 2022 13:50:03 -0700 Subject: [PATCH] cli: draw bar in progress report --- Cargo.lock | 207 ++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/progress.rs | 64 ++++++++++++--- src/ui.rs | 4 + 4 files changed, 267 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 944c5947f..12d594e75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,6 +424,31 @@ dependencies = [ "once_cell", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -692,6 +717,7 @@ dependencies = [ "config", "criterion", "criterion_bencher_compat", + "crossterm", "dirs", "git2", "hex", @@ -804,6 +830,16 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -846,6 +882,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.36.1", +] + [[package]] name = "nom" version = "7.1.1" @@ -938,6 +986,29 @@ version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.42.0", +] + [[package]] name = "pathdiff" version = "0.2.1" @@ -1333,12 +1404,48 @@ dependencies = [ "digest", ] +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + [[package]] name = "similar" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803" +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + [[package]] name = "smawk" version = "0.3.1" @@ -1725,6 +1832,106 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 3c658c730..179cf36e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ clap = { version = "4.0.18", features = ["derive", "deprecated"] } clap_complete = "4.0.3" clap_mangen = "0.2.3" config = { version = "0.13.2", default-features = false, features = ["toml"] } +crossterm = { version = "0.25", default-features = false } dirs = "4.0.0" git2 = "0.15.0" hex = "0.4.3" diff --git a/src/progress.rs b/src/progress.rs index c871c63c6..cdeed1982 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -1,5 +1,6 @@ use std::time::{Duration, Instant}; +use crossterm::terminal::{Clear, ClearType}; use jujutsu_lib::git; use crate::ui::Ui; @@ -36,19 +37,25 @@ impl<'a> Progress<'a> { self.printed = true; self.next_print = now.min(self.next_print + Duration::from_secs(1) / UPDATE_HZ); - const CLEAR_TRAILING: &str = "\x1b[K"; self.buffer.clear(); - write!( - self.buffer, - "\r{}{: >3.0}%", - CLEAR_TRAILING, - 100.0 * progress.overall - ) - .unwrap(); + write!(self.buffer, "\r{}", Clear(ClearType::CurrentLine)).unwrap(); + let control_chars = self.buffer.len(); + write!(self.buffer, "{: >3.0}% ", 100.0 * progress.overall).unwrap(); if let Some(estimate) = rate { let (scaled, prefix) = binary_prefix(estimate); - write!(self.buffer, " at {: >5.1} {}B/s", scaled, prefix).unwrap(); + write!(self.buffer, " at {: >5.1} {}B/s ", scaled, prefix).unwrap(); } + + let bar_width = self + .ui + .size() + .map(|(cols, _rows)| usize::from(cols)) + .unwrap_or(0) + .saturating_sub(self.buffer.len() - control_chars + 2); + self.buffer.push('['); + draw_progress(progress.overall, &mut self.buffer, bar_width); + self.buffer.push(']'); + _ = write!(self.ui, "{}", self.buffer); _ = self.ui.flush(); } @@ -62,6 +69,23 @@ impl Drop for Progress<'_> { } } +fn draw_progress(progress: f32, buffer: &mut String, width: usize) { + const CHARS: [char; 9] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']; + const RESOLUTION: usize = CHARS.len() - 1; + let ticks = (width as f32 * progress.min(1.0).max(0.0) * RESOLUTION as f32).round() as usize; + let whole = ticks / RESOLUTION; + for _ in 0..whole { + buffer.push(CHARS[CHARS.len() - 1]); + } + if whole < width { + let fraction = ticks % RESOLUTION; + buffer.push(CHARS[fraction]); + } + for _ in (whole + 1)..width { + buffer.push(CHARS[0]); + } +} + const UPDATE_HZ: u32 = 30; const INITIAL_DELAY: Duration = Duration::from_millis(250); @@ -129,3 +153,25 @@ impl RateEstimateState { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bar() { + let mut buf = String::new(); + draw_progress(0.0, &mut buf, 10); + assert_eq!(buf, " "); + buf.clear(); + draw_progress(1.0, &mut buf, 10); + assert_eq!(buf, "██████████"); + buf.clear(); + draw_progress(0.5, &mut buf, 10); + assert_eq!(buf, "█████ "); + buf.clear(); + draw_progress(0.54, &mut buf, 10); + assert_eq!(buf, "█████▍ "); + buf.clear(); + } +} diff --git a/src/ui.rs b/src/ui.rs index 69d05ed4a..aa53b87c8 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -176,6 +176,10 @@ impl Ui { UiOutputPair::Terminal { stdout, .. } => stdout.flush(), } } + + pub fn size(&self) -> Option<(u16, u16)> { + crossterm::terminal::size().ok() + } } enum UiOutputPair {