use std::io::{self, BufRead, Write as IoWrite};
use crate::console::Console;
use crate::style::Style;
use crate::text::Text;
#[cfg(feature = "readline")]
#[derive(Clone)]
struct ListCompleter {
candidates: Vec<String>,
}
#[cfg(feature = "readline")]
impl rustyline::completion::Completer for ListCompleter {
type Candidate = String;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<String>)> {
let prefix = &line[..pos];
let matches: Vec<String> = self
.candidates
.iter()
.filter(|c| c.starts_with(prefix))
.cloned()
.collect();
Ok((0, matches))
}
}
#[cfg(feature = "readline")]
impl rustyline::hint::Hinter for ListCompleter {
type Hint = String;
}
#[cfg(feature = "readline")]
impl rustyline::highlight::Highlighter for ListCompleter {}
#[cfg(feature = "readline")]
impl rustyline::validate::Validator for ListCompleter {}
#[cfg(feature = "readline")]
impl rustyline::Helper for ListCompleter {}
#[derive(Debug, PartialEq)]
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>,
pub completions: Option<Vec<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,
completions: 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
}
#[must_use]
pub fn with_completions(mut self, completions: Vec<String>) -> Self {
self.completions = Some(completions);
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");
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");
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"
);
}
#[cfg(feature = "readline")]
if self.completions.is_some() {
return self.ask_readline();
}
let stdin = io::stdin();
let mut handle = stdin.lock();
self.ask_with_input(&mut handle)
}
#[cfg(feature = "readline")]
fn ask_readline(&self) -> String {
let candidates = self.completions.clone().unwrap_or_default();
let helper = ListCompleter { candidates };
let config = rustyline::Config::builder()
.completion_type(rustyline::CompletionType::List)
.build();
let mut editor = rustyline::Editor::with_config(config).expect("Failed to create editor");
editor.set_helper(Some(helper));
loop {
let prompt = self.make_prompt();
let prompt_str = prompt.plain().to_string();
match editor.readline(&prompt_str) {
Ok(line) => {
let value = line
.trim_end_matches('\n')
.trim_end_matches('\r')
.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;
}
Err(rustyline::error::ReadlineError::Eof) => {
if let Some(ref default) = self.default {
return default.clone();
}
return String::new();
}
Err(rustyline::error::ReadlineError::Interrupted) => {
return String::new();
}
Err(_) => {
if let Some(ref default) = self.default {
return default.clone();
}
return String::new();
}
}
}
}
#[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"),
highlight_style: Style::parse("cyan bold"),
}
}
#[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, InvalidResponse> {
let trimmed = input.trim();
if trimmed.is_empty() {
if let Some(default) = self.default {
if default < self.choices.len() {
return Ok(default);
}
return Err(InvalidResponse {
message: format!(
"Default index {} is out of range (1-{})",
default + 1,
self.choices.len()
),
});
}
return Err(InvalidResponse {
message: "Please enter a number".to_string(),
});
}
let num: usize = trimmed.parse().map_err(|_| InvalidResponse {
message: format!("'{}' is not a valid number", trimmed),
})?;
if num < 1 || num > self.choices.len() {
return Err(InvalidResponse {
message: 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"),
highlight_style: Style::parse("cyan bold"),
}
}
#[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>, InvalidResponse> {
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(InvalidResponse {
message: 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(|_| InvalidResponse {
message: format!("'{}' is not a valid number", part),
})?;
if num < 1 || num > self.choices.len() {
return Err(InvalidResponse {
message: 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>, InvalidResponse> {
if indices.len() < self.min_selections {
return Err(InvalidResponse {
message: 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(InvalidResponse {
message: 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)]
#[path = "prompt_tests.rs"]
mod tests;