d877899920
<!-- Thank you for contributing to uv! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary Adds the enhancement proposed in #15470. Each package in the dependency tree now shows its compressed wheel file size, reading the wheel sizes directly from the lockfile (uv.lock). Doesn't break existing tree formatting or options. If no wheel size is available, nothing is added. Now, developers can identify large packages in their dependency tree. The tree still shows extras exactly as before, and then appends a size for the package. ## Test Plan Manually tested : ``` harsh@fcr-node:~/uv/test-uv-tree-sizes$ ../target/debug/uv tree Using CPython 3.13.7 warning: No `requires-python` value found in the workspace. Defaulting to `>=3.13`. Resolved 4 packages in 6ms pure-python v0.1.0 ├── click v8.2.1 └── six v1.17.0 harsh@fcr-node:~/uv/test-uv-tree-sizes$ ../target/debug/uv tree --show-sizes Using CPython 3.13.7 warning: No `requires-python` value found in the workspace. Defaulting to `>=3.13`. Resolved 4 packages in 6ms pure-python v0.1.0 ├── click v8.2.1 (99.8KiB) └── six v1.17.0 (10.8KiB) ```
302 lines
11 KiB
Rust
302 lines
11 KiB
Rust
use console::{Key, Term, measure_text_width, style};
|
||
use std::{cmp::Ordering, iter};
|
||
|
||
/// Prompt the user for confirmation in the given [`Term`].
|
||
///
|
||
/// This is a slimmed-down version of `dialoguer::Confirm`, with the post-confirmation report
|
||
/// enabled.
|
||
pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result<bool> {
|
||
confirm_inner(message, None, term, default)
|
||
}
|
||
|
||
/// Prompt the user for confirmation in the given [`Term`], with a hint.
|
||
pub fn confirm_with_hint(
|
||
message: &str,
|
||
hint: &str,
|
||
term: &Term,
|
||
default: bool,
|
||
) -> std::io::Result<bool> {
|
||
confirm_inner(message, Some(hint), term, default)
|
||
}
|
||
|
||
fn confirm_inner(
|
||
message: &str,
|
||
hint: Option<&str>,
|
||
term: &Term,
|
||
default: bool,
|
||
) -> std::io::Result<bool> {
|
||
let prompt = format!(
|
||
"{} {} {} {} {}",
|
||
style("?".to_string()).for_stderr().yellow(),
|
||
style(message).for_stderr().bold(),
|
||
style("[y/n]").for_stderr().black().bright(),
|
||
style("›").for_stderr().black().bright(),
|
||
style(if default { "yes" } else { "no" })
|
||
.for_stderr()
|
||
.cyan(),
|
||
);
|
||
|
||
term.write_str(&prompt)?;
|
||
if let Some(hint) = hint {
|
||
term.write_str(&format!(
|
||
"\n\n{}{} {hint}",
|
||
style("hint").for_stderr().bold().cyan(),
|
||
style(":").for_stderr().bold()
|
||
))?;
|
||
}
|
||
term.hide_cursor()?;
|
||
term.flush()?;
|
||
|
||
// Match continuously on every keystroke, and do not wait for user to hit the
|
||
// `Enter` key.
|
||
let response = loop {
|
||
let input = term.read_key_raw()?;
|
||
match input {
|
||
Key::Char('y' | 'Y') => break true,
|
||
Key::Char('n' | 'N') => break false,
|
||
Key::Enter => break default,
|
||
Key::CtrlC => {
|
||
let term = Term::stderr();
|
||
term.show_cursor()?;
|
||
term.write_str("\n")?;
|
||
term.flush()?;
|
||
|
||
#[allow(clippy::exit, clippy::cast_possible_wrap)]
|
||
std::process::exit(if cfg!(windows) {
|
||
0xC000_013A_u32 as i32
|
||
} else {
|
||
130
|
||
});
|
||
}
|
||
_ => {}
|
||
}
|
||
};
|
||
|
||
let report = format!(
|
||
"{} {} {} {}",
|
||
style("✔".to_string()).for_stderr().green(),
|
||
style(message).for_stderr().bold(),
|
||
style("·").for_stderr().black().bright(),
|
||
style(if response { "yes" } else { "no" })
|
||
.for_stderr()
|
||
.cyan(),
|
||
);
|
||
|
||
if hint.is_some() {
|
||
term.clear_last_lines(2)?;
|
||
// It's not clear why we need to clear to the end of the screen here, but it fixes lingering
|
||
// display of the hint on `bash` (the issue did not reproduce on `zsh`).
|
||
term.clear_to_end_of_screen()?;
|
||
} else {
|
||
term.clear_line()?;
|
||
}
|
||
term.write_line(&report)?;
|
||
term.show_cursor()?;
|
||
term.flush()?;
|
||
|
||
Ok(response)
|
||
}
|
||
|
||
/// Prompt the user for password in the given [`Term`].
|
||
///
|
||
/// This is a slimmed-down version of `dialoguer::Password`.
|
||
pub fn password(prompt: &str, term: &Term) -> std::io::Result<String> {
|
||
term.write_str(prompt)?;
|
||
term.show_cursor()?;
|
||
term.flush()?;
|
||
|
||
let input = term.read_secure_line()?;
|
||
|
||
term.clear_line()?;
|
||
|
||
Ok(input)
|
||
}
|
||
|
||
/// Prompt the user for input text in the given [`Term`].
|
||
///
|
||
/// This is a slimmed-down version of `dialoguer::Input`.
|
||
#[allow(
|
||
// Suppress Clippy lints triggered by `dialoguer::Input`.
|
||
clippy::cast_possible_truncation,
|
||
clippy::cast_possible_wrap,
|
||
clippy::cast_sign_loss
|
||
)]
|
||
pub fn input(prompt: &str, term: &Term) -> std::io::Result<String> {
|
||
term.write_str(prompt)?;
|
||
term.show_cursor()?;
|
||
term.flush()?;
|
||
|
||
let prompt_len = measure_text_width(prompt);
|
||
|
||
let mut chars: Vec<char> = Vec::new();
|
||
let mut position = 0;
|
||
loop {
|
||
match term.read_key()? {
|
||
Key::Backspace if position > 0 => {
|
||
position -= 1;
|
||
chars.remove(position);
|
||
let line_size = term.size().1 as usize;
|
||
// Case we want to delete last char of a line so the cursor is at the beginning of the next line
|
||
if (position + prompt_len) % (line_size - 1) == 0 {
|
||
term.clear_line()?;
|
||
term.move_cursor_up(1)?;
|
||
term.move_cursor_right(line_size + 1)?;
|
||
} else {
|
||
term.clear_chars(1)?;
|
||
}
|
||
|
||
let tail: String = chars[position..].iter().collect();
|
||
|
||
if !tail.is_empty() {
|
||
term.write_str(&tail)?;
|
||
|
||
let total = position + prompt_len + tail.chars().count();
|
||
let total_line = total / line_size;
|
||
let line_cursor = (position + prompt_len) / line_size;
|
||
term.move_cursor_up(total_line - line_cursor)?;
|
||
|
||
term.move_cursor_left(line_size)?;
|
||
term.move_cursor_right((position + prompt_len) % line_size)?;
|
||
}
|
||
|
||
term.flush()?;
|
||
}
|
||
Key::Char(chr) if !chr.is_ascii_control() => {
|
||
chars.insert(position, chr);
|
||
position += 1;
|
||
let tail: String = iter::once(&chr).chain(chars[position..].iter()).collect();
|
||
term.write_str(&tail)?;
|
||
term.move_cursor_left(tail.chars().count() - 1)?;
|
||
term.flush()?;
|
||
}
|
||
Key::ArrowLeft if position > 0 => {
|
||
if (position + prompt_len) % term.size().1 as usize == 0 {
|
||
term.move_cursor_up(1)?;
|
||
term.move_cursor_right(term.size().1 as usize)?;
|
||
} else {
|
||
term.move_cursor_left(1)?;
|
||
}
|
||
position -= 1;
|
||
term.flush()?;
|
||
}
|
||
Key::ArrowRight if position < chars.len() => {
|
||
if (position + prompt_len) % (term.size().1 as usize - 1) == 0 {
|
||
term.move_cursor_down(1)?;
|
||
term.move_cursor_left(term.size().1 as usize)?;
|
||
} else {
|
||
term.move_cursor_right(1)?;
|
||
}
|
||
position += 1;
|
||
term.flush()?;
|
||
}
|
||
Key::UnknownEscSeq(seq) if seq == vec!['b'] => {
|
||
let line_size = term.size().1 as usize;
|
||
let nb_space = chars[..position]
|
||
.iter()
|
||
.rev()
|
||
.take_while(|c| c.is_whitespace())
|
||
.count();
|
||
let find_last_space = chars[..position - nb_space]
|
||
.iter()
|
||
.rposition(|c| c.is_whitespace());
|
||
|
||
// If we find a space we set the cursor to the next char else we set it to the beginning of the input
|
||
if let Some(mut last_space) = find_last_space {
|
||
if last_space < position {
|
||
last_space += 1;
|
||
let new_line = (prompt_len + last_space) / line_size;
|
||
let old_line = (prompt_len + position) / line_size;
|
||
let diff_line = old_line - new_line;
|
||
if diff_line != 0 {
|
||
term.move_cursor_up(old_line - new_line)?;
|
||
}
|
||
|
||
let new_pos_x = (prompt_len + last_space) % line_size;
|
||
let old_pos_x = (prompt_len + position) % line_size;
|
||
let diff_pos_x = new_pos_x as i64 - old_pos_x as i64;
|
||
if diff_pos_x < 0 {
|
||
term.move_cursor_left(-diff_pos_x as usize)?;
|
||
} else {
|
||
term.move_cursor_right((diff_pos_x) as usize)?;
|
||
}
|
||
position = last_space;
|
||
}
|
||
} else {
|
||
term.move_cursor_left(position)?;
|
||
position = 0;
|
||
}
|
||
|
||
term.flush()?;
|
||
}
|
||
Key::UnknownEscSeq(seq) if seq == vec!['f'] => {
|
||
let line_size = term.size().1 as usize;
|
||
let find_next_space = chars[position..].iter().position(|c| c.is_whitespace());
|
||
|
||
// If we find a space we set the cursor to the next char else we set it to the beginning of the input
|
||
if let Some(mut next_space) = find_next_space {
|
||
let nb_space = chars[position + next_space..]
|
||
.iter()
|
||
.take_while(|c| c.is_whitespace())
|
||
.count();
|
||
next_space += nb_space;
|
||
let new_line = (prompt_len + position + next_space) / line_size;
|
||
let old_line = (prompt_len + position) / line_size;
|
||
term.move_cursor_down(new_line - old_line)?;
|
||
|
||
let new_pos_x = (prompt_len + position + next_space) % line_size;
|
||
let old_pos_x = (prompt_len + position) % line_size;
|
||
let diff_pos_x = new_pos_x as i64 - old_pos_x as i64;
|
||
if diff_pos_x < 0 {
|
||
term.move_cursor_left(-diff_pos_x as usize)?;
|
||
} else {
|
||
term.move_cursor_right((diff_pos_x) as usize)?;
|
||
}
|
||
position += next_space;
|
||
} else {
|
||
let new_line = (prompt_len + chars.len()) / line_size;
|
||
let old_line = (prompt_len + position) / line_size;
|
||
term.move_cursor_down(new_line - old_line)?;
|
||
|
||
let new_pos_x = (prompt_len + chars.len()) % line_size;
|
||
let old_pos_x = (prompt_len + position) % line_size;
|
||
let diff_pos_x = new_pos_x as i64 - old_pos_x as i64;
|
||
match diff_pos_x.cmp(&0) {
|
||
Ordering::Less => {
|
||
term.move_cursor_left((-diff_pos_x - 1) as usize)?;
|
||
}
|
||
Ordering::Equal => {}
|
||
Ordering::Greater => {
|
||
term.move_cursor_right((diff_pos_x) as usize)?;
|
||
}
|
||
}
|
||
position = chars.len();
|
||
}
|
||
|
||
term.flush()?;
|
||
}
|
||
Key::Enter => break,
|
||
_ => (),
|
||
}
|
||
}
|
||
let input = chars.iter().collect::<String>();
|
||
term.write_line("")?;
|
||
|
||
Ok(input)
|
||
}
|
||
|
||
/// Formats a number of bytes into a human readable SI-prefixed size (binary units).
|
||
///
|
||
/// Returns a tuple of `(quantity, units)`.
|
||
#[allow(
|
||
clippy::cast_possible_truncation,
|
||
clippy::cast_possible_wrap,
|
||
clippy::cast_precision_loss,
|
||
clippy::cast_sign_loss
|
||
)]
|
||
pub fn human_readable_bytes(bytes: u64) -> (f32, &'static str) {
|
||
const UNITS: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
|
||
let bytes_f32 = bytes as f32;
|
||
let i = ((bytes_f32.log2() / 10.0) as usize).min(UNITS.len() - 1);
|
||
(bytes_f32 / 1024_f32.powi(i as i32), UNITS[i])
|
||
}
|