use crate::color::Stylize;
use crate::io::input::InputConfig;
use regex::Regex;
use std::io::{self, Write};
#[derive(Debug, Clone)]
pub struct ConfirmConfig {
pub default: Option<bool>,
pub case_sensitive: bool,
}
impl Default for ConfirmConfig {
fn default() -> Self {
Self {
default: None,
case_sensitive: false,
}
}
}
#[derive(Debug, Clone)]
pub struct RegexConfig {
pub error_message: Option<String>,
pub max_attempts: Option<usize>,
pub show_pattern: bool,
}
impl Default for RegexConfig {
fn default() -> Self {
Self {
error_message: None,
max_attempts: Some(3),
show_pattern: false,
}
}
}
#[derive(Debug, Clone)]
pub struct NumberConfig {
pub min: Option<i64>,
pub max: Option<i64>,
pub error_message: Option<String>,
pub max_attempts: Option<usize>,
}
impl Default for NumberConfig {
fn default() -> Self {
Self {
min: None,
max: None,
error_message: None,
max_attempts: Some(3),
}
}
}
pub fn confirm(message: &str, cfg: &ConfirmConfig, input_cfg: &InputConfig) -> io::Result<bool> {
let indicator = match cfg.default {
Some(true) => "[Y/n]",
Some(false) => "[y/N]",
None => "[y/n]",
};
loop {
let prompt = format!("{} {} {}: ", input_cfg.prefix.trim(), message, indicator);
print_styled(&prompt, input_cfg);
io::stdout().flush()?;
let mut line = String::new();
io::stdin().read_line(&mut line)?;
let input = line.trim();
if input.is_empty() {
if let Some(def) = cfg.default {
return Ok(def);
}
}
let val = if cfg.case_sensitive {
input.to_string()
} else {
input.to_lowercase()
};
match val.as_str() {
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => {
print_error("Please enter 'y' or 'n'", input_cfg);
}
}
}
}
pub fn read_matching(
message: &str,
pattern: &Regex,
cfg: &RegexConfig,
input_cfg: &InputConfig,
) -> io::Result<String> {
let mut attempts = 0;
loop {
let hint = if cfg.show_pattern {
format!(" (pattern: {})", pattern.as_str())
} else {
String::new()
};
let prompt = format!("{} {}{}: ", input_cfg.prefix.trim(), message, hint);
print_styled(&prompt, input_cfg);
io::stdout().flush()?;
let mut line = String::new();
io::stdin().read_line(&mut line)?;
let input = line.trim().to_string();
if pattern.is_match(&input) {
return Ok(input);
}
attempts += 1;
if let Some(max) = cfg.max_attempts {
if attempts >= max {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
cfg.error_message
.clone()
.unwrap_or_else(|| "Invalid input".into()),
));
}
}
print_error(
cfg.error_message
.as_deref()
.unwrap_or("Input does not match pattern"),
input_cfg,
);
}
}
pub fn read_number(message: &str, cfg: &NumberConfig, input_cfg: &InputConfig) -> io::Result<i64> {
let mut attempts = 0;
loop {
let range = match (cfg.min, cfg.max) {
(Some(min), Some(max)) => format!(" ({}-{})", min, max),
(Some(min), None) => format!(" (>= {})", min),
(None, Some(max)) => format!(" (<= {})", max),
(None, None) => "".to_string(),
};
let prompt = format!("{} {}{}: ", input_cfg.prefix.trim(), message, range);
print_styled(&prompt, input_cfg);
io::stdout().flush()?;
let mut line = String::new();
io::stdin().read_line(&mut line)?;
let text = line.trim();
match text.parse::<i64>() {
Ok(num) if cfg.min.map_or(true, |m| num >= m) && cfg.max.map_or(true, |m| num <= m) => {
return Ok(num);
}
_ => {
attempts += 1;
if let Some(max) = cfg.max_attempts {
if attempts >= max {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
cfg.error_message
.clone()
.unwrap_or_else(|| "Invalid number input".into()),
));
}
}
let msg = cfg.error_message.as_deref().unwrap_or("Invalid number");
print_error(msg, input_cfg);
}
}
}
}
fn print_styled(text: &str, cfg: &InputConfig) {
let mut styled = String::new();
if cfg.indent_level > 0 {
styled.push_str(&" ".repeat(cfg.indent_level));
}
styled.push_str(&cfg.prefix);
styled.push_str(text);
print!("{}", styled.with(cfg.prompt_color.into()));
}
fn print_error(message: &str, cfg: &InputConfig) {
eprintln!(
"{}",
format!("Error: {}", message).with(cfg.input_text_color.into())
);
}