deno/cli/util/progress_bar/renderer.rs

282 lines
7.9 KiB
Rust

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std::time::Duration;
use deno_terminal::colors;
use crate::util::display::human_download_size;
use super::ProgressMessagePrompt;
#[derive(Clone)]
pub struct ProgressDataDisplayEntry {
pub prompt: ProgressMessagePrompt,
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 + Sync + 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 = (data.terminal_width as i32 - 5).clamp(10, 75) 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!(
"{} {}{}{}",
data.display_entry.prompt.as_text(),
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!("[{minutes:0>2}:{seconds:0>2}]")
}
#[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 {
prompt: ProgressMessagePrompt::Download,
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 {
prompt: ProgressMessagePrompt::Blocking,
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, "Blocking 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, "Blocking data");
}
}