use crate::errors::Result;
use crate::style::Style;
use std::io::{self, BufRead, Write};
#[derive(Debug, Clone)]
pub struct Prompt {
message: String,
default: Option<String>,
choices: Option<Vec<String>>,
case_sensitive: bool,
show_default: bool,
show_choices: bool,
password: bool,
prompt_style: Option<Style>,
}
impl Prompt {
#[must_use]
#[inline]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
default: None,
choices: None,
case_sensitive: true,
show_default: true,
show_choices: true,
password: false,
prompt_style: None,
}
}
#[must_use]
#[inline]
pub fn default(mut self, default: impl Into<String>) -> Self {
self.default = Some(default.into());
self
}
#[must_use]
pub fn choices<I, S>(mut self, choices: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.choices = Some(choices.into_iter().map(Into::into).collect());
self
}
#[must_use]
#[inline]
pub const fn case_sensitive(mut self, sensitive: bool) -> Self {
self.case_sensitive = sensitive;
self
}
#[must_use]
#[inline]
pub const fn show_default(mut self, show: bool) -> Self {
self.show_default = show;
self
}
#[must_use]
#[inline]
pub const fn show_choices(mut self, show: bool) -> Self {
self.show_choices = show;
self
}
#[must_use]
#[inline]
pub const fn password(mut self, is_password: bool) -> Self {
self.password = is_password;
self
}
#[must_use]
#[inline]
pub fn style(mut self, style: Style) -> Self {
self.prompt_style = Some(style);
self
}
fn build_prompt(&self) -> String {
let mut prompt = self.message.clone();
if self.show_choices {
if let Some(ref choices) = self.choices {
prompt.push_str(&format!(" [{}]", choices.join("/")));
}
}
if self.show_default {
if let Some(ref default) = self.default {
prompt.push_str(&format!(" ({})", default));
}
}
prompt.push_str(": ");
prompt
}
fn validate(&self, input: &str) -> bool {
if let Some(ref choices) = self.choices {
if self.case_sensitive {
choices.iter().any(|c| c == input)
} else {
let lower = input.to_lowercase();
choices.iter().any(|c| c.to_lowercase() == lower)
}
} else {
true
}
}
pub fn ask(&self) -> Result<String> {
let prompt = self.build_prompt();
let stdin = io::stdin();
let mut stdout = io::stdout();
loop {
if let Some(ref style) = self.prompt_style {
eprint!("{}", style.to_ansi());
}
eprint!("{}", prompt);
if self.prompt_style.is_some() {
eprint!("\x1b[0m");
}
stdout.flush()?;
let mut input = String::new();
stdin.lock().read_line(&mut input)?;
let input = input.trim().to_string();
if input.is_empty() {
if let Some(ref default) = self.default {
return Ok(default.clone());
}
}
if self.validate(&input) {
return Ok(input);
}
eprintln!(
"Invalid choice. Please select from: {}",
self.choices
.as_ref()
.map(|c| c.join(", "))
.unwrap_or_default()
);
}
}
}
#[derive(Debug, Clone)]
pub struct Confirm {
message: String,
default: Option<bool>,
prompt_style: Option<Style>,
}
impl Confirm {
#[must_use]
#[inline]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
default: None,
prompt_style: None,
}
}
#[must_use]
#[inline]
pub const fn default(mut self, default: bool) -> Self {
self.default = Some(default);
self
}
#[must_use]
#[inline]
pub fn style(mut self, style: Style) -> Self {
self.prompt_style = Some(style);
self
}
pub fn ask(&self) -> Result<bool> {
let choices = match self.default {
Some(true) => "[Y/n]",
Some(false) => "[y/N]",
None => "[y/n]",
};
let prompt = format!("{} {}: ", self.message, choices);
let stdin = io::stdin();
let mut stdout = io::stdout();
loop {
if let Some(ref style) = self.prompt_style {
eprint!("{}", style.to_ansi());
}
eprint!("{}", prompt);
if self.prompt_style.is_some() {
eprint!("\x1b[0m");
}
stdout.flush()?;
let mut input = String::new();
stdin.lock().read_line(&mut input)?;
let input = input.trim().to_lowercase();
if input.is_empty() {
if let Some(default) = self.default {
return Ok(default);
}
eprintln!("Please enter y or n");
continue;
}
match input.as_str() {
"y" | "yes" | "true" | "1" => return Ok(true),
"n" | "no" | "false" | "0" => return Ok(false),
_ => {
eprintln!("Please enter y or n");
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct IntPrompt {
message: String,
default: Option<i64>,
min: Option<i64>,
max: Option<i64>,
prompt_style: Option<Style>,
}
impl IntPrompt {
#[must_use]
#[inline]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
default: None,
min: None,
max: None,
prompt_style: None,
}
}
#[must_use]
#[inline]
pub const fn default(mut self, default: i64) -> Self {
self.default = Some(default);
self
}
#[must_use]
#[inline]
pub const fn min(mut self, min: i64) -> Self {
self.min = Some(min);
self
}
#[must_use]
#[inline]
pub const fn max(mut self, max: i64) -> Self {
self.max = Some(max);
self
}
#[must_use]
#[inline]
pub fn style(mut self, style: Style) -> Self {
self.prompt_style = Some(style);
self
}
pub fn ask(&self) -> Result<i64> {
let mut prompt = self.message.clone();
if let Some(default) = self.default {
prompt.push_str(&format!(" ({})", default));
}
prompt.push_str(": ");
let stdin = io::stdin();
let mut stdout = io::stdout();
loop {
if let Some(ref style) = self.prompt_style {
eprint!("{}", style.to_ansi());
}
eprint!("{}", prompt);
if self.prompt_style.is_some() {
eprint!("\x1b[0m");
}
stdout.flush()?;
let mut input = String::new();
stdin.lock().read_line(&mut input)?;
let input = input.trim();
if input.is_empty() {
if let Some(default) = self.default {
return Ok(default);
}
eprintln!("Please enter a number");
continue;
}
match input.parse::<i64>() {
Ok(n) => {
if let Some(min) = self.min {
if n < min {
eprintln!("Value must be at least {}", min);
continue;
}
}
if let Some(max) = self.max {
if n > max {
eprintln!("Value must be at most {}", max);
continue;
}
}
return Ok(n);
}
Err(_) => {
eprintln!("Please enter a valid integer");
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct FloatPrompt {
message: String,
default: Option<f64>,
min: Option<f64>,
max: Option<f64>,
prompt_style: Option<Style>,
}
impl FloatPrompt {
#[must_use]
#[inline]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
default: None,
min: None,
max: None,
prompt_style: None,
}
}
#[must_use]
#[inline]
pub fn default(mut self, default: f64) -> Self {
self.default = Some(default);
self
}
#[must_use]
#[inline]
pub fn min(mut self, min: f64) -> Self {
self.min = Some(min);
self
}
#[must_use]
#[inline]
pub fn max(mut self, max: f64) -> Self {
self.max = Some(max);
self
}
#[must_use]
#[inline]
pub fn style(mut self, style: Style) -> Self {
self.prompt_style = Some(style);
self
}
pub fn ask(&self) -> Result<f64> {
let mut prompt = self.message.clone();
if let Some(default) = self.default {
prompt.push_str(&format!(" ({})", default));
}
prompt.push_str(": ");
let stdin = io::stdin();
let mut stdout = io::stdout();
loop {
if let Some(ref style) = self.prompt_style {
eprint!("{}", style.to_ansi());
}
eprint!("{}", prompt);
if self.prompt_style.is_some() {
eprint!("\x1b[0m");
}
stdout.flush()?;
let mut input = String::new();
stdin.lock().read_line(&mut input)?;
let input = input.trim();
if input.is_empty() {
if let Some(default) = self.default {
return Ok(default);
}
eprintln!("Please enter a number");
continue;
}
match input.parse::<f64>() {
Ok(n) => {
if let Some(min) = self.min {
if n < min {
eprintln!("Value must be at least {}", min);
continue;
}
}
if let Some(max) = self.max {
if n > max {
eprintln!("Value must be at most {}", max);
continue;
}
}
return Ok(n);
}
Err(_) => {
eprintln!("Please enter a valid number");
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prompt_new() {
let prompt = Prompt::new("Test?");
assert_eq!(prompt.message, "Test?");
assert!(prompt.default.is_none());
assert!(prompt.choices.is_none());
assert!(prompt.case_sensitive);
assert!(prompt.show_default);
assert!(prompt.show_choices);
assert!(!prompt.password);
assert!(prompt.prompt_style.is_none());
}
#[test]
fn test_prompt_default() {
let prompt = Prompt::new("Test?").default("foo");
assert_eq!(prompt.default, Some("foo".to_string()));
}
#[test]
fn test_prompt_choices() {
let prompt = Prompt::new("Test?").choices(["a", "b", "c"]);
assert!(prompt.choices.is_some());
assert_eq!(prompt.choices.as_ref().map(Vec::len), Some(3));
}
#[test]
fn test_prompt_case_sensitive() {
let prompt = Prompt::new("Test?").case_sensitive(false);
assert!(!prompt.case_sensitive);
}
#[test]
fn test_prompt_show_default() {
let prompt = Prompt::new("Test?").show_default(false);
assert!(!prompt.show_default);
}
#[test]
fn test_prompt_show_choices() {
let prompt = Prompt::new("Test?").show_choices(false);
assert!(!prompt.show_choices);
}
#[test]
fn test_prompt_password() {
let prompt = Prompt::new("Password?").password(true);
assert!(prompt.password);
}
#[test]
fn test_prompt_style() {
let prompt = Prompt::new("Test?").style(Style::new().bold());
assert!(prompt.prompt_style.is_some());
}
#[test]
fn test_prompt_validate_with_choices() {
let prompt = Prompt::new("Test?").choices(["yes", "no"]);
assert!(prompt.validate("yes"));
assert!(prompt.validate("no"));
assert!(!prompt.validate("maybe"));
assert!(!prompt.validate("YES")); }
#[test]
fn test_prompt_validate_case_insensitive() {
let prompt = Prompt::new("Test?")
.choices(["yes", "no"])
.case_sensitive(false);
assert!(prompt.validate("YES"));
assert!(prompt.validate("Yes"));
assert!(prompt.validate("yes"));
assert!(prompt.validate("NO"));
assert!(!prompt.validate("maybe"));
}
#[test]
fn test_prompt_validate_no_choices() {
let prompt = Prompt::new("Test?");
assert!(prompt.validate("anything"));
assert!(prompt.validate(""));
}
#[test]
fn test_prompt_build_prompt() {
let prompt = Prompt::new("Choose").choices(["a", "b"]).default("a");
let built = prompt.build_prompt();
assert!(built.contains("Choose"));
assert!(built.contains("[a/b]"));
assert!(built.contains("(a)"));
assert!(built.ends_with(": "));
}
#[test]
fn test_prompt_build_prompt_no_choices() {
let prompt = Prompt::new("Name").default("John");
let built = prompt.build_prompt();
assert!(built.contains("Name"));
assert!(built.contains("(John)"));
assert!(!built.contains("["));
}
#[test]
fn test_prompt_build_prompt_hide_default() {
let prompt = Prompt::new("Name").default("John").show_default(false);
let built = prompt.build_prompt();
assert!(built.contains("Name"));
assert!(!built.contains("(John)"));
}
#[test]
fn test_prompt_build_prompt_hide_choices() {
let prompt = Prompt::new("Choose")
.choices(["a", "b"])
.show_choices(false);
let built = prompt.build_prompt();
assert!(built.contains("Choose"));
assert!(!built.contains("[a/b]"));
}
#[test]
fn test_confirm_new() {
let confirm = Confirm::new("Sure?");
assert_eq!(confirm.message, "Sure?");
assert!(confirm.default.is_none());
assert!(confirm.prompt_style.is_none());
}
#[test]
fn test_confirm_default_true() {
let confirm = Confirm::new("Sure?").default(true);
assert_eq!(confirm.default, Some(true));
}
#[test]
fn test_confirm_default_false() {
let confirm = Confirm::new("Sure?").default(false);
assert_eq!(confirm.default, Some(false));
}
#[test]
fn test_confirm_style() {
let confirm = Confirm::new("Sure?").style(Style::new().bold());
assert!(confirm.prompt_style.is_some());
}
#[test]
fn test_int_prompt_new() {
let prompt = IntPrompt::new("Number?");
assert_eq!(prompt.message, "Number?");
assert!(prompt.default.is_none());
assert!(prompt.min.is_none());
assert!(prompt.max.is_none());
assert!(prompt.prompt_style.is_none());
}
#[test]
fn test_int_prompt_default() {
let prompt = IntPrompt::new("Number?").default(42);
assert_eq!(prompt.default, Some(42));
}
#[test]
fn test_int_prompt_range() {
let prompt = IntPrompt::new("Number?").min(0).max(100);
assert_eq!(prompt.min, Some(0));
assert_eq!(prompt.max, Some(100));
}
#[test]
fn test_int_prompt_style() {
let prompt = IntPrompt::new("Number?").style(Style::new().bold());
assert!(prompt.prompt_style.is_some());
}
#[test]
fn test_float_prompt_new() {
let prompt = FloatPrompt::new("Value?");
assert_eq!(prompt.message, "Value?");
assert!(prompt.default.is_none());
assert!(prompt.min.is_none());
assert!(prompt.max.is_none());
assert!(prompt.prompt_style.is_none());
}
#[test]
fn test_float_prompt_default() {
let prompt = FloatPrompt::new("Value?").default(42.5);
assert_eq!(prompt.default, Some(42.5));
}
#[test]
fn test_float_prompt_range() {
let prompt = FloatPrompt::new("Value?").min(0.0).max(1.0);
assert_eq!(prompt.min, Some(0.0));
assert_eq!(prompt.max, Some(1.0));
}
#[test]
fn test_float_prompt_style() {
let prompt = FloatPrompt::new("Value?").style(Style::new().bold());
assert!(prompt.prompt_style.is_some());
}
}