uv/crates/uv-console/src/lib.rs
2024-10-15 10:00:43 +02:00

262 lines
9.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use console::{measure_text_width, style, Key, Term};
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> {
// Set the Ctrl-C handler to exit the process.
let result = ctrlc::set_handler(move || {
let term = Term::stderr();
term.show_cursor().ok();
term.flush().ok();
#[allow(clippy::exit, clippy::cast_possible_wrap)]
std::process::exit(if cfg!(windows) {
0xC000_013A_u32 as i32
} else {
130
});
});
match result {
Ok(()) => {}
Err(ctrlc::Error::MultipleHandlers) => {
// If multiple handlers were set, we assume that the existing handler is our
// confirmation handler, and continue.
}
Err(err) => return Err(std::io::Error::new(std::io::ErrorKind::Other, err)),
}
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)?;
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()?;
match input {
Key::Char('y' | 'Y') => break true,
Key::Char('n' | 'N') => break false,
Key::Enter => break default,
_ => {}
};
};
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(),
);
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)
}