use std::io::{self, BufRead, Write as IoWrite};
use crate::console::Console;
use crate::style::Style;
use crate::text::Text;
#[derive(Debug)]
pub struct InvalidResponse {
pub message: String,
}
impl std::fmt::Display for InvalidResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for InvalidResponse {}
pub struct Prompt {
pub prompt_text: Text,
pub password: bool,
pub choices: Option<Vec<String>>,
pub case_sensitive: bool,
pub show_default: bool,
pub show_choices: bool,
pub default: Option<String>,
console: Console,
}
impl Prompt {
pub fn new(prompt: &str) -> Self {
let prompt_text = crate::markup::render(prompt, Style::null())
.unwrap_or_else(|_| Text::new(prompt, Style::null()));
Prompt {
prompt_text,
password: false,
choices: None,
case_sensitive: true,
show_default: true,
show_choices: true,
default: None,
console: Console::new(),
}
}
#[must_use]
pub fn with_console(mut self, console: Console) -> Self {
self.console = console;
self
}
#[must_use]
pub fn with_password(mut self, password: bool) -> Self {
self.password = password;
self
}
#[must_use]
pub fn with_choices(mut self, choices: Vec<String>) -> Self {
self.choices = Some(choices);
self
}
#[must_use]
pub fn with_default(mut self, default: &str) -> Self {
self.default = Some(default.to_string());
self
}
#[must_use]
pub fn with_case_sensitive(mut self, case: bool) -> Self {
self.case_sensitive = case;
self
}
#[must_use]
pub fn with_show_default(mut self, show: bool) -> Self {
self.show_default = show;
self
}
#[must_use]
pub fn with_show_choices(mut self, show: bool) -> Self {
self.show_choices = show;
self
}
pub fn make_prompt(&self) -> Text {
let mut prompt = self.prompt_text.clone();
prompt.end = String::new();
if self.show_choices {
if let Some(ref choices) = self.choices {
let choices_str = format!("[{}]", choices.join("/"));
let choices_style = Style::parse("magenta bold").unwrap_or_else(|_| Style::null());
prompt.append_str(" ", None);
prompt.append_str(&choices_str, Some(choices_style));
}
}
if self.show_default {
if let Some(ref default) = self.default {
let default_str = format!("({})", default);
let default_style = Style::parse("cyan bold").unwrap_or_else(|_| Style::null());
prompt.append_str(" ", None);
prompt.append_str(&default_str, Some(default_style));
}
}
prompt.append_str(": ", None);
prompt
}
fn check_choice(&self, value: &str) -> bool {
match &self.choices {
None => true,
Some(choices) => {
let trimmed = value.trim();
if self.case_sensitive {
choices.iter().any(|c| c == trimmed)
} else {
let lower = trimmed.to_lowercase();
choices.iter().any(|c| c.to_lowercase() == lower)
}
}
}
}
fn resolve_choice(&self, value: &str) -> String {
let trimmed = value.trim();
match &self.choices {
None => trimmed.to_string(),
Some(choices) => {
if self.case_sensitive {
trimmed.to_string()
} else {
let lower = trimmed.to_lowercase();
choices
.iter()
.find(|c| c.to_lowercase() == lower)
.cloned()
.unwrap_or_else(|| trimmed.to_string())
}
}
}
}
pub fn ask_with_input<R: BufRead>(&self, input: &mut R) -> String {
loop {
let prompt = self.make_prompt();
let prompt_str = prompt.plain().to_string();
print!("{}", prompt_str);
let _ = io::stdout().flush();
let mut line = String::new();
match input.read_line(&mut line) {
Ok(0) => {
if let Some(ref default) = self.default {
return default.clone();
}
return String::new();
}
Ok(_) => {}
Err(_) => {
if let Some(ref default) = self.default {
return default.clone();
}
return String::new();
}
}
let trimmed = line.trim_end_matches('\n').trim_end_matches('\r');
let value = trimmed.to_string();
if value.trim().is_empty() {
if let Some(ref default) = self.default {
return default.clone();
}
}
if self.choices.is_some() {
if !self.check_choice(&value) {
eprintln!("Please select one of the available options");
continue;
}
return self.resolve_choice(&value);
}
return value;
}
}
pub fn ask(&self) -> String {
#[cfg(feature = "interactive")]
if self.password {
return self.ask_password();
}
#[cfg(not(feature = "interactive"))]
if self.password {
eprintln!(
"warning: gilt built without `interactive` feature; password input will be visible"
);
}
let stdin = io::stdin();
let mut handle = stdin.lock();
self.ask_with_input(&mut handle)
}
#[cfg(feature = "interactive")]
fn ask_password(&self) -> String {
loop {
let prompt = self.make_prompt();
let prompt_str = prompt.plain().to_string();
print!("{}", prompt_str);
let _ = io::stdout().flush();
let value = match rpassword::read_password() {
Ok(v) => v,
Err(_) => {
if let Some(ref default) = self.default {
return default.clone();
}
return String::new();
}
};
if value.trim().is_empty() {
if let Some(ref default) = self.default {
return default.clone();
}
}
if self.choices.is_some() {
if !self.check_choice(&value) {
eprintln!("Please select one of the available options");
continue;
}
return self.resolve_choice(&value);
}
return value;
}
}
}
pub fn confirm(prompt: &str) -> bool {
confirm_with_input(prompt, &mut io::stdin().lock())
}
pub fn confirm_with_input<R: BufRead>(prompt: &str, input: &mut R) -> bool {
let p = Prompt::new(prompt)
.with_choices(vec!["y".into(), "n".into()])
.with_case_sensitive(false)
.with_show_choices(true);
loop {
let prompt_text = p.make_prompt();
let prompt_str = prompt_text.plain().to_string();
print!("{}", prompt_str);
let _ = io::stdout().flush();
let mut line = String::new();
match input.read_line(&mut line) {
Ok(0) => return false,
Ok(_) => {}
Err(_) => return false,
}
let value = line.trim().to_lowercase();
match value.as_str() {
"y" | "yes" => return true,
"n" | "no" => return false,
_ => {
eprintln!("Please enter Y or N");
continue;
}
}
}
}
pub fn ask_int(prompt: &str) -> i64 {
ask_int_with_input(prompt, &mut io::stdin().lock())
}
pub fn ask_int_with_input<R: BufRead>(prompt: &str, input: &mut R) -> i64 {
loop {
let prompt_text = Prompt::new(prompt).make_prompt();
let prompt_str = prompt_text.plain().to_string();
print!("{}", prompt_str);
let _ = io::stdout().flush();
let mut line = String::new();
match input.read_line(&mut line) {
Ok(0) => {
eprintln!("Please enter a valid integer number");
continue;
}
Ok(_) => {}
Err(_) => {
eprintln!("Please enter a valid integer number");
continue;
}
}
match line.trim().parse::<i64>() {
Ok(v) => return v,
Err(_) => {
eprintln!("Please enter a valid integer number");
continue;
}
}
}
}
pub fn ask_float(prompt: &str) -> f64 {
ask_float_with_input(prompt, &mut io::stdin().lock())
}
pub fn ask_float_with_input<R: BufRead>(prompt: &str, input: &mut R) -> f64 {
loop {
let prompt_text = Prompt::new(prompt).make_prompt();
let prompt_str = prompt_text.plain().to_string();
print!("{}", prompt_str);
let _ = io::stdout().flush();
let mut line = String::new();
match input.read_line(&mut line) {
Ok(0) => {
eprintln!("Please enter a valid number");
continue;
}
Ok(_) => {}
Err(_) => {
eprintln!("Please enter a valid number");
continue;
}
}
match line.trim().parse::<f64>() {
Ok(v) => return v,
Err(_) => {
eprintln!("Please enter a valid number");
continue;
}
}
}
}
pub struct Select {
pub prompt: String,
pub choices: Vec<String>,
pub default: Option<usize>,
pub style: Style,
pub highlight_style: Style,
}
impl Select {
pub fn new(prompt: &str, choices: Vec<String>) -> Self {
Select {
prompt: prompt.to_string(),
choices,
default: None,
style: Style::parse("bold").unwrap_or_else(|_| Style::null()),
highlight_style: Style::parse("cyan bold").unwrap_or_else(|_| Style::null()),
}
}
#[must_use]
pub fn with_default(mut self, index: usize) -> Self {
self.default = Some(index);
self
}
#[must_use]
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
pub fn with_highlight_style(mut self, style: Style) -> Self {
self.highlight_style = style;
self
}
pub fn format_choices(&self) -> String {
let mut output = String::new();
output.push_str(&format!("? {}:\n", self.prompt));
for (i, choice) in self.choices.iter().enumerate() {
output.push_str(&format!(" {}) {}\n", i + 1, choice));
}
output
}
pub fn format_input_prompt(&self) -> String {
let n = self.choices.len();
let mut prompt = format!("Enter choice [1-{}]", n);
if let Some(default) = self.default {
prompt.push_str(&format!(" ({})", default + 1));
}
prompt.push_str(": ");
prompt
}
pub fn parse_input(&self, input: &str) -> Result<usize, String> {
let trimmed = input.trim();
if trimmed.is_empty() {
if let Some(default) = self.default {
if default < self.choices.len() {
return Ok(default);
}
return Err(format!(
"Default index {} is out of range (1-{})",
default + 1,
self.choices.len()
));
}
return Err("Please enter a number".to_string());
}
let num: usize = trimmed
.parse()
.map_err(|_| format!("'{}' is not a valid number", trimmed))?;
if num < 1 || num > self.choices.len() {
return Err(format!(
"Please enter a number between 1 and {}",
self.choices.len()
));
}
Ok(num - 1) }
pub fn ask(&self, console: &mut Console) -> Result<usize, InvalidResponse> {
if self.choices.is_empty() {
return Err(InvalidResponse {
message: "No choices provided".to_string(),
});
}
let stdin = io::stdin();
let mut handle = stdin.lock();
self.ask_with_input(console, &mut handle)
}
pub fn ask_with_input<R: BufRead>(
&self,
console: &mut Console,
input: &mut R,
) -> Result<usize, InvalidResponse> {
if self.choices.is_empty() {
return Err(InvalidResponse {
message: "No choices provided".to_string(),
});
}
let choices_display = self.format_choices();
console.print_text(&choices_display);
loop {
let prompt_line = self.format_input_prompt();
print!("{}", prompt_line);
let _ = io::stdout().flush();
let mut line = String::new();
match input.read_line(&mut line) {
Ok(0) => {
if let Some(default) = self.default {
if default < self.choices.len() {
return Ok(default);
}
}
return Err(InvalidResponse {
message: "No input provided".to_string(),
});
}
Ok(_) => {}
Err(e) => {
return Err(InvalidResponse {
message: format!("Input error: {}", e),
});
}
}
match self.parse_input(&line) {
Ok(index) => return Ok(index),
Err(msg) => {
eprintln!("{}", msg);
continue;
}
}
}
}
pub fn ask_value(&self, console: &mut Console) -> Result<String, InvalidResponse> {
let index = self.ask(console)?;
Ok(self.choices[index].clone())
}
pub fn ask_value_with_input<R: BufRead>(
&self,
console: &mut Console,
input: &mut R,
) -> Result<String, InvalidResponse> {
let index = self.ask_with_input(console, input)?;
Ok(self.choices[index].clone())
}
}
pub struct MultiSelect {
pub prompt: String,
pub choices: Vec<String>,
pub defaults: Vec<usize>,
pub min_selections: usize,
pub max_selections: Option<usize>,
pub style: Style,
pub highlight_style: Style,
}
impl MultiSelect {
pub fn new(prompt: &str, choices: Vec<String>) -> Self {
MultiSelect {
prompt: prompt.to_string(),
choices,
defaults: Vec::new(),
min_selections: 0,
max_selections: None,
style: Style::parse("bold").unwrap_or_else(|_| Style::null()),
highlight_style: Style::parse("cyan bold").unwrap_or_else(|_| Style::null()),
}
}
#[must_use]
pub fn with_defaults(mut self, indices: Vec<usize>) -> Self {
self.defaults = indices;
self
}
#[must_use]
pub fn with_min(mut self, min: usize) -> Self {
self.min_selections = min;
self
}
#[must_use]
pub fn with_max(mut self, max: usize) -> Self {
self.max_selections = Some(max);
self
}
#[must_use]
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
pub fn with_highlight_style(mut self, style: Style) -> Self {
self.highlight_style = style;
self
}
pub fn format_choices(&self) -> String {
let mut output = String::new();
output.push_str(&format!("? {} (comma-separated):\n", self.prompt));
for (i, choice) in self.choices.iter().enumerate() {
output.push_str(&format!(" {}) {}\n", i + 1, choice));
}
output
}
pub fn format_input_prompt(&self) -> String {
let n = self.choices.len();
let mut prompt = format!("Enter choices [1-{}, e.g. 1,3]", n);
if !self.defaults.is_empty() {
let defaults_str: Vec<String> =
self.defaults.iter().map(|d| (d + 1).to_string()).collect();
prompt.push_str(&format!(" ({})", defaults_str.join(",")));
}
prompt.push_str(": ");
prompt
}
pub fn parse_input(&self, input: &str) -> Result<Vec<usize>, String> {
let trimmed = input.trim();
if trimmed.is_empty() {
if !self.defaults.is_empty() {
for &d in &self.defaults {
if d >= self.choices.len() {
return Err(format!(
"Default index {} is out of range (1-{})",
d + 1,
self.choices.len()
));
}
}
return self.validate_count(&self.defaults);
}
return self.validate_count(&[]);
}
if trimmed.eq_ignore_ascii_case("all") {
let all: Vec<usize> = (0..self.choices.len()).collect();
return self.validate_count(&all);
}
let mut indices = Vec::new();
for part in trimmed.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
let num: usize = part
.parse()
.map_err(|_| format!("'{}' is not a valid number", part))?;
if num < 1 || num > self.choices.len() {
return Err(format!(
"Number {} is out of range (1-{})",
num,
self.choices.len()
));
}
let index = num - 1;
if !indices.contains(&index) {
indices.push(index);
}
}
self.validate_count(&indices)
}
fn validate_count(&self, indices: &[usize]) -> Result<Vec<usize>, String> {
if indices.len() < self.min_selections {
return Err(format!(
"Please select at least {} option{}",
self.min_selections,
if self.min_selections == 1 { "" } else { "s" }
));
}
if let Some(max) = self.max_selections {
if indices.len() > max {
return Err(format!(
"Please select at most {} option{}",
max,
if max == 1 { "" } else { "s" }
));
}
}
Ok(indices.to_vec())
}
pub fn ask(&self, console: &mut Console) -> Result<Vec<usize>, InvalidResponse> {
if self.choices.is_empty() {
return Err(InvalidResponse {
message: "No choices provided".to_string(),
});
}
let stdin = io::stdin();
let mut handle = stdin.lock();
self.ask_with_input(console, &mut handle)
}
pub fn ask_with_input<R: BufRead>(
&self,
console: &mut Console,
input: &mut R,
) -> Result<Vec<usize>, InvalidResponse> {
if self.choices.is_empty() {
return Err(InvalidResponse {
message: "No choices provided".to_string(),
});
}
let choices_display = self.format_choices();
console.print_text(&choices_display);
loop {
let prompt_line = self.format_input_prompt();
print!("{}", prompt_line);
let _ = io::stdout().flush();
let mut line = String::new();
match input.read_line(&mut line) {
Ok(0) => {
if !self.defaults.is_empty() {
match self.validate_count(&self.defaults) {
Ok(indices) => return Ok(indices),
Err(_) => {
return Err(InvalidResponse {
message: "No input provided".to_string(),
});
}
}
}
match self.validate_count(&[]) {
Ok(indices) => return Ok(indices),
Err(_) => {
return Err(InvalidResponse {
message: "No input provided".to_string(),
});
}
}
}
Ok(_) => {}
Err(e) => {
return Err(InvalidResponse {
message: format!("Input error: {}", e),
});
}
}
match self.parse_input(&line) {
Ok(indices) => return Ok(indices),
Err(msg) => {
eprintln!("{}", msg);
continue;
}
}
}
}
pub fn ask_values(&self, console: &mut Console) -> Result<Vec<String>, InvalidResponse> {
let indices = self.ask(console)?;
Ok(indices.iter().map(|&i| self.choices[i].clone()).collect())
}
pub fn ask_values_with_input<R: BufRead>(
&self,
console: &mut Console,
input: &mut R,
) -> Result<Vec<String>, InvalidResponse> {
let indices = self.ask_with_input(console, input)?;
Ok(indices.iter().map(|&i| self.choices[i].clone()).collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_simple_prompt_returns_input() {
let p = Prompt::new("Enter name");
let mut input = Cursor::new(b"Alice\n" as &[u8]);
let result = p.ask_with_input(&mut input);
assert_eq!(result, "Alice");
}
#[test]
fn test_prompt_with_default_empty_returns_default() {
let p = Prompt::new("Enter name").with_default("Bob");
let mut input = Cursor::new(b"\n" as &[u8]);
let result = p.ask_with_input(&mut input);
assert_eq!(result, "Bob");
}
#[test]
fn test_prompt_with_default_non_empty_returns_input() {
let p = Prompt::new("Enter name").with_default("Bob");
let mut input = Cursor::new(b"Charlie\n" as &[u8]);
let result = p.ask_with_input(&mut input);
assert_eq!(result, "Charlie");
}
#[test]
fn test_prompt_with_choices_valid() {
let p = Prompt::new("Pick fruit").with_choices(vec![
"apple".into(),
"orange".into(),
"pear".into(),
]);
let mut input = Cursor::new(b"apple\n" as &[u8]);
let result = p.ask_with_input(&mut input);
assert_eq!(result, "apple");
}
#[test]
fn test_prompt_with_choices_invalid_then_valid() {
let p = Prompt::new("Pick fruit").with_choices(vec![
"apple".into(),
"orange".into(),
"pear".into(),
]);
let mut input = Cursor::new(b"banana\norange\n" as &[u8]);
let result = p.ask_with_input(&mut input);
assert_eq!(result, "orange");
}
#[test]
fn test_case_insensitive_choices() {
let p = Prompt::new("Pick")
.with_choices(vec!["Apple".into(), "Orange".into()])
.with_case_sensitive(false);
let mut input = Cursor::new(b"apple\n" as &[u8]);
let result = p.ask_with_input(&mut input);
assert_eq!(result, "Apple");
}
#[test]
fn test_case_sensitive_choices_reject_wrong_case() {
let p = Prompt::new("Pick")
.with_choices(vec!["Apple".into(), "Orange".into()])
.with_case_sensitive(true);
let mut input = Cursor::new(b"apple\nApple\n" as &[u8]);
let result = p.ask_with_input(&mut input);
assert_eq!(result, "Apple");
}
#[test]
fn test_confirm_yes() {
let mut input = Cursor::new(b"y\n" as &[u8]);
let result = confirm_with_input("Continue?", &mut input);
assert!(result);
}
#[test]
fn test_confirm_yes_full() {
let mut input = Cursor::new(b"yes\n" as &[u8]);
let result = confirm_with_input("Continue?", &mut input);
assert!(result);
}
#[test]
fn test_confirm_no() {
let mut input = Cursor::new(b"n\n" as &[u8]);
let result = confirm_with_input("Continue?", &mut input);
assert!(!result);
}
#[test]
fn test_confirm_no_full() {
let mut input = Cursor::new(b"no\n" as &[u8]);
let result = confirm_with_input("Continue?", &mut input);
assert!(!result);
}
#[test]
fn test_confirm_case_insensitive() {
let mut input = Cursor::new(b"Y\n" as &[u8]);
let result = confirm_with_input("Continue?", &mut input);
assert!(result);
}
#[test]
fn test_confirm_invalid_then_valid() {
let mut input = Cursor::new(b"maybe\ny\n" as &[u8]);
let result = confirm_with_input("Continue?", &mut input);
assert!(result);
}
#[test]
fn test_ask_int_valid() {
let mut input = Cursor::new(b"42\n" as &[u8]);
let result = ask_int_with_input("Enter number", &mut input);
assert_eq!(result, 42);
}
#[test]
fn test_ask_int_negative() {
let mut input = Cursor::new(b"-7\n" as &[u8]);
let result = ask_int_with_input("Enter number", &mut input);
assert_eq!(result, -7);
}
#[test]
fn test_ask_int_invalid_then_valid() {
let mut input = Cursor::new(b"abc\n42\n" as &[u8]);
let result = ask_int_with_input("Enter number", &mut input);
assert_eq!(result, 42);
}
#[test]
fn test_ask_float_valid() {
let mut input = Cursor::new(b"3.14\n" as &[u8]);
let result = ask_float_with_input("Enter number", &mut input);
assert!((result - 3.14).abs() < f64::EPSILON);
}
#[test]
fn test_ask_float_integer_input() {
let mut input = Cursor::new(b"7\n" as &[u8]);
let result = ask_float_with_input("Enter number", &mut input);
assert!((result - 7.0).abs() < f64::EPSILON);
}
#[test]
fn test_ask_float_invalid_then_valid() {
let mut input = Cursor::new(b"xyz\n2.718\n" as &[u8]);
let result = ask_float_with_input("Enter number", &mut input);
assert!((result - 2.718).abs() < f64::EPSILON);
}
#[test]
fn test_prompt_text_includes_choices() {
let p = Prompt::new("Pick fruit")
.with_choices(vec!["apple".into(), "orange".into()])
.with_show_choices(true);
let text = p.make_prompt();
let plain = text.plain().to_string();
assert!(plain.contains("[apple/orange]"));
}
#[test]
fn test_prompt_text_hides_choices_when_disabled() {
let p = Prompt::new("Pick fruit")
.with_choices(vec!["apple".into(), "orange".into()])
.with_show_choices(false);
let text = p.make_prompt();
let plain = text.plain().to_string();
assert!(!plain.contains("apple"));
assert!(!plain.contains("orange"));
}
#[test]
fn test_prompt_text_includes_default() {
let p = Prompt::new("Enter name")
.with_default("World")
.with_show_default(true);
let text = p.make_prompt();
let plain = text.plain().to_string();
assert!(plain.contains("(World)"));
}
#[test]
fn test_prompt_text_hides_default_when_disabled() {
let p = Prompt::new("Enter name")
.with_default("World")
.with_show_default(false);
let text = p.make_prompt();
let plain = text.plain().to_string();
assert!(!plain.contains("(World)"));
}
#[test]
fn test_password_flag() {
let p = Prompt::new("Password").with_password(true);
assert!(p.password);
let p2 = Prompt::new("Password").with_password(false);
assert!(!p2.password);
}
#[test]
fn test_builder_with_choices() {
let p = Prompt::new("test").with_choices(vec!["a".into(), "b".into()]);
assert_eq!(p.choices, Some(vec!["a".to_string(), "b".to_string()]));
}
#[test]
fn test_builder_with_default() {
let p = Prompt::new("test").with_default("val");
assert_eq!(p.default, Some("val".to_string()));
}
#[test]
fn test_builder_with_case_sensitive() {
let p = Prompt::new("test").with_case_sensitive(false);
assert!(!p.case_sensitive);
}
#[test]
fn test_builder_with_show_default() {
let p = Prompt::new("test").with_show_default(false);
assert!(!p.show_default);
}
#[test]
fn test_builder_with_show_choices() {
let p = Prompt::new("test").with_show_choices(false);
assert!(!p.show_choices);
}
#[test]
fn test_builder_with_password() {
let p = Prompt::new("test").with_password(true);
assert!(p.password);
}
#[test]
fn test_prompt_suffix() {
let p = Prompt::new("Enter value");
let text = p.make_prompt();
let plain = text.plain().to_string();
assert!(plain.ends_with(": "));
}
#[test]
fn test_default_on_eof() {
let p = Prompt::new("Enter").with_default("fallback");
let mut input = Cursor::new(b"" as &[u8]); let result = p.ask_with_input(&mut input);
assert_eq!(result, "fallback");
}
#[test]
fn test_no_default_empty_returns_empty() {
let p = Prompt::new("Enter");
let mut input = Cursor::new(b"\n" as &[u8]);
let result = p.ask_with_input(&mut input);
assert_eq!(result, "");
}
#[test]
fn test_prompt_text_choices_and_default() {
let p = Prompt::new("Pick")
.with_choices(vec!["a".into(), "b".into()])
.with_default("a")
.with_show_choices(true)
.with_show_default(true);
let text = p.make_prompt();
let plain = text.plain().to_string();
assert!(plain.contains("[a/b]"));
assert!(plain.contains("(a)"));
assert!(plain.ends_with(": "));
}
#[test]
fn test_choices_default_on_empty() {
let p = Prompt::new("Pick")
.with_choices(vec!["a".into(), "b".into()])
.with_default("a");
let mut input = Cursor::new(b"\n" as &[u8]);
let result = p.ask_with_input(&mut input);
assert_eq!(result, "a");
}
#[test]
fn test_prompt_has_styled_choices_span() {
let p = Prompt::new("Pick")
.with_choices(vec!["x".into(), "y".into()])
.with_show_choices(true);
let text = p.make_prompt();
assert!(!text.spans().is_empty());
}
#[test]
fn test_prompt_has_styled_default_span() {
let p = Prompt::new("Pick")
.with_default("z")
.with_show_default(true);
let text = p.make_prompt();
assert!(!text.spans().is_empty());
}
#[test]
fn test_invalid_response_display() {
let err = InvalidResponse {
message: "bad input".to_string(),
};
assert_eq!(format!("{}", err), "bad input");
}
#[test]
fn test_invalid_response_debug() {
let err = InvalidResponse {
message: "bad".to_string(),
};
let debug = format!("{:?}", err);
assert!(debug.contains("InvalidResponse"));
assert!(debug.contains("bad"));
}
#[test]
fn test_password_ask_with_input_reads_normally() {
let p = Prompt::new("Password").with_password(true);
let mut input = Cursor::new(b"secret123\n" as &[u8]);
let result = p.ask_with_input(&mut input);
assert_eq!(result, "secret123");
}
#[test]
fn test_password_with_default_on_empty() {
let p = Prompt::new("Password")
.with_password(true)
.with_default("default_pass");
let mut input = Cursor::new(b"\n" as &[u8]);
let result = p.ask_with_input(&mut input);
assert_eq!(result, "default_pass");
}
#[test]
fn test_password_with_default_on_eof() {
let p = Prompt::new("Password")
.with_password(true)
.with_default("fallback");
let mut input = Cursor::new(b"" as &[u8]);
let result = p.ask_with_input(&mut input);
assert_eq!(result, "fallback");
}
#[test]
fn test_password_prompt_text_unchanged() {
let p1 = Prompt::new("Enter password").with_password(true);
let p2 = Prompt::new("Enter password").with_password(false);
let text1 = p1.make_prompt().plain().to_string();
let text2 = p2.make_prompt().plain().to_string();
assert_eq!(text1, text2);
}
#[test]
fn test_select_parse_single_number() {
let s = Select::new("Pick", vec!["A".into(), "B".into(), "C".into()]);
assert_eq!(s.parse_input("2"), Ok(1)); }
#[test]
fn test_select_parse_first_choice() {
let s = Select::new("Pick", vec!["A".into(), "B".into()]);
assert_eq!(s.parse_input("1"), Ok(0));
}
#[test]
fn test_select_parse_last_choice() {
let s = Select::new("Pick", vec!["A".into(), "B".into(), "C".into(), "D".into()]);
assert_eq!(s.parse_input("4"), Ok(3));
}
#[test]
fn test_select_validate_in_range() {
let s = Select::new("Pick", vec!["A".into(), "B".into(), "C".into()]);
assert!(s.parse_input("1").is_ok());
assert!(s.parse_input("2").is_ok());
assert!(s.parse_input("3").is_ok());
}
#[test]
fn test_select_validate_out_of_range_high() {
let s = Select::new("Pick", vec!["A".into(), "B".into()]);
let result = s.parse_input("3");
assert!(result.is_err());
assert!(result.unwrap_err().contains("between 1 and 2"));
}
#[test]
fn test_select_validate_out_of_range_zero() {
let s = Select::new("Pick", vec!["A".into(), "B".into()]);
let result = s.parse_input("0");
assert!(result.is_err());
}
#[test]
fn test_select_validate_not_a_number() {
let s = Select::new("Pick", vec!["A".into(), "B".into()]);
let result = s.parse_input("abc");
assert!(result.is_err());
assert!(result.unwrap_err().contains("not a valid number"));
}
#[test]
fn test_select_default_on_empty() {
let s = Select::new("Pick", vec!["A".into(), "B".into(), "C".into()]).with_default(1);
assert_eq!(s.parse_input(""), Ok(1));
}
#[test]
fn test_select_no_default_on_empty() {
let s = Select::new("Pick", vec!["A".into(), "B".into()]);
let result = s.parse_input("");
assert!(result.is_err());
assert!(result.unwrap_err().contains("enter a number"));
}
#[test]
fn test_select_whitespace_input() {
let s = Select::new("Pick", vec!["A".into(), "B".into(), "C".into()]);
assert_eq!(s.parse_input(" 2 "), Ok(1));
}
#[test]
fn test_select_empty_choices() {
let s = Select::new("Pick", vec![]);
let mut console = Console::builder().quiet(true).build();
let mut input = Cursor::new(b"1\n" as &[u8]);
let result = s.ask_with_input(&mut console, &mut input);
assert!(result.is_err());
assert_eq!(result.unwrap_err().message, "No choices provided");
}
#[test]
fn test_select_format_choices() {
let s = Select::new(
"Select a color",
vec!["Red".into(), "Green".into(), "Blue".into()],
);
let output = s.format_choices();
assert!(output.contains("? Select a color:"));
assert!(output.contains(" 1) Red"));
assert!(output.contains(" 2) Green"));
assert!(output.contains(" 3) Blue"));
}
#[test]
fn test_select_format_input_prompt_no_default() {
let s = Select::new("Pick", vec!["A".into(), "B".into(), "C".into()]);
let prompt = s.format_input_prompt();
assert_eq!(prompt, "Enter choice [1-3]: ");
}
#[test]
fn test_select_format_input_prompt_with_default() {
let s = Select::new("Pick", vec!["A".into(), "B".into(), "C".into()]).with_default(1);
let prompt = s.format_input_prompt();
assert_eq!(prompt, "Enter choice [1-3] (2): ");
}
#[test]
fn test_select_ask_with_input_valid() {
let s = Select::new("Pick", vec!["A".into(), "B".into(), "C".into()]);
let mut console = Console::builder().quiet(true).build();
let mut input = Cursor::new(b"2\n" as &[u8]);
let result = s.ask_with_input(&mut console, &mut input);
assert_eq!(result.unwrap(), 1);
}
#[test]
fn test_select_ask_with_input_invalid_then_valid() {
let s = Select::new("Pick", vec!["A".into(), "B".into()]);
let mut console = Console::builder().quiet(true).build();
let mut input = Cursor::new(b"5\n1\n" as &[u8]);
let result = s.ask_with_input(&mut console, &mut input);
assert_eq!(result.unwrap(), 0);
}
#[test]
fn test_select_ask_value_with_input() {
let s = Select::new("Pick", vec!["Red".into(), "Green".into(), "Blue".into()]);
let mut console = Console::builder().quiet(true).build();
let mut input = Cursor::new(b"3\n" as &[u8]);
let result = s.ask_value_with_input(&mut console, &mut input);
assert_eq!(result.unwrap(), "Blue");
}
#[test]
fn test_select_default_on_eof() {
let s = Select::new("Pick", vec!["A".into(), "B".into()]).with_default(0);
let mut console = Console::builder().quiet(true).build();
let mut input = Cursor::new(b"" as &[u8]);
let result = s.ask_with_input(&mut console, &mut input);
assert_eq!(result.unwrap(), 0);
}
#[test]
fn test_select_builder_with_default() {
let s = Select::new("Pick", vec!["A".into()]).with_default(0);
assert_eq!(s.default, Some(0));
}
#[test]
fn test_select_builder_with_style() {
let style = Style::parse("red bold").unwrap();
let s = Select::new("Pick", vec!["A".into()]).with_style(style);
assert_eq!(s.style.bold(), Some(true));
}
#[test]
fn test_select_builder_with_highlight_style() {
let style = Style::parse("green").unwrap();
let s = Select::new("Pick", vec!["A".into()]).with_highlight_style(style);
assert!(s.highlight_style.color().is_some());
}
#[test]
fn test_multiselect_parse_comma_separated() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into(), "D".into()]);
assert_eq!(ms.parse_input("1,3"), Ok(vec![0, 2]));
}
#[test]
fn test_multiselect_parse_single() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into()]);
assert_eq!(ms.parse_input("2"), Ok(vec![1]));
}
#[test]
fn test_multiselect_parse_all() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into()]);
assert_eq!(ms.parse_input("all"), Ok(vec![0, 1, 2]));
}
#[test]
fn test_multiselect_parse_all_case_insensitive() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into()]);
assert_eq!(ms.parse_input("ALL"), Ok(vec![0, 1]));
assert_eq!(ms.parse_input("All"), Ok(vec![0, 1]));
}
#[test]
fn test_multiselect_whitespace_input() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into()]);
assert_eq!(ms.parse_input(" 1 , 3 "), Ok(vec![0, 2]));
}
#[test]
fn test_multiselect_trailing_comma() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into()]);
assert_eq!(ms.parse_input("1,2,"), Ok(vec![0, 1]));
}
#[test]
fn test_multiselect_default_on_empty() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into()])
.with_defaults(vec![0, 2]);
assert_eq!(ms.parse_input(""), Ok(vec![0, 2]));
}
#[test]
fn test_multiselect_min_validation() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into()]).with_min(2);
let result = ms.parse_input("1");
assert!(result.is_err());
assert!(result.unwrap_err().contains("at least 2"));
}
#[test]
fn test_multiselect_min_satisfied() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into()]).with_min(2);
assert_eq!(ms.parse_input("1,2"), Ok(vec![0, 1]));
}
#[test]
fn test_multiselect_max_validation() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into()]).with_max(1);
let result = ms.parse_input("1,2");
assert!(result.is_err());
assert!(result.unwrap_err().contains("at most 1"));
}
#[test]
fn test_multiselect_max_satisfied() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into()]).with_max(2);
assert_eq!(ms.parse_input("1,3"), Ok(vec![0, 2]));
}
#[test]
fn test_multiselect_min_max_combined() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into(), "D".into()])
.with_min(1)
.with_max(3);
assert!(ms.parse_input("").is_err()); assert!(ms.parse_input("1").is_ok()); assert!(ms.parse_input("1,2,3").is_ok()); assert!(ms.parse_input("1,2,3,4").is_err()); }
#[test]
fn test_multiselect_empty_choices() {
let ms = MultiSelect::new("Pick", vec![]);
let mut console = Console::builder().quiet(true).build();
let mut input = Cursor::new(b"1\n" as &[u8]);
let result = ms.ask_with_input(&mut console, &mut input);
assert!(result.is_err());
assert_eq!(result.unwrap_err().message, "No choices provided");
}
#[test]
fn test_multiselect_out_of_range() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into()]);
let result = ms.parse_input("3");
assert!(result.is_err());
assert!(result.unwrap_err().contains("out of range"));
}
#[test]
fn test_multiselect_zero() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into()]);
let result = ms.parse_input("0");
assert!(result.is_err());
}
#[test]
fn test_multiselect_format_choices() {
let ms = MultiSelect::new(
"Select colors",
vec!["Red".into(), "Green".into(), "Blue".into()],
);
let output = ms.format_choices();
assert!(output.contains("? Select colors (comma-separated):"));
assert!(output.contains(" 1) Red"));
assert!(output.contains(" 2) Green"));
assert!(output.contains(" 3) Blue"));
}
#[test]
fn test_multiselect_format_input_prompt_no_defaults() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into()]);
let prompt = ms.format_input_prompt();
assert_eq!(prompt, "Enter choices [1-3, e.g. 1,3]: ");
}
#[test]
fn test_multiselect_format_input_prompt_with_defaults() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into()])
.with_defaults(vec![0, 2]);
let prompt = ms.format_input_prompt();
assert_eq!(prompt, "Enter choices [1-3, e.g. 1,3] (1,3): ");
}
#[test]
fn test_multiselect_ask_with_input_valid() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into()]);
let mut console = Console::builder().quiet(true).build();
let mut input = Cursor::new(b"1,3\n" as &[u8]);
let result = ms.ask_with_input(&mut console, &mut input);
assert_eq!(result.unwrap(), vec![0, 2]);
}
#[test]
fn test_multiselect_ask_with_input_invalid_then_valid() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into()]).with_min(1);
let mut console = Console::builder().quiet(true).build();
let mut input = Cursor::new(b"\n2\n" as &[u8]);
let result = ms.ask_with_input(&mut console, &mut input);
assert_eq!(result.unwrap(), vec![1]);
}
#[test]
fn test_multiselect_ask_values_with_input() {
let ms = MultiSelect::new("Pick", vec!["Red".into(), "Green".into(), "Blue".into()]);
let mut console = Console::builder().quiet(true).build();
let mut input = Cursor::new(b"1,3\n" as &[u8]);
let result = ms.ask_values_with_input(&mut console, &mut input);
assert_eq!(result.unwrap(), vec!["Red".to_string(), "Blue".to_string()]);
}
#[test]
fn test_multiselect_deduplicate() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into()]);
assert_eq!(ms.parse_input("1,1,2"), Ok(vec![0, 1]));
}
#[test]
fn test_multiselect_empty_input_min_zero() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into()]);
assert_eq!(ms.parse_input(""), Ok(vec![]));
}
#[test]
fn test_multiselect_builder_with_defaults() {
let ms = MultiSelect::new("Pick", vec!["A".into()]).with_defaults(vec![0]);
assert_eq!(ms.defaults, vec![0]);
}
#[test]
fn test_multiselect_builder_with_min() {
let ms = MultiSelect::new("Pick", vec!["A".into()]).with_min(1);
assert_eq!(ms.min_selections, 1);
}
#[test]
fn test_multiselect_builder_with_max() {
let ms = MultiSelect::new("Pick", vec!["A".into()]).with_max(3);
assert_eq!(ms.max_selections, Some(3));
}
#[test]
fn test_multiselect_builder_with_style() {
let style = Style::parse("red bold").unwrap();
let ms = MultiSelect::new("Pick", vec!["A".into()]).with_style(style);
assert_eq!(ms.style.bold(), Some(true));
}
#[test]
fn test_multiselect_builder_with_highlight_style() {
let style = Style::parse("green").unwrap();
let ms = MultiSelect::new("Pick", vec!["A".into()]).with_highlight_style(style);
assert!(ms.highlight_style.color().is_some());
}
#[test]
fn test_select_display_via_capture() {
let s = Select::new(
"Pick a fruit",
vec!["Apple".into(), "Banana".into(), "Cherry".into()],
);
let mut console = Console::builder().width(80).force_terminal(true).build();
console.begin_capture();
console.print_text(&s.format_choices());
let captured = console.end_capture();
assert!(captured.contains("? Pick a fruit:"));
assert!(captured.contains("1) Apple"));
assert!(captured.contains("2) Banana"));
assert!(captured.contains("3) Cherry"));
}
#[test]
fn test_multiselect_display_via_capture() {
let ms = MultiSelect::new(
"Pick colors",
vec!["Red".into(), "Green".into(), "Blue".into(), "Yellow".into()],
);
let mut console = Console::builder().width(80).force_terminal(true).build();
console.begin_capture();
console.print_text(&ms.format_choices());
let captured = console.end_capture();
assert!(captured.contains("? Pick colors (comma-separated):"));
assert!(captured.contains("1) Red"));
assert!(captured.contains("2) Green"));
assert!(captured.contains("3) Blue"));
assert!(captured.contains("4) Yellow"));
}
#[test]
fn test_multiselect_all_with_max() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into(), "C".into()]).with_max(2);
let result = ms.parse_input("all");
assert!(result.is_err());
assert!(result.unwrap_err().contains("at most 2"));
}
#[test]
fn test_select_negative_number() {
let s = Select::new("Pick", vec!["A".into(), "B".into()]);
let result = s.parse_input("-1");
assert!(result.is_err());
}
#[test]
fn test_multiselect_invalid_number_in_list() {
let ms = MultiSelect::new("Pick", vec!["A".into(), "B".into()]);
let result = ms.parse_input("1,abc");
assert!(result.is_err());
assert!(result.unwrap_err().contains("not a valid number"));
}
}