mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -05:00
c8d229dbf0
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
345 lines
10 KiB
Rust
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");
|
|
}
|
|
}
|