use std::io;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use console::Key;
#[derive(Debug, Clone)]
pub struct Select<'a> {
pub title: &'a str,
pub question: &'a str,
pub lines: &'a [&'a str],
pub yes: &'a str,
pub no: &'a str,
pub default_yes: bool,
pub timeout: Option<Duration>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SelectOutcome {
Answered(bool),
Eof,
ReadError(ReadFailure),
TimedOut,
Unrecognized,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadFailure {
pub kind: io::ErrorKind,
pub os_code: Option<i32>,
}
impl From<&io::Error> for ReadFailure {
fn from(e: &io::Error) -> Self {
Self {
kind: e.kind(),
os_code: e.raw_os_error(),
}
}
}
const MAX_LINE_ATTEMPTS: usize = 3;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LineRead {
Line(String),
Eof,
Error(ReadFailure),
TimedOut,
}
pub fn parse_answer(answer: &str, default_yes: bool) -> Option<bool> {
match answer.trim().to_ascii_lowercase().as_str() {
"" => Some(default_yes),
"y" | "yes" => Some(true),
"n" | "no" => Some(false),
_ => None,
}
}
pub fn ask_line(
default_yes: bool,
timeout: Option<Duration>,
mut read: impl FnMut(Option<Duration>) -> LineRead,
mut reprompt: impl FnMut(),
) -> SelectOutcome {
let deadline = timeout.map(|t| Instant::now() + t);
for attempt in 1..=MAX_LINE_ATTEMPTS {
let left = deadline.map(|d| d.saturating_duration_since(Instant::now()));
match read(left) {
LineRead::Line(line) => {
if let Some(yes) = parse_answer(&line, default_yes) {
return SelectOutcome::Answered(yes);
}
if attempt < MAX_LINE_ATTEMPTS {
reprompt();
}
}
LineRead::Eof => return SelectOutcome::Eof,
LineRead::Error(failure) => return SelectOutcome::ReadError(failure),
LineRead::TimedOut => return SelectOutcome::TimedOut,
}
}
SelectOutcome::Unrecognized
}
pub fn read_line_within<F>(timeout: Option<Duration>, read_line: F) -> LineRead
where
F: FnOnce() -> io::Result<(usize, String)> + Send + 'static,
{
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(read_line());
});
let got = match timeout {
Some(t) => rx.recv_timeout(t).map_err(|_| ()),
None => rx.recv().map_err(|_| ()),
};
match got {
Ok(Ok((0, _))) => LineRead::Eof,
Ok(Ok((_, line))) => LineRead::Line(line),
Ok(Err(e)) => LineRead::Error(ReadFailure::from(&e)),
Err(()) => LineRead::TimedOut,
}
}
pub fn read_stdin_line(timeout: Option<Duration>) -> LineRead {
read_line_within(timeout, || {
let mut line = String::new();
let n = io::stdin().read_line(&mut line)?;
Ok((n, line))
})
}
pub fn discard_typeahead() {
#[cfg(windows)]
{
use winapi::um::consoleapi::GetConsoleMode;
use winapi::um::processenv::GetStdHandle;
use winapi::um::winbase::STD_INPUT_HANDLE;
use winapi::um::wincon::FlushConsoleInputBuffer;
unsafe {
let handle = GetStdHandle(STD_INPUT_HANDLE);
let mut mode = 0;
if GetConsoleMode(handle, &mut mode) != 0 {
FlushConsoleInputBuffer(handle);
}
}
}
#[cfg(unix)]
{
unsafe {
if libc::isatty(libc::STDIN_FILENO) == 1 {
libc::tcflush(libc::STDIN_FILENO, libc::TCIFLUSH);
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyState {
pub yes: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyStep {
Moved,
Answer(bool),
Eof,
Ignore,
}
impl KeyState {
pub fn apply(&mut self, key: &Key) -> KeyStep {
match key {
Key::ArrowLeft | Key::ArrowRight | Key::Tab | Key::BackTab => {
self.yes = !self.yes;
KeyStep::Moved
}
Key::ArrowUp | Key::Home => self.go(true),
Key::ArrowDown | Key::End => self.go(false),
Key::Enter => KeyStep::Answer(self.yes),
Key::Char('y' | 'Y') => KeyStep::Answer(true),
Key::Char('n' | 'N') => KeyStep::Answer(false),
Key::Char('\u{4}') => KeyStep::Eof,
_ => KeyStep::Ignore,
}
}
fn go(&mut self, yes: bool) -> KeyStep {
if self.yes == yes {
KeyStep::Ignore
} else {
self.yes = yes;
KeyStep::Moved
}
}
}
const MAX_UNKNOWN_KEYS: usize = 64;
pub fn ask_keys(
default_yes: bool,
mut read_key: impl FnMut() -> io::Result<Key>,
mut redraw: impl FnMut(KeyState),
) -> SelectOutcome {
let mut state = KeyState { yes: default_yes };
let mut unknown = 0;
loop {
let key = match read_key() {
Ok(k) => k,
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return SelectOutcome::Eof,
Err(e) => return SelectOutcome::ReadError(ReadFailure::from(&e)),
};
if key == Key::Unknown {
unknown += 1;
if unknown >= MAX_UNKNOWN_KEYS {
return SelectOutcome::Eof;
}
continue;
}
unknown = 0;
match state.apply(&key) {
KeyStep::Moved => redraw(state),
KeyStep::Answer(yes) => return SelectOutcome::Answered(yes),
KeyStep::Eof => return SelectOutcome::Eof,
KeyStep::Ignore => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn failed(kind: io::ErrorKind) -> ReadFailure {
ReadFailure::from(&io::Error::from(kind))
}
#[test]
fn test_parse_answer() {
assert_eq!(parse_answer("", true), Some(true));
assert_eq!(parse_answer("\r\n", false), Some(false));
assert_eq!(parse_answer("Y\r\n", false), Some(true));
assert_eq!(parse_answer(" yes ", false), Some(true));
assert_eq!(parse_answer("N", true), Some(false));
assert_eq!(parse_answer("No\n", true), Some(false));
assert_eq!(parse_answer("maybe", true), None);
}
fn scripted(reads: Vec<LineRead>) -> impl FnMut(Option<Duration>) -> LineRead {
let mut reads = reads.into_iter();
move |_| reads.next().expect("no more scripted reads")
}
#[test]
fn test_line_classifier() {
let ask = |reads| ask_line(false, None, scripted(reads), || {});
assert_eq!(
ask(vec![LineRead::Line("Y\r\n".into())]),
SelectOutcome::Answered(true)
);
assert_eq!(ask(vec![LineRead::Eof]), SelectOutcome::Eof);
assert_eq!(
ask(vec![LineRead::Error(failed(io::ErrorKind::InvalidData))]),
SelectOutcome::ReadError(failed(io::ErrorKind::InvalidData))
);
assert_eq!(ask(vec![LineRead::TimedOut]), SelectOutcome::TimedOut);
assert_eq!(
ask(vec![
LineRead::Line("what".into()),
LineRead::Line("n\n".into())
]),
SelectOutcome::Answered(false)
);
assert_eq!(
ask(vec![
LineRead::Line("a".into()),
LineRead::Line("b".into()),
LineRead::Line("c".into())
]),
SelectOutcome::Unrecognized
);
}
#[test]
fn test_line_reprompts_between_unrecognized_answers() {
let mut reprompts = 0;
let outcome = ask_line(
true,
None,
scripted(vec![LineRead::Line("?".into()), LineRead::Line("".into())]),
|| reprompts += 1,
);
assert_eq!(outcome, SelectOutcome::Answered(true));
assert_eq!(reprompts, 1);
}
#[test]
fn test_read_line_within_distinguishes_all_four() {
let t = Some(Duration::from_secs(5));
assert_eq!(
read_line_within(t, || Ok((2, "y\n".into()))),
LineRead::Line("y\n".into())
);
assert_eq!(
read_line_within(t, || Ok((0, String::new()))),
LineRead::Eof
);
assert_eq!(
read_line_within(t, || Err(io::Error::from(io::ErrorKind::InvalidData))),
LineRead::Error(failed(io::ErrorKind::InvalidData))
);
assert_eq!(
read_line_within(Some(Duration::from_millis(20)), || {
std::thread::sleep(Duration::from_secs(2));
Ok((2, "y\n".into()))
}),
LineRead::TimedOut
);
}
fn keys(seq: Vec<io::Result<Key>>) -> impl FnMut() -> io::Result<Key> {
let mut seq = seq.into_iter();
move || seq.next().expect("no more scripted keys")
}
#[test]
fn test_key_classifier() {
let ask = |default, seq| ask_keys(default, keys(seq), |_| {});
assert_eq!(
ask(true, vec![Ok(Key::Enter)]),
SelectOutcome::Answered(true)
);
assert_eq!(
ask(true, vec![Ok(Key::ArrowRight), Ok(Key::Enter)]),
SelectOutcome::Answered(false)
);
assert_eq!(
ask(false, vec![Ok(Key::Tab), Ok(Key::Tab), Ok(Key::Enter)]),
SelectOutcome::Answered(false)
);
assert_eq!(
ask(false, vec![Ok(Key::Char('Y'))]),
SelectOutcome::Answered(true)
);
assert_eq!(
ask(true, vec![Ok(Key::Char('n'))]),
SelectOutcome::Answered(false)
);
assert_eq!(ask(true, vec![Ok(Key::Char('\u{4}'))]), SelectOutcome::Eof);
assert_eq!(
ask(
true,
vec![Err(io::Error::from(io::ErrorKind::UnexpectedEof))]
),
SelectOutcome::Eof
);
assert_eq!(
ask(true, vec![Err(io::Error::from(io::ErrorKind::Other))]),
SelectOutcome::ReadError(failed(io::ErrorKind::Other))
);
let unknowns = (0..MAX_UNKNOWN_KEYS).map(|_| Ok(Key::Unknown)).collect();
assert_eq!(ask(true, unknowns), SelectOutcome::Eof);
}
#[test]
fn test_read_failure_keeps_the_os_code() {
let failure = ReadFailure::from(&io::Error::from_raw_os_error(6));
assert_eq!(failure.os_code, Some(6));
assert_eq!(
read_line_within(None, || Err(io::Error::from_raw_os_error(6))),
LineRead::Error(failure)
);
}
#[test]
fn test_key_redraw_only_on_move() {
let mut draws = Vec::new();
let outcome = ask_keys(
true,
keys(vec![
Ok(Key::ArrowUp),
Ok(Key::Char('x')),
Ok(Key::ArrowDown),
Ok(Key::Enter),
]),
|s| draws.push(s.yes),
);
assert_eq!(outcome, SelectOutcome::Answered(false));
assert_eq!(draws, vec![false], "only ArrowDown moved the highlight");
}
}