use crate::styles::{paint, DIM, WHITE};
use super::core::{Frame, Key, Prompt, RenderCtx, Step};
use super::error::{PromptError, Result};
use super::theme;
#[derive(Clone)]
struct KeyOption {
key: char,
label: String,
hint: Option<String>,
}
#[derive(Debug, Clone)]
pub struct KeySelected {
pub key: char,
pub label: String,
}
pub struct SelectKeyPrompt {
label: String,
options: Vec<KeyOption>,
case_sensitive: bool,
}
pub fn select_key(label: impl Into<String>) -> SelectKeyPrompt {
SelectKeyPrompt {
label: label.into(),
options: Vec::new(),
case_sensitive: false,
}
}
impl SelectKeyPrompt {
pub fn option(mut self, key: char, label: impl Into<String>) -> Self {
self.options.push(KeyOption {
key,
label: label.into(),
hint: None,
});
self
}
pub fn hint(mut self, text: impl Into<String>) -> Self {
if let Some(last) = self.options.last_mut() {
last.hint = Some(text.into());
}
self
}
pub fn case_sensitive(mut self, v: bool) -> Self {
self.case_sensitive = v;
self
}
pub fn run(self) -> Result<KeySelected> {
assert!(!self.options.is_empty(), "select_key: no options added");
super::core::run(self)
}
}
impl Prompt for SelectKeyPrompt {
type Output = KeySelected;
fn handle(&mut self, key: Key) -> Step<KeySelected> {
match key {
Key::Char(c) => {
let hit = self.options.iter().find(|o| {
if self.case_sensitive {
o.key == c
} else {
o.key.eq_ignore_ascii_case(&c)
}
});
match hit {
Some(o) => Step::Submit(KeySelected {
key: o.key,
label: o.label.clone(),
}),
None => Step::Continue,
}
}
Key::Escape | Key::Interrupt => Step::Cancel,
_ => Step::Continue,
}
}
fn render(&self, _ctx: RenderCtx) -> Frame {
let c = super::settings::colors();
let mut f: Frame = Vec::with_capacity(self.options.len() + 4);
f.push(theme::label(&self.label));
f.push(theme::hint(""));
let _ = DIM;
for o in &self.options {
let badge = paint(c.accent, &format!(" {} ", o.key));
let label = paint(WHITE, &o.label);
let hint = theme::hint_inline(o.hint.as_deref());
f.push(format!(
"{} {} {}{}",
paint(c.active, "│"),
badge,
label,
hint
));
}
f.push(theme::hint("press the highlighted key"));
f.push(theme::frame_bot(None));
f
}
fn render_answered(&self, value: &KeySelected) -> Frame {
theme::answered(&self.label, &value.label)
.split("\r\n")
.map(String::from)
.collect()
}
fn run_fallback(self) -> Result<KeySelected> {
use std::io::Write;
let mut out = std::io::stderr();
writeln!(out, " {}", self.label).map_err(PromptError::Io)?;
for o in &self.options {
writeln!(out, " [{}] {}", o.key, o.label).map_err(PromptError::Io)?;
}
write!(out, " Press a key (and Enter): ").map_err(PromptError::Io)?;
out.flush().map_err(PromptError::Io)?;
let line = super::engine::fallback::read_line_raw()?;
let ch = line.chars().next().ok_or(PromptError::Interrupted)?;
let hit = self
.options
.iter()
.find(|o| {
if self.case_sensitive {
o.key == ch
} else {
o.key.eq_ignore_ascii_case(&ch)
}
})
.ok_or(PromptError::Interrupted)?;
Ok(KeySelected {
key: hit.key,
label: hit.label.clone(),
})
}
}