uv-console 0.0.40

This is an internal component crate of uv
Documentation
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()?;

                #[expect(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 username in the given [`Term`].
pub fn username(prompt: &str, term: &Term) -> std::io::Result<String> {
    term.write_str(prompt)?;
    term.show_cursor()?;
    term.flush()?;

    let input = term.read_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).is_multiple_of(line_size - 1) {
                    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).is_multiple_of(term.size().1 as usize) {
                    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).is_multiple_of(term.size().1 as usize - 1) {
                    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])
}