use std::{
fmt::Display,
io::{self, Stdout, Write},
};
use crossterm::{
QueueableCommand,
cursor::{MoveToColumn, MoveUp},
event::{self, Event, KeyCode, KeyEvent},
style::{Color, Print, PrintStyledContent, Stylize},
terminal::{self, Clear, ClearType},
};
use crate::prompts::{AskPrompter, Choice, ChoicePrompt, ChoicePrompter, OkayPrompter};
const NUM_MAP: &str = "1234567890abcdefgimnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
#[derive(Debug)]
pub struct CliPrompter;
impl CliPrompter {
fn draw_options<C: Display>(
stdout: &mut Stdout,
index: usize,
options: &[Choice<C>],
) -> io::Result<()> {
for (i, opt) in options.iter().enumerate() {
stdout
.queue(MoveToColumn(0))?
.queue(Clear(ClearType::CurrentLine))?;
if i == index {
stdout
.queue(PrintStyledContent("> ".with(Color::Green).bold()))?
.queue(PrintStyledContent(
format!(
"[{shortcut}] {opt}\n",
shortcut = NUM_MAP.chars().nth(i).unwrap_or(' ')
)
.with(Color::Cyan)
.bold(),
))?;
} else {
stdout.queue(Print(format!(
" [{shortcut}] {opt}\n",
shortcut = NUM_MAP.chars().nth(i).unwrap_or(' ')
)))?;
}
}
stdout.queue(MoveToColumn(0))?.flush()
}
}
impl ChoicePrompter for CliPrompter {
fn choose<'a, C: Display>(&self, mut prompt: ChoicePrompt<'a, C>) -> Option<C> {
let option_count = prompt.options.len();
if option_count == 0 {
return None;
}
let mut selected_index = prompt.default.min(option_count - 1);
let mut stdout = io::stdout();
stdout
.queue(PrintStyledContent(
format!("==> {}\n", prompt.prompt).with(Color::Green).bold(),
))
.ok()?;
Self::draw_options(&mut stdout, selected_index, &prompt.options).ok()?;
terminal::enable_raw_mode().ok()?;
let result = loop {
match event::read().ok()? {
Event::Key(KeyEvent { code, kind, .. }) if kind.is_press() => match code {
KeyCode::Up | KeyCode::Char('k') => {
selected_index =
((selected_index as isize - 1) as usize).rem_euclid(option_count);
stdout.queue(MoveUp(option_count as u16)).ok()?;
Self::draw_options(&mut stdout, selected_index, &prompt.options).ok()?;
}
KeyCode::Down | KeyCode::Char('j') => {
selected_index = (selected_index + 1) % option_count;
stdout.queue(MoveUp(option_count as u16)).ok()?;
Self::draw_options(&mut stdout, selected_index, &prompt.options).ok()?;
}
KeyCode::Enter => break Some(selected_index),
KeyCode::Esc | KeyCode::Char('q') => {
break None;
}
KeyCode::Char(c) => {
if let Some(index) = NUM_MAP.find(c)
&& index < option_count
{
break Some(index);
}
}
_ => (),
},
_ => (),
}
};
terminal::disable_raw_mode().ok()?;
result.map(|i| prompt.options.remove(i).into_value())
}
}
impl OkayPrompter for CliPrompter {
fn okay<T: Display>(&self, msg: T) {
let mut output = io::stdout();
writeln!(output, "{msg}\nPress [ENTER] to continue.\n").unwrap();
let _ = io::stdin().read_line(&mut String::new());
}
}
impl AskPrompter for CliPrompter {
fn ask<T: Display>(&self, msg: T, default: bool) -> Option<bool> {
let prompt = if default { "[Y/n]" } else { "[y/N]" };
let mut output = io::stdout();
writeln!(output, "==> Confirmation\n{msg}\n{prompt}").unwrap();
loop {
let mut input = String::new();
io::stdin().read_line(&mut input).ok()?;
match input.to_ascii_lowercase().as_str().trim() {
"" => return Some(default),
"y" | "yes" => return Some(true),
"n" | "no" => return Some(false),
_ => writeln!(output, "Please enter 'y' for yes or 'n' for no.").unwrap(),
}
}
}
}