1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-28 16:20:57 -05:00

feat: refresh "Download" progress bar with a spinner (#24913)

This commit adds a spinner to "Download" progress bar and makes
it multiline, showing up to 4 lines of documents being downloaded.
This commit is contained in:
Bartek Iwańczuk 2024-08-08 14:54:29 +01:00 committed by GitHub
parent 4c56353594
commit 6e8612f319
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 103 additions and 52 deletions

View file

@ -223,23 +223,23 @@ impl DrawThreadRenderer for ProgressBarInner {
if state.entries.is_empty() { if state.entries.is_empty() {
return String::new(); return String::new();
} }
let preferred_entry = state let display_entries = state
.entries .entries
.iter() .iter()
.find(|e| e.percent() > 0f64) .map(|e| ProgressDataDisplayEntry {
.or_else(|| state.entries.iter().last()) prompt: e.prompt,
.unwrap(); message: e.message.to_string(),
position: e.position(),
total_size: e.total_size(),
})
.collect::<Vec<_>>();
ProgressData { ProgressData {
duration: state.start_time.elapsed(), duration: state.start_time.elapsed(),
terminal_width: size.cols, terminal_width: size.cols,
pending_entries: state.entries.len(), pending_entries: state.entries.len(),
total_entries: state.total_entries, total_entries: state.total_entries,
display_entry: ProgressDataDisplayEntry { display_entries,
prompt: preferred_entry.prompt,
message: preferred_entry.message.clone(),
position: preferred_entry.position(),
total_size: preferred_entry.total_size(),
},
percent_done: { percent_done: {
let mut total_percent_sum = 0f64; let mut total_percent_sum = 0f64;
for entry in &state.entries { for entry in &state.entries {
@ -273,7 +273,7 @@ impl ProgressBar {
Arc::new(renderer::BarProgressBarRenderer) Arc::new(renderer::BarProgressBarRenderer)
} }
ProgressBarStyle::TextOnly => { ProgressBarStyle::TextOnly => {
Arc::new(renderer::TextOnlyProgressBarRenderer) Arc::new(renderer::TextOnlyProgressBarRenderer::default())
} }
}), }),
} }

View file

@ -1,5 +1,7 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::time::Duration; use std::time::Duration;
use deno_terminal::colors; use deno_terminal::colors;
@ -19,7 +21,7 @@ pub struct ProgressDataDisplayEntry {
#[derive(Clone)] #[derive(Clone)]
pub struct ProgressData { pub struct ProgressData {
pub terminal_width: u32, pub terminal_width: u32,
pub display_entry: ProgressDataDisplayEntry, pub display_entries: Vec<ProgressDataDisplayEntry>,
pub pending_entries: usize, pub pending_entries: usize,
pub percent_done: f64, pub percent_done: f64,
pub total_entries: usize, pub total_entries: usize,
@ -36,9 +38,13 @@ pub struct BarProgressBarRenderer;
impl ProgressBarRenderer for BarProgressBarRenderer { impl ProgressBarRenderer for BarProgressBarRenderer {
fn render(&self, data: ProgressData) -> String { fn render(&self, data: ProgressData) -> String {
// In `ProgressBarRenderer` we only care about first entry.
let Some(display_entry) = &data.display_entries.first() else {
return String::new();
};
let (bytes_text, bytes_text_max_width) = { let (bytes_text, bytes_text_max_width) = {
let total_size = data.display_entry.total_size; let total_size = display_entry.total_size;
let pos = data.display_entry.position; let pos = display_entry.position;
if total_size == 0 { if total_size == 0 {
(String::new(), 0) (String::new(), 0)
} else { } else {
@ -69,11 +75,11 @@ impl ProgressBarRenderer for BarProgressBarRenderer {
let elapsed_text = get_elapsed_text(data.duration); let elapsed_text = get_elapsed_text(data.duration);
let mut text = String::new(); let mut text = String::new();
if !data.display_entry.message.is_empty() { if !display_entry.message.is_empty() {
text.push_str(&format!( text.push_str(&format!(
"{} {}{}\n", "{} {}{}\n",
colors::green("Download"), colors::green("Download"),
data.display_entry.message, display_entry.message,
bytes_text, bytes_text,
)); ));
} }
@ -106,7 +112,7 @@ impl ProgressBarRenderer for BarProgressBarRenderer {
text.push(']'); text.push(']');
// suffix // suffix
if data.display_entry.message.is_empty() { if display_entry.message.is_empty() {
text.push_str(&colors::gray(bytes_text).to_string()); text.push_str(&colors::gray(bytes_text).to_string());
} }
text.push_str(&colors::gray(total_text).to_string()); text.push_str(&colors::gray(total_text).to_string());
@ -116,13 +122,61 @@ impl ProgressBarRenderer for BarProgressBarRenderer {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct TextOnlyProgressBarRenderer; pub struct TextOnlyProgressBarRenderer {
last_tick: AtomicUsize,
start_time: std::time::Instant,
}
impl Default for TextOnlyProgressBarRenderer {
fn default() -> Self {
Self {
last_tick: Default::default(),
start_time: std::time::Instant::now(),
}
}
}
const SPINNER_CHARS: [&str; 8] = ["", "", "", "", "", "", "", ""];
impl ProgressBarRenderer for TextOnlyProgressBarRenderer { impl ProgressBarRenderer for TextOnlyProgressBarRenderer {
fn render(&self, data: ProgressData) -> String { fn render(&self, data: ProgressData) -> String {
let last_tick = {
let last_tick = self.last_tick.load(Ordering::Relaxed);
let last_tick = (last_tick + 1) % 8;
self.last_tick.store(last_tick, Ordering::Relaxed);
last_tick
};
let current_time = std::time::Instant::now();
let mut display_str = format!(
"{} {} ",
data.display_entries[0].prompt.as_text(),
SPINNER_CHARS[last_tick]
);
let elapsed_time = current_time - self.start_time;
let fmt_elapsed_time = get_elapsed_text(elapsed_time);
let total_text = if data.total_entries <= 1 {
String::new()
} else {
format!(
" {}/{}",
data.total_entries - data.pending_entries,
data.total_entries
)
};
display_str.push_str(&format!("{}{}\n", fmt_elapsed_time, total_text));
for i in 0..4 {
let Some(display_entry) = data.display_entries.get(i) else {
display_str.push('\n');
continue;
};
let bytes_text = { let bytes_text = {
let total_size = data.display_entry.total_size; let total_size = display_entry.total_size;
let pos = data.display_entry.position; let pos = display_entry.position;
if total_size == 0 { if total_size == 0 {
String::new() String::new()
} else { } else {
@ -133,23 +187,17 @@ impl ProgressBarRenderer for TextOnlyProgressBarRenderer {
) )
} }
}; };
let total_text = if data.total_entries <= 1 {
String::new()
} else {
format!(
" ({}/{})",
data.total_entries - data.pending_entries,
data.total_entries
)
};
format!( let message = display_entry
"{} {}{}{}", .message
data.display_entry.prompt.as_text(), .replace("https://registry.npmjs.org/", "npm:")
data.display_entry.message, .replace("https://jsr.io/", "jsr:");
colors::gray(bytes_text), display_str.push_str(
colors::gray(total_text), &colors::gray(format!(" - {}{}\n", message, bytes_text)).to_string(),
) );
}
display_str
} }
} }
@ -165,6 +213,7 @@ mod test {
use super::*; use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use std::time::Duration; use std::time::Duration;
use test_util::assert_contains;
#[test] #[test]
fn should_get_elapsed_text() { fn should_get_elapsed_text() {
@ -197,12 +246,12 @@ mod test {
fn should_render_bar_progress() { fn should_render_bar_progress() {
let renderer = BarProgressBarRenderer; let renderer = BarProgressBarRenderer;
let mut data = ProgressData { let mut data = ProgressData {
display_entry: ProgressDataDisplayEntry { display_entries: vec![ProgressDataDisplayEntry {
prompt: ProgressMessagePrompt::Download, prompt: ProgressMessagePrompt::Download,
message: "data".to_string(), message: "data".to_string(),
position: 0, position: 0,
total_size: 10 * BYTES_TO_KIB, total_size: 10 * BYTES_TO_KIB,
}, }],
duration: Duration::from_secs(1), duration: Duration::from_secs(1),
pending_entries: 1, pending_entries: 1,
total_entries: 1, total_entries: 1,
@ -220,8 +269,8 @@ mod test {
); );
data.percent_done = 0.5f64; data.percent_done = 0.5f64;
data.display_entry.position = 5 * BYTES_TO_KIB; data.display_entries[0].position = 5 * BYTES_TO_KIB;
data.display_entry.message = String::new(); data.display_entries[0].message = "".to_string();
data.total_entries = 3; data.total_entries = 3;
let text = renderer.render(data.clone()); let text = renderer.render(data.clone());
let text = test_util::strip_ansi_codes(&text); let text = test_util::strip_ansi_codes(&text);
@ -235,14 +284,14 @@ mod test {
data.terminal_width = 50; data.terminal_width = 50;
data.pending_entries = 0; data.pending_entries = 0;
data.display_entry.position = 10 * BYTES_TO_KIB; data.display_entries[0].position = 10 * BYTES_TO_KIB;
data.percent_done = 1.0f64; data.percent_done = 1.0f64;
let text = renderer.render(data.clone()); let text = renderer.render(data.clone());
let text = test_util::strip_ansi_codes(&text); let text = test_util::strip_ansi_codes(&text);
assert_eq!(text, "[00:01] [###########] 10.00KiB/10.00KiB (3/3)",); assert_eq!(text, "[00:01] [###########] 10.00KiB/10.00KiB (3/3)",);
data.display_entry.position = 0; data.display_entries[0].position = 0;
data.display_entry.total_size = 0; data.display_entries[0].total_size = 0;
data.pending_entries = 0; data.pending_entries = 0;
data.total_entries = 1; data.total_entries = 1;
let text = renderer.render(data); let text = renderer.render(data);
@ -252,14 +301,14 @@ mod test {
#[test] #[test]
fn should_render_text_only_progress() { fn should_render_text_only_progress() {
let renderer = TextOnlyProgressBarRenderer; let renderer = TextOnlyProgressBarRenderer::default();
let mut data = ProgressData { let mut data = ProgressData {
display_entry: ProgressDataDisplayEntry { display_entries: vec![ProgressDataDisplayEntry {
prompt: ProgressMessagePrompt::Blocking, prompt: ProgressMessagePrompt::Blocking,
message: "data".to_string(), message: "data".to_string(),
position: 0, position: 0,
total_size: 10 * BYTES_TO_KIB, total_size: 10 * BYTES_TO_KIB,
}, }],
duration: Duration::from_secs(1), duration: Duration::from_secs(1),
pending_entries: 1, pending_entries: 1,
total_entries: 3, total_entries: 3,
@ -268,14 +317,16 @@ mod test {
}; };
let text = renderer.render(data.clone()); let text = renderer.render(data.clone());
let text = test_util::strip_ansi_codes(&text); let text = test_util::strip_ansi_codes(&text);
assert_eq!(text, "Blocking data 0.00KiB/10.00KiB (2/3)"); assert_contains!(text, "Blocking ⣯");
assert_contains!(text, "2/3\n - data 0.00KiB/10.00KiB\n\n\n\n");
data.pending_entries = 0; data.pending_entries = 0;
data.total_entries = 1; data.total_entries = 1;
data.display_entry.position = 0; data.display_entries[0].position = 0;
data.display_entry.total_size = 0; data.display_entries[0].total_size = 0;
let text = renderer.render(data); let text = renderer.render(data);
let text = test_util::strip_ansi_codes(&text); let text = test_util::strip_ansi_codes(&text);
assert_eq!(text, "Blocking data"); assert_contains!(text, "Blocking ⣟");
assert_contains!(text, "\n - data\n\n\n\n");
} }
} }