1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-28 16:20:57 -05:00
denoland-deno/cli/util/progress_bar/renderer.rs
Marvin Hagemeister c8d229dbf0
fix(install): percent encodings in interactive progress bar (#26600)
Fixes percent encodings showing up when installing scoped packages via
`deno add` or `deno install`. The issue is caused by us trying to map
back the package name from the resolved http url. This doesn't and has
never worked with private registries. The proper solution would be to
pass the original specifier into here, but that's a bit of a bigger
refactor.

So for now the quickest workaround is to replace `%2f` back to `/`.

Fixes https://github.com/denoland/deno/issues/26576
2024-10-29 23:35:42 +00:00

345 lines
10 KiB
Rust

// 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 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_entries: Vec<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 {
pub display_human_download_size: bool,
}
impl ProgressBarRenderer for BarProgressBarRenderer {
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 total_size = display_entry.total_size;
let pos = display_entry.position;
if total_size == 0 {
(String::new(), 0)
} else {
let (pos_str, total_size_str) = if self.display_human_download_size {
(
human_download_size(pos, total_size),
human_download_size(total_size, total_size),
)
} else {
(pos.to_string(), total_size.to_string())
};
(
format!(" {}/{}", pos_str, 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 !display_entry.message.is_empty() {
text.push_str(&format!(
"{} {}{}\n",
colors::green("Download"),
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 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 {
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 {
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 total_size = display_entry.total_size;
let pos = display_entry.position;
if total_size == 0 {
String::new()
} else {
format!(
" {}/{}",
human_download_size(pos, total_size),
human_download_size(total_size, total_size)
)
}
};
// TODO(@marvinhagemeister): We're trying to reconstruct the original
// specifier from the resolved one, but we lack the information about
// private registries URLs and other things here.
let message = display_entry
.message
.replace("https://registry.npmjs.org/", "npm:")
.replace("https://jsr.io/", "jsr:")
.replace("%2f", "/")
.replace("%2F", "/");
display_str.push_str(
&colors::gray(format!(" - {}{}\n", message, bytes_text)).to_string(),
);
}
display_str
}
}
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;
use test_util::assert_contains;
#[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 {
display_human_download_size: true,
};
let mut data = ProgressData {
display_entries: vec![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_entries[0].position = 5 * BYTES_TO_KIB;
data.display_entries[0].message = "".to_string();
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_entries[0].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_entries[0].position = 0;
data.display_entries[0].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::default();
let mut data = ProgressData {
display_entries: vec![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_contains!(text, "Blocking ⣯");
assert_contains!(text, "2/3\n - data 0.00KiB/10.00KiB\n\n\n\n");
data.pending_entries = 0;
data.total_entries = 1;
data.display_entries[0].position = 0;
data.display_entries[0].total_size = 0;
let text = renderer.render(data);
let text = test_util::strip_ansi_codes(&text);
assert_contains!(text, "Blocking ⣟");
assert_contains!(text, "\n - data\n\n\n\n");
}
}