From 88cbc98eec0e047c7e99290462bca3b5eeb0026f Mon Sep 17 00:00:00 2001 From: Ahmed Ilyas Date: Tue, 15 Oct 2024 10:00:43 +0200 Subject: [PATCH] Support interactive input in `uv publish` (#8158) --- Cargo.lock | 2 + crates/uv-console/src/lib.rs | 190 +++++++++++++++++++++++++++++- crates/uv/Cargo.toml | 2 + crates/uv/src/commands/publish.rs | 23 +++- 4 files changed, 214 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6504c5a2a..fd4711fb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4106,6 +4106,7 @@ dependencies = [ "base64 0.22.1", "byteorder", "clap", + "console", "ctrlc", "etcetera", "filetime", @@ -4149,6 +4150,7 @@ dependencies = [ "uv-cli", "uv-client", "uv-configuration", + "uv-console", "uv-dispatch", "uv-distribution", "uv-distribution-filename", diff --git a/crates/uv-console/src/lib.rs b/crates/uv-console/src/lib.rs index 83d50f0cd..165edd1de 100644 --- a/crates/uv-console/src/lib.rs +++ b/crates/uv-console/src/lib.rs @@ -1,4 +1,5 @@ -use console::{style, Key, Term}; +use console::{measure_text_width, style, Key, Term}; +use std::{cmp::Ordering, iter}; /// Prompt the user for confirmation in the given [`Term`]. /// @@ -72,3 +73,190 @@ pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result std::io::Result { + 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 { + term.write_str(prompt)?; + term.show_cursor()?; + term.flush()?; + + let prompt_len = measure_text_width(prompt); + + let mut chars: Vec = 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::(); + term.write_line("")?; + + Ok(input) +} diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index f7d396df2..50bb7a0fe 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -22,6 +22,7 @@ uv-cache-key = { workspace = true } uv-cli = { workspace = true } uv-client = { workspace = true } uv-configuration = { workspace = true } +uv-console = { workspace = true } uv-dispatch = { workspace = true } uv-distribution = { workspace = true } uv-distribution-filename = { workspace = true } @@ -59,6 +60,7 @@ axoupdater = { workspace = true, features = [ "tokio", ], optional = true } clap = { workspace = true, features = ["derive", "string", "wrap_help"] } +console = { workspace = true } ctrlc = { workspace = true } flate2 = { workspace = true, default-features = false } fs-err = { workspace = true, features = ["tokio"] } diff --git a/crates/uv/src/commands/publish.rs b/crates/uv/src/commands/publish.rs index 38e2e1989..edd7e332f 100644 --- a/crates/uv/src/commands/publish.rs +++ b/crates/uv/src/commands/publish.rs @@ -1,7 +1,8 @@ use crate::commands::reporters::PublishReporter; use crate::commands::{human_readable_bytes, ExitStatus}; use crate::printer::Printer; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; +use console::Term; use owo_colors::OwoColorize; use std::fmt::Write; use std::sync::Arc; @@ -69,10 +70,15 @@ pub(crate) async fn publish( &oidc_client.client(), ) .await?; + let (username, password) = if let Some(password) = trusted_publishing_token { (Some("__token__".to_string()), Some(password.into())) } else { - (username, password) + if username.is_none() && password.is_none() { + prompt_username_and_password()? + } else { + (username, password) + } }; for (file, filename) in files { @@ -109,3 +115,16 @@ pub(crate) async fn publish( Ok(ExitStatus::Success) } + +fn prompt_username_and_password() -> Result<(Option, Option)> { + let term = Term::stderr(); + if !term.is_term() { + return Ok((None, None)); + } + let username_prompt = "Enter username ('__token__' if using a token): "; + let password_prompt = "Enter password: "; + let username = uv_console::input(username_prompt, &term).context("Failed to read username")?; + let password = + uv_console::password(password_prompt, &term).context("Failed to read password")?; + Ok((Some(username), Some(password))) +}