use std::io::{self, BufRead};
use crate::Console;
use crate::text::Text;
#[derive(Debug, Clone)]
pub enum PromptError {
Io(String),
InvalidResponse(InvalidResponse),
Interrupted,
}
impl std::fmt::Display for PromptError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PromptError::Io(msg) => write!(f, "I/O error: {}", msg),
PromptError::InvalidResponse(err) => write!(f, "{}", err.message),
PromptError::Interrupted => write!(f, "Input interrupted"),
}
}
}
impl std::error::Error for PromptError {}
impl From<io::Error> for PromptError {
fn from(err: io::Error) -> Self {
match err.kind() {
io::ErrorKind::UnexpectedEof | io::ErrorKind::Interrupted => PromptError::Interrupted,
_ => PromptError::Io(err.to_string()),
}
}
}
impl From<InvalidResponse> for PromptError {
fn from(err: InvalidResponse) -> Self {
PromptError::InvalidResponse(err)
}
}
#[derive(Debug, Clone)]
pub struct InvalidResponse {
pub message: String,
}
impl InvalidResponse {
pub fn new(message: impl Into<String>) -> Self {
InvalidResponse {
message: message.into(),
}
}
}
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 type Result<T> = std::result::Result<T, PromptError>;
pub trait PromptBase<T> {
const VALIDATE_ERROR_MESSAGE: &'static str = "[prompt.invalid]Please enter a valid value";
const ILLEGAL_CHOICE_MESSAGE: &'static str =
"[prompt.invalid.choice]Please select one of the available options";
const PROMPT_SUFFIX: &'static str = ": ";
fn process_response(&self, value: &str) -> std::result::Result<T, InvalidResponse>;
fn render_default(&self, default: &T) -> String;
}
pub struct Prompt {
prompt: String,
default: Option<String>,
choices: Option<Vec<String>>,
case_sensitive: bool,
show_default: bool,
show_choices: bool,
password: bool,
stream: Option<Box<dyn BufRead + Send>>,
pre_prompt: Option<Box<dyn Fn() + Send + Sync>>,
}
impl Default for Prompt {
fn default() -> Self {
Self::new("")
}
}
impl Prompt {
pub fn new(prompt: impl Into<String>) -> Self {
Prompt {
prompt: prompt.into(),
default: None,
choices: None,
case_sensitive: true,
show_default: true,
show_choices: true,
password: false,
stream: None,
pre_prompt: None,
}
}
pub fn with_default(mut self, default: impl Into<String>) -> Self {
self.default = Some(default.into());
self
}
pub fn with_choices(mut self, choices: &[&str]) -> Self {
self.choices = Some(choices.iter().map(|s| s.to_string()).collect());
self
}
pub fn case_sensitive(mut self, sensitive: bool) -> Self {
self.case_sensitive = sensitive;
self
}
pub fn show_default(mut self, show: bool) -> Self {
self.show_default = show;
self
}
pub fn show_choices(mut self, show: bool) -> Self {
self.show_choices = show;
self
}
pub fn password(mut self, is_password: bool) -> Self {
self.password = is_password;
self
}
pub fn with_stream(mut self, stream: impl BufRead + Send + 'static) -> Self {
self.stream = Some(Box::new(stream));
self
}
pub fn with_pre_prompt(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
self.pre_prompt = Some(Box::new(f));
self
}
pub fn ask(prompt: impl Into<String>) -> Result<String> {
Prompt::new(prompt).run()
}
pub fn has_stream(&self) -> bool {
self.stream.is_some()
}
pub fn run(&mut self) -> Result<String> {
let mut console = Console::new();
self.run_with_console(&mut console)
}
pub fn run_with_console(&mut self, console: &mut Console) -> Result<String> {
loop {
if let Some(ref pre_prompt) = self.pre_prompt {
pre_prompt();
}
let prompt_text = self.make_prompt();
let value = if let Some(ref mut stream) = self.stream {
let _ = console.print(&prompt_text, None, None, None, false, "");
let mut line = String::new();
stream.read_line(&mut line).map_err(PromptError::from)?;
if line.is_empty() {
return Err(PromptError::Interrupted);
}
line.trim_end_matches('\n')
.trim_end_matches('\r')
.to_string()
} else {
console.input(&prompt_text, self.password)?
};
if value.is_empty() {
if let Some(ref default) = self.default {
return Ok(default.clone());
}
}
match self.process_response(&value) {
Ok(result) => return Ok(result),
Err(err) => {
let error_text = Text::from_markup(&err.message, false)
.unwrap_or_else(|_| Text::plain(&err.message));
let _ = console.print(&error_text, None, None, None, false, "\n");
}
}
}
}
fn make_prompt(&self) -> Text {
let mut parts = vec![self.prompt.clone()];
if self.show_choices {
if let Some(ref choices) = self.choices {
let choices_str = choices.join("/");
parts.push(format!(" [prompt.choices]\\[{}][/]", choices_str));
}
}
if self.show_default {
if let Some(ref default) = self.default {
parts.push(format!(" [prompt.default]({})[/]", default));
}
}
parts.push(": ".to_string());
let markup = parts.join("");
Text::from_markup(&markup, false).unwrap_or_else(|_| Text::plain(&markup))
}
fn check_choice(&self, value: &str) -> bool {
if let Some(ref choices) = self.choices {
if self.case_sensitive {
choices.iter().any(|c| c == value)
} else {
let value_lower = value.to_lowercase();
choices.iter().any(|c| c.to_lowercase() == value_lower)
}
} else {
true
}
}
fn get_original_choice(&self, value: &str) -> String {
if let Some(ref choices) = self.choices {
if !self.case_sensitive {
let value_lower = value.to_lowercase();
for choice in choices {
if choice.to_lowercase() == value_lower {
return choice.clone();
}
}
}
}
value.to_string()
}
fn process_response(&self, value: &str) -> std::result::Result<String, InvalidResponse> {
let value = value.trim();
if self.choices.is_some() && !self.check_choice(value) {
return Err(InvalidResponse::new(
"[prompt.invalid.choice]Please select one of the available options",
));
}
if !self.case_sensitive && self.choices.is_some() {
Ok(self.get_original_choice(value))
} else {
Ok(value.to_string())
}
}
}
pub struct IntPrompt {
prompt: String,
default: Option<i64>,
show_default: bool,
choices: Option<Vec<i64>>,
stream: Option<Box<dyn BufRead + Send>>,
pre_prompt: Option<Box<dyn Fn() + Send + Sync>>,
}
impl Default for IntPrompt {
fn default() -> Self {
Self::new("")
}
}
impl IntPrompt {
pub fn new(prompt: impl Into<String>) -> Self {
IntPrompt {
prompt: prompt.into(),
default: None,
show_default: true,
choices: None,
stream: None,
pre_prompt: None,
}
}
pub fn with_default(mut self, default: i64) -> Self {
self.default = Some(default);
self
}
pub fn show_default(mut self, show: bool) -> Self {
self.show_default = show;
self
}
pub fn with_choices(mut self, choices: Vec<i64>) -> Self {
self.choices = Some(choices);
self
}
pub fn with_stream(mut self, stream: impl BufRead + Send + 'static) -> Self {
self.stream = Some(Box::new(stream));
self
}
pub fn with_pre_prompt(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
self.pre_prompt = Some(Box::new(f));
self
}
pub fn ask(prompt: impl Into<String>) -> Result<i64> {
IntPrompt::new(prompt).run()
}
pub fn run(&mut self) -> Result<i64> {
let mut console = Console::new();
self.run_with_console(&mut console)
}
pub fn run_with_console(&mut self, console: &mut Console) -> Result<i64> {
loop {
if let Some(ref pre_prompt) = self.pre_prompt {
pre_prompt();
}
let prompt_text = self.make_prompt();
let value = if let Some(ref mut stream) = self.stream {
let _ = console.print(&prompt_text, None, None, None, false, "");
let mut line = String::new();
stream.read_line(&mut line).map_err(PromptError::from)?;
if line.is_empty() {
return Err(PromptError::Interrupted);
}
line.trim_end_matches('\n')
.trim_end_matches('\r')
.to_string()
} else {
console.input(&prompt_text, false)?
};
if value.is_empty() {
if let Some(default) = self.default {
return Ok(default);
}
}
match self.process_response(&value) {
Ok(result) => return Ok(result),
Err(err) => {
let error_text = Text::from_markup(&err.message, false)
.unwrap_or_else(|_| Text::plain(&err.message));
let _ = console.print(&error_text, None, None, None, false, "\n");
}
}
}
}
fn make_prompt(&self) -> Text {
let mut parts = vec![self.prompt.clone()];
if let Some(ref choices) = self.choices {
let choices_str: Vec<String> = choices.iter().map(|c| c.to_string()).collect();
parts.push(format!(" [prompt.choices]\\[{}][/]", choices_str.join("/")));
}
if self.show_default {
if let Some(default) = self.default {
parts.push(format!(" [prompt.default]({})[/]", default));
}
}
parts.push(": ".to_string());
let markup = parts.join("");
Text::from_markup(&markup, false).unwrap_or_else(|_| Text::plain(&markup))
}
fn process_response(&self, value: &str) -> std::result::Result<i64, InvalidResponse> {
let value = value.trim();
let parsed = value.parse::<i64>().map_err(|_| {
InvalidResponse::new("[prompt.invalid]Please enter a valid integer number")
})?;
if let Some(ref choices) = self.choices {
if !choices.contains(&parsed) {
return Err(InvalidResponse::new(
"[prompt.invalid.choice]Please select one of the available options",
));
}
}
Ok(parsed)
}
}
pub struct FloatPrompt {
prompt: String,
default: Option<f64>,
show_default: bool,
choices: Option<Vec<f64>>,
stream: Option<Box<dyn BufRead + Send>>,
pre_prompt: Option<Box<dyn Fn() + Send + Sync>>,
}
impl Default for FloatPrompt {
fn default() -> Self {
Self::new("")
}
}
impl FloatPrompt {
pub fn new(prompt: impl Into<String>) -> Self {
FloatPrompt {
prompt: prompt.into(),
default: None,
show_default: true,
choices: None,
stream: None,
pre_prompt: None,
}
}
pub fn with_default(mut self, default: f64) -> Self {
self.default = Some(default);
self
}
pub fn show_default(mut self, show: bool) -> Self {
self.show_default = show;
self
}
pub fn with_choices(mut self, choices: Vec<f64>) -> Self {
self.choices = Some(choices);
self
}
pub fn with_stream(mut self, stream: impl BufRead + Send + 'static) -> Self {
self.stream = Some(Box::new(stream));
self
}
pub fn with_pre_prompt(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
self.pre_prompt = Some(Box::new(f));
self
}
pub fn ask(prompt: impl Into<String>) -> Result<f64> {
FloatPrompt::new(prompt).run()
}
pub fn run(&mut self) -> Result<f64> {
let mut console = Console::new();
self.run_with_console(&mut console)
}
pub fn run_with_console(&mut self, console: &mut Console) -> Result<f64> {
loop {
if let Some(ref pre_prompt) = self.pre_prompt {
pre_prompt();
}
let prompt_text = self.make_prompt();
let value = if let Some(ref mut stream) = self.stream {
let _ = console.print(&prompt_text, None, None, None, false, "");
let mut line = String::new();
stream.read_line(&mut line).map_err(PromptError::from)?;
if line.is_empty() {
return Err(PromptError::Interrupted);
}
line.trim_end_matches('\n')
.trim_end_matches('\r')
.to_string()
} else {
console.input(&prompt_text, false)?
};
if value.is_empty() {
if let Some(default) = self.default {
return Ok(default);
}
}
match self.process_response(&value) {
Ok(result) => return Ok(result),
Err(err) => {
let error_text = Text::from_markup(&err.message, false)
.unwrap_or_else(|_| Text::plain(&err.message));
let _ = console.print(&error_text, None, None, None, false, "\n");
}
}
}
}
fn make_prompt(&self) -> Text {
let mut parts = vec![self.prompt.clone()];
if let Some(ref choices) = self.choices {
let choices_str: Vec<String> = choices.iter().map(|c| c.to_string()).collect();
parts.push(format!(" [prompt.choices]\\[{}][/]", choices_str.join("/")));
}
if self.show_default {
if let Some(default) = self.default {
parts.push(format!(" [prompt.default]({})[/]", default));
}
}
parts.push(": ".to_string());
let markup = parts.join("");
Text::from_markup(&markup, false).unwrap_or_else(|_| Text::plain(&markup))
}
fn process_response(&self, value: &str) -> std::result::Result<f64, InvalidResponse> {
let value = value.trim();
let parsed = value
.parse::<f64>()
.map_err(|_| InvalidResponse::new("[prompt.invalid]Please enter a number"))?;
if let Some(ref choices) = self.choices {
if !choices.iter().any(|c| (*c - parsed).abs() < f64::EPSILON) {
return Err(InvalidResponse::new(
"[prompt.invalid.choice]Please select one of the available options",
));
}
}
Ok(parsed)
}
}
pub struct Confirm {
prompt: String,
default: Option<bool>,
show_default: bool,
yes_choice: String,
no_choice: String,
stream: Option<Box<dyn BufRead + Send>>,
pre_prompt: Option<Box<dyn Fn() + Send + Sync>>,
}
impl Default for Confirm {
fn default() -> Self {
Self::new("")
}
}
impl Confirm {
pub fn new(prompt: impl Into<String>) -> Self {
Confirm {
prompt: prompt.into(),
default: None,
show_default: true,
yes_choice: "y".to_string(),
no_choice: "n".to_string(),
stream: None,
pre_prompt: None,
}
}
pub fn with_default(mut self, default: bool) -> Self {
self.default = Some(default);
self
}
pub fn show_default(mut self, show: bool) -> Self {
self.show_default = show;
self
}
pub fn with_choices(mut self, yes: impl Into<String>, no: impl Into<String>) -> Self {
self.yes_choice = yes.into();
self.no_choice = no.into();
self
}
pub fn with_stream(mut self, stream: impl BufRead + Send + 'static) -> Self {
self.stream = Some(Box::new(stream));
self
}
pub fn with_pre_prompt(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
self.pre_prompt = Some(Box::new(f));
self
}
pub fn ask(prompt: impl Into<String>) -> Result<bool> {
Confirm::new(prompt).run()
}
pub fn run(&mut self) -> Result<bool> {
let mut console = Console::new();
self.run_with_console(&mut console)
}
pub fn run_with_console(&mut self, console: &mut Console) -> Result<bool> {
loop {
if let Some(ref pre_prompt) = self.pre_prompt {
pre_prompt();
}
let prompt_text = self.make_prompt();
let value = if let Some(ref mut stream) = self.stream {
let _ = console.print(&prompt_text, None, None, None, false, "");
let mut line = String::new();
stream.read_line(&mut line).map_err(PromptError::from)?;
if line.is_empty() {
return Err(PromptError::Interrupted);
}
line.trim_end_matches('\n')
.trim_end_matches('\r')
.to_string()
} else {
console.input(&prompt_text, false)?
};
if value.is_empty() {
if let Some(default) = self.default {
return Ok(default);
}
}
match self.process_response(&value) {
Ok(result) => return Ok(result),
Err(err) => {
let error_text = Text::from_markup(&err.message, false)
.unwrap_or_else(|_| Text::plain(&err.message));
let _ = console.print(&error_text, None, None, None, false, "\n");
}
}
}
}
fn make_prompt(&self) -> Text {
let mut parts = vec![self.prompt.clone()];
let choices_str = format!("{}/{}", self.yes_choice, self.no_choice);
parts.push(format!(" [prompt.choices]\\[{}][/]", choices_str));
if self.show_default {
if let Some(default) = self.default {
let default_str = if default {
&self.yes_choice
} else {
&self.no_choice
};
parts.push(format!(" [prompt.default]({})[/]", default_str));
}
}
parts.push(": ".to_string());
let markup = parts.join("");
Text::from_markup(&markup, false).unwrap_or_else(|_| Text::plain(&markup))
}
fn process_response(&self, value: &str) -> std::result::Result<bool, InvalidResponse> {
let value = value.trim().to_lowercase();
let yes_lower = self.yes_choice.to_lowercase();
let no_lower = self.no_choice.to_lowercase();
if value == yes_lower {
Ok(true)
} else if value == no_lower {
Ok(false)
} else {
Err(InvalidResponse::new("[prompt.invalid]Please enter Y or N"))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prompt_new() {
let prompt = Prompt::new("Enter name");
assert_eq!(prompt.prompt, "Enter name");
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.has_stream());
}
#[test]
fn test_prompt_with_options() {
let prompt = Prompt::new("Choose")
.with_default("Alice")
.with_choices(&["Alice", "Bob", "Charlie"])
.case_sensitive(false)
.password(true);
assert_eq!(prompt.default, Some("Alice".to_string()));
assert_eq!(
prompt.choices,
Some(vec![
"Alice".to_string(),
"Bob".to_string(),
"Charlie".to_string()
])
);
assert!(!prompt.case_sensitive);
assert!(prompt.password);
}
#[test]
fn test_prompt_check_choice_case_sensitive() {
let prompt = Prompt::new("Choose")
.with_choices(&["Alice", "Bob"])
.case_sensitive(true);
assert!(prompt.check_choice("Alice"));
assert!(prompt.check_choice("Bob"));
assert!(!prompt.check_choice("alice"));
assert!(!prompt.check_choice("Charlie"));
}
#[test]
fn test_prompt_check_choice_case_insensitive() {
let prompt = Prompt::new("Choose")
.with_choices(&["Alice", "Bob"])
.case_sensitive(false);
assert!(prompt.check_choice("Alice"));
assert!(prompt.check_choice("alice"));
assert!(prompt.check_choice("ALICE"));
assert!(!prompt.check_choice("Charlie"));
}
#[test]
fn test_prompt_get_original_choice() {
let prompt = Prompt::new("Choose")
.with_choices(&["Alice", "Bob"])
.case_sensitive(false);
assert_eq!(prompt.get_original_choice("alice"), "Alice");
assert_eq!(prompt.get_original_choice("ALICE"), "Alice");
assert_eq!(prompt.get_original_choice("bob"), "Bob");
}
#[test]
fn test_prompt_process_response_valid() {
let prompt = Prompt::new("Enter name");
let result = prompt.process_response(" John ");
assert_eq!(result.unwrap(), "John");
}
#[test]
fn test_prompt_process_response_invalid_choice() {
let prompt = Prompt::new("Choose").with_choices(&["Alice", "Bob"]);
let result = prompt.process_response("Charlie");
assert!(result.is_err());
}
#[test]
fn test_int_prompt_new() {
let prompt = IntPrompt::new("Enter number");
assert_eq!(prompt.prompt, "Enter number");
assert!(prompt.default.is_none());
assert!(prompt.show_default);
assert!(prompt.choices.is_none());
}
#[test]
fn test_int_prompt_with_default() {
let prompt = IntPrompt::new("Enter number").with_default(42);
assert_eq!(prompt.default, Some(42));
}
#[test]
fn test_int_prompt_with_choices() {
let prompt = IntPrompt::new("Pick").with_choices(vec![1, 2, 3]);
assert_eq!(prompt.choices, Some(vec![1, 2, 3]));
assert!(prompt.process_response("2").is_ok());
assert!(prompt.process_response("5").is_err());
}
#[test]
fn test_int_prompt_process_response_valid() {
let prompt = IntPrompt::new("Enter number");
assert_eq!(prompt.process_response("42").unwrap(), 42);
assert_eq!(prompt.process_response(" -10 ").unwrap(), -10);
}
#[test]
fn test_int_prompt_process_response_invalid() {
let prompt = IntPrompt::new("Enter number");
assert!(prompt.process_response("abc").is_err());
assert!(prompt.process_response("3.14").is_err());
}
#[test]
fn test_float_prompt_new() {
let prompt = FloatPrompt::new("Enter number");
assert_eq!(prompt.prompt, "Enter number");
assert!(prompt.default.is_none());
assert!(prompt.choices.is_none());
}
#[test]
fn test_float_prompt_with_default() {
let prompt = FloatPrompt::new("Enter number").with_default(3.125);
assert_eq!(prompt.default, Some(3.125));
}
#[test]
fn test_float_prompt_with_choices() {
let prompt = FloatPrompt::new("Pick").with_choices(vec![1.0, 2.5, 3.125]);
assert!(prompt.process_response("2.5").is_ok());
assert!(prompt.process_response("9.9").is_err());
}
#[test]
fn test_float_prompt_process_response_valid() {
let prompt = FloatPrompt::new("Enter number");
assert!((prompt.process_response("3.125").unwrap() - 3.125).abs() < f64::EPSILON);
assert!((prompt.process_response(" -2.5 ").unwrap() - (-2.5)).abs() < f64::EPSILON);
assert!((prompt.process_response("42").unwrap() - 42.0).abs() < f64::EPSILON);
}
#[test]
fn test_float_prompt_process_response_invalid() {
let prompt = FloatPrompt::new("Enter number");
assert!(prompt.process_response("abc").is_err());
}
#[test]
fn test_confirm_new() {
let confirm = Confirm::new("Continue?");
assert_eq!(confirm.prompt, "Continue?");
assert!(confirm.default.is_none());
assert_eq!(confirm.yes_choice, "y");
assert_eq!(confirm.no_choice, "n");
assert!(confirm.stream.is_none());
}
#[test]
fn test_confirm_with_default() {
let confirm = Confirm::new("Continue?").with_default(true);
assert_eq!(confirm.default, Some(true));
}
#[test]
fn test_confirm_with_choices() {
let confirm = Confirm::new("Continue?").with_choices("yes", "no");
assert_eq!(confirm.yes_choice, "yes");
assert_eq!(confirm.no_choice, "no");
}
#[test]
fn test_confirm_process_response_yes() {
let confirm = Confirm::new("Continue?");
assert!(confirm.process_response("y").unwrap());
assert!(confirm.process_response("Y").unwrap());
}
#[test]
fn test_confirm_process_response_no() {
let confirm = Confirm::new("Continue?");
assert!(!confirm.process_response("n").unwrap());
assert!(!confirm.process_response("N").unwrap());
}
#[test]
fn test_confirm_process_response_invalid() {
let confirm = Confirm::new("Continue?");
assert!(confirm.process_response("x").is_err());
assert!(confirm.process_response("yes").is_err()); }
#[test]
fn test_confirm_custom_choices() {
let confirm = Confirm::new("Continue?").with_choices("yes", "no");
assert!(confirm.process_response("yes").unwrap());
assert!(!confirm.process_response("no").unwrap());
assert!(confirm.process_response("y").is_err()); }
#[test]
fn test_invalid_response() {
let err = InvalidResponse::new("Test error");
assert_eq!(err.message, "Test error");
assert_eq!(format!("{}", err), "Test error");
}
#[test]
fn test_prompt_error_display() {
let io_err = PromptError::Io("test error".to_string());
assert!(format!("{}", io_err).contains("test error"));
let invalid_err = PromptError::InvalidResponse(InvalidResponse::new("invalid"));
assert!(format!("{}", invalid_err).contains("invalid"));
let interrupted = PromptError::Interrupted;
assert!(format!("{}", interrupted).contains("interrupted"));
}
#[test]
fn test_prompt_with_stream() {
let input = std::io::Cursor::new(b"Alice\n");
let mut prompt = Prompt::new("Name")
.with_stream(input)
.with_choices(&["Alice", "Bob"]);
let mut console = Console::new();
let result = prompt.run_with_console(&mut console).unwrap();
assert_eq!(result, "Alice");
}
#[test]
fn test_prompt_make_prompt() {
let prompt = Prompt::new("Enter name")
.with_default("John")
.with_choices(&["John", "Jane"]);
let text = prompt.make_prompt();
let plain = text.plain_text();
assert!(plain.contains("Enter name"));
assert!(plain.contains(":")); }
}