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:
parent
4c56353594
commit
6e8612f319
2 changed files with 103 additions and 52 deletions
|
@ -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())
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue