use std::io;
use ui::{
backend::Backend,
events::{EventIterator, KeyCode, KeyEvent},
style::Stylize,
widgets, Prompt, Validation, Widget,
};
use super::{AutoComplete, ChoiceList, Filter, Transform, Validate, ValidateOnKey};
use crate::{Answer, Answers};
pub use builder::InputBuilder;
mod builder;
#[cfg(test)]
mod tests;
#[derive(Debug)]
pub(super) struct Input<'a> {
default: Option<(String, usize)>,
filter: Filter<'a, String>,
validate: Validate<'a, str>,
validate_on_key: ValidateOnKey<'a, str>,
transform: Transform<'a, str>,
auto_complete: AutoComplete<'a, String>,
page_size: usize,
should_loop: bool,
}
impl<'a> Default for Input<'a> {
fn default() -> Self {
Self {
default: None,
filter: Filter::None,
validate: Validate::None,
validate_on_key: ValidateOnKey::None,
transform: Transform::None,
auto_complete: AutoComplete::None,
page_size: 15,
should_loop: true,
}
}
}
type CompletionSelector = widgets::Select<ChoiceList<widgets::Text<String>>>;
struct InputPrompt<'i, 'a> {
prompt: widgets::Prompt<&'a str, String>,
input_opts: Input<'i>,
input: widgets::StringInput,
select: Option<CompletionSelector>,
is_valid: bool,
answers: &'a Answers,
}
impl InputPrompt<'_, '_> {
fn maybe_select_op<T, F: FnOnce(&mut CompletionSelector) -> T>(&mut self, op: F) -> Option<T> {
let mut res = None;
let Self { input, select, .. } = self;
if let Some(select) = select {
input.replace_with(|mut s| {
std::mem::swap(
&mut s,
&mut select.selected_mut().as_mut().unwrap_choice().text,
);
res = Some(op(select));
std::mem::swap(
&mut s,
&mut select.selected_mut().as_mut().unwrap_choice().text,
);
s
});
}
res
}
fn get_remaining_default(&self) -> Option<&str> {
if self.select.is_none() {
if let Some((ref default, _)) = self.input_opts.default {
let input = self.input.value();
if default.starts_with(self.input.value()) {
return Some(&default[input.len()..]);
}
}
}
None
}
fn check_complete_default(&mut self) -> bool {
if self.get_remaining_default().is_some() {
let (default, default_len) = self.input_opts.default.as_ref().unwrap();
self.input.set_value(default.clone());
self.input.set_at(*default_len);
self.is_valid = true;
true
} else {
false
}
}
}
impl Widget for InputPrompt<'_, '_> {
fn render<B: Backend>(&mut self, layout: &mut ui::layout::Layout, b: &mut B) -> io::Result<()> {
let mut original_layout = *layout;
self.prompt.render(layout, b)?;
if !self.is_valid {
b.set_fg(ui::style::Color::Red)?;
}
self.input.render(layout, b)?;
if !self.is_valid {
b.set_fg(ui::style::Color::Reset)?;
}
if let Some(default) = self.get_remaining_default() {
b.set_fg(ui::style::Color::DarkGrey)?;
write!(b, "{}", default)?;
b.set_fg(ui::style::Color::Reset)?;
self.height(&mut original_layout);
*layout = original_layout;
} else {
self.maybe_select_op(|select| select.render(layout, b))
.transpose()?;
}
Ok(())
}
fn height(&mut self, layout: &mut ui::layout::Layout) -> u16 {
let mut height = self.prompt.height(layout) - 1;
if self.get_remaining_default().is_some() {
let mut width = self.input_opts.default.as_ref().unwrap().1 as u16;
if width > layout.line_width() {
width -= layout.line_width();
layout.line_offset = width % layout.width;
layout.offset_y += 1 + width / layout.width;
height += 2 + width / layout.width;
} else {
layout.line_offset += width;
height += 1;
}
} else {
height = self.input.height(layout);
}
if let Some(picker_height) = self.maybe_select_op(|select| select.height(layout)) {
height += picker_height - 1;
}
height
}
fn handle_key(&mut self, mut key: KeyEvent) -> bool {
if key.code == KeyCode::Tab {
if let AutoComplete::Sync(ref mut ac) = self.input_opts.auto_complete {
if self.select.is_some() {
key.code = KeyCode::Down;
} else {
let page_size = self.input_opts.page_size;
let should_loop = self.input_opts.should_loop;
let Self {
input,
answers,
select,
..
} = self;
input.replace_with(|s| {
let mut completions = ac(s, answers);
assert!(!completions.is_empty());
if completions.len() == 1 {
completions.pop().unwrap()
} else {
let res = std::mem::take(&mut completions[0]);
let mut choices: ChoiceList<_> =
completions.into_iter().map(widgets::Text::new).collect();
choices.set_page_size(page_size);
choices.set_should_loop(should_loop);
*select = Some(widgets::Select::new(choices));
res
}
});
return true;
}
} else if self.check_complete_default() {
return true;
}
}
if self.input.handle_key(key) {
if let ValidateOnKey::Sync(ref mut validate) = self.input_opts.validate_on_key {
self.is_valid = validate(self.input.value(), self.answers);
}
self.select = None;
return true;
} else if key.code == KeyCode::Right && self.check_complete_default() {
return true;
}
self.maybe_select_op(|select| select.handle_key(key))
.unwrap_or(false)
}
fn cursor_pos(&mut self, layout: ui::layout::Layout) -> (u16, u16) {
self.input
.cursor_pos(layout.with_cursor_pos(self.prompt.cursor_pos(layout)))
}
}
impl Prompt for InputPrompt<'_, '_> {
type ValidateErr = widgets::Text<String>;
type Output = String;
fn finish(self) -> Self::Output {
let mut ans = self.input.finish();
if ans.is_empty() {
if let Some((default, _)) = self.input_opts.default {
ans = default;
}
}
if let Filter::Sync(filter) = self.input_opts.filter {
ans = filter(ans, self.answers);
}
ans
}
fn validate(&mut self) -> Result<Validation, Self::ValidateErr> {
if self.select.is_some() {
self.select = None;
return Ok(Validation::Continue);
}
if self.input.value().is_empty() && self.input_opts.default.is_some() {
return Ok(Validation::Finish);
}
if let Validate::Sync(ref mut validate) = self.input_opts.validate {
validate(self.input.value(), self.answers)?;
}
Ok(Validation::Finish)
}
}
impl<'i> Input<'i> {
fn into_input_prompt<'a>(self, message: &'a str, answers: &'a Answers) -> InputPrompt<'i, 'a> {
InputPrompt {
prompt: widgets::Prompt::new(message),
input_opts: self,
input: widgets::StringInput::default(),
select: None,
is_valid: true,
answers,
}
}
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_input_prompt(&message, answers), b)
.on_esc(on_esc)
.run(events)?;
crate::write_final!(transform, message, ans [ref], answers, b, |ans| b
.write_styled(&ans.as_str().cyan())?)
}
}