use std::io;
use ui::{
backend::Backend,
events::{EventIterator, KeyEvent},
style::{Color, Stylize},
widgets::{self, List, Text},
Prompt, Validation, Widget,
};
use super::{Choice, Transform};
use crate::{Answer, Answers, ListItem};
pub use builder::RawSelectBuilder;
mod builder;
#[cfg(test)]
mod tests;
#[derive(Debug, Default)]
pub(super) struct RawSelect<'a> {
choices: super::ChoiceList<(usize, Text<String>)>,
transform: Transform<'a, ListItem>,
max_index_width: u16,
}
struct RawSelectPrompt<'a> {
prompt: widgets::Prompt<&'a str>,
select: widgets::Select<RawSelect<'a>>,
input: widgets::StringInput,
}
impl RawSelectPrompt<'_> {
fn finish_index(self, index: usize) -> ListItem {
ListItem {
index,
text: self
.select
.into_inner()
.choices
.choices
.swap_remove(index)
.unwrap_choice()
.1
.text,
}
}
}
impl Prompt for RawSelectPrompt<'_> {
type ValidateErr = &'static str;
type Output = ListItem;
fn validate(&mut self) -> Result<Validation, Self::ValidateErr> {
if self.select.get_at() >= self.select.list.len() {
Err("Please enter a valid choice")
} else {
Ok(Validation::Finish)
}
}
fn finish(self) -> Self::Output {
let index = self.select.get_at();
self.finish_index(index)
}
}
const ANSWER_PROMPT: &[u8] = b" Answer: ";
impl Widget for RawSelectPrompt<'_> {
fn render<B: Backend>(&mut self, layout: &mut ui::layout::Layout, b: &mut B) -> io::Result<()> {
self.prompt.render(layout, b)?;
self.select.render(layout, b)?;
b.write_all(ANSWER_PROMPT)?;
layout.line_offset += ANSWER_PROMPT.len() as u16;
self.input.render(layout, b)
}
fn height(&mut self, layout: &mut ui::layout::Layout) -> u16 {
let height = self.prompt.height(layout) + self.select.height(layout);
layout.line_offset = ANSWER_PROMPT.len() as u16;
height + self.input.height(layout) - 1
}
fn handle_key(&mut self, key: KeyEvent) -> bool {
if self.input.handle_key(key) {
if let Ok(n) = self.input.value().parse::<usize>() {
if n <= self.select.list.len() && n > 0 {
let pos = self.select.list.choices.choices[(n - 1)..]
.iter()
.position(|choice| matches!(choice, Choice::Choice((i, _)) if *i == n));
if let Some(pos) = pos {
self.select.set_at(pos + n - 1);
return true;
}
}
}
self.select.set_at(self.select.list.len() + 1);
true
} else if self.select.handle_key(key) {
let at = self.select.get_at();
let index = self.select.list.choices[at].as_ref().unwrap_choice().0;
self.input.set_value(index.to_string());
true
} else {
false
}
}
fn cursor_pos(&mut self, mut layout: ui::layout::Layout) -> (u16, u16) {
let w = self
.input
.cursor_pos(layout.with_line_offset(ANSWER_PROMPT.len() as u16))
.0;
let offset_y = layout.offset_y;
(w, self.height(&mut layout) - 1 + offset_y)
}
}
impl widgets::List for RawSelect<'_> {
fn render_item<B: Backend>(
&mut self,
index: usize,
hovered: bool,
mut layout: ui::layout::Layout,
b: &mut B,
) -> io::Result<()> {
match &mut self.choices[index] {
&mut Choice::Choice((index, ref mut text)) => {
if hovered {
b.set_fg(Color::Cyan)?;
}
write!(
b,
" {:>width$}. ",
index,
width = self.max_index_width as usize
)?;
layout.offset_x += self.max_index_width + 4;
text.render(&mut layout, b)?;
if hovered {
b.set_fg(Color::Reset)?;
}
}
separator => {
b.set_fg(Color::DarkGrey)?;
b.write_all(b" ")?;
super::get_sep_str(separator).render(&mut layout.with_line_offset(2), b)?;
b.set_fg(Color::Reset)?;
}
}
Ok(())
}
fn is_selectable(&self, index: usize) -> bool {
!self.choices[index].is_separator()
}
fn height_at(&mut self, index: usize, mut layout: ui::layout::Layout) -> u16 {
match self.choices[index] {
Choice::Choice((_, ref mut c)) => {
layout.offset_x += self.max_index_width + 4;
c.height(&mut layout)
}
_ => 1,
}
}
fn len(&self) -> usize {
self.choices.len()
}
fn page_size(&self) -> usize {
self.choices.page_size()
}
fn should_loop(&self) -> bool {
self.choices.should_loop()
}
}
impl<'a> RawSelect<'a> {
fn into_prompt(self, message: &'a str) -> RawSelectPrompt<'a> {
let mut select = widgets::Select::new(self);
if let Some(default) = select.list.choices.default() {
select.set_at(default);
}
RawSelectPrompt {
input: widgets::StringInput::with_filter_map(|c| {
if c.is_ascii_digit() {
Some(c)
} else {
None
}
}),
select,
prompt: widgets::Prompt::new(message),
}
}
pub(crate) fn ask<B: Backend, E: EventIterator>(
mut self,
message: String,
on_esc: ui::OnEsc,
answers: &Answers,
b: &mut B,
events: &mut E,
) -> ui::Result<Option<Answer>> {
let transform = self.transform.take();
let ans = ui::Input::new(self.into_prompt(&message), b)
.on_esc(on_esc)
.run(events)?;
crate::write_final!(transform, message, ans [ref], answers, b, |ans| b.write_styled(
&ans.text
.lines()
.next()
.expect("There must be at least one line in a `str`")
.cyan()
)?)
}
}