Added docs for discourse
This commit is contained in:
parent
d1c1272a1a
commit
f5067a0111
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"rust-analyzer.cargo.allFeatures": true,
|
||||
"rust-analyzer.diagnostics.disabled": [
|
||||
"incorrect-ident-case",
|
||||
"inactive-code"
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use discourse::plugin::ui::style::Stylize;
|
||||
use discourse::plugin::style::Stylize;
|
||||
use discourse::Question;
|
||||
|
||||
fn map_err<E>(_: E) {}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
180
src/answer.rs
180
src/answer.rs
|
@ -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>,
|
||||
|
|
237
src/lib.rs
237
src/lib.rs
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -4,7 +4,7 @@ fn main() {
|
|||
default: 'c',
|
||||
transform: |_, _, _| Ok(()),
|
||||
choices: [('c', "choice")],
|
||||
page_size: 0,
|
||||
page_size: 10,
|
||||
should_loop: true,
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
discourse::questions![Input { page_size: todo!() }];
|
||||
}
|
|
@ -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!() }];
|
||||
| ^^^^^^^^^
|
|
@ -1,5 +0,0 @@
|
|||
fn main() {
|
||||
discourse::questions![Input {
|
||||
should_loop: todo!()
|
||||
}];
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
error: option `should_loop` does not exist for kind `input`
|
||||
--> $DIR/should_loop.rs:3:9
|
||||
|
|
||||
3 | should_loop: todo!()
|
||||
| ^^^^^^^^^^^
|
|
@ -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],
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ fn main() {
|
|||
"choice" default true,
|
||||
choice default default_choice || false,
|
||||
],
|
||||
page_size: 0,
|
||||
page_size: 10,
|
||||
should_loop: true,
|
||||
}
|
||||
];
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -4,7 +4,7 @@ fn main() {
|
|||
default: 0,
|
||||
transform: |_, _, _| Ok(()),
|
||||
choices: ["choice"],
|
||||
page_size: 0,
|
||||
page_size: 10,
|
||||
should_loop: true,
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ fn main() {
|
|||
default: 0,
|
||||
transform: |_, _, _| Ok(()),
|
||||
choices: ["choice"],
|
||||
page_size: 0,
|
||||
page_size: 10,
|
||||
should_loop: true,
|
||||
}];
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue