improved answers and fixed some FIXMEs
This commit is contained in:
parent
43afdee327
commit
cf98ff42ee
|
@ -11,7 +11,7 @@ members = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
ahash = "0.7.2"
|
||||
crossterm = "0.19.0"
|
||||
fxhash = "0.2.1"
|
||||
tempfile = "3"
|
||||
ui = { path = "./ui" }
|
||||
|
|
171
src/answer.rs
171
src/answer.rs
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
13
src/error.rs
13
src/error.rs
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
96
src/lib.rs
96
src/lib.rs
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user