requestty/question/
mod.rs

1//! A module that contains things related to [`Question`]s.
2
3mod choice;
4mod confirm;
5mod editor;
6mod expand;
7mod handler;
8#[macro_use]
9mod impl_macros;
10mod input;
11mod multi_select;
12mod number;
13mod order_select;
14#[macro_use]
15mod options;
16mod custom_prompt;
17mod password;
18mod raw_select;
19mod select;
20
21pub use choice::Choice;
22pub use confirm::ConfirmBuilder;
23pub use custom_prompt::{CustomPromptBuilder, Prompt};
24pub use editor::EditorBuilder;
25pub use expand::ExpandBuilder;
26pub use input::InputBuilder;
27pub use multi_select::MultiSelectBuilder;
28pub use number::{FloatBuilder, IntBuilder};
29pub use order_select::{builder::OrderSelectBuilder, OrderSelectItem};
30pub use password::PasswordBuilder;
31pub use raw_select::RawSelectBuilder;
32pub use select::SelectBuilder;
33
34use ui::{backend::Backend, events::EventIterator};
35
36use crate::{Answer, Answers};
37use choice::{get_sep_str, ChoiceList};
38use custom_prompt::CustomPromptInteral;
39use handler::{
40    AutoComplete, Filter, Transform, TransformByVal, Validate, ValidateByVal, ValidateOnKey,
41    ValidateOnKeyByVal,
42};
43use options::Options;
44
45/// A `Question` that can be asked.
46///
47/// There are 12 variants.
48///
49/// - [`input`](Question::input)
50/// - [`password`](Question::password)
51/// - [`editor`](Question::editor)
52/// - [`confirm`](Question::confirm)
53/// - [`int`](Question::int)
54/// - [`float`](Question::float)
55/// - [`expand`](Question::expand)
56/// - [`select`](Question::select)
57/// - [`raw_select`](Question::raw_select)
58/// - [`multi_select`](Question::multi_select)
59/// - [`order_select`](Question::order_select)
60/// - [`custom`](Question::custom)
61///
62/// Every [`Question`] has 4 common options.
63///
64/// - `name` (required): This is used as the key in [`Answers`].
65///   It is not shown to the user unless `message` is unspecified.
66///
67/// - `message`: The message to display when the prompt is rendered in the terminal.
68///   If it is not given, the `message` defaults to "\<name\>: ". It is recommended to set this as
69///   `name` is meant to be a programmatic `id`.
70///
71/// - `when`: Whether to ask the question or not.
72///   This can be used to have context based questions. If it is not given, it defaults to `true`.
73///
74/// - `ask_if_answered`: Prompt the question even if it is answered.
75///   By default if an answer with the given `name` already exists, the question will be skipped.
76///   This can be override by setting `ask_if_answered` is set to `true`.
77///
78/// A `Question` can be asked by creating a [`PromptModule`] or using [`prompt_one`] or
79/// [`prompt_one_with`].
80///
81/// # Examples
82///
83/// ```
84/// use requestty::Question;
85///
86/// let question = Question::input("name")
87///     .message("What is your name?")
88///     .default("John Doe")
89///     .transform(|name, previous_answers, backend| {
90///         write!(backend, "Hello, {}!", name)
91///     })
92///     .build();
93/// ```
94///
95/// [`PromptModule`]: crate::PromptModule
96/// [`prompt_one`]: crate::prompt_one
97/// [`prompt_one_with`]: crate::prompt_one_with
98#[derive(Debug)]
99pub struct Question<'a> {
100    kind: QuestionKind<'a>,
101    opts: Options<'a>,
102}
103
104impl<'a> Question<'a> {
105    fn new(opts: Options<'a>, kind: QuestionKind<'a>) -> Self {
106        Self { kind, opts }
107    }
108}
109
110impl Question<'static> {
111    /// Prompt that takes user input and returns a [`String`]
112    ///
113    /// <img
114    ///   src="https://raw.githubusercontent.com/lutetium-vanadium/requestty/master/assets/input.gif"
115    ///   style="max-height: 11rem"
116    /// />
117    ///
118    /// See the various methods on the [`builder`] for more details on each available option.
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use requestty::Question;
124    ///
125    /// let input = Question::input("name")
126    ///     .message("What is your name?")
127    ///     .default("John Doe")
128    ///     .transform(|name, previous_answers, backend| {
129    ///         write!(backend, "Hello, {}!", name)
130    ///     })
131    ///     .build();
132    /// ```
133    ///
134    /// [`builder`]: InputBuilder
135    pub fn input<N: Into<String>>(name: N) -> InputBuilder<'static> {
136        InputBuilder::new(name.into())
137    }
138
139    /// Prompt that takes user input and hides it.
140    ///
141    /// How it looks if you set a mask:
142    ///
143    /// <img
144    ///   src="https://raw.githubusercontent.com/lutetium-vanadium/requestty/master/assets/password-mask.gif"
145    ///   style="max-height: 11rem"
146    /// />
147    ///
148    /// How it looks if you do not set a mask:
149    ///
150    /// <img
151    ///   src="https://raw.githubusercontent.com/lutetium-vanadium/requestty/master/assets/password-hidden.gif"
152    ///   style="max-height: 11rem"
153    /// />
154    ///
155    /// See the various methods on the [`builder`] for more details on each available option.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use requestty::Question;
161    ///
162    /// let password = Question::password("password")
163    ///     .message("What is your password?")
164    ///     .mask('*')
165    ///     .build();
166    /// ```
167    ///
168    /// [`builder`]: PasswordBuilder
169    pub fn password<N: Into<String>>(name: N) -> PasswordBuilder<'static> {
170        PasswordBuilder::new(name.into())
171    }
172
173    /// Prompt that takes launches the users preferred editor on a temporary file
174    ///
175    /// Once the user exits their editor, the contents of the temporary file are read in as the
176    /// result. The editor to use can be specified by the [`editor`] method. If unspecified, the
177    /// editor is determined by the `$VISUAL` or `$EDITOR` environment variables. If neither of
178    /// those are present, `vim` (for unix) or `notepad` (for windows) is used.
179    ///
180    /// <img
181    ///   src="https://raw.githubusercontent.com/lutetium-vanadium/requestty/master/assets/editor.gif"
182    ///   style="max-height: 30rem"
183    /// />
184    ///
185    /// See the various methods on the [`builder`] for more details on each available option.
186    ///
187    /// # Examples
188    ///
189    /// ```
190    /// use requestty::Question;
191    ///
192    /// let editor = Question::editor("description")
193    ///     .message("Please enter a short description about yourself")
194    ///     .extension(".md")
195    ///     .build();
196    /// ```
197    ///
198    /// [`builder`]: EditorBuilder
199    /// [`editor`]: EditorBuilder::editor
200    pub fn editor<N: Into<String>>(name: N) -> EditorBuilder<'static> {
201        EditorBuilder::new(name.into())
202    }
203
204    /// Prompt that returns `true` or `false`.
205    ///
206    /// <img
207    ///   src="https://raw.githubusercontent.com/lutetium-vanadium/requestty/master/assets/confirm.gif"
208    ///   style="max-height: 11rem"
209    /// />
210    ///
211    /// See the various methods on the [`builder`] for more details on each available option.
212    ///
213    /// # Examples
214    ///
215    /// ```
216    /// use requestty::Question;
217    ///
218    /// let confirm = Question::confirm("anonymous")
219    ///     .message("Do you want to remain anonymous?")
220    ///     .build();
221    /// ```
222    ///
223    /// [`builder`]: ConfirmBuilder
224    pub fn confirm<N: Into<String>>(name: N) -> ConfirmBuilder<'static> {
225        ConfirmBuilder::new(name.into())
226    }
227
228    /// Prompt that takes a [`i64`] as input.
229    ///
230    /// The number is parsed using [`from_str`].
231    ///
232    /// <img
233    ///   src="https://raw.githubusercontent.com/lutetium-vanadium/requestty/master/assets/int.gif"
234    ///   style="max-height: 11rem"
235    /// />
236    ///
237    /// See the various methods on the [`builder`] for more details on each available option.
238    ///
239    /// # Examples
240    ///
241    /// ```
242    /// use requestty::Question;
243    ///
244    /// let int = Question::int("age")
245    ///     .message("What is your age?")
246    ///     .validate(|age, previous_answers| {
247    ///         if age > 0 && age < 130 {
248    ///             Ok(())
249    ///         } else {
250    ///             Err(format!("You cannot be {} years old!", age))
251    ///         }
252    ///     })
253    ///     .build();
254    /// ```
255    ///
256    /// [`builder`]: IntBuilder
257    /// [`from_str`]: https://doc.rust-lang.org/std/primitive.i64.html#method.from_str
258    pub fn int<N: Into<String>>(name: N) -> IntBuilder<'static> {
259        IntBuilder::new(name.into())
260    }
261
262    /// Prompt that takes a [`f64`] as input.
263    ///
264    /// The number is parsed using [`from_str`], but cannot be `NaN`.
265    ///
266    /// <img
267    ///   src="https://raw.githubusercontent.com/lutetium-vanadium/requestty/master/assets/float.gif"
268    ///   style="max-height: 11rem"
269    /// />
270    ///
271    /// See the various methods on the [`builder`] for more details on each available option.
272    ///
273    /// # Examples
274    ///
275    /// ```
276    /// use requestty::Question;
277    ///
278    /// let float = Question::float("number")
279    ///     .message("What is your favourite number?")
280    ///     .validate(|num, previous_answers| {
281    ///         if num.is_finite() {
282    ///             Ok(())
283    ///         } else {
284    ///             Err("Please enter a finite number".to_owned())
285    ///         }
286    ///     })
287    ///     .build();
288    /// ```
289    ///
290    /// [`builder`]: FloatBuilder
291    /// [`from_str`]: https://doc.rust-lang.org/std/primitive.f64.html#method.from_str
292    pub fn float<N: Into<String>>(name: N) -> FloatBuilder<'static> {
293        FloatBuilder::new(name.into())
294    }
295
296    /// Prompt that allows the user to select from a list of options by key
297    ///
298    /// The keys are ascii case-insensitive characters. The 'h' option is added by the prompt and
299    /// shouldn't be defined.
300    ///
301    /// The choices are represented with the [`Choice`] enum. [`Choice::Choice`] can be multi-line,
302    /// but [`Choice::Separator`]s can only be single line.
303    ///
304    /// <img
305    ///   src="https://raw.githubusercontent.com/lutetium-vanadium/requestty/master/assets/expand.gif"
306    ///   style="max-height: 15rem"
307    /// />
308    ///
309    /// See the various methods on the [`builder`] for more details on each available option.
310    ///
311    /// # Examples
312    ///
313    /// ```
314    /// use requestty::Question;
315    ///
316    /// let expand = Question::expand("overwrite")
317    ///     .message("Conflict on `file.rs`")
318    ///     .choices(vec![
319    ///         ('y', "Overwrite"),
320    ///         ('a', "Overwrite this one and all next"),
321    ///         ('d', "Show diff"),
322    ///     ])
323    ///     .default_separator()
324    ///     .choice('x', "Abort")
325    ///     .build();
326    /// ```
327    ///
328    /// [`builder`]: ExpandBuilder
329    pub fn expand<N: Into<String>>(name: N) -> ExpandBuilder<'static> {
330        ExpandBuilder::new(name.into())
331    }
332
333    /// Prompt that allows the user to select from a list of options
334    ///
335    /// The choices are represented with the [`Choice`] enum. [`Choice::Choice`] can be multi-line,
336    /// but [`Choice::Separator`]s can only be single line.
337    ///
338    /// <img
339    ///   src="https://raw.githubusercontent.com/lutetium-vanadium/requestty/master/assets/select.gif"
340    ///   style="max-height: 15rem"
341    /// />
342    ///
343    /// See the various methods on the [`builder`] for more details on each available option.
344    ///
345    /// # Examples
346    ///
347    /// ```
348    /// use requestty::{Question, DefaultSeparator};
349    ///
350    /// let select = Question::select("theme")
351    ///     .message("What do you want to do?")
352    ///     .choices(vec![
353    ///         "Order a pizza".into(),
354    ///         "Make a reservation".into(),
355    ///         DefaultSeparator,
356    ///         "Ask for opening hours".into(),
357    ///         "Contact support".into(),
358    ///         "Talk to the receptionist".into(),
359    ///     ])
360    ///     .build();
361    /// ```
362    ///
363    /// [`builder`]: SelectBuilder
364    pub fn select<N: Into<String>>(name: N) -> SelectBuilder<'static> {
365        SelectBuilder::new(name.into())
366    }
367
368    /// Prompt that allows the user to select from a list of options with indices
369    ///
370    /// The choices are represented with the [`Choice`] enum. [`Choice::Choice`] can be multi-line,
371    /// but [`Choice::Separator`]s can only be single line.
372    ///
373    /// <img
374    ///   src="https://raw.githubusercontent.com/lutetium-vanadium/requestty/master/assets/raw-select.gif"
375    ///   style="max-height: 15rem"
376    /// />
377    ///
378    /// See the various methods on the [`builder`] for more details on each available option.
379    ///
380    /// # Examples
381    ///
382    /// ```
383    /// use requestty::{Question, DefaultSeparator};
384    ///
385    /// let raw_select = Question::raw_select("theme")
386    ///     .message("What do you want to do?")
387    ///     .choices(vec![
388    ///         "Order a pizza".into(),
389    ///         "Make a reservation".into(),
390    ///         DefaultSeparator,
391    ///         "Ask for opening hours".into(),
392    ///         "Contact support".into(),
393    ///         "Talk to the receptionist".into(),
394    ///     ])
395    ///     .build();
396    /// ```
397    ///
398    /// [`builder`]: RawSelectBuilder
399    pub fn raw_select<N: Into<String>>(name: N) -> RawSelectBuilder<'static> {
400        RawSelectBuilder::new(name.into())
401    }
402
403    /// Prompt that allows the user to select multiple items from a list of options
404    ///
405    /// Unlike the other list based prompts, this has a per choice boolean default.
406    ///
407    /// The choices are represented with the [`Choice`] enum. [`Choice::Choice`] can be multi-line,
408    /// but [`Choice::Separator`]s can only be single line.
409    ///
410    /// <img
411    ///   src="https://raw.githubusercontent.com/lutetium-vanadium/requestty/master/assets/multi-select.gif"
412    ///   style="max-height: 20rem"
413    /// />
414    ///
415    /// See the various methods on the [`builder`] for more details on each available option.
416    ///
417    /// # Examples
418    ///
419    /// ```
420    /// use requestty::{Question, DefaultSeparator};
421    ///
422    /// let multi_select = Question::multi_select("cheese")
423    ///     .message("What cheese do you want?")
424    ///     .choice_with_default("Mozzarella", true)
425    ///     .choices(vec![
426    ///         "Cheddar",
427    ///         "Parmesan",
428    ///     ])
429    ///     .build();
430    /// ```
431    ///
432    /// [`builder`]: MultiSelectBuilder
433    pub fn multi_select<N: Into<String>>(name: N) -> MultiSelectBuilder<'static> {
434        MultiSelectBuilder::new(name.into())
435    }
436
437    /// Prompt that allows the user to organize a list of options.
438    ///
439    /// The choices are [`String`]s and can be multiline.
440    ///
441    /// <img
442    ///   src="https://raw.githubusercontent.com/lutetium-vanadium/requestty/master/assets/order-select.gif"
443    ///   style="max-height: 20rem"
444    /// />
445    ///
446    /// See the various methods on the [`builder`] for more details on each available option.
447    ///
448    /// # Examples
449    ///
450    /// ```
451    /// use requestty::{Question, DefaultSeparator};
452    ///
453    /// let multi_select = Question::order_select("tasks")
454    ///     .message("Please organize the tasks to be done at home")
455    ///     .choices(vec![
456    ///         "Make the bed",
457    ///         "Clean the dishes",
458    ///         "Mow the lawn",
459    ///     ])
460    ///     .build();
461    /// ```
462    ///
463    /// [`builder`]: OrderSelectBuilder
464    pub fn order_select<N: Into<String>>(name: N) -> OrderSelectBuilder<'static> {
465        OrderSelectBuilder::new(name.into())
466    }
467
468    /// Create a [`Question`] from a custom prompt.
469    ///
470    /// See [`Prompt`] for more information on writing custom prompts and the various methods on the
471    /// [`builder`] for more details on each available option.
472    ///
473    /// # Examples
474    ///
475    /// ```
476    /// use requestty::{prompt, Question};
477    ///
478    /// #[derive(Debug)]
479    /// struct MyPrompt { /* ... */ }
480    ///
481    /// # impl MyPrompt {
482    /// #     fn new() -> MyPrompt {
483    /// #         MyPrompt {}
484    /// #     }
485    /// # }
486    ///
487    /// impl prompt::Prompt for MyPrompt {
488    ///     fn ask(
489    ///         self,
490    ///         message: String,
491    ///         answers: &prompt::Answers,
492    ///         backend: &mut dyn prompt::Backend,
493    ///         events: &mut dyn prompt::EventIterator,
494    ///     ) -> requestty::Result<Option<prompt::Answer>> {
495    /// #       todo!()
496    ///         /* ... */
497    ///     }
498    /// }
499    ///
500    /// let prompt = Question::custom("my-prompt", MyPrompt::new())
501    ///     .message("Hello from MyPrompt!")
502    ///     .build();
503    /// ```
504    ///
505    /// [`builder`]: CustomPromptBuilder
506    pub fn custom<'a, N, P>(name: N, prompt: P) -> CustomPromptBuilder<'a>
507    where
508        N: Into<String>,
509        P: Prompt + 'a,
510    {
511        CustomPromptBuilder::new(name.into(), Box::new(Some(prompt)))
512    }
513}
514
515#[derive(Debug)]
516enum QuestionKind<'a> {
517    Input(input::Input<'a>),
518    Int(number::Int<'a>),
519    Float(number::Float<'a>),
520    Confirm(confirm::Confirm<'a>),
521    Select(select::Select<'a>),
522    RawSelect(raw_select::RawSelect<'a>),
523    Expand(expand::Expand<'a>),
524    MultiSelect(multi_select::MultiSelect<'a>),
525    OrderSelect(order_select::OrderSelect<'a>),
526    Password(password::Password<'a>),
527    Editor(editor::Editor<'a>),
528    Custom(Box<dyn CustomPromptInteral + 'a>),
529}
530
531impl Question<'_> {
532    pub(crate) fn ask<B: Backend, I: EventIterator>(
533        self,
534        answers: &Answers,
535        b: &mut B,
536        events: &mut I,
537    ) -> ui::Result<Option<(String, Answer)>> {
538        // Already asked
539        if !self.opts.ask_if_answered && answers.contains_key(&self.opts.name) {
540            return Ok(None);
541        }
542
543        // Shouldn't be asked
544        if !self.opts.when.get(answers) {
545            return Ok(None);
546        }
547
548        let name = self.opts.name;
549        let message = self
550            .opts
551            .message
552            .map(|message| message.get(answers))
553            .unwrap_or_else(|| name.clone() + ":");
554        let on_esc = self.opts.on_esc.get(answers);
555
556        let res = match self.kind {
557            QuestionKind::Input(i) => i.ask(message, on_esc, answers, b, events)?,
558            QuestionKind::Int(i) => i.ask(message, on_esc, answers, b, events)?,
559            QuestionKind::Float(f) => f.ask(message, on_esc, answers, b, events)?,
560            QuestionKind::Confirm(c) => c.ask(message, on_esc, answers, b, events)?,
561            QuestionKind::Select(l) => l.ask(message, on_esc, answers, b, events)?,
562            QuestionKind::RawSelect(r) => r.ask(message, on_esc, answers, b, events)?,
563            QuestionKind::Expand(e) => e.ask(message, on_esc, answers, b, events)?,
564            QuestionKind::MultiSelect(c) => c.ask(message, on_esc, answers, b, events)?,
565            QuestionKind::OrderSelect(c) => c.ask(message, on_esc, answers, b, events)?,
566            QuestionKind::Password(p) => p.ask(message, on_esc, answers, b, events)?,
567            QuestionKind::Editor(e) => e.ask(message, on_esc, answers, b, events)?,
568            QuestionKind::Custom(mut o) => o.ask(message, answers, b, events)?,
569        };
570
571        Ok(res.map(|res| (name, res)))
572    }
573}
574
575/// The type which needs to be returned by the [`auto_complete`] function.
576///
577/// [`auto_complete`]: InputBuilder::auto_complete
578#[cfg(feature = "smallvec")]
579pub type Completions<T> = smallvec::SmallVec<[T; 1]>;
580
581/// The type which needs to be returned by the [`auto_complete`] function.
582///
583/// [`auto_complete`]: InputBuilder::auto_complete
584#[cfg(not(feature = "smallvec"))]
585pub type Completions<T> = Vec<T>;
586
587#[cfg(feature = "smallvec")]
588pub use smallvec::smallvec as completions;
589
590#[cfg(not(feature = "smallvec"))]
591pub use std::vec as completions;