Added docs for discourse

This commit is contained in:
Lutetium-Vanadium 2021-07-13 20:04:21 +05:30
parent d1c1272a1a
commit f5067a0111
39 changed files with 2941 additions and 388 deletions

View File

@ -1,4 +1,5 @@
{
"rust-analyzer.cargo.allFeatures": true,
"rust-analyzer.diagnostics.disabled": [
"incorrect-ident-case",
"inactive-code"

View File

@ -25,7 +25,7 @@ insta = { version = "1.7.1", default-features = false }
rand = "0.8"
rand_chacha = "0.3"
regex = "1.5" # examples/prompt_module.rs
regex = "1.5" # examples/{prompt_module,macro}.rs
fuzzy-matcher = "0.3" # examples/file.rs
[features]

View File

@ -7,15 +7,16 @@ use syn::{parse::Parse, spanned::Spanned};
use crate::helpers::*;
bitflags::bitflags! {
pub struct BuilderMethods: u8 {
const DEFAULT = 0b0000_0001;
const TRANSFORM = 0b0000_0010;
const VAL_FIL = 0b0000_0100;
const AUTO_COMPLETE = 0b0000_1000;
const LIST = 0b0001_0000;
const MASK = 0b0010_0000;
const EXTENSION = 0b0100_0000;
const PLUGIN = 0b1000_0000;
pub struct BuilderMethods: u16 {
const DEFAULT = 0b00000_0001;
const TRANSFORM = 0b00000_0010;
const VAL_FIL = 0b00000_0100;
const AUTO_COMPLETE = 0b00000_1000;
const LOOP_PAGE_SIZE = 0b00001_0000;
const CHOICES = 0b00010_0000;
const MASK = 0b00100_0000;
const EXTENSION = 0b01000_0000;
const PLUGIN = 0b10000_0000;
}
}
@ -58,16 +59,23 @@ impl QuestionKind {
| BuilderMethods::TRANSFORM
| BuilderMethods::VAL_FIL
| BuilderMethods::AUTO_COMPLETE
| BuilderMethods::LOOP_PAGE_SIZE
}
QuestionKind::Int | QuestionKind::Float => {
BuilderMethods::DEFAULT | BuilderMethods::TRANSFORM | BuilderMethods::VAL_FIL
}
QuestionKind::Confirm => BuilderMethods::DEFAULT | BuilderMethods::TRANSFORM,
QuestionKind::Select | QuestionKind::RawSelect | QuestionKind::Expand => {
BuilderMethods::DEFAULT | BuilderMethods::TRANSFORM | BuilderMethods::LIST
BuilderMethods::DEFAULT
| BuilderMethods::TRANSFORM
| BuilderMethods::LOOP_PAGE_SIZE
| BuilderMethods::CHOICES
}
QuestionKind::MultiSelect => {
BuilderMethods::TRANSFORM | BuilderMethods::VAL_FIL | BuilderMethods::LIST
BuilderMethods::TRANSFORM
| BuilderMethods::VAL_FIL
| BuilderMethods::LOOP_PAGE_SIZE
| BuilderMethods::CHOICES
}
QuestionKind::Password => {
BuilderMethods::TRANSFORM | BuilderMethods::VAL_FIL | BuilderMethods::MASK
@ -188,8 +196,10 @@ fn check_allowed(ident: &syn::Ident, kind: QuestionKind) -> syn::Result<()> {
BuilderMethods::VAL_FIL
} else if ident == "auto_complete" {
BuilderMethods::AUTO_COMPLETE
} else if ident == "choices" || ident == "page_size" || ident == "should_loop" {
BuilderMethods::LIST
} else if ident == "choices" {
BuilderMethods::CHOICES
} else if ident == "page_size" || ident == "should_loop" {
BuilderMethods::LOOP_PAGE_SIZE
} else if ident == "mask" {
BuilderMethods::MASK
} else if ident == "extension" {
@ -316,7 +326,11 @@ impl quote::ToTokens for Question {
let name = &self.name;
if let QuestionKind::Plugin = self.kind {
let plugin = self.opts.plugin.as_ref().unwrap();
let plugin = self
.opts
.plugin
.as_ref()
.expect("Parsing would error if no plugin was there");
// If just the name was passed into Question::plugin, type errors associated
// with its conversion to a string would take the span _including_ that of
// plugin. Explicitly performing `String::from`, makes the error span due to

View File

@ -49,7 +49,10 @@ impl Default for TermionEvents {
impl EventIterator for TermionEvents {
fn next_event(&mut self) -> io::Result<super::KeyEvent> {
let e = self.events.next().unwrap()?;
let e = self
.events
.next()
.expect("TermionEvents ran out of user input!?")?;
e.try_into()
}
}

View File

@ -11,13 +11,9 @@
//! # Backends
//!
//! This crate currently supports 2 backends:
//! - [`crossterm`](https://crates.io/crates/crossterm) (default)
//! - [`crossterm`](https://crates.io/crates/crossterm)
//! - [`termion`](https://crates.io/crates/termion)
//!
//! The default backend is `crossterm` for the following reasons:
//! - Wider terminal support
//! - Better event processing (in my experience)
//!
//! The different backends can be enabled using the features of the same name.
// TODO: [`discourse`]: https://crates.io/crates/discourse
#![deny(

View File

@ -42,13 +42,14 @@ pub trait List {
/// skipped during navigation.
fn is_selectable(&self, index: usize) -> bool;
/// The maximum height that can be taken by the list. If the total height exceeds the page size,
/// the list will be scrollable.
/// The maximum height that can be taken by the list.
///
/// If the total height exceeds the page size, the list will be scrollable.
fn page_size(&self) -> usize;
/// Whether to wrap around when user gets to the last element.
///
/// This only applies when the list is not scrollable, i.e. page size > total height.
/// This only applies when the list is scrollable, i.e. page size > total height.
fn should_loop(&self) -> bool;
/// The height of the element at an index will take to render
@ -275,7 +276,11 @@ impl<L: List> Select<L> {
_ => unreachable!(),
};
let heights = &self.heights.as_ref().unwrap().heights[..];
let heights = &self
.heights
.as_ref()
.expect("`adjust_page` called before `height` or `render`")
.heights[..];
// -1 since the message at the end takes one line
let max_height = self.page_size() - 1;
@ -379,7 +384,11 @@ impl<L: List> Select<L> {
}
fn init_page(&mut self) {
let heights = &self.heights.as_ref().unwrap().heights[..];
let heights = &self
.heights
.as_ref()
.expect("`init_page` called before `height` or `render`")
.heights[..];
self.page_start = 0;
self.page_start_height = heights[self.page_start];
@ -412,7 +421,11 @@ impl<L: List> Select<L> {
old_layout: &mut Layout,
b: &mut B,
) -> io::Result<()> {
let heights = &self.heights.as_ref().unwrap().heights[..];
let heights = &self
.heights
.as_ref()
.expect("`render_in` called from someplace other than `render`")
.heights[..];
// Create a new local copy of the layout to operate on to avoid changes in max_height and
// render_region to be reflected upstream
@ -630,7 +643,7 @@ impl<L: List> super::Widget for Select<L> {
.max(
self.heights
.as_ref()
.unwrap()
.expect("`maybe_update_heights` should set `self.heights` if missing")
.heights
.get(self.at)
.unwrap_or(&0)

View File

@ -1,9 +1,8 @@
# Discourse Examples
The examples given show some basic usage of the library in different
ways. `templates/pizza.rs` has a question iterator declaration which is
used in some examples which demonstrate things other than declaring
questions.
ways. You can also see the documentation for the `Question` type for
examples on each prompt type.
## Required features

View File

@ -1,7 +1,6 @@
fn main() {
let question = discourse::Question::expand("overwrite")
.message("Conflict on `file.rs`")
.separator(" = The Meats = ")
.choices(vec![
('y', "Overwrite"),
('a', "Overwrite this one and all next"),

View File

@ -2,7 +2,7 @@ use std::path::Path;
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use discourse::question::Completions;
use discourse::question::{completions, Completions};
fn auto_complete(p: String) -> Completions<String> {
let current: &Path = p.as_ref();
@ -35,12 +35,12 @@ fn auto_complete(p: String) -> Completions<String> {
})
.collect(),
Err(_) => {
return Completions::from([p]);
return completions![p];
}
};
if files.is_empty() {
Completions::from([p])
return completions![p];
} else {
let fuzzer = SkimMatcherV2::default();
files.sort_by_cached_key(|file| fuzzer.fuzzy_match(file, last).unwrap_or(i64::MAX));

View File

@ -1,4 +1,4 @@
use discourse::plugin::ui::style::Stylize;
use discourse::plugin::style::Stylize;
use discourse::Question;
fn map_err<E>(_: E) {}

View File

@ -11,14 +11,17 @@ fn ask() -> discourse::Result<Vec<String>> {
discourse::Question::input("tv_show").message("What's your favourite TV show?"),
)?
.try_into_string()
.unwrap(),
.expect("Question::input returns a string"),
);
let ask_again = discourse::Question::confirm("ask_again")
.message("Want to enter another TV show favorite (just hit enter for YES)?")
.default(true);
if !discourse::prompt_one(ask_again)?.try_into_bool().unwrap() {
if !discourse::prompt_one(ask_again)?
.as_bool()
.expect("Question::confirm returns a bool")
{
break;
}
}

View File

@ -5,23 +5,59 @@ use std::{
ops::{Deref, DerefMut},
};
/// The different answer types that can be returned by the [`Question`]s
///
/// [`Question`]: crate::question::Question
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum Answer {
/// Strings will be returned by [`input`], [`password`] and [`editor`].
///
/// [`input`]: crate::question::Question::input
/// [`password`]: crate::question::Question::password
/// [`editor`]: crate::question::Question::editor
String(String),
/// ListItems will be returned by [`select`] and [`raw_select`].
///
/// [`select`]: crate::question::Question::select
/// [`raw_select`]: crate::question::Question::raw_select
ListItem(ListItem),
/// ExpandItems will be returned by [`expand`].
///
/// [`expand`]: crate::question::Question::expand
ExpandItem(ExpandItem<String>),
/// Ints will be returned by [`int`].
///
/// [`int`]: crate::question::Question::int
Int(i64),
/// Floats will be returned by [`float`].
///
/// [`float`]: crate::question::Question::float
Float(f64),
/// Bools will be returned by [`confirm`].
///
/// [`confirm`]: crate::question::Question::confirm
Bool(bool),
/// ListItems will be returned by [`multi_select`].
///
/// [`multi_select`]: crate::question::Question::multi_select
ListItems(Vec<ListItem>),
}
impl Answer {
/// Returns `true` if the answer is [`String`].
/// Returns `true` if the answer is [`Answer::String`].
pub fn is_string(&self) -> bool {
matches!(self, Self::String(..))
}
/// Returns [`Some`] if it is a [`Answer::String`], otherwise returns [`None`].
pub fn as_string(&self) -> Option<&str> {
match self {
Self::String(v) => Some(v),
_ => None,
}
}
/// Returns the `Ok(String)` if it is one, otherwise returns itself as an [`Err`].
pub fn try_into_string(self) -> Result<String, Self> {
match self {
Self::String(v) => Ok(v),
@ -29,11 +65,20 @@ impl Answer {
}
}
/// Returns `true` if the answer is [`ListItem`].
/// Returns `true` if the answer is [`Answer::ListItem`].
pub fn is_list_item(&self) -> bool {
matches!(self, Self::ListItem(..))
}
/// Returns [`Some`] if it is a [`Answer::ListItem`], otherwise returns [`None`].
pub fn as_list_item(&self) -> Option<&ListItem> {
match self {
Self::ListItem(v) => Some(v),
_ => None,
}
}
/// Returns the `Ok(ListItem)` if it is one, otherwise returns itself as an [`Err`].
pub fn try_into_list_item(self) -> Result<ListItem, Self> {
match self {
Self::ListItem(v) => Ok(v),
@ -41,11 +86,20 @@ impl Answer {
}
}
/// Returns `true` if the answer is [`ExpandItem`].
/// Returns `true` if the answer is [`Answer::ExpandItem`].
pub fn is_expand_item(&self) -> bool {
matches!(self, Self::ExpandItem(..))
}
/// Returns [`Some`] if it is [`Answer::ExpandItem`], otherwise returns [`None`].
pub fn as_expand_item(&self) -> Option<&ExpandItem<String>> {
match self {
Self::ExpandItem(v) => Some(v),
_ => None,
}
}
/// Returns the `Ok(ExpandItem)` if it is one, otherwise returns itself as an [`Err`].
pub fn try_into_expand_item(self) -> Result<ExpandItem<String>, Self> {
match self {
Self::ExpandItem(v) => Ok(v),
@ -53,11 +107,20 @@ impl Answer {
}
}
/// Returns `true` if the answer is [`Int`].
/// Returns `true` if the answer is [`Answer::Int`].
pub fn is_int(&self) -> bool {
matches!(self, Self::Int(..))
}
/// Returns [`Some`] if it is [`Answer::Int`], otherwise returns [`None`].
pub fn as_int(&self) -> Option<i64> {
match self {
Self::Int(v) => Some(*v),
_ => None,
}
}
/// Returns the `Ok(i64)` if it is one, otherwise returns itself as an [`Err`].
pub fn try_into_int(self) -> Result<i64, Self> {
match self {
Self::Int(v) => Ok(v),
@ -65,11 +128,20 @@ impl Answer {
}
}
/// Returns `true` if the answer is [`Float`].
/// Returns `true` if the answer is [`Answer::Float`].
pub fn is_float(&self) -> bool {
matches!(self, Self::Float(..))
}
/// Returns [`Some`] if it is [`Answer::Float`], otherwise returns [`None`].
pub fn as_float(&self) -> Option<f64> {
match self {
Self::Float(v) => Some(*v),
_ => None,
}
}
/// Returns the `Ok(f64)` if it is one, otherwise returns itself as an [`Err`].
pub fn try_into_float(self) -> Result<f64, Self> {
match self {
Self::Float(v) => Ok(v),
@ -77,11 +149,20 @@ impl Answer {
}
}
/// Returns `true` if the answer is [`Bool`].
/// Returns `true` if the answer is [`Answer::Bool`].
pub fn is_bool(&self) -> bool {
matches!(self, Self::Bool(..))
}
/// Returns [`Some`] if it is [`Answer::Bool`], otherwise returns [`None`].
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(v) => Some(*v),
_ => None,
}
}
/// Returns the `Ok(bool)` if it is one, otherwise returns itself as an [`Err`].
pub fn try_into_bool(self) -> Result<bool, Self> {
match self {
Self::Bool(v) => Ok(v),
@ -89,78 +170,40 @@ impl Answer {
}
}
/// Returns `true` if the answer is [`ListItems`].
/// Returns `true` if the answer is [`Answer::ListItems`].
pub fn is_list_items(&self) -> bool {
matches!(self, Self::ListItems(..))
}
/// Returns [`Some`] if it is [`Answer::ListItems`], otherwise returns [`None`].
pub fn as_list_items(&self) -> Option<&[ListItem]> {
match self {
Self::ListItems(v) => Some(v),
_ => None,
}
}
/// Returns the `Ok(Vec<ListItem>)` if it is one, otherwise returns itself as an [`Err`].
pub fn try_into_list_items(self) -> Result<Vec<ListItem>, Self> {
match self {
Self::ListItems(v) => Ok(v),
_ => Err(self),
}
}
pub fn as_string(&self) -> Option<&String> {
if let Self::String(v) = self {
Some(v)
} else {
None
}
}
pub fn as_list_item(&self) -> Option<&ListItem> {
if let Self::ListItem(v) = self {
Some(v)
} else {
None
}
}
pub fn as_expand_item(&self) -> Option<&ExpandItem<String>> {
if let Self::ExpandItem(v) = self {
Some(v)
} else {
None
}
}
pub fn as_int(&self) -> Option<i64> {
if let Self::Int(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_float(&self) -> Option<f64> {
if let Self::Float(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_bool(&self) -> Option<bool> {
if let Self::Bool(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_list_items(&self) -> Option<&Vec<ListItem>> {
if let Self::ListItems(v) = self {
Some(v)
} else {
None
}
}
}
/// A representation of a [`Choice`] at a particular index.
///
/// It will be returned by [`select`] and [`raw_select`].
///
/// [`Choice`]: crate::Choice
/// [`select`]: crate::question::Question::select
/// [`raw_select`]: crate::question::Question::raw_select
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ListItem {
/// The index of the choice
pub index: usize,
/// The actual choice
pub name: String,
}
@ -170,9 +213,17 @@ impl From<(usize, String)> for ListItem {
}
}
/// A representation of a [`Choice`] for a particular key.
///
/// It will be returned by [`expand`].
///
/// [`Choice`]: crate::Choice
/// [`expand`]: crate::question::Question::expand
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ExpandItem<S> {
/// The key associated with the choice
pub key: char,
/// The actual choice
pub name: S,
}
@ -207,6 +258,9 @@ impl<S: ui::Widget> ui::Widget for ExpandItem<S> {
}
}
/// A collections of answers of previously asked [`Question`]s.
///
/// [`Question`]: crate::question::Question
#[derive(Default, Clone, PartialEq)]
pub struct Answers {
answers: HashMap<String, Answer>,

View File

@ -1,9 +1,152 @@
//! `discourse` is an easy-to-use collection of interactive cli prompts inspired by [Inquirer.js].
//!
//! [Inquirer.js]: https://github.com/SBoudrias/Inquirer.js/
//!
//! # Questions
//!
//! This crate is based on creating [`Question`]s, and then prompting them to the user. There are 10
//! in-built [`Question`]s, but if none of them fit your need, you can [create your own!](#plugins)
//!
//! There are 2 ways of creating [`Question`]s.
//!
//! ### Using builders
//!
//! ```
//! use discourse::{Question, Answers};
//!
//! let question = Question::expand("toppings")
//! .message("What toppings do you want?")
//! .when(|answers: &Answers| !answers["custom_toppings"].as_bool().unwrap())
//! .choice('p', "Pepperoni and cheese")
//! .choice('a', "All dressed")
//! .choice('w', "Hawaiian")
//! .build();
//! ```
//!
//! See [`Question`] for more information on the builders.
//!
//! ### Using macros
//!
//! Unlike the builder api, the macros can only be used to create a list of questions.
//!
//! ```
//! use discourse::{questions, Answers};
//!
//! let questions = questions! [
//! Expand {
//! name: "toppings",
//! message: "What toppings do you want?",
//! when: |answers: &Answers| !answers["custom_toppings"].as_bool().unwrap(),
//! choices: [
//! ('p', "Pepperoni and cheese"),
//! ('a', "All dressed"),
//! ('w', "Hawaiian"),
//! ]
//! }
//! ];
//! ```
//!
//! See [`questions`] and [`prompt_module`] for more information on the macros.
//!
//! ### Prompting
//!
//! [`Question`]s can be asked in 2 main ways.
//!
//! - Using direct [functions](#functions) provided by the crate.
//! ```no_run
//! let questions = vec![
//! // Declare the questions you want to ask
//! ];
//!
//! let answers = discourse::prompt(questions)?;
//! # Result::<_, discourse::ErrorKind>::Ok(())
//! ```
//!
//! - Using [`PromptModule`]
//! ```no_run
//! use discourse::PromptModule;
//!
//! let questions = PromptModule::new(vec![
//! // Declare the questions you want to ask
//! ]);
//!
//! let answers = questions.prompt_all()?;
//! # Result::<_, discourse::ErrorKind>::Ok(())
//! ```
//! This is mainly useful if you need more control over prompting the questions, and using
//! previous [`Answers`].
//!
//! See the documentation of [`Question`] for more information on the different in-built questions.
//!
//! # Terminal Interaction
//!
//! Terminal interaction is handled by 2 traits: [`Backend`] and [`EventIterator`].
//!
//! The traits are already implemented for terminal libraries:
//! - [`crossterm`](https://crates.io/crates/crossterm) (default)
//! - [`termion`](https://crates.io/crates/termion)
//!
//! The default backend is `crossterm` for the following reasons:
//! - Wider terminal support
//! - Better event processing (in my experience)
//!
//! [`Backend`]: plugin::Backend
//! [`EventIterator`]: plugin::EventIterator
//!
//! # Plugins
//!
//! If the crate's in-built prompts does not satisfy your needs, you can build your own custom
//! prompts using the [`Plugin`](question::Plugin) trait.
//!
//! # Optional features
//!
//! - `smallvec` (default): Enabling this feature will use [`SmallVec`] instead of [`Vec`] for [auto
//! completions]. This allows inlining single completions.
//!
//! - `crossterm` (default): Enabling this feature will use the [`crossterm`](https://crates.io/crates/crossterm)
//! library for terminal interactions such as drawing and receiving events.
//!
//! - `termion`: Enabling this feature will use the [`termion`](https://crates.io/crates/termion)
//! library for terminal interactions such as drawing and receiving events.
//!
//! [`SmallVec`]: https://docs.rs/smallvec/1.6.1/smallvec/struct.SmallVec.html
//! [auto completions]: crate::question::InputBuilder::auto_complete
//!
//! # Examples
//!
//! ```no_run
//! use discourse::Question;
//!
//! let password = Question::password("password")
//! .message("What is your password?")
//! .mask('*')
//! .build();
//!
//! let answer = discourse::prompt_one(password)?;
//!
//! println!("Your password was: {}", answer.as_string().expect("password returns a string"));
//! # Result::<_, discourse::ErrorKind>::Ok(())
//! ```
//!
//! For more examples, see the documentation for the various in-built questions, and the
//! [`examples`] directory.
//!
//! [`examples`]: https://github.com/lutetium-vanadium/discourse/tree/master/examples
#![deny(
missing_docs,
missing_doc_code_examples,
missing_debug_implementations,
unreachable_pub,
broken_intra_doc_links
)]
#![warn(rust_2018_idioms)]
mod answer;
pub mod question;
use ui::{
backend,
events::{self, EventIterator},
backend::{get_backend, Backend},
events::{get_events, EventIterator},
};
pub use answer::{Answer, Answers, ExpandItem, ListItem};
@ -71,7 +214,7 @@ pub use ui::{ErrorKind, Result};
/// See also [`prompt_module`].
pub use macros::questions;
/// A macro to easily get a [`PromptModule`].
/// A macro to easily write a [`PromptModule`].
///
/// # Usage
///
@ -137,11 +280,22 @@ macro_rules! prompt_module {
};
}
/// A module that re-exports all the things required for writing [`Plugin`]s.
///
/// [`Plugin`]: plugin::Plugin
pub mod plugin {
pub use crate::{question::Plugin, Answer, Answers};
pub use ui::{self, backend::Backend, events::EventIterator};
pub use ui::{
backend::{self, Backend},
events::{self, EventIterator},
style,
};
}
/// A collection of questions and answers for previously answered questions.
///
/// Unlike [`prompt`], this allows you to control how many questions you want to ask, and ask with
/// previous answers as well.
#[derive(Debug, Clone, PartialEq)]
pub struct PromptModule<Q> {
questions: Q,
@ -152,6 +306,7 @@ impl<'a, Q> PromptModule<Q>
where
Q: Iterator<Item = Question<'a>>,
{
/// Creates a new `PromptModule` with the given questions
pub fn new<I>(questions: I) -> Self
where
I: IntoIterator<IntoIter = Q, Item = Question<'a>>,
@ -162,18 +317,34 @@ where
}
}
/// Creates a `PromptModule` with the given questions and answers
pub fn with_answers(mut self, answers: Answers) -> Self {
self.answers = answers;
self
}
/// Prompt a single question with the default [`Backend`] and [`EventIterator`].
///
/// This may or may not actually prompt the question based on what `when` and `ask_if_answered`
/// is set to for that particular question.
pub fn prompt(&mut self) -> Result<Option<&mut Answer>> {
let stdout = std::io::stdout();
let mut stdout = get_backend(stdout.lock())?;
self.prompt_with(&mut stdout, &mut get_events())
}
/// Prompt a single question with the given [`Backend`] and [`EventIterator`].
///
/// This may or may not actually prompt the question based on what `when` and `ask_if_answered`
/// is set to for that particular question.
pub fn prompt_with<B, E>(
&mut self,
backend: &mut B,
events: &mut E,
) -> Result<Option<&mut Answer>>
where
B: backend::Backend,
B: Backend,
E: EventIterator,
{
while let Some(question) = self.questions.next() {
@ -185,16 +356,23 @@ where
Ok(None)
}
pub fn prompt(&mut self) -> Result<Option<&mut Answer>> {
/// Prompt all remaining questions with the default [`Backend`] and [`EventIterator`].
///
/// It consumes `self` and returns the answers to all the questions asked.
pub fn prompt_all(self) -> Result<Answers> {
let stdout = std::io::stdout();
let mut stdout = backend::get_backend(stdout.lock())?;
let mut stdout = get_backend(stdout.lock())?;
let mut events = get_events();
self.prompt_with(&mut stdout, &mut events::get_events())
self.prompt_all_with(&mut stdout, &mut events)
}
/// Prompt all remaining questions with the given [`Backend`] and [`EventIterator`].
///
/// It consumes `self` and returns the answers to all the questions asked.
pub fn prompt_all_with<B, E>(mut self, backend: &mut B, events: &mut E) -> Result<Answers>
where
B: backend::Backend,
B: Backend,
E: EventIterator,
{
self.answers.reserve(self.questions.size_hint().0);
@ -204,46 +382,55 @@ where
Ok(self.answers)
}
pub fn prompt_all(self) -> Result<Answers> {
let stdout = std::io::stdout();
let mut stdout = backend::get_backend(stdout.lock())?;
let mut events = events::get_events();
self.prompt_all_with(&mut stdout, &mut events)
}
/// Consumes `self` returning the answers to the previously asked questions.
pub fn into_answers(self) -> Answers {
self.answers
}
}
/// Prompt all the questions in the given iterator, with the default [`Backend`] and [`EventIterator`].
pub fn prompt<'a, Q>(questions: Q) -> Result<Answers>
where
Q: IntoIterator<Item = Question<'a>>,
{
PromptModule::new(questions).prompt_all()
PromptModule::new(questions.into_iter()).prompt_all()
}
/// Prompt the given question, with the default [`Backend`] and [`EventIterator`].
///
/// # Panics
///
/// This will panic if `when` on the [`Question`] prevents the question from being asked.
pub fn prompt_one<'a, I: Into<Question<'a>>>(question: I) -> Result<Answer> {
let ans = prompt(std::iter::once(question.into()))?;
Ok(ans.into_iter().next().unwrap().1)
let stdout = std::io::stdout();
let mut stdout = get_backend(stdout.lock())?;
let mut events = get_events();
prompt_one_with(question.into(), &mut stdout, &mut events)
}
/// Prompt all the questions in the given iterator, with the given [`Backend`] and [`EventIterator`].
pub fn prompt_with<'a, Q, B, E>(questions: Q, backend: &mut B, events: &mut E) -> Result<Answers>
where
Q: IntoIterator<Item = Question<'a>>,
B: backend::Backend,
B: Backend,
E: EventIterator,
{
PromptModule::new(questions).prompt_all_with(backend, events)
PromptModule::new(questions.into_iter()).prompt_all_with(backend, events)
}
/// Prompt the given question, with the given [`Backend`] and [`EventIterator`].
///
/// # Panics
///
/// This will panic if `when` on the [`Question`] prevents the question from being asked.
pub fn prompt_one_with<'a, Q, B, E>(question: Q, backend: &mut B, events: &mut E) -> Result<Answer>
where
Q: Into<Question<'a>>,
B: backend::Backend,
B: Backend,
E: EventIterator,
{
let ans = prompt_with(std::iter::once(question.into()), backend, events)?;
Ok(ans.into_iter().next().unwrap().1)
let ans = question.into().ask(&Answers::default(), backend, events)?;
Ok(ans.expect("The question wasn't asked").1)
}

View File

@ -90,11 +90,7 @@ fn main() {
.message("multi select 1")
.choice_with_default("0", true)
.default_separator()
.choices_with_default(vec![
("1".into(), false),
("2".into(), true),
("3".into(), false),
])
.choices_with_default(vec![("1", false), ("2", true), ("3", false)])
.separator("== Hello separator")
.into(),
Question::multi_select("b")

View File

@ -141,14 +141,23 @@ impl<T: Widget> List for ChoiceList<T> {
}
}
/// A possible choice in a list.
#[derive(Debug, Clone)]
pub enum Choice<T> {
/// The main variant which represents a choice the user can pick from.
Choice(T),
/// A separator is a _single line_ of text that can be used to annotate other lines. It is not
/// selectable and is skipped over when users navigate.
///
/// If the line more than one line, it will be cut-off.
Separator(String),
/// A separator which prints a line: "──────────────"
DefaultSeparator,
}
impl<T> Choice<T> {
/// Maps an Choice<T> to Choice<U> by applying a function to the contained [`Choice::Choice`] if
/// any.
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Choice<U> {
match self {
Choice::Choice(c) => Choice::Choice(f(c)),
@ -157,10 +166,14 @@ impl<T> Choice<T> {
}
}
/// Returns `true` if the choice is a separator.
pub fn is_separator(&self) -> bool {
matches!(self, Choice::Separator(_) | Choice::DefaultSeparator)
}
/// Converts `&Choice<T>` to `Choice<&T>`.
///
/// This will clone the [`Choice::Separator`] if any.
pub fn as_ref(&self) -> Choice<&T> {
match self {
Choice::Choice(t) => Choice::Choice(t),
@ -169,6 +182,9 @@ impl<T> Choice<T> {
}
}
/// Converts `&mut Choice<T>` to `Choice<&mut T>`.
///
/// This will clone the [`Choice::Separator`] if any.
pub fn as_mut(&mut self) -> Choice<&mut T> {
match self {
Choice::Choice(t) => Choice::Choice(t),
@ -177,6 +193,11 @@ impl<T> Choice<T> {
}
}
/// Returns the contained [`Choice::Choice`] value, consuming `self`.
///
/// # Panics
///
/// This will panic if `self` is not a [`Choice::Choice`].
pub fn unwrap_choice(self) -> T {
match self {
Choice::Choice(c) => c,
@ -220,19 +241,14 @@ impl<T: ui::Widget> ui::Widget for Choice<T> {
}
}
/// This will panic if called
fn cursor_pos(&mut self, _: ui::layout::Layout) -> (u16, u16) {
unimplemented!("This should not be called")
}
}
impl<T> From<T> for Choice<T> {
fn from(t: T) -> Self {
Choice::Choice(t)
}
}
impl From<&'_ str> for Choice<String> {
fn from(s: &str) -> Self {
impl<I: Into<String>> From<I> for Choice<String> {
fn from(s: I) -> Self {
Choice::Choice(s.into())
}
}
@ -245,3 +261,9 @@ impl<I: Into<String>> From<(char, I)> for Choice<ExpandItem<String>> {
})
}
}
impl<I: Into<String>> From<(I, bool)> for Choice<(String, bool)> {
fn from((name, checked): (I, bool)) -> Self {
Choice::Choice((name.into(), checked))
}
}

View File

@ -11,7 +11,7 @@ use super::{Options, TransformByVal as Transform};
use crate::{Answer, Answers};
#[derive(Debug, Default)]
pub struct Confirm<'a> {
pub(super) struct Confirm<'a> {
default: Option<bool>,
transform: Transform<'a, bool>,
}
@ -65,7 +65,10 @@ impl Prompt for ConfirmPrompt<'_> {
match self.input.value() {
Some('y') | Some('Y') => true,
Some('n') | Some('N') => false,
_ => self.confirm.default.unwrap(),
_ => self
.confirm
.default
.expect("Validation would fail if there was no answer and no default"),
}
}
}
@ -105,6 +108,22 @@ impl<'a> Confirm<'a> {
}
}
/// The builder for a [`confirm`] prompt.
///
/// See the various methods for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let int = Question::confirm("anonymous")
/// .message("Do you want to remain anonymous?")
/// .build();
/// ```
///
/// [`confirm`]: crate::question::Question::confirm
#[derive(Debug)]
pub struct ConfirmBuilder<'a> {
opts: Options<'a>,
confirm: Confirm<'a>,
@ -118,20 +137,93 @@ impl<'a> ConfirmBuilder<'a> {
}
}
crate::impl_options_builder! {
message
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let confirm = Question::confirm("anonymous")
/// .message("Do you want to remain anonymous?")
/// .build();
/// ```
when
/// # Examples
///
/// ```
/// use discourse::{Question, Answers};
///
/// let confirm = Question::confirm("anonymous")
/// .when(|previous_answers: &Answers| match previous_answers.get("auth") {
/// Some(ans) => !ans.as_bool().unwrap(),
/// None => true,
/// })
/// .build();
/// ```
ask_if_answered
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let confirm = Question::confirm("anonymous")
/// .ask_if_answered(true)
/// .build();
/// ```
}
/// Set a default value for the confirm
///
/// If the input text is empty, the `default` is taken as the answer.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let confirm = Question::confirm("anonymous")
/// .default(false)
/// .build();
/// ```
pub fn default(mut self, default: bool) -> Self {
self.confirm.default = Some(default);
self
}
crate::impl_options_builder!();
crate::impl_transform_builder!(by val bool; confirm);
crate::impl_transform_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let confirm = Question::confirm("anonymous")
/// .transform(|anonymous, previous_answers, backend| {
/// if anonymous {
/// write!(backend, "Ok, you are now anonymous!")
/// } else {
/// write!(backend, "Please enter your details in the later prompts!")
/// }
/// })
/// .build();
/// ```
by val bool; confirm
}
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
pub fn build(self) -> super::Question<'a> {
super::Question::new(self.opts, super::QuestionKind::Confirm(self.confirm))
}
}
impl<'a> From<ConfirmBuilder<'a>> for super::Question<'a> {
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
fn from(builder: ConfirmBuilder<'a>) -> Self {
builder.build()
}
@ -142,7 +234,7 @@ mod tests {
use super::*;
use ui::{backend::TestBackend, events::KeyCode, layout::Layout};
fn confirm(default: Option<bool>, message: &str) -> ConfirmPrompt {
fn confirm(default: Option<bool>, message: &str) -> ConfirmPrompt<'_> {
Confirm {
default,
..Default::default()

View File

@ -14,7 +14,7 @@ use super::{Filter, Options, Transform, Validate};
use crate::{Answer, Answers};
#[derive(Debug)]
pub struct Editor<'a> {
pub(super) struct Editor<'a> {
extension: Option<String>,
default: Option<String>,
editor: OsString,
@ -180,6 +180,27 @@ impl Editor<'_> {
}
}
/// The builder for the [`editor`] prompt.
///
/// Once the user exits their editor, the contents of the temporary file are read in as the
/// result. The editor to use is determined by the `$VISUAL` or `$EDITOR` environment variables.
/// If neither of those are present, `vim` (for unix) or `notepad` (for windows) is used.
///
/// See the various methods for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let editor = Question::editor("description")
/// .message("Please enter a short description about yourself")
/// .extension(".md")
/// .build();
/// ```
///
/// [`editor`]: crate::question::Question::editor
#[derive(Debug)]
pub struct EditorBuilder<'a> {
opts: Options<'a>,
editor: Editor<'a>,
@ -193,28 +214,144 @@ impl<'a> EditorBuilder<'a> {
}
}
crate::impl_options_builder! {
message
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let editor = Question::editor("description")
/// .message("Please enter a short description about yourself")
/// .build();
/// ```
when
/// # Examples
///
/// ```
/// use discourse::{Question, Answers};
///
/// let editor = Question::editor("description")
/// .when(|previous_answers: &Answers| match previous_answers.get("anonymous") {
/// Some(ans) => !ans.as_bool().unwrap(),
/// None => true,
/// })
/// .build();
/// ```
ask_if_answered
/// # Examples
///
/// ```
/// use discourse::{Question, Answers};
///
/// let editor = Question::editor("description")
/// .ask_if_answered(true)
/// .build();
/// ```
}
/// Set a default value for the file
///
/// If set, when the user first opens the file, it will contain the `default` value. Subsequent
/// times will contain what was last written.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let editor = Question::editor("description")
/// .default("My name is ")
/// .build();
/// ```
pub fn default<I: Into<String>>(mut self, default: I) -> Self {
self.editor.default = Some(default.into());
self
}
/// Set an extension on the temporary file
///
/// If set, the extension will be concatenated with the randomly generated filename. This is a
/// useful way to signify accepted styles of input, and provide syntax highlighting on supported
/// editors.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let editor = Question::editor("description")
/// .extension(".md")
/// .build();
/// ```
pub fn extension<I: Into<String>>(mut self, extension: I) -> Self {
self.editor.extension = Some(extension.into());
self
}
crate::impl_options_builder!();
crate::impl_filter_builder!(String; editor);
crate::impl_validate_builder!(str; editor);
crate::impl_transform_builder!(str; editor);
crate::impl_filter_builder! {
/// # Examples
///
/// ```
/// # fn parse_markdown(s: String) -> String { s }
/// use discourse::Question;
///
/// let editor = Question::editor("description")
/// .filter(|description, previous_answers| parse_markdown(description))
/// .build();
/// ```
String; editor
}
crate::impl_validate_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let editor = Question::editor("description")
/// .validate(|description, previous_answers| if description.lines().count() >= 2 {
/// Ok(())
/// } else {
/// Err("Please enter a few lines".to_owned())
/// })
/// .build();
/// ```
str; editor
}
crate::impl_transform_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let editor = Question::editor("description")
/// .transform(|description, previous_answers, backend| {
/// write!(backend, "\n{}", description)
/// })
/// .build();
/// ```
str; editor
}
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
pub fn build(self) -> super::Question<'a> {
super::Question::new(self.opts, super::QuestionKind::Editor(self.editor))
}
}
impl<'a> From<EditorBuilder<'a>> for super::Question<'a> {
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
fn from(builder: EditorBuilder<'a>) -> Self {
builder.build()
}
}
// TODO: figure out a way to write tests for this

View File

@ -15,7 +15,7 @@ use crate::{Answer, Answers, ExpandItem};
mod tests;
#[derive(Debug)]
pub struct Expand<'a> {
pub(super) struct Expand<'a> {
choices: super::ChoiceList<ExpandItem<Text<String>>>,
selected: Option<char>,
default: char,
@ -68,7 +68,7 @@ impl<F: Fn(char) -> Option<char>> ExpandPrompt<'_, F> {
_ => None,
})
.find(|item| item.key == c)
.unwrap();
.expect("Validation would fail unless an option was chosen");
ExpandItem {
name: item.name.text,
@ -333,13 +333,42 @@ impl Expand<'_> {
&ans,
answers,
b,
b.write_styled(&ans.name.lines().next().unwrap().cyan())?
b.write_styled(
&ans.name
.lines()
.next()
.expect("There must be at least one line in a `str`")
.cyan()
)?
);
Ok(Answer::ExpandItem(ans))
}
}
/// The builder for a [`expand`] prompt.
///
/// See the various methods for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let expand = Question::expand("overwrite")
/// .message("Conflict on `file.rs`")
/// .choices(vec![
/// ('y', "Overwrite"),
/// ('a', "Overwrite this one and all next"),
/// ('d', "Show diff"),
/// ])
/// .default_separator()
/// .choice('x', "Abort")
/// .build();
/// ```
///
/// [`expand`]: crate::question::Question::expand
#[derive(Debug)]
pub struct ExpandBuilder<'a> {
opts: Options<'a>,
expand: Expand<'a>,
@ -355,26 +384,142 @@ impl<'a> ExpandBuilder<'a> {
}
}
crate::impl_options_builder! {
message
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let expand = Question::expand("overwrite")
/// .message("Conflict on `file.rs`")
/// .build();
/// ```
when
/// # Examples
///
/// ```
/// use discourse::{Question, Answers};
///
/// let expand = Question::expand("overwrite")
/// .when(|previous_answers: &Answers| match previous_answers.get("ignore-conflicts") {
/// Some(ans) => ans.as_bool().unwrap(),
/// None => true,
/// })
/// .build();
/// ```
ask_if_answered
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let expand = Question::expand("overwrite")
/// .ask_if_answered(true)
/// .build();
/// ```
}
/// Set a default key for the expand
///
/// If no key is entered by the user and they press `Enter`, the default key is used.
///
/// If `default` is unspecified, it defaults to the 'h' key.
///
/// # Panics
///
/// If the default given is not a key, it will cause a panic on [`build`]
///
/// [`build`]: Self::build
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let expand = Question::expand("overwrite")
/// .choice('d', "Show diff")
/// .default('d')
/// .build();
/// ```
pub fn default(mut self, default: char) -> Self {
self.expand.default = default;
self
}
pub fn separator<I: Into<String>>(mut self, text: I) -> Self {
self.expand
.choices
.choices
.push(Choice::Separator(text.into()));
/// The maximum height that can be taken by the expanded list
///
/// If the total height exceeds the page size, the list will be scrollable.
///
/// The `page_size` must be a minimum of 5. If `page_size` is not set, it will default to 15. It
/// will only be used if the user expands the prompt.
///
/// # Panics
///
/// It will panic if the `page_size` is less than 5.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let expand = Question::expand("overwrite")
/// .page_size(10)
/// .build();
/// ```
pub fn page_size(mut self, page_size: usize) -> Self {
assert!(page_size >= 5, "page size can be a minimum of 5");
self.expand.choices.set_page_size(page_size);
self
}
pub fn default_separator(mut self) -> Self {
self.expand.choices.choices.push(Choice::DefaultSeparator);
/// Whether to wrap around when user gets to the last element.
///
/// This only applies when the list is scrollable, i.e. page size > total height.
///
/// If `should_loop` is not set, it will default to `true`. It will only be used if the user
/// expands the prompt.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let expand = Question::expand("overwrite")
/// .should_loop(false)
/// .build();
/// ```
pub fn should_loop(mut self, should_loop: bool) -> Self {
self.expand.choices.set_should_loop(should_loop);
self
}
/// Inserts a [`Choice`] with the given key
///
/// See [`expand`] for more information.
///
/// [`Choice`]: super::Choice::Choice
/// [`expand`]: super::Question::expand
///
/// # Panics
///
/// It will panic if the key is 'h' or a duplicate.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let expand = Question::expand("overwrite")
/// .choice('x', "Abort")
/// .build();
/// ```
pub fn choice<I: Into<String>>(mut self, mut key: char, name: I) -> Self {
key = key.to_ascii_lowercase();
if key == 'h' {
panic!("Reserved key 'h'");
}
@ -392,6 +537,75 @@ impl<'a> ExpandBuilder<'a> {
self
}
/// Inserts a [`Separator`] with the given text
///
/// See [`expand`] for more information.
///
/// [`Separator`]: super::Choice::Separator
/// [`expand`]: super::Question::expand
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let expand = Question::expand("overwrite")
/// .separator("-- custom separator text --")
/// .build();
/// ```
pub fn separator<I: Into<String>>(mut self, text: I) -> Self {
self.expand
.choices
.choices
.push(Choice::Separator(text.into()));
self
}
/// Inserts a [`DefaultSeparator`]
///
/// See [`expand`] for more information.
///
/// [`DefaultSeparator`]: super::Choice::DefaultSeparator
/// [`expand`]: super::Question::expand
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let expand = Question::expand("overwrite")
/// .default_separator()
/// .build();
/// ```
pub fn default_separator(mut self) -> Self {
self.expand.choices.choices.push(Choice::DefaultSeparator);
self
}
/// Extends the given iterator of [`Choice`]s
///
/// See [`expand`] for more information.
///
/// [`Choice`]: super::Choice
/// [`expand`]: super::Question::expand
///
/// # Panics
///
/// It will panic if the key of any choice is 'h' or a duplicate.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let expand = Question::expand("overwrite")
/// .choices(vec![
/// ('y', "Overwrite"),
/// ('a', "Overwrite this one and all next"),
/// ('d', "Show diff"),
/// ])
/// .build();
/// ```
pub fn choices<I, T>(mut self, choices: I) -> Self
where
T: Into<Choice<ExpandItem<String>>>,
@ -424,19 +638,24 @@ impl<'a> ExpandBuilder<'a> {
self
}
pub fn page_size(mut self, page_size: usize) -> Self {
self.expand.choices.set_page_size(page_size);
self
crate::impl_transform_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let expand = Question::expand("overwrite")
/// .transform(|choice, previous_answers, backend| {
/// write!(backend, "({}) {}", choice.key, choice.name)
/// })
/// .build();
/// ```
ExpandItem<String>; expand
}
pub fn should_loop(mut self, should_loop: bool) -> Self {
self.expand.choices.set_should_loop(should_loop);
self
}
crate::impl_options_builder!();
crate::impl_transform_builder!(ExpandItem<String>; expand);
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
pub fn build(self) -> super::Question<'a> {
if !self.expand.has_valid_default() {
panic!(
@ -450,6 +669,9 @@ impl<'a> ExpandBuilder<'a> {
}
impl<'a> From<ExpandBuilder<'a>> for super::Question<'a> {
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
fn from(builder: ExpandBuilder<'a>) -> Self {
builder.build()
}

View File

@ -10,13 +10,29 @@ use ui::{
use super::{AutoComplete, ChoiceList, Completions, Filter, Options, Transform, Validate};
use crate::{Answer, Answers};
#[derive(Debug, Default)]
pub struct Input<'a> {
#[derive(Debug)]
pub(super) struct Input<'a> {
default: Option<String>,
filter: Filter<'a, String>,
validate: Validate<'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,
transform: Transform::None,
auto_complete: AutoComplete::None,
page_size: 15,
should_loop: true,
}
}
}
type CompletionSelector = widgets::Select<ChoiceList<widgets::Text<String>>>;
@ -93,6 +109,9 @@ impl Widget for InputPrompt<'_, '_> {
if select.is_some() {
key.code = KeyCode::Down;
} else {
let page_size = input_opts.page_size;
let should_loop = input_opts.should_loop;
input.replace_with(|s| {
let mut completions = ac(s, answers);
assert!(!completions.is_empty());
@ -101,9 +120,12 @@ impl Widget for InputPrompt<'_, '_> {
} else {
let res = std::mem::take(&mut completions[0]);
*select = Some(widgets::Select::new(
completions.into_iter().map(widgets::Text::new).collect(),
));
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
}
@ -205,6 +227,26 @@ impl<'i> Input<'i> {
}
}
/// The builder for an [`input`] prompt.
///
/// See the various methods for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let input = Question::input("name")
/// .message("What is your name?")
/// .default("John Doe")
/// .transform(|name, previous_answers, backend| {
/// write!(backend, "Hello, {}!", name)
/// })
/// .build();
/// ```
///
/// [`input`]: crate::question::Question::input
#[derive(Debug)]
pub struct InputBuilder<'a> {
opts: Options<'a>,
input: Input<'a>,
@ -218,23 +260,194 @@ impl<'a> InputBuilder<'a> {
}
}
crate::impl_options_builder! {
message
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let input = Question::input("name")
/// .message("What is your name?")
/// .build();
/// ```
when
/// # Examples
///
/// ```
/// use discourse::{Question, Answers};
///
/// let input = Question::input("name")
/// .when(|previous_answers: &Answers| match previous_answers.get("anonymous") {
/// Some(ans) => !ans.as_bool().unwrap(),
/// None => true,
/// })
/// .build();
/// ```
ask_if_answered
/// # Examples
///
/// ```
/// use discourse::{Question, Answers};
///
/// let input = Question::input("name")
/// .ask_if_answered(true)
/// .build();
/// ```
}
/// Set a default value for the input
///
/// If set and the user presses `Enter` without typing any text, the `default` is taken as the
/// answer.
///
/// If `default` is used, validation is skipped, but `filter` is still called.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let input = Question::input("name")
/// .default("John Doe")
/// .build();
/// ```
pub fn default<I: Into<String>>(mut self, default: I) -> Self {
self.input.default = Some(default.into());
self
}
crate::impl_options_builder!();
crate::impl_auto_complete_builder!(String; input);
crate::impl_filter_builder!(String; input);
crate::impl_validate_builder!(str; input);
crate::impl_transform_builder!(str; input);
crate::impl_auto_complete_builder! {
/// # Examples
///
/// ```
/// use discourse::{Question, question::completions};
///
/// let input = Question::input("name")
/// .auto_complete(|name, previous_answers| {
/// completions![name, "John Doe".to_owned()]
/// })
/// .build();
/// ```
///
/// For a better example on `auto_complete`, see [`examples/file.rs`]
///
/// [`examples/file.rs`]: https://github.com/Lutetium-Vanadium/discourse/blob/master/examples/file.rs
String; input
}
/// The maximum height that can be taken by the [`auto_complete`] selection list
///
/// If the total height exceeds the page size, the list will be scrollable.
///
/// The `page_size` must be a minimum of 5. If `page_size` is not set, it will default to 15. It
/// will only be used if [`auto_complete`] is set, and returns more than 1 completions.
///
/// [`auto_complete`]: InputBuilder::auto_complete
///
/// # Panics
///
/// It will panic if the `page_size` is less than 5.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let input = Question::input("name")
/// .page_size(10)
/// .build();
/// ```
pub fn page_size(mut self, page_size: usize) -> Self {
assert!(page_size >= 5, "page size can be a minimum of 5");
self.input.page_size = page_size;
self
}
/// Whether to wrap around when user gets to the last element.
///
/// This only applies when the list is scrollable, i.e. page size > total height.
///
/// If `should_loop` is not set, it will default to `true`. It will only be used if
/// [`auto_complete`] is set, and returns more than 1 completions.
///
/// [`auto_complete`]: InputBuilder::auto_complete
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let input = Question::input("name")
/// .should_loop(false)
/// .build();
/// ```
pub fn should_loop(mut self, should_loop: bool) -> Self {
self.input.should_loop = should_loop;
self
}
crate::impl_filter_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let input = Question::input("name")
/// .filter(|name, previous_answers| name + "!")
/// .build();
/// ```
String; input
}
crate::impl_validate_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let input = Question::input("name")
/// .validate(|name, previous_answers| if name.split_whitespace().count() >= 2 {
/// Ok(())
/// } else {
/// Err("Please enter your first and last name".to_owned())
/// })
/// .build();
/// ```
str; input
}
crate::impl_transform_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
/// use discourse::plugin::style::Stylize; // for .bold()
///
/// let input = Question::input("name")
/// .transform(|name, previous_answers, backend| {
/// backend.write_styled(&name.bold())
/// })
/// .build();
/// ```
str; input
}
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
pub fn build(self) -> super::Question<'a> {
super::Question::new(self.opts, super::QuestionKind::Input(self.input))
}
}
impl<'a> From<InputBuilder<'a>> for super::Question<'a> {
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
fn from(builder: InputBuilder<'a>) -> Self {
builder.build()
}

View File

@ -1,3 +1,5 @@
//! A module that contains things related to [`Question`]s.
mod choice;
mod confirm;
mod editor;
@ -12,6 +14,7 @@ mod password;
mod plugin;
mod raw_select;
pub use choice::Choice;
pub use confirm::ConfirmBuilder;
pub use editor::EditorBuilder;
pub use expand::ExpandBuilder;
@ -19,20 +22,70 @@ pub use input::InputBuilder;
pub use multi_select::MultiSelectBuilder;
pub use number::{FloatBuilder, IntBuilder};
pub use password::PasswordBuilder;
pub use plugin::PluginBuilder;
pub use plugin::{Plugin, PluginBuilder};
pub use raw_select::RawSelectBuilder;
pub use select::SelectBuilder;
use crate::{Answer, Answers};
pub use choice::Choice;
use choice::{get_sep_str, ChoiceList};
use options::Options;
pub use plugin::Plugin;
use plugin::PluginInteral;
use std::fmt;
use ui::{backend::Backend, events::EventIterator};
use std::fmt;
use crate::{Answer, Answers};
use choice::{get_sep_str, ChoiceList};
use options::Options;
use plugin::PluginInteral;
/// A `Question` that can be asked.
///
/// There are 11 variants.
///
/// - [`input`](Question::input)
/// - [`password`](Question::password)
/// - [`editor`](Question::editor)
/// - [`confirm`](Question::confirm)
/// - [`int`](Question::int)
/// - [`float`](Question::float)
/// - [`expand`](Question::expand)
/// - [`select`](Question::select)
/// - [`raw_select`](Question::raw_select)
/// - [`multi_select`](Question::multi_select)
/// - [`plugin`](Question::plugin)
///
/// Every [`Question`] has 4 common options.
///
/// - `name` (required): This is used as the key in [`Answers`].
/// It is not shown to the user unless `message` is unspecified.
///
/// - `message`: The message to display when the prompt is rendered in the terminal.
/// If it is not given, the `message` defaults to "\<name\>: ". It is recommended to set this as
/// `name` is meant to be a programmatic `id`.
///
/// - `when`: Whether to ask the question or not.
/// This can be used to have context based questions. If it is not given, it defaults to `true`.
///
/// - `ask_if_answered`: Prompt the question even if it is answered.
/// By default if an answer with the given `name` already exists, the question will be skipped.
/// This can be override by setting `ask_if_answered` is set to `true`.
///
/// A `Question` can be asked by creating a [`PromptModule`] or using [`prompt_one`] or
/// [`prompt_one_with`].
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let question = Question::input("name")
/// .message("What is your name?")
/// .default("John Doe")
/// .transform(|name, previous_answers, backend| {
/// write!(backend, "Hello, {}!", name)
/// })
/// .build();
/// ```
///
/// [`PromptModule`]: crate::PromptModule
/// [`prompt_one`]: crate::prompt_one
/// [`prompt_one_with`]: crate::prompt_one_with
#[derive(Debug)]
pub struct Question<'a> {
kind: QuestionKind<'a>,
@ -46,46 +99,308 @@ impl<'a> Question<'a> {
}
impl Question<'static> {
/// Prompt that takes user input and returns a [`String`]
///
/// See the various methods on the [`builder`] for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let input = Question::input("name")
/// .message("What is your name?")
/// .default("John Doe")
/// .transform(|name, previous_answers, backend| {
/// write!(backend, "Hello, {}!", name)
/// })
/// .build();
/// ```
///
/// [`builder`]: InputBuilder
pub fn input<N: Into<String>>(name: N) -> InputBuilder<'static> {
InputBuilder::new(name.into())
}
pub fn int<N: Into<String>>(name: N) -> IntBuilder<'static> {
IntBuilder::new(name.into())
}
pub fn float<N: Into<String>>(name: N) -> FloatBuilder<'static> {
FloatBuilder::new(name.into())
}
pub fn confirm<N: Into<String>>(name: N) -> ConfirmBuilder<'static> {
ConfirmBuilder::new(name.into())
}
pub fn select<N: Into<String>>(name: N) -> SelectBuilder<'static> {
SelectBuilder::new(name.into())
}
pub fn raw_select<N: Into<String>>(name: N) -> RawSelectBuilder<'static> {
RawSelectBuilder::new(name.into())
}
pub fn expand<N: Into<String>>(name: N) -> ExpandBuilder<'static> {
ExpandBuilder::new(name.into())
}
pub fn multi_select<N: Into<String>>(name: N) -> MultiSelectBuilder<'static> {
MultiSelectBuilder::new(name.into())
}
/// Prompt that takes user input and hides it.
///
/// See the various methods on the [`builder`] for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let password = Question::password("password")
/// .message("What is your password?")
/// .mask('*')
/// .build();
/// ```
///
/// [`builder`]: PasswordBuilder
pub fn password<N: Into<String>>(name: N) -> PasswordBuilder<'static> {
PasswordBuilder::new(name.into())
}
/// Prompt that takes launches the users preferred editor on a temporary file
///
/// Once the user exits their editor, the contents of the temporary file are read in as the
/// result. The editor to use is determined by the `$VISUAL` or `$EDITOR` environment variables.
/// If neither of those are present, `vim` (for unix) or `notepad` (for windows) is used.
///
/// See the various methods on the [`builder`] for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let password = Question::editor("description")
/// .message("Please enter a short description about yourself")
/// .extension(".md")
/// .build();
/// ```
///
/// [`builder`]: EditorBuilder
pub fn editor<N: Into<String>>(name: N) -> EditorBuilder<'static> {
EditorBuilder::new(name.into())
}
/// Prompt that returns `true` or `false`.
///
/// See the various methods on the [`builder`] for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let int = Question::confirm("anonymous")
/// .message("Do you want to remain anonymous?")
/// .build();
/// ```
///
/// [`builder`]: ConfirmBuilder
pub fn confirm<N: Into<String>>(name: N) -> ConfirmBuilder<'static> {
ConfirmBuilder::new(name.into())
}
/// Prompt that takes a [`i64`] as input.
///
/// The number is parsed using [`from_str`].
///
/// See the various methods on the [`builder`] for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let int = Question::int("age")
/// .message("What is your age?")
/// .validate(|age, previous_answers| {
/// if age > 0 && age < 130 {
/// Ok(())
/// } else {
/// Err(format!("You cannot be {} years old!", age))
/// }
/// })
/// .build();
/// ```
///
/// [`builder`]: IntBuilder
/// [`from_str`]: https://doc.rust-lang.org/std/primitive.i64.html#method.from_str
pub fn int<N: Into<String>>(name: N) -> IntBuilder<'static> {
IntBuilder::new(name.into())
}
/// Prompt that takes a [`f64`] as input.
///
/// The number is parsed using [`from_str`], but cannot be `NaN`.
///
/// See the various methods on the [`builder`] for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let float = Question::float("number")
/// .message("What is your favourite number?")
/// .validate(|num, previous_answers| {
/// if num.is_finite() {
/// Ok(())
/// } else {
/// Err("Please enter a finite number".to_owned())
/// }
/// })
/// .build();
/// ```
///
/// [`builder`]: FloatBuilder
/// [`from_str`]: https://doc.rust-lang.org/std/primitive.f64.html#method.from_str
pub fn float<N: Into<String>>(name: N) -> FloatBuilder<'static> {
FloatBuilder::new(name.into())
}
/// Prompt that allows the user to select from a list of options by key
///
/// The keys are ascii case-insensitive characters. The 'h' option is added by the prompt and
/// shouldn't be defined.
///
/// The choices are represented with the [`Choice`] enum. [`Choice::Choice`] can be multi-line,
/// but [`Choice::Separator`]s can only be single line.
///
/// See the various methods on the [`builder`] for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let expand = Question::expand("overwrite")
/// .message("Conflict on `file.rs`")
/// .choices(vec![
/// ('y', "Overwrite"),
/// ('a', "Overwrite this one and all next"),
/// ('d', "Show diff"),
/// ])
/// .default_separator()
/// .choice('x', "Abort")
/// .build();
/// ```
///
/// [`builder`]: ExpandBuilder
pub fn expand<N: Into<String>>(name: N) -> ExpandBuilder<'static> {
ExpandBuilder::new(name.into())
}
/// Prompt that allows the user to select from a list of options
///
/// The choices are represented with the [`Choice`] enum. [`Choice::Choice`] can be multi-line,
/// but [`Choice::Separator`]s can only be single line.
///
/// See the various methods on the [`builder`] for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::{Question, DefaultSeparator};
///
/// let select = Question::select("theme")
/// .message("What do you want to do?")
/// .choices(vec![
/// "Order a pizza".into(),
/// "Make a reservation".into(),
/// DefaultSeparator,
/// "Ask for opening hours".into(),
/// "Contact support".into(),
/// "Talk to the receptionist".into(),
/// ])
/// .build();
/// ```
///
/// [`builder`]: SelectBuilder
pub fn select<N: Into<String>>(name: N) -> SelectBuilder<'static> {
SelectBuilder::new(name.into())
}
/// Prompt that allows the user to select from a list of options with indices
///
/// The choices are represented with the [`Choice`] enum. [`Choice::Choice`] can be multi-line,
/// but [`Choice::Separator`]s can only be single line.
///
/// See the various methods on the [`builder`] for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::{Question, DefaultSeparator};
///
/// let raw_select = Question::raw_select("theme")
/// .message("What do you want to do?")
/// .choices(vec![
/// "Order a pizza".into(),
/// "Make a reservation".into(),
/// DefaultSeparator,
/// "Ask for opening hours".into(),
/// "Contact support".into(),
/// "Talk to the receptionist".into(),
/// ])
/// .build();
/// ```
///
/// [`builder`]: RawSelectBuilder
pub fn raw_select<N: Into<String>>(name: N) -> RawSelectBuilder<'static> {
RawSelectBuilder::new(name.into())
}
/// Prompt that allows the user to select multiple items from a list of options
///
/// Unlike the other list based prompts, this has a per choice boolean default.
///
/// The choices are represented with the [`Choice`] enum. [`Choice::Choice`] can be multi-line,
/// but [`Choice::Separator`]s can only be single line.
///
/// See the various methods on the [`builder`] for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::{Question, DefaultSeparator};
///
/// let multi_select = Question::multi_select("cheese")
/// .message("What cheese do you want?")
/// .choice_with_default("Mozzarella", true)
/// .choices(vec![
/// "Cheddar",
/// "Parmesan",
/// ])
/// .build();
/// ```
///
/// [`builder`]: MultiSelectBuilder
pub fn multi_select<N: Into<String>>(name: N) -> MultiSelectBuilder<'static> {
MultiSelectBuilder::new(name.into())
}
/// Create a [`Question`] from a custom prompt.
///
/// See [`Plugin`] for more information on writing custom prompts.
///
/// # Examples
///
/// ```
/// use discourse::{plugin, Question};
///
/// #[derive(Debug)]
/// struct MyPlugin { /* ... */ }
///
/// # impl MyPlugin {
/// # fn new() -> MyPlugin {
/// # MyPlugin {}
/// # }
/// # }
///
/// impl plugin::Plugin for MyPlugin {
/// fn ask(
/// self,
/// message: String,
/// answers: &plugin::Answers,
/// backend: &mut dyn plugin::Backend,
/// events: &mut dyn plugin::EventIterator,
/// ) -> discourse::Result<plugin::Answer> {
/// # todo!()
/// /* ... */
/// }
/// }
///
/// let plugin = Question::plugin("my-plugin", MyPlugin::new())
/// .message("Hello from MyPlugin!")
/// .build();
/// ```
///
/// [`builder`]: PluginBuilder
pub fn plugin<'a, N, P>(name: N, plugin: P) -> PluginBuilder<'a>
where
N: Into<String>,
@ -117,9 +432,13 @@ impl Question<'_> {
b: &mut B,
events: &mut I,
) -> ui::Result<Option<(String, Answer)>> {
if (!self.opts.ask_if_answered && answers.contains_key(&self.opts.name))
|| !self.opts.when.get(answers)
{
// Already asked
if !self.opts.ask_if_answered && answers.contains_key(&self.opts.name) {
return Ok(None);
}
// Shouldn't be asked
if !self.opts.when.get(answers) {
return Ok(None);
}
@ -209,11 +528,30 @@ macro_rules! handler {
};
}
/// The type which needs to be returned by the [`auto_complete`] function.
///
/// [`auto_complete`]: InputBuilder::auto_complete
#[cfg(feature = "smallvec")]
pub type Completions<T> = smallvec::SmallVec<[T; 1]>;
/// The type which needs to be returned by the [`auto_complete`] function.
///
/// [`auto_complete`]: InputBuilder::auto_complete
#[cfg(not(feature = "smallvec"))]
pub type Completions<T> = Vec<T>;
#[cfg(feature = "smallvec")]
pub use smallvec::smallvec as completions;
#[cfg(not(feature = "smallvec"))]
pub use std::vec as completions;
#[doc(hidden)]
#[macro_export]
macro_rules! __completions_count {
($e:expr) => (1);
($e:expr, $($rest:expr)+) => (1 + $(+ $crate::question::__completions_count!($rest) )+);
}
handler!(Filter, FnOnce(T, &Answers) -> T);
handler!(AutoComplete, FnMut(T, &Answers) -> Completions<T>);
handler!(Validate, ?Sized FnMut(&T, &Answers) -> Result<(), String>);
@ -227,7 +565,21 @@ handler!(
#[doc(hidden)]
#[macro_export]
macro_rules! impl_filter_builder {
($t:ty; $inner:ident) => {
// NOTE: the 2 extra lines at the end of each doc comment is intentional -- it makes sure that
// other docs that come from the macro invocation have appropriate spacing
($(#[$meta:meta])+ $t:ty; $inner:ident) => {
/// Function to change the final submitted value before it is displayed to the user and
/// added to the [`Answers`].
///
/// It is a [`FnOnce`] that is given the answer and the previous [`Answers`], and should
/// return the new answer.
///
/// This will be called after the answer has been validated.
///
/// [`Answers`]: crate::Answers
///
///
$(#[$meta])+
pub fn filter<F>(mut self, filter: F) -> Self
where
F: FnOnce($t, &crate::Answers) -> $t + 'a,
@ -241,7 +593,27 @@ macro_rules! impl_filter_builder {
#[doc(hidden)]
#[macro_export]
macro_rules! impl_auto_complete_builder {
($t:ty; $inner:ident) => {
// NOTE: the 2 extra lines at the end of each doc comment is intentional -- it makes sure that
// other docs that come from the macro invocation have appropriate spacing
($(#[$meta:meta])+ $t:ty; $inner:ident) => {
/// Function to suggest completions to the answer when the user presses `Tab`.
///
/// It is a [`FnMut`] that is given the current state of the answer and the previous
/// [`Answers`], and should return a list of completions.
///
/// There must be at least 1 completion. Returning 0 completions will cause a panic. If
/// there are no completions to give, you can simply return the state of the answer passed
/// to you.
///
/// If there is 1 completion, then the state of the answer becomes that completion.
///
/// If there are 2 or more completions, a list of completions is displayed from which the
/// user can pick one completion.
///
/// [`Answers`]: crate::Answers
///
///
$(#[$meta])+
pub fn auto_complete<F>(mut self, auto_complete: F) -> Self
where
F: FnMut($t, &crate::Answers) -> Completions<$t> + 'a,
@ -256,22 +628,34 @@ macro_rules! impl_auto_complete_builder {
#[doc(hidden)]
#[macro_export]
macro_rules! impl_validate_builder {
($t:ty; $inner:ident) => {
pub fn validate<F>(mut self, filter: F) -> Self
where
F: FnMut(&$t, &crate::Answers) -> Result<(), String> + 'a,
{
self.$inner.validate = crate::question::Validate::Sync(Box::new(filter));
self
}
($(#[$meta:meta])+ $t:ty; $inner:ident) => {
crate::impl_validate_builder!($(#[$meta])* impl &$t; $inner Validate);
};
(by val $t:ty; $inner:ident) => {
($(#[$meta:meta])+ by val $t:ty; $inner:ident) => {
crate::impl_validate_builder!($(#[$meta])* impl $t; $inner ValidateByVal);
};
// NOTE: the 2 extra lines at the end of each doc comment is intentional -- it makes sure that
// other docs that come from the macro invocation have appropriate spacing
($(#[$meta:meta])+ impl $t:ty; $inner:ident $handler:ident) => {
/// Function to validate the submitted value before it's returned.
///
/// It is a [`FnMut`] that is given the answer and the previous [`Answers`], and should
/// return `Ok(())` if the given answer is valid. If it is invalid, it should return an
/// [`Err`] with the error message to display to the user.
///
/// This will be called when the user presses the `Enter` key.
///
/// [`Answers`]: crate::Answers
///
///
$(#[$meta])*
pub fn validate<F>(mut self, filter: F) -> Self
where
F: FnMut($t, &crate::Answers) -> Result<(), String> + 'a,
{
self.$inner.validate = crate::question::ValidateByVal::Sync(Box::new(filter));
self.$inner.validate = crate::question::$handler::Sync(Box::new(filter));
self
}
};
@ -280,22 +664,34 @@ macro_rules! impl_validate_builder {
#[doc(hidden)]
#[macro_export]
macro_rules! impl_transform_builder {
($t:ty; $inner:ident) => {
pub fn transform<F>(mut self, transform: F) -> Self
where
F: FnOnce(&$t, &crate::Answers, &mut dyn Backend) -> std::io::Result<()> + 'a,
{
self.$inner.transform = crate::question::Transform::Sync(Box::new(transform));
self
}
($(#[$meta:meta])+ $t:ty; $inner:ident) => {
crate::impl_transform_builder!($(#[$meta])* impl &$t; $inner Transform);
};
(by val $t:ty; $inner:ident) => {
($(#[$meta:meta])+ by val $t:ty; $inner:ident) => {
crate::impl_transform_builder!($(#[$meta])* impl $t; $inner TransformByVal);
};
// NOTE: the 2 extra lines at the end of each doc comment is intentional -- it makes sure that
// other docs that come from the macro invocation have appropriate spacing
($(#[$meta:meta])+ impl $t:ty; $inner:ident $handler:ident) => {
/// Change the way the answer looks when displayed to the user.
///
/// It is a [`FnOnce`] that is given the answer, previous [`Answers`] and the [`Backend`] to
/// display the answer on. After the `transform` is called, a new line is also added.
///
/// It will only be called once the user finishes answering the question.
///
/// [`Answers`]: crate::Answers
/// [`Backend`]: crate::plugin::Backend
///
///
$(#[$meta])*
pub fn transform<F>(mut self, transform: F) -> Self
where
F: FnOnce($t, &crate::Answers, &mut dyn Backend) -> std::io::Result<()> + 'a,
{
self.$inner.transform = crate::question::TransformByVal::Sync(Box::new(transform));
self.$inner.transform = crate::question::$handler::Sync(Box::new(transform));
self
}
};

View File

@ -15,7 +15,7 @@ use crate::{Answer, Answers, ListItem};
mod tests;
#[derive(Debug, Default)]
pub struct MultiSelect<'a> {
pub(super) struct MultiSelect<'a> {
choices: super::ChoiceList<Text<String>>,
selected: Vec<bool>,
filter: Filter<'a, Vec<bool>>,
@ -23,6 +23,12 @@ pub struct MultiSelect<'a> {
transform: Transform<'a, [ListItem]>,
}
fn set_seperators_false(selected: &mut [bool], choices: &[Choice<Text<String>>]) {
for (i, choice) in choices.iter().enumerate() {
selected[i] &= !choice.is_separator();
}
}
struct MultiSelectPrompt<'a, 'c> {
prompt: widgets::Prompt<&'a str>,
select: widgets::Select<MultiSelect<'c>>,
@ -53,6 +59,10 @@ impl Prompt for MultiSelectPrompt<'_, '_> {
fn validate(&mut self) -> Result<Validation, Self::ValidateErr> {
if let Validate::Sync(ref mut validate) = self.select.list.validate {
set_seperators_false(
&mut self.select.list.selected,
&self.select.list.choices.choices,
);
validate(&self.select.list.selected, self.answers)?;
}
Ok(Validation::Finish)
@ -67,6 +77,8 @@ impl Prompt for MultiSelectPrompt<'_, '_> {
} = self.select.into_inner();
if let Filter::Sync(filter) = filter {
set_seperators_false(&mut selected, &choices.choices);
selected = filter(selected, self.answers);
}
@ -203,7 +215,15 @@ impl<'c> MultiSelect<'c> {
crate::write_final!(transform, message, &ans, answers, b, {
b.set_fg(Color::Cyan)?;
print_comma_separated(ans.iter().map(|item| item.name.lines().next().unwrap()), b)?;
print_comma_separated(
ans.iter().map(|item| {
item.name
.lines()
.next()
.expect("There must be at least one line in a `str`")
}),
b,
)?;
b.set_fg(Color::Reset)?;
});
@ -211,6 +231,29 @@ impl<'c> MultiSelect<'c> {
}
}
/// The builder for a [`multi_select`] prompt.
///
/// Unlike the other list based prompts, this has a per choice boolean default.
///
/// See the various methods for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::{Question, DefaultSeparator};
///
/// let multi_select = Question::multi_select("cheese")
/// .message("What cheese do you want?")
/// .choice_with_default("Mozzarella", true)
/// .choices(vec![
/// "Cheddar",
/// "Parmesan",
/// ])
/// .build();
/// ```
///
/// [`multi_select`]: crate::question::Question::multi_select
#[derive(Debug)]
pub struct MultiSelectBuilder<'a> {
opts: Options<'a>,
multi_select: MultiSelect<'a>,
@ -224,28 +267,129 @@ impl<'a> MultiSelectBuilder<'a> {
}
}
pub fn separator<I: Into<String>>(mut self, text: I) -> Self {
self.multi_select
.choices
.choices
.push(Choice::Separator(text.into()));
self.multi_select.selected.push(false);
crate::impl_options_builder! {
message
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let multi_select = Question::multi_select("cheese")
/// .message("What cheese do you want?")
/// .build();
/// ```
when
/// # Examples
///
/// ```
/// use discourse::{Answers, Question};
///
/// let multi_select = Question::multi_select("cheese")
/// .when(|previous_answers: &Answers| match previous_answers.get("vegan") {
/// Some(ans) => ans.as_bool().unwrap(),
/// None => true,
/// })
/// .build();
/// ```
ask_if_answered
/// # Examples
///
/// ```
/// use discourse::{Answers, Question};
///
/// let multi_select = Question::multi_select("cheese")
/// .ask_if_answered(true)
/// .build();
/// ```
}
/// The maximum height that can be taken by the list
///
/// If the total height exceeds the page size, the list will be scrollable.
///
/// The `page_size` must be a minimum of 5. If `page_size` is not set, it will default to 15.
///
/// # Panics
///
/// It will panic if the `page_size` is less than 5.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let multi_select = Question::multi_select("cheese")
/// .page_size(10)
/// .build();
/// ```
pub fn page_size(mut self, page_size: usize) -> Self {
assert!(page_size >= 5, "page size can be a minimum of 5");
self.multi_select.choices.set_page_size(page_size);
self
}
pub fn default_separator(mut self) -> Self {
self.multi_select
.choices
.choices
.push(Choice::DefaultSeparator);
self.multi_select.selected.push(false);
/// Whether to wrap around when user gets to the last element.
///
/// This only applies when the list is scrollable, i.e. page size > total height.
///
/// If `should_loop` is not set, it will default to `true`.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let multi_select = Question::multi_select("cheese")
/// .should_loop(false)
/// .build();
/// ```
pub fn should_loop(mut self, should_loop: bool) -> Self {
self.multi_select.choices.set_should_loop(should_loop);
self
}
/// Inserts a [`Choice`] with its default checked state as `false`.
///
/// If you want to set the default checked state, use [`choice_with_default`].
///
/// See [`multi_select`] for more information.
///
/// [`Choice`]: super::Choice::Choice
/// [`choice_with_default`]: Self::choice_with_default
/// [`multi_select`]: super::Question::multi_select
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let multi_select = Question::multi_select("cheese")
/// .choice("Cheddar")
/// .build();
/// ```
pub fn choice<I: Into<String>>(self, choice: I) -> Self {
self.choice_with_default(choice, false)
self.choice_with_default(choice.into(), false)
}
/// Inserts a [`Choice`] with a given default checked state.
///
/// See [`multi_select`] for more information.
///
/// [`Choice`]: super::Choice::Choice
/// [`multi_select`]: super::Question::multi_select
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let multi_select = Question::multi_select("cheese")
/// .choice_with_default("Mozzarella", true)
/// .build();
/// ```
pub fn choice_with_default<I: Into<String>>(mut self, choice: I, default: bool) -> Self {
self.multi_select
.choices
@ -255,6 +399,80 @@ impl<'a> MultiSelectBuilder<'a> {
self
}
/// Inserts a [`Separator`] with the given text
///
/// See [`multi_select`] for more information.
///
/// [`Separator`]: super::Choice::Separator
/// [`multi_select`]: super::Question::multi_select
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let multi_select = Question::multi_select("cheese")
/// .separator("-- custom separator text --")
/// .build();
/// ```
pub fn separator<I: Into<String>>(mut self, text: I) -> Self {
self.multi_select
.choices
.choices
.push(Choice::Separator(text.into()));
self.multi_select.selected.push(false);
self
}
/// Inserts a [`DefaultSeparator`]
///
/// See [`multi_select`] for more information.
///
/// [`DefaultSeparator`]: super::Choice::DefaultSeparator
/// [`multi_select`]: super::Question::multi_select
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let multi_select = Question::multi_select("cheese")
/// .default_separator()
/// .build();
/// ```
pub fn default_separator(mut self) -> Self {
self.multi_select
.choices
.choices
.push(Choice::DefaultSeparator);
self.multi_select.selected.push(false);
self
}
/// Extends the given iterator of [`Choice`]s
///
/// Every [`Choice::Choice`] within will have a default checked value of `false`. If you want to
/// set the default checked value, use [`choices_with_default`].
///
/// See [`multi_select`] for more information.
///
/// [`Choice`]: super::Choice
/// [`choices_with_default`]: Self::choices_with_default
/// [`multi_select`]: super::Question::multi_select
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let multi_select = Question::multi_select("cheese")
/// .choices(vec![
/// "Mozzarella",
/// "Cheddar",
/// "Parmesan",
/// ])
/// .build();
/// ```
pub fn choices<I, T>(mut self, choices: I) -> Self
where
T: Into<Choice<String>>,
@ -270,6 +488,26 @@ impl<'a> MultiSelectBuilder<'a> {
self
}
/// Extends the given iterator of [`Choice`]s with the given default checked value.
///
/// See [`multi_select`] for more information.
///
/// [`Choice`]: super::Choice
/// [`multi_select`]: super::Question::multi_select
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let multi_select = Question::multi_select("cheese")
/// .choices_with_default(vec![
/// ("Mozzarella", true),
/// ("Cheddar", false),
/// ("Parmesan", false),
/// ])
/// .build();
/// ```
pub fn choices_with_default<I, T>(mut self, choices: I) -> Self
where
T: Into<Choice<(String, bool)>>,
@ -309,21 +547,67 @@ impl<'a> MultiSelectBuilder<'a> {
self
}
pub fn page_size(mut self, page_size: usize) -> Self {
self.multi_select.choices.set_page_size(page_size);
self
crate::impl_filter_builder! {
/// NOTE: The boolean [`Vec`] contains a boolean value for each index even if it is a separator.
/// However it is guaranteed that all the separator indices will be false.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let multi_select = Question::multi_select("evil-cheese")
/// .filter(|mut cheeses, previous_answers| {
/// cheeses.iter_mut().for_each(|checked| *checked = !*checked);
/// cheeses
/// })
/// .build();
/// ```
Vec<bool>; multi_select
}
crate::impl_validate_builder! {
/// NOTE: The boolean [`slice`] contains a boolean value for each index even if it is a
/// separator. However it is guaranteed that all the separator indices will be false.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let multi_select = Question::multi_select("cheese")
/// .validate(|cheeses, previous_answers| {
/// if cheeses.iter().filter(|&&a| a).count() < 1 {
/// Err("You must choose at least one cheese.".into())
/// } else {
/// Ok(())
/// }
/// })
/// .build();
/// ```
[bool]; multi_select
}
pub fn should_loop(mut self, should_loop: bool) -> Self {
self.multi_select.choices.set_should_loop(should_loop);
self
crate::impl_transform_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let multi_select = Question::multi_select("cheese")
/// .transform(|cheeses, previous_answers, backend| {
/// for cheese in cheeses {
/// write!(backend, "({}) {}, ", cheese.index, cheese.name)?;
/// }
/// Ok(())
/// })
/// .build();
/// ```
[ListItem]; multi_select
}
crate::impl_options_builder!();
crate::impl_filter_builder!(Vec<bool>; multi_select);
crate::impl_validate_builder!([bool]; multi_select);
crate::impl_transform_builder!([ListItem]; multi_select);
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
pub fn build(self) -> super::Question<'a> {
super::Question::new(
self.opts,
@ -333,6 +617,9 @@ impl<'a> MultiSelectBuilder<'a> {
}
impl<'a> From<MultiSelectBuilder<'a>> for super::Question<'a> {
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
fn from(builder: MultiSelectBuilder<'a>) -> Self {
builder.build()
}

View File

@ -11,7 +11,7 @@ use super::{Filter, Options, TransformByVal as Transform, ValidateByVal as Valid
use crate::{Answer, Answers};
#[derive(Debug, Default)]
pub struct Float<'a> {
pub(super) struct Float<'a> {
default: Option<f64>,
filter: Filter<'a, f64>,
validate: Validate<'a, f64>,
@ -19,7 +19,7 @@ pub struct Float<'a> {
}
#[derive(Debug, Default)]
pub struct Int<'a> {
pub(super) struct Int<'a> {
default: Option<i64>,
filter: Filter<'a, i64>,
validate: Validate<'a, i64>,
@ -62,7 +62,7 @@ impl Float<'_> {
}
fn filter_map(c: char) -> Option<char> {
if Int::filter_map(c).is_some() || c == '.' || c == 'e' || c == 'E' {
if Int::filter_map(c).is_some() || ['.', 'e', 'E', 'i', 'n', 'f'].contains(&c) {
Some(c)
} else {
None
@ -117,7 +117,7 @@ macro_rules! impl_number_prompt {
self.input.replace_with(|mut s| {
s.clear();
write!(s, "{}", n).unwrap();
write!(s, "{}", n).expect("Failed to write number to the string");
s
});
true
@ -147,11 +147,13 @@ macro_rules! impl_number_prompt {
}
fn finish(self) -> Self::Output {
if self.input.value().is_empty() && self.number.default.is_some() {
return self.number.default.unwrap();
}
let n = match self.number.default {
Some(default) if self.input.value().is_empty() => default,
_ => self
.parse()
.expect("Validation would fail if number cannot be parsed"),
};
let n = self.parse().unwrap();
match self.number.filter {
Filter::Sync(filter) => filter(n, self.answers),
_ => n,
@ -204,7 +206,14 @@ impl_ask!(Int, IntPrompt);
impl_ask!(Float, FloatPrompt);
macro_rules! builder {
($builder_name:ident, $type:ident, $inner_ty:ty, $kind:expr) => {
($(#[$meta:meta])* struct $builder_name:ident : $type:ident -> $inner_ty:ty, $litral:expr;
declare = $declare:expr;
default = $default:expr;
filter = $filter:expr;
validate = $validate:expr;
) => {
$(#[$meta])*
#[derive(Debug)]
pub struct $builder_name<'a> {
opts: Options<'a>,
inner: $type<'a>,
@ -218,22 +227,123 @@ macro_rules! builder {
}
}
crate::impl_options_builder! {
message
/// # Examples
///
/// ```
/// use discourse::Question;
///
#[doc = $declare]
/// .message("Please enter a number")
/// .build();
/// ```
when
/// # Examples
///
/// ```
/// use discourse::{Question, Answers};
///
#[doc = $declare]
/// .when(|previous_answers: &Answers| match previous_answers.get("ask_number") {
/// Some(ans) => ans.as_bool().unwrap(),
/// None => true,
/// })
/// .build();
/// ```
ask_if_answered
/// # Examples
///
/// ```
/// use discourse::{Question, Answers};
///
#[doc = $declare]
/// .ask_if_answered(true)
/// .build();
/// ```
}
/// Set a default value
///
/// If the input text is empty, the `default` is taken as the answer.
///
/// If `default` is used, validation is skipped, but `filter` is still called.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
#[doc = $declare]
#[doc = $default]
/// .build();
/// ```
pub fn default(mut self, default: $inner_ty) -> Self {
self.inner.default = Some(default);
self
}
crate::impl_options_builder!();
crate::impl_filter_builder!($inner_ty; inner);
crate::impl_validate_builder!(by val $inner_ty; inner);
crate::impl_transform_builder!(by val $inner_ty; inner);
crate::impl_filter_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
#[doc = $declare]
#[doc = $filter]
/// .build();
/// ```
$inner_ty; inner
}
crate::impl_validate_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
#[doc = $declare]
/// .validate(|n, previous_answers| {
#[doc = $validate]
/// Ok(())
/// } else {
/// Err("Please enter a positive number".to_owned())
/// }
/// })
/// .build();
/// ```
by val $inner_ty; inner
}
crate::impl_transform_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
#[doc = $declare]
/// .transform(|n, previous_answers, backend| {
/// write!(backend, "{:e}", n)
/// })
/// .build();
/// ```
by val $inner_ty; inner
}
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
pub fn build(self) -> super::Question<'a> {
super::Question::new(self.opts, $kind(self.inner))
super::Question::new(self.opts, super::QuestionKind::$type(self.inner))
}
}
impl<'a> From<$builder_name<'a>> for super::Question<'a> {
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
fn from(builder: $builder_name<'a>) -> Self {
builder.build()
}
@ -241,8 +351,71 @@ macro_rules! builder {
};
}
builder!(IntBuilder, Int, i64, super::QuestionKind::Int);
builder!(FloatBuilder, Float, f64, super::QuestionKind::Float);
builder! {
/// The builder for an [`int`] prompt.
///
/// The number is parsed using [`from_str`].
///
/// See the various methods for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let int = Question::int("age")
/// .message("What is your age?")
/// .validate(|age, previous_answers| {
/// if age > 0 && age < 130 {
/// Ok(())
/// } else {
/// Err(format!("You cannot be {} years old!", age))
/// }
/// })
/// .build();
/// ```
///
/// [`from_str`]: https://doc.rust-lang.org/std/primitive.i64.html#method.from_str
/// [`int`]: crate::question::Question::int
struct IntBuilder: Int -> i64, 10;
declare = r#"let question = Question::int("int")"#;
default = " .default(10)";
filter = " .filter(|n, previous_answers| n + 10)";
validate = " if n.is_positive() {";
}
builder! {
/// The builder for a [`float`] prompt.
///
/// The number is parsed using [`from_str`], but cannot be `NaN`.
///
/// See the various methods for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let float = Question::float("number")
/// .message("What is your favourite number?")
/// .validate(|num, previous_answers| {
/// if num.is_finite() {
/// Ok(())
/// } else {
/// Err("Please enter a finite number".to_owned())
/// }
/// })
/// .build();
/// ```
///
/// [`float`]: crate::question::Question::float
/// [`from_str`]: https://doc.rust-lang.org/std/primitive.f64.html#method.from_str
struct FloatBuilder: Float -> f64, 10.0;
declare = r#"let question = Question::float("float")"#;
default = " .default(10.0)";
filter = " .filter(|n, previous_answers| (n * 10000.0).round() / 10000.0)";
validate = " if n.is_sign_positive() {";
}
macro_rules! test_numbers {
(mod $mod_name:ident { $prompt_name:ident, $default:expr }) => {

View File

@ -24,7 +24,21 @@ impl<'a> Options<'a> {
#[doc(hidden)]
#[macro_export]
macro_rules! impl_options_builder {
() => {
// NOTE: the 2 extra lines at the end of each doc comment is intentional -- it makes sure that
// other docs that come from the macro invocation have appropriate spacing
(message $(#[$message_meta:meta])+ when $(#[$when_meta:meta])+ ask_if_answered $(#[$ask_if_answered_meta:meta])+) => {
/// The message to display when the prompt is rendered in the terminal.
///
/// It can be either a [`String`] or a [`FnOnce`] that returns a [`String`]. If it is a
/// function, it is passed all the previous [`Answers`], and will be called right before the
/// question is prompted to the user.
///
/// If it is not given, the `message` defaults to "\<name\>: ".
///
/// [`Answers`]: crate::Answers
///
///
$(#[$message_meta])*
pub fn message<M>(mut self, message: M) -> Self
where
M: Into<crate::question::options::Getter<'a, String>>,
@ -33,6 +47,18 @@ macro_rules! impl_options_builder {
self
}
/// Whether to ask the question (`true`) or not (`false`).
///
/// It can be either a [`bool`] or a [`FnOnce`] that returns a [`bool`]. If it is a
/// function, it is passed all the previous [`Answers`], and will be called right before the
/// question is prompted to the user.
///
/// If it is not given, it defaults to `true`.
///
/// [`Answers`]: crate::Answers
///
///
$(#[$when_meta])*
pub fn when<W>(mut self, when: W) -> Self
where
W: Into<crate::question::options::Getter<'a, bool>>,
@ -41,18 +67,33 @@ macro_rules! impl_options_builder {
self
}
/// Prompt the question even if it is answered.
///
/// By default if an answer with the given `name` already exists, the question will be
/// skipped. This can be override by setting `ask_if_answered` is set to `true`.
///
/// If this is not given, it defaults to `false`.
///
/// If you need to dynamically decide whether the question should be asked, use [`when`].
///
/// [`Answers`]: crate::Answers
/// [`when`]: Self::when
///
///
$(#[$ask_if_answered_meta])*
pub fn ask_if_answered(mut self, ask_if_answered: bool) -> Self {
self.opts.ask_if_answered = ask_if_answered;
self
}
};
($t:ident; ($self:ident, $opts:ident) => $body:expr) => {
#[rustfmt::skip]
crate::impl_options_builder!($t<>; ($self, $opts) => $body);
};
}
/// Optionally dynamically get a value.
///
/// It can either be a [`FnOnce`] that results in a value, or the value itself.
///
/// This should not need to be constructed manually, as it is used with the [`Into`] trait.
#[allow(missing_docs)]
pub enum Getter<'a, T> {
Function(Box<dyn FnOnce(&Answers) -> T + 'a>),
Value(T),
@ -78,11 +119,7 @@ impl<T> Getter<'_, T> {
macro_rules! impl_getter_from_val {
($T:ty, $I:ty) => {
impl<'a> From<$I> for Getter<'a, $T> {
fn from(value: $I) -> Self {
Self::Value(value.into())
}
}
impl_getter_from_val!($T, $I, value => value.into());
};
($T:ty, $I:ty, $value:ident => $body:expr) => {

View File

@ -11,7 +11,7 @@ use super::{Filter, Options, Transform, Validate};
use crate::{Answer, Answers};
#[derive(Debug, Default)]
pub struct Password<'a> {
pub(super) struct Password<'a> {
mask: Option<char>,
filter: Filter<'a, String>,
validate: Validate<'a, str>,
@ -108,6 +108,23 @@ impl<'p> Password<'p> {
}
}
/// The builder for an [`password`] prompt.
///
/// See the various methods for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let password = Question::password("password")
/// .message("What is your password?")
/// .mask('*')
/// .build();
/// ```
///
/// [`password`]: crate::question::Question::password
#[derive(Debug)]
pub struct PasswordBuilder<'a> {
opts: Options<'a>,
password: Password<'a>,
@ -121,22 +138,127 @@ impl<'a> PasswordBuilder<'a> {
}
}
crate::impl_options_builder! {
message
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let password = Question::password("password")
/// .message("What is your password?")
/// .build();
/// ```
when
/// # Examples
///
/// ```
/// use discourse::{Answers, Question};
///
/// let password = Question::password("password")
/// .when(|previous_answers: &Answers| match previous_answers.get("anonymous") {
/// Some(ans) => !ans.as_bool().unwrap(),
/// None => true,
/// })
/// .build();
/// ```
ask_if_answered
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let password = Question::password("password")
/// .ask_if_answered(true)
/// .build();
/// ```
}
/// Set a mask to print instead of the characters
///
/// Each character when printed to the terminal will be replaced by the given mask. If a mask is
/// not provided, then input will be hidden.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let password = Question::password("password")
/// .mask('*')
/// .build();
/// ```
pub fn mask(mut self, mask: char) -> Self {
self.password.mask = Some(mask);
self
}
crate::impl_options_builder!();
crate::impl_filter_builder!(String; password);
crate::impl_validate_builder!(str; password);
crate::impl_transform_builder!(str; password);
crate::impl_filter_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// # fn encrypt(s: String) -> String { s }
///
/// let password = Question::password("password")
/// .filter(|password, previous_answers| encrypt(password))
/// .build();
/// ```
String; password
}
crate::impl_validate_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let password = Question::password("password")
/// .validate(|password, previous_answers| if password.chars().count() >= 5 {
/// Ok(())
/// } else {
/// Err("Your password be at least 5 characters long".to_owned())
/// })
/// .build();
/// ```
str; password
}
crate::impl_transform_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
/// use discourse::plugin::style::Color;
///
/// let password = Question::password("password")
/// .transform(|password, previous_answers, backend| {
/// backend.set_fg(Color::Cyan)?;
/// for _ in password.chars() {
/// write!(backend, "*")?;
/// }
/// backend.set_fg(Color::Reset)
/// })
/// .build();
/// ```
str; password
}
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
pub fn build(self) -> super::Question<'a> {
super::Question::new(self.opts, super::QuestionKind::Password(self.password))
}
}
impl<'a> From<PasswordBuilder<'a>> for super::Question<'a> {
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
fn from(builder: PasswordBuilder<'a>) -> Self {
builder.build()
}

View File

@ -3,12 +3,24 @@ use ui::{backend::Backend, events::EventIterator};
use super::{Options, Question, QuestionKind};
use crate::{Answer, Answers};
/// Plugins are a way to write custom [`Question`]s.
///
/// The plugin is given a `message`, the previous [`Answers`] and a [`Backend`] and
/// [`EventIterator`]. Using these, it is responsible for doing everything from rendering to user
/// interaction. While no particular look is enforced, it is recommended to keep a similar look to
/// the rest of the in-built questions.
///
/// You can use the `discourse-ui` crate to build the prompts. You can see the implementations of
/// the in-built questions for examples on how to use it.
///
/// See also [`Question::plugin`]
pub trait Plugin: std::fmt::Debug {
/// Prompt the user with the given message, [`Answers`], [`Backend`] and [`EventIterator`]
fn ask(
self,
message: String,
answers: &Answers,
stdout: &mut dyn Backend,
backend: &mut dyn Backend,
events: &mut dyn EventIterator,
) -> ui::Result<Answer>;
}
@ -25,7 +37,7 @@ pub(super) trait PluginInteral: std::fmt::Debug {
&mut self,
message: String,
answers: &Answers,
stdout: &mut dyn Backend,
backend: &mut dyn Backend,
events: &mut dyn EventIterator,
) -> ui::Result<Answer>;
}
@ -35,15 +47,51 @@ impl<T: Plugin> PluginInteral for Option<T> {
&mut self,
message: String,
answers: &Answers,
stdout: &mut dyn Backend,
backend: &mut dyn Backend,
events: &mut dyn EventIterator,
) -> ui::Result<Answer> {
self.take()
.expect("Plugin::ask called twice")
.ask(message, answers, stdout, events)
.ask(message, answers, backend, events)
}
}
/// The builder for custom questions.
///
/// See [`Plugin`] for more information on writing custom prompts.
///
/// # Examples
///
/// ```
/// use discourse::{plugin, Question};
///
/// #[derive(Debug)]
/// struct MyPlugin { /* ... */ }
///
/// # impl MyPlugin {
/// # fn new() -> MyPlugin {
/// # MyPlugin {}
/// # }
/// # }
///
/// impl plugin::Plugin for MyPlugin {
/// fn ask(
/// self,
/// message: String,
/// answers: &plugin::Answers,
/// backend: &mut dyn plugin::Backend,
/// events: &mut dyn plugin::EventIterator,
/// ) -> discourse::Result<plugin::Answer> {
/// // ...
/// # todo!()
/// }
/// }
///
/// let plugin = Question::plugin("my-plugin", MyPlugin::new())
/// .message("Hello from MyPlugin!")
/// .build();
/// ```
#[derive(Debug)]
pub struct PluginBuilder<'a> {
opts: Options<'a>,
plugin: Box<dyn PluginInteral + 'a>,
@ -63,14 +111,118 @@ impl<'a> PluginBuilder<'a> {
}
}
crate::impl_options_builder!();
crate::impl_options_builder! {
message
/// # Examples
///
/// ```
/// use discourse::{plugin, Question};
///
/// #[derive(Debug)]
/// struct MyPlugin { /* ... */ }
///
/// # impl MyPlugin {
/// # fn new() -> MyPlugin {
/// # MyPlugin {}
/// # }
/// # }
///
/// impl plugin::Plugin for MyPlugin {
/// fn ask(
/// self,
/// message: String,
/// answers: &plugin::Answers,
/// backend: &mut dyn plugin::Backend,
/// events: &mut dyn plugin::EventIterator,
/// ) -> discourse::Result<plugin::Answer> {
/// // ...
/// # todo!()
/// }
/// }
///
/// let plugin = Question::plugin("my-plugin", MyPlugin::new())
/// .message("Hello from MyPlugin!")
/// .build();
/// ```
when
/// # Examples
///
/// ```
/// use discourse::{plugin, Question, Answers};
///
/// #[derive(Debug)]
/// struct MyPlugin { /* ... */ }
///
/// # impl MyPlugin {
/// # fn new() -> MyPlugin {
/// # MyPlugin {}
/// # }
/// # }
///
/// impl plugin::Plugin for MyPlugin {
/// fn ask(
/// self,
/// message: String,
/// answers: &plugin::Answers,
/// backend: &mut dyn plugin::Backend,
/// events: &mut dyn plugin::EventIterator,
/// ) -> discourse::Result<plugin::Answer> {
/// // ...
/// # todo!()
/// }
/// }
///
/// let plugin = Question::plugin("my-plugin", MyPlugin::new())
/// .when(|previous_answers: &Answers| match previous_answers.get("use-custom-prompt") {
/// Some(ans) => !ans.as_bool().unwrap(),
/// None => true,
/// })
/// .build();
/// ```
ask_if_answered
/// # Examples
///
/// ```
/// use discourse::{plugin, Question};
///
/// #[derive(Debug)]
/// struct MyPlugin { /* ... */ }
///
/// # impl MyPlugin {
/// # fn new() -> MyPlugin {
/// # MyPlugin {}
/// # }
/// # }
///
/// impl plugin::Plugin for MyPlugin {
/// fn ask(
/// self,
/// message: String,
/// answers: &plugin::Answers,
/// backend: &mut dyn plugin::Backend,
/// events: &mut dyn plugin::EventIterator,
/// ) -> discourse::Result<plugin::Answer> {
/// // ...
/// # todo!()
/// }
/// }
///
/// let plugin = Question::plugin("my-plugin", MyPlugin::new())
/// .ask_if_answered(true)
/// .build();
/// ```
}
/// Consumes the builder returning a [`Question`]
pub fn build(self) -> Question<'a> {
Question::new(self.opts, QuestionKind::Plugin(self.plugin))
}
}
impl<'a> From<PluginBuilder<'a>> for Question<'a> {
/// Consumes the builder returning a [`Question`]
fn from(builder: PluginBuilder<'a>) -> Self {
builder.build()
}

View File

@ -16,7 +16,7 @@ mod tests;
// Kind of a bad name
#[derive(Debug, Default)]
pub struct RawSelect<'a> {
pub(super) struct RawSelect<'a> {
choices: super::ChoiceList<(usize, Text<String>)>,
transform: Transform<'a, ListItem>,
}
@ -214,16 +214,46 @@ impl<'a> RawSelect<'a> {
&ans,
answers,
b,
b.write_styled(&ans.name.lines().next().unwrap().cyan())?
b.write_styled(
&ans.name
.lines()
.next()
.expect("There must be at least one line in a `str`")
.cyan()
)?
);
Ok(Answer::ListItem(ans))
}
}
/// The builder for a [`raw_select`] prompt.
///
/// See the various methods for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::{Question, DefaultSeparator};
///
/// let raw_select = Question::raw_select("theme")
/// .message("What do you want to do?")
/// .choices(vec![
/// "Order a pizza".into(),
/// "Make a reservation".into(),
/// DefaultSeparator,
/// "Ask for opening hours".into(),
/// "Contact support".into(),
/// "Talk to the receptionist".into(),
/// ])
/// .build();
/// ```
///
/// [`raw_select`]: crate::question::Question::raw_select
#[derive(Debug)]
pub struct RawSelectBuilder<'a> {
opts: Options<'a>,
list: RawSelect<'a>,
raw_select: RawSelect<'a>,
choice_count: usize,
}
@ -231,32 +261,149 @@ impl<'a> RawSelectBuilder<'a> {
pub(crate) fn new(name: String) -> Self {
RawSelectBuilder {
opts: Options::new(name),
list: Default::default(),
raw_select: Default::default(),
// It is one indexed for the user
choice_count: 1,
}
}
crate::impl_options_builder! {
message
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let raw_select = Question::raw_select("theme")
/// .message("What do you want to do?")
/// .build();
/// ```
when
/// # Examples
///
/// ```
/// use discourse::{Question, Answers};
///
/// let raw_select = Question::raw_select("theme")
/// .when(|previous_answers: &Answers| match previous_answers.get("use-default-theme") {
/// Some(ans) => ans.as_bool().unwrap(),
/// None => true,
/// })
/// .build();
/// ```
ask_if_answered
/// # Examples
///
/// ```
/// use discourse::{Question, Answers};
///
/// let raw_select = Question::raw_select("theme")
/// .ask_if_answered(true)
/// .build();
/// ```
}
/// Set a default index for the select
///
/// The given index will be hovered in the beginning.
///
/// If `default` is unspecified, the first [`Choice`] will be hovered.
///
/// # Panics
///
/// If the default given is not a [`Choice`], it will cause a panic on [`build`]
///
/// [`Choice`]: super::Choice
/// [`build`]: Self::build
///
/// # Examples
///
/// ```
/// use discourse::{Question, DefaultSeparator};
///
/// let raw_select = Question::raw_select("theme")
/// .choices(vec![
/// "Order a pizza".into(),
/// "Make a reservation".into(),
/// DefaultSeparator,
/// "Ask for opening hours".into(),
/// "Contact support".into(),
/// "Talk to the receptionist".into(),
/// ])
/// .default(1)
/// .build();
/// ```
pub fn default(mut self, default: usize) -> Self {
self.list.choices.set_default(default);
self.raw_select.choices.set_default(default);
self
}
pub fn separator<I: Into<String>>(mut self, text: I) -> Self {
self.list
.choices
.choices
.push(Choice::Separator(text.into()));
/// The maximum height that can be taken by the list
///
/// If the total height exceeds the page size, the list will be scrollable.
///
/// The `page_size` must be a minimum of 5. If `page_size` is not set, it will default to 15.
///
/// # Panics
///
/// It will panic if the `page_size` is less than 5.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let raw_select = Question::raw_select("theme")
/// .page_size(10)
/// .build();
/// ```
pub fn page_size(mut self, page_size: usize) -> Self {
assert!(page_size >= 5, "page size can be a minimum of 5");
self.raw_select.choices.set_page_size(page_size);
self
}
pub fn default_separator(mut self) -> Self {
self.list.choices.choices.push(Choice::DefaultSeparator);
/// Whether to wrap around when user gets to the last element.
///
/// This only applies when the list is scrollable, i.e. page size > total height.
///
/// If `should_loop` is not set, it will default to `true`.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let raw_select = Question::raw_select("theme")
/// .should_loop(false)
/// .build();
/// ```
pub fn should_loop(mut self, should_loop: bool) -> Self {
self.raw_select.choices.set_should_loop(should_loop);
self
}
/// Inserts a [`Choice`].
///
/// See [`raw_select`] for more information.
///
/// [`Choice`]: super::Choice::Choice
/// [`raw_select`]: super::Question::raw_select
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let raw_select = Question::raw_select("theme")
/// .choice("Order a Pizza")
/// .build();
/// ```
pub fn choice<I: Into<String>>(mut self, choice: I) -> Self {
self.list.choices.choices.push(Choice::Choice((
self.raw_select.choices.choices.push(Choice::Choice((
self.choice_count,
Text::new(choice.into()),
)));
@ -264,13 +411,84 @@ impl<'a> RawSelectBuilder<'a> {
self
}
/// Inserts a [`Separator`] with the given text
///
/// See [`raw_select`] for more information.
///
/// [`Separator`]: super::Choice::Separator
/// [`raw_select`]: super::Question::raw_select
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let raw_select = Question::raw_select("theme")
/// .separator("-- custom separator text --")
/// .build();
/// ```
pub fn separator<I: Into<String>>(mut self, text: I) -> Self {
self.raw_select
.choices
.choices
.push(Choice::Separator(text.into()));
self
}
/// Inserts a [`DefaultSeparator`]
///
/// See [`raw_select`] for more information.
///
/// [`DefaultSeparator`]: super::Choice::DefaultSeparator
/// [`raw_select`]: super::Question::raw_select
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let raw_select = Question::raw_select("theme")
/// .default_separator()
/// .build();
/// ```
pub fn default_separator(mut self) -> Self {
self.raw_select
.choices
.choices
.push(Choice::DefaultSeparator);
self
}
/// Extends the given iterator of [`Choice`]s
///
/// See [`raw_select`] for more information.
///
/// [`Choice`]: super::Choice
/// [`raw_select`]: super::Question::raw_select
///
/// # Examples
///
/// ```
/// use discourse::{Question, DefaultSeparator};
///
/// let raw_select = Question::raw_select("theme")
/// .choices(vec![
/// "Order a pizza".into(),
/// "Make a reservation".into(),
/// DefaultSeparator,
/// "Ask for opening hours".into(),
/// "Contact support".into(),
/// "Talk to the receptionist".into(),
/// ])
/// .build();
/// ```
pub fn choices<I, T>(mut self, choices: I) -> Self
where
T: Into<Choice<String>>,
I: IntoIterator<Item = T>,
{
let choice_count = &mut self.choice_count;
self.list
self.raw_select
.choices
.choices
.extend(choices.into_iter().map(|choice| {
@ -283,25 +501,33 @@ impl<'a> RawSelectBuilder<'a> {
self
}
pub fn page_size(mut self, page_size: usize) -> Self {
self.list.choices.set_page_size(page_size);
self
crate::impl_transform_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let raw_select = Question::raw_select("theme")
/// .transform(|choice, previous_answers, backend| {
/// write!(backend, "({}) {}", choice.index, choice.name)
/// })
/// .build();
/// ```
ListItem; raw_select
}
pub fn should_loop(mut self, should_loop: bool) -> Self {
self.list.choices.set_should_loop(should_loop);
self
}
crate::impl_options_builder!();
crate::impl_transform_builder!(ListItem; list);
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
pub fn build(self) -> super::Question<'a> {
super::Question::new(self.opts, super::QuestionKind::RawSelect(self.list))
super::Question::new(self.opts, super::QuestionKind::RawSelect(self.raw_select))
}
}
impl<'a> From<RawSelectBuilder<'a>> for super::Question<'a> {
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
fn from(builder: RawSelectBuilder<'a>) -> Self {
builder.build()
}

View File

@ -32,7 +32,7 @@ fn unwrap_select<'a>(question: impl Into<Question<'a>>) -> RawSelect<'a> {
}
}
fn raw_select(message: &str) -> RawSelectPrompt {
fn raw_select(message: &str) -> RawSelectPrompt<'_> {
unwrap_select(RawSelectBuilder::new("name".into()).choices(choices(10))).into_prompt(message)
}

View File

@ -12,7 +12,7 @@ use super::{Options, Transform};
use crate::{Answer, Answers, ListItem};
#[derive(Debug, Default)]
pub struct Select<'a> {
pub(super) struct Select<'a> {
choices: super::ChoiceList<Text<String>>,
transform: Transform<'a, ListItem>,
}
@ -130,61 +130,276 @@ impl<'a> Select<'a> {
&ans,
answers,
b,
b.write_styled(&ans.name.lines().next().unwrap().cyan())?
b.write_styled(
&ans.name
.lines()
.next()
.expect("There must be at least one line in a `str`")
.cyan()
)?
);
Ok(Answer::ListItem(ans))
}
}
/// The builder for a [`select`] prompt.
///
/// See the various methods for more details on each available option.
///
/// # Examples
///
/// ```
/// use discourse::{Question, DefaultSeparator};
///
/// let select = Question::select("theme")
/// .message("What do you want to do?")
/// .choices(vec![
/// "Order a pizza".into(),
/// "Make a reservation".into(),
/// DefaultSeparator,
/// "Ask for opening hours".into(),
/// "Contact support".into(),
/// "Talk to the receptionist".into(),
/// ])
/// .build();
/// ```
///
/// [`select`]: crate::question::Question::select
#[derive(Debug)]
pub struct SelectBuilder<'a> {
opts: Options<'a>,
list: Select<'a>,
select: Select<'a>,
}
impl<'a> SelectBuilder<'a> {
pub(crate) fn new(name: String) -> Self {
SelectBuilder {
opts: Options::new(name),
list: Default::default(),
select: Default::default(),
}
}
crate::impl_options_builder! {
message
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let select = Question::select("theme")
/// .message("What do you want to do?")
/// .build();
/// ```
when
/// # Examples
///
/// ```
/// use discourse::{Question, Answers};
///
/// let select = Question::select("theme")
/// .when(|previous_answers: &Answers| match previous_answers.get("use-default-theme") {
/// Some(ans) => ans.as_bool().unwrap(),
/// None => true,
/// })
/// .build();
/// ```
ask_if_answered
/// # Examples
///
/// ```
/// use discourse::{Question, Answers};
///
/// let select = Question::select("theme")
/// .ask_if_answered(true)
/// .build();
/// ```
}
/// Set a default index for the select
///
/// The given index will be hovered in the beginning.
///
/// If `default` is unspecified, the first [`Choice`] will be hovered.
///
/// # Panics
///
/// If the default given is not a [`Choice`], it will cause a panic on [`build`]
///
/// [`Choice`]: super::Choice
/// [`build`]: Self::build
///
/// # Examples
///
/// ```
/// use discourse::{Question, DefaultSeparator};
///
/// let select = Question::select("theme")
/// .choices(vec![
/// "Order a pizza".into(),
/// "Make a reservation".into(),
/// DefaultSeparator,
/// "Ask for opening hours".into(),
/// "Contact support".into(),
/// "Talk to the receptionist".into(),
/// ])
/// .default(1)
/// .build();
/// ```
pub fn default(mut self, default: usize) -> Self {
self.list.choices.set_default(default);
self.select.choices.set_default(default);
self
}
pub fn separator<I: Into<String>>(mut self, text: I) -> Self {
self.list
.choices
.choices
.push(super::Choice::Separator(text.into()));
/// The maximum height that can be taken by the list
///
/// If the total height exceeds the page size, the list will be scrollable.
///
/// The `page_size` must be a minimum of 5. If `page_size` is not set, it will default to 15.
///
/// # Panics
///
/// It will panic if the `page_size` is less than 5.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let select = Question::select("theme")
/// .page_size(10)
/// .build();
/// ```
pub fn page_size(mut self, page_size: usize) -> Self {
assert!(page_size >= 5, "page size can be a minimum of 5");
self.select.choices.set_page_size(page_size);
self
}
pub fn default_separator(mut self) -> Self {
self.list
.choices
.choices
.push(super::Choice::DefaultSeparator);
/// Whether to wrap around when user gets to the last element.
///
/// This only applies when the list is scrollable, i.e. page size > total height.
///
/// If `should_loop` is not set, it will default to `true`.
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let select = Question::select("theme")
/// .should_loop(false)
/// .build();
/// ```
pub fn should_loop(mut self, should_loop: bool) -> Self {
self.select.choices.set_should_loop(should_loop);
self
}
/// Inserts a [`Choice`].
///
/// See [`select`] for more information.
///
/// [`Choice`]: super::Choice::Choice
/// [`select`]: super::Question::select
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let select = Question::select("theme")
/// .choice("Order a Pizza")
/// .build();
/// ```
pub fn choice<I: Into<String>>(mut self, choice: I) -> Self {
self.list
self.select
.choices
.choices
.push(super::Choice::Choice(Text::new(choice.into())));
self
}
/// Inserts a [`Separator`] with the given text
///
/// See [`select`] for more information.
///
/// [`Separator`]: super::Choice::Separator
/// [`select`]: super::Question::select
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let select = Question::select("theme")
/// .separator("-- custom separator text --")
/// .build();
/// ```
pub fn separator<I: Into<String>>(mut self, text: I) -> Self {
self.select
.choices
.choices
.push(super::Choice::Separator(text.into()));
self
}
/// Inserts a [`DefaultSeparator`]
///
/// See [`select`] for more information.
///
/// [`DefaultSeparator`]: super::Choice::DefaultSeparator
/// [`select`]: super::Question::select
///
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let select = Question::select("theme")
/// .default_separator()
/// .build();
/// ```
pub fn default_separator(mut self) -> Self {
self.select
.choices
.choices
.push(super::Choice::DefaultSeparator);
self
}
/// Extends the given iterator of [`Choice`]s
///
/// See [`select`] for more information.
///
/// [`Choice`]: super::Choice
/// [`select`]: super::Question::select
///
/// # Examples
///
/// ```
/// use discourse::{Question, DefaultSeparator};
///
/// let select = Question::select("theme")
/// .choices(vec![
/// "Order a pizza".into(),
/// "Make a reservation".into(),
/// DefaultSeparator,
/// "Ask for opening hours".into(),
/// "Contact support".into(),
/// "Talk to the receptionist".into(),
/// ])
/// .build();
/// ```
pub fn choices<I, T>(mut self, choices: I) -> Self
where
T: Into<super::Choice<String>>,
I: IntoIterator<Item = T>,
{
self.list.choices.choices.extend(
self.select.choices.choices.extend(
choices
.into_iter()
.map(|choice| choice.into().map(Text::new)),
@ -192,25 +407,39 @@ impl<'a> SelectBuilder<'a> {
self
}
pub fn page_size(mut self, page_size: usize) -> Self {
self.list.choices.set_page_size(page_size);
self
crate::impl_transform_builder! {
/// # Examples
///
/// ```
/// use discourse::Question;
///
/// let raw_select = Question::raw_select("theme")
/// .transform(|choice, previous_answers, backend| {
/// write!(backend, "({}) {}", choice.index, choice.name)
/// })
/// .build();
/// ```
ListItem; select
}
pub fn should_loop(mut self, should_loop: bool) -> Self {
self.list.choices.set_should_loop(should_loop);
self
}
crate::impl_options_builder!();
crate::impl_transform_builder!(ListItem; list);
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
pub fn build(self) -> super::Question<'a> {
super::Question::new(self.opts, super::QuestionKind::Select(self.list))
if let Some(default) = self.select.choices.default() {
if self.select.choices[default].is_separator() {
panic!("Invalid default '{}' is not a `Choice`", default);
}
}
super::Question::new(self.opts, super::QuestionKind::Select(self.select))
}
}
impl<'a> From<SelectBuilder<'a>> for super::Question<'a> {
/// Consumes the builder returning a [`Question`]
///
/// [`Question`]: crate::question::Question
fn from(builder: SelectBuilder<'a>) -> Self {
builder.build()
}

View File

@ -135,8 +135,6 @@ fn test_float() {
t.pass("valid");
t.compile_fail("auto_complete");
t.compile_fail("choices");
t.compile_fail("should_loop");
t.compile_fail("page_size");
t.compile_fail("mask");
t.compile_fail("extension");
t.compile_fail("plugin");
@ -149,8 +147,6 @@ fn test_input() {
t.pass("valid");
t.compile_fail("choices");
t.compile_fail("should_loop");
t.compile_fail("page_size");
t.compile_fail("mask");
t.compile_fail("extension");
t.compile_fail("plugin");

View File

@ -4,7 +4,7 @@ fn main() {
default: 'c',
transform: |_, _, _| Ok(()),
choices: [('c', "choice")],
page_size: 0,
page_size: 10,
should_loop: true,
}];
}

View File

@ -1,3 +0,0 @@
fn main() {
discourse::questions![Input { page_size: todo!() }];
}

View File

@ -1,5 +0,0 @@
error: option `page_size` does not exist for kind `input`
--> $DIR/page_size.rs:2:35
|
2 | discourse::questions![Input { page_size: todo!() }];
| ^^^^^^^^^

View File

@ -1,5 +0,0 @@
fn main() {
discourse::questions![Input {
should_loop: todo!()
}];
}

View File

@ -1,5 +0,0 @@
error: option `should_loop` does not exist for kind `input`
--> $DIR/should_loop.rs:3:9
|
3 | should_loop: todo!()
| ^^^^^^^^^^^

View File

@ -2,9 +2,11 @@ fn main() {
discourse::questions![Input {
name: "name",
default: "hello world",
should_loop: true,
page_size: 10,
transform: |_, _, _| Ok(()),
validate: |_, _| Ok(()),
filter: |t, _| t,
auto_complete: |t, _| discourse::question::Completions::from([t]),
auto_complete: |t, _| discourse::question::completions![t],
}];
}

View File

@ -17,7 +17,7 @@ fn main() {
"choice" default true,
choice default default_choice || false,
],
page_size: 0,
page_size: 10,
should_loop: true,
}
];

View File

@ -8,7 +8,7 @@ impl Plugin for TestPlugin {
self,
_message: String,
_answers: &Answers,
_stdout: &mut dyn Backend,
_backend: &mut dyn Backend,
_events: &mut dyn EventIterator,
) -> discourse::Result<Answer> {
Ok(Answer::Int(0))

View File

@ -4,7 +4,7 @@ fn main() {
default: 0,
transform: |_, _, _| Ok(()),
choices: ["choice"],
page_size: 0,
page_size: 10,
should_loop: true,
}];
}

View File

@ -4,7 +4,7 @@ fn main() {
default: 0,
transform: |_, _, _| Ok(()),
choices: ["choice"],
page_size: 0,
page_size: 10,
should_loop: true,
}];
}