From 4aed9dd0d62cf0a0974404ae702de88f854790c0 Mon Sep 17 00:00:00 2001 From: Ahmed Ilyas Date: Sun, 13 Oct 2024 15:58:05 +0200 Subject: [PATCH] Support interactive input in `uv publish` --- crates/uv-console/src/lib.rs | 203 +++++++++++++++++++++++++++++- crates/uv/Cargo.toml | 2 + crates/uv/src/commands/publish.rs | 23 +++- 3 files changed, 225 insertions(+), 3 deletions(-) diff --git a/crates/uv-console/src/lib.rs b/crates/uv-console/src/lib.rs index 83d50f0cdb3e..bfe33694a7b5 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,203 @@ pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result std::io::Result { + loop { + term.write_str(prompt)?; + term.show_cursor()?; + term.flush()?; + + let input = term.read_secure_line()?; + + term.clear_line()?; + + if !input.is_empty() { + return Ok(input); + } + } +} + +/// Prompt the user for input text in the given [`Term`]. +/// +/// This is a slimmed-down version of `dialoguer::Input`. +pub fn input(prompt: &str, term: &Term) -> std::io::Result { + loop { + 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; + //println!("new_pos_x = {}, old_pos_x = {}, diff = {}", new_pos_x, old_pos_x, diff_pos_x); + 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("")?; + + match input.parse::() { + Ok(value) => { + term.flush()?; + return Ok(value); + } + Err(err) => { + term.write_line(&err.to_string())?; + term.show_cursor()?; + term.flush()?; + + continue; + } + } + } +} diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index fd7a8443a2bf..dba49f90680d 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 } @@ -58,6 +59,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 38e2e19893ed..efead89f9bea 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() { + anyhow::bail!("Not a terminal"); + } + 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))) +}