improved answers and fixed some FIXMEs

This commit is contained in:
Lutetium-Vanadium 2021-04-19 18:25:50 +05:30
parent 43afdee327
commit cf98ff42ee
8 changed files with 263 additions and 128 deletions

View File

@ -11,7 +11,7 @@ members = [
]
[dependencies]
ahash = "0.7.2"
crossterm = "0.19.0"
fxhash = "0.2.1"
tempfile = "3"
ui = { path = "./ui" }

View File

@ -1,6 +1,12 @@
use fxhash::FxHashSet as HashSet;
use std::{
collections::hash_map::{Entry, IntoIter},
hash::Hash,
ops::{Deref, DerefMut},
};
#[derive(Debug, Clone)]
use ahash::AHashMap as HashMap;
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum Answer {
String(String),
ListItem(ListItem),
@ -8,17 +14,172 @@ pub enum Answer {
Int(i64),
Float(f64),
Bool(bool),
ListItems(HashSet<ListItem>),
ListItems(Vec<ListItem>),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
impl Answer {
/// Returns `true` if the answer is [`String`].
pub fn is_string(&self) -> bool {
matches!(self, Self::String(..))
}
pub fn try_into_string(self) -> Result<String, Self> {
match self {
Self::String(v) => Ok(v),
_ => Err(self),
}
}
/// Returns `true` if the answer is [`ListItem`].
pub fn is_list_item(&self) -> bool {
matches!(self, Self::ListItem(..))
}
pub fn try_into_list_item(self) -> Result<ListItem, Self> {
match self {
Self::ListItem(v) => Ok(v),
_ => Err(self),
}
}
/// Returns `true` if the answer is [`ExpandItem`].
pub fn is_expand_item(&self) -> bool {
matches!(self, Self::ExpandItem(..))
}
pub fn try_into_expand_item(self) -> Result<ExpandItem, Self> {
match self {
Self::ExpandItem(v) => Ok(v),
_ => Err(self),
}
}
/// Returns `true` if the answer is [`Int`].
pub fn is_int(&self) -> bool {
matches!(self, Self::Int(..))
}
pub fn try_into_int(self) -> Result<i64, Self> {
match self {
Self::Int(v) => Ok(v),
_ => Err(self),
}
}
/// Returns `true` if the answer is [`Float`].
pub fn is_float(&self) -> bool {
matches!(self, Self::Float(..))
}
pub fn try_into_float(self) -> Result<f64, Self> {
match self {
Self::Float(v) => Ok(v),
_ => Err(self),
}
}
/// Returns `true` if the answer is [`Bool`].
pub fn is_bool(&self) -> bool {
matches!(self, Self::Bool(..))
}
pub fn try_into_bool(self) -> Result<bool, Self> {
match self {
Self::Bool(v) => Ok(v),
_ => Err(self),
}
}
/// Returns `true` if the answer is [`ListItems`].
pub fn is_list_items(&self) -> bool {
matches!(self, Self::ListItems(..))
}
pub fn try_into_list_items(self) -> Result<Vec<ListItem>, Self> {
match self {
Self::ListItems(v) => Ok(v),
_ => Err(self),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ListItem {
pub index: usize,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
impl From<(usize, String)> for ListItem {
fn from((index, name): (usize, String)) -> Self {
Self { index, name }
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ExpandItem {
pub key: char,
pub name: String,
}
impl From<(char, String)> for ExpandItem {
fn from((key, name): (char, String)) -> Self {
Self { key, name }
}
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Answers {
answers: HashMap<String, Answer>,
}
impl Answers {
pub(crate) fn insert(&mut self, name: String, answer: Answer) -> &mut Answer {
match self.answers.entry(name) {
Entry::Occupied(entry) => {
let entry = entry.into_mut();
*entry = answer;
entry
}
Entry::Vacant(entry) => entry.insert(answer),
}
}
}
impl Extend<(String, Answer)> for Answers {
fn extend<T: IntoIterator<Item = (String, Answer)>>(&mut self, iter: T) {
self.answers.extend(iter)
}
#[cfg(nightly)]
fn extend_one(&mut self, item: (String, Answer)) {
self.answers.extend_one(item);
}
#[cfg(nightly)]
fn extend_reserve(&mut self, additional: usize) {
self.answers.extend_reserve(additional)
}
}
impl Deref for Answers {
type Target = HashMap<String, Answer>;
fn deref(&self) -> &Self::Target {
&self.answers
}
}
impl DerefMut for Answers {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.answers
}
}
impl IntoIterator for Answers {
type Item = (String, Answer);
type IntoIter = IntoIter<String, Answer>;
fn into_iter(self) -> Self::IntoIter {
self.answers.into_iter()
}
}

View File

@ -3,11 +3,13 @@ use std::{fmt, io};
pub type Result<T> = std::result::Result<T, InquirerError>;
#[derive(Debug)]
#[non_exhaustive]
pub enum InquirerError {
IoError(io::Error),
FmtError(fmt::Error),
Utf8Error(std::string::FromUtf8Error),
ParseIntError(std::num::ParseIntError),
NotATty,
}
impl std::error::Error for InquirerError {
@ -17,16 +19,19 @@ impl std::error::Error for InquirerError {
InquirerError::FmtError(e) => Some(e),
InquirerError::Utf8Error(e) => Some(e),
InquirerError::ParseIntError(e) => Some(e),
InquirerError::NotATty => None,
}
}
}
// TODO: better display impl
impl fmt::Display for InquirerError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
InquirerError::IoError(_) => write!(fmt, "IO-error occurred"),
_ => write!(fmt, "Some error has occurred"),
match self {
InquirerError::IoError(e) => write!(fmt, "IoError: {}", e),
InquirerError::FmtError(e) => write!(fmt, "FmtError: {}", e),
InquirerError::Utf8Error(e) => write!(fmt, "Utf8Error: {}", e),
InquirerError::ParseIntError(e) => write!(fmt, "ParseIntError: {}", e),
InquirerError::NotATty => write!(fmt, "Not a tty"),
}
}
}

View File

@ -1,47 +1,27 @@
use std::{borrow::Borrow, hash::Hash};
use fxhash::FxHashMap as HashMap;
mod answer;
mod error;
mod question;
pub use answer::{Answer, ExpandItem, ListItem};
pub use answer::{Answer, Answers, ExpandItem, ListItem};
use crossterm::tty::IsTty;
pub use question::{Choice::Choice, Choice::Separator, Question};
#[derive(Debug, Default)]
pub struct Answers {
answers: HashMap<String, Answer>,
}
impl Answers {
fn insert(&mut self, name: String, answer: Answer) {
self.answers.insert(name, answer);
}
fn reserve(&mut self, capacity: usize) {
self.answers.reserve(capacity - self.answers.len())
}
pub fn contains<Q: ?Sized>(&self, question: &Q) -> bool
where
String: Borrow<Q>,
Q: Hash + Eq,
{
self.answers.contains_key(question)
}
}
pub struct PromptModule<'m, 'w, 'f, 'v, 't> {
questions: Vec<Question<'m, 'w, 'f, 'v, 't>>,
pub struct PromptModule<Q> {
questions: Q,
answers: Answers,
}
impl<'m, 'w, 'f, 'v, 't> PromptModule<'m, 'w, 'f, 'v, 't> {
pub fn new(questions: Vec<Question<'m, 'w, 'f, 'v, 't>>) -> Self {
impl<'m, 'w, 'f, 'v, 't, Q> PromptModule<Q>
where
Q: Iterator<Item = Question<'m, 'w, 'f, 'v, 't>>,
{
pub fn new<I>(questions: I) -> Self
where
I: IntoIterator<IntoIter = Q>,
{
Self {
answers: Answers::default(),
questions,
questions: questions.into_iter(),
}
}
@ -50,24 +30,44 @@ impl<'m, 'w, 'f, 'v, 't> PromptModule<'m, 'w, 'f, 'v, 't> {
self
}
pub fn prompt_all(self) -> error::Result<Answers> {
let PromptModule {
questions,
mut answers,
} = self;
answers.reserve(questions.len());
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
for question in questions {
if let Some((name, answer)) = question.ask(&answers, &mut stdout)? {
answers.insert(name, answer);
fn prompt_impl<W: std::io::Write>(
&mut self,
stdout: &mut W,
) -> error::Result<Option<&mut Answer>> {
while let Some(question) = self.questions.next() {
if let Some((name, answer)) = question.ask(&self.answers, stdout)? {
return Ok(Some(self.answers.insert(name, answer)));
}
}
Ok(answers)
Ok(None)
}
pub fn prompt(&mut self) -> error::Result<Option<&mut Answer>> {
let stdout = std::io::stdout();
if !stdout.is_tty() {
return Err(error::InquirerError::NotATty);
}
let mut stdout = stdout.lock();
self.prompt_impl(&mut stdout)
}
pub fn prompt_all(mut self) -> error::Result<Answers> {
self.answers.reserve(self.questions.size_hint().0);
let stdout = std::io::stdout();
if !stdout.is_tty() {
return Err(error::InquirerError::NotATty);
}
let mut stdout = stdout.lock();
while self.prompt_impl(&mut stdout)?.is_some() {}
Ok(self.answers)
}
pub fn into_answers(self) -> Answers {
self.answers
}
}

View File

@ -16,7 +16,7 @@ pub struct Checkbox<'f, 'v, 't> {
selected: Vec<bool>,
filter: Option<Box<Filter<'f, Vec<bool>>>>,
validate: Option<Box<Validate<'v, [bool]>>>,
transformer: Option<Box<Transformer<'t, [bool]>>>,
transformer: Option<Box<Transformer<'t, [ListItem]>>>,
}
impl fmt::Debug for Checkbox<'_, '_, '_> {
@ -40,9 +40,9 @@ struct CheckboxPrompt<'f, 'v, 't, 'a> {
answers: &'a Answers,
}
impl<'f, 'v, 't> ui::Prompt for CheckboxPrompt<'f, 'v, 't, '_> {
impl ui::Prompt for CheckboxPrompt<'_, '_, '_, '_> {
type ValidateErr = String;
type Output = Checkbox<'f, 'v, 't>;
type Output = Vec<ListItem>;
fn prompt(&self) -> &str {
&self.message
@ -60,7 +60,26 @@ impl<'f, 'v, 't> ui::Prompt for CheckboxPrompt<'f, 'v, 't, '_> {
}
fn finish(self) -> Self::Output {
self.picker.finish()
let Checkbox {
mut selected,
choices,
filter,
..
} = self.picker.finish();
if let Some(filter) = filter {
selected = filter(selected, self.answers);
}
selected
.into_iter()
.enumerate()
.zip(choices.choices.into_iter())
.filter_map(|((index, is_selected), name)| match (is_selected, name) {
(true, Choice::Choice(name)) => Some(ListItem { index, name }),
_ => None,
})
.collect()
}
fn has_default(&self) -> bool {
@ -164,16 +183,9 @@ impl Checkbox<'_, '_, '_> {
answers: &Answers,
w: &mut W,
) -> error::Result<Answer> {
let filter = self.filter.take();
let transformer = self.transformer.take();
// We cannot simply process the Vec<bool> to a HashSet<ListItem> inside the widget since we
// want to print the selected ones in order
let Checkbox {
mut selected,
choices,
..
} = ui::Input::new(CheckboxPrompt {
let ans = ui::Input::new(CheckboxPrompt {
message,
picker: widgets::ListPicker::new(self),
answers,
@ -181,40 +193,17 @@ impl Checkbox<'_, '_, '_> {
.hide_cursor()
.run(w)?;
if let Some(filter) = filter {
selected = filter(selected, answers);
}
match transformer {
Some(transformer) => transformer(&selected, answers, w)?,
Some(transformer) => transformer(&ans, answers, w)?,
None => {
queue!(w, SetForegroundColor(Color::DarkCyan))?;
print_comma_separated(
selected
.iter()
.zip(choices.choices.iter())
.filter_map(|item| match item {
(true, Choice::Choice(name)) => Some(name.as_str()),
_ => None,
}),
w,
)?;
print_comma_separated(ans.iter().map(|item| item.name.as_str()), w)?;
w.write_all(b"\n")?;
execute!(w, ResetColor)?;
}
}
let ans = selected
.into_iter()
.enumerate()
.zip(choices.choices.into_iter())
.filter_map(|((index, is_selected), name)| match (is_selected, name) {
(true, Choice::Choice(name)) => Some(ListItem { index, name }),
_ => None,
})
.collect();
Ok(Answer::ListItems(ans))
}
}
@ -353,7 +342,7 @@ crate::impl_validate_builder!(CheckboxBuilder<'m, 'w, 'f, v, 't> [bool]; (this,
}
});
crate::impl_transformer_builder!(CheckboxBuilder<'m, 'w, 'f, 'v, t> [bool]; (this, transformer) => {
crate::impl_transformer_builder!(CheckboxBuilder<'m, 'w, 'f, 'v, t> [ListItem]; (this, transformer) => {
CheckboxBuilder {
opts: this.opts,
checkbox: Checkbox {

View File

@ -1,11 +1,11 @@
use std::fmt;
use ahash::AHashSet as HashSet;
use crossterm::{
cursor, queue,
style::{Color, Colorize, ResetColor, SetForegroundColor},
terminal,
};
use fxhash::FxHashSet as HashSet;
use ui::{widgets, Validation, Widget};
use crate::{error, Answer, Answers, ExpandItem};
@ -417,7 +417,7 @@ impl super::Question<'static, 'static, 'static, 'static, 'static> {
ExpandBuilder {
opts: Options::new(name.into()),
expand: Default::default(),
keys: Default::default(),
keys: HashSet::default(),
}
}
}

View File

@ -64,7 +64,7 @@ impl Question<'_, '_, '_, '_, '_> {
answers: &Answers,
w: &mut W,
) -> error::Result<Option<(String, Answer)>> {
if (!self.opts.ask_if_answered && answers.contains(&self.opts.name))
if (!self.opts.ask_if_answered && answers.contains_key(&self.opts.name))
|| !self.opts.when.get(answers)
{
return Ok(None);

View File

@ -172,10 +172,8 @@ impl<P: Prompt> Input<P> {
let prompt_len = u16::try_from(prompt.chars().count() + 3).expect("really big prompt");
let _raw = RawMode::enable()?;
let _cursor = if self.hide_cursor {
Some(HideCursor::enable(stdout)?)
} else {
None
if self.hide_cursor {
queue!(stdout, cursor::Hide)?;
};
let height = self.prompt.height();
@ -228,7 +226,9 @@ impl<P: Prompt> Input<P> {
cursor::MoveTo(0, self.base_row + self.prompt.height() as u16)
)?;
drop(_raw);
drop(_cursor);
if self.hide_cursor {
queue!(stdout, cursor::Show)?;
}
exit()
}
event::KeyCode::Null => {
@ -237,7 +237,9 @@ impl<P: Prompt> Input<P> {
cursor::MoveTo(0, self.base_row + self.prompt.height() as u16)
)?;
drop(_raw);
drop(_cursor);
if self.hide_cursor {
queue!(stdout, cursor::Show)?;
}
exit()
}
event::KeyCode::Esc if self.prompt.has_default() => {
@ -303,7 +305,7 @@ fn exit() -> ! {
}
/// Simple helper to make sure if the code panics in between, raw mode is disabled
pub struct RawMode {
struct RawMode {
_private: (),
}
@ -320,25 +322,3 @@ impl Drop for RawMode {
let _ = terminal::disable_raw_mode();
}
}
/// Simple helper to make sure if the code panics in between, cursor is shown
pub struct HideCursor {
_private: (),
}
impl HideCursor {
/// Hide the cursor in the terminal
/// note: it is implicitly bound to stdout because it is required in the destructor
pub fn enable<W: io::Write>(stdout: &mut W) -> crossterm::Result<Self> {
queue!(stdout, cursor::Hide)?;
Ok(Self { _private: () })
}
}
impl Drop for HideCursor {
fn drop(&mut self) {
// FIXME: this implicitly binds the hiding cursor to stdout, even though enable is generic
// over any write
let _ = queue!(io::stdout(), cursor::Show);
}
}