use std::io::{self, BufRead, IsTerminal, Write};
pub struct Prompt {
enabled: bool,
}
impl Prompt {
pub fn new(no_input: bool) -> Self {
let enabled = !no_input
&& std::env::var_os("DAY_NO_INPUT").is_none()
&& io::stdin().is_terminal()
&& io::stderr().is_terminal();
Prompt { enabled }
}
pub fn enabled(&self) -> bool {
self.enabled
}
fn read_line() -> Option<String> {
let mut s = String::new();
match io::stdin().lock().read_line(&mut s) {
Ok(0) => None, Ok(_) => Some(s.trim().to_string()),
Err(_) => None,
}
}
pub fn line(&self, question: &str, default: Option<&str>) -> String {
if !self.enabled {
return default.unwrap_or_default().to_string();
}
loop {
match default {
Some(d) => eprint!("{question} [{d}]: "),
None => eprint!("{question}: "),
}
let _ = io::stderr().flush();
match Self::read_line() {
None => return default.unwrap_or_default().to_string(), Some(s) if s.is_empty() => {
if let Some(d) = default {
return d.to_string();
}
}
Some(s) => return s,
}
}
}
pub fn choose(&self, question: &str, options: &[String], default: usize) -> usize {
if !self.enabled || options.is_empty() {
return default.min(options.len().saturating_sub(1));
}
eprintln!("{question}");
for (i, opt) in options.iter().enumerate() {
eprintln!(" {}. {opt}", i + 1);
}
loop {
eprint!("Choose [{}]: ", default + 1);
let _ = io::stderr().flush();
match Self::read_line() {
None => return default,
Some(s) if s.is_empty() => return default,
Some(s) => match s.parse::<usize>() {
Ok(n) if (1..=options.len()).contains(&n) => return n - 1,
_ => eprintln!(" please enter a number from 1 to {}", options.len()),
},
}
}
}
pub fn choose_multi(
&self,
question: &str,
options: &[String],
preselected: &[usize],
) -> Vec<usize> {
let normalize = |idxs: &[usize]| -> Vec<usize> {
(0..options.len()).filter(|i| idxs.contains(i)).collect()
};
if !self.enabled || options.is_empty() {
return normalize(preselected);
}
let default_label = if preselected.is_empty() {
"none".to_string()
} else {
preselected
.iter()
.map(|i| (i + 1).to_string())
.collect::<Vec<_>>()
.join(",")
};
eprintln!("{question}");
for (i, opt) in options.iter().enumerate() {
let mark = if preselected.contains(&i) { "x" } else { " " };
eprintln!(" [{mark}] {}. {opt}", i + 1);
}
eprintln!(" (enter numbers separated by commas, or `all` / `none`)");
loop {
eprint!("Select [{default_label}]: ");
let _ = io::stderr().flush();
let ans = match Self::read_line() {
None => return normalize(preselected),
Some(s) => s,
};
if ans.is_empty() {
return normalize(preselected);
}
let lower = ans.to_ascii_lowercase();
if lower == "all" {
return (0..options.len()).collect();
}
if lower == "none" {
return Vec::new();
}
let mut picked = Vec::new();
let mut ok = true;
for tok in ans.split([',', ' ']).filter(|t| !t.is_empty()) {
match tok.parse::<usize>() {
Ok(n) if (1..=options.len()).contains(&n) => {
if !picked.contains(&(n - 1)) {
picked.push(n - 1);
}
}
_ => {
eprintln!(" `{tok}` is not a choice from 1 to {}", options.len());
ok = false;
break;
}
}
}
if ok {
if picked.is_empty() {
eprintln!(" enter at least one number, or `all` / `none`");
continue;
}
return normalize(&picked);
}
}
}
}