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;