inquire/prompts/
text.rs

1use std::cmp::min;
2
3use crate::{
4    autocompletion::{Autocomplete, NoAutoCompletion, Replacement},
5    config::{self, get_configuration},
6    error::{InquireError, InquireResult},
7    formatter::{StringFormatter, DEFAULT_STRING_FORMATTER},
8    input::Input,
9    list_option::ListOption,
10    terminal::get_default_terminal,
11    ui::{Backend, Key, KeyModifiers, RenderConfig, TextBackend},
12    utils::paginate,
13    validator::{ErrorMessage, StringValidator, Validation},
14};
15
16const DEFAULT_HELP_MESSAGE_WITH_AC: &str = "↑↓ to move, tab to autocomplete, enter to submit";
17
18/// Standard text prompt that returns the user string input.
19///
20/// This is the standard the standard kind of prompt you would expect from a library like this one. It displays a message to the user, prompting them to type something back. The user's input is then stored in a `String` and returned to the prompt caller.
21///
22///
23/// ## Configuration options
24///
25/// - **Prompt message**: Main message when prompting the user for input, `"What is your name?"` in the example below.
26/// - **Help message**: Message displayed at the line below the prompt.
27/// - **Default value**: Default value returned when the user submits an empty response.
28/// - **Initial value**: Initial value of the prompt's text input, in case you want to display the prompt with something already filled in.
29/// - **Placeholder**: Short hint that describes the expected value of the input.
30/// - **Validators**: Custom validators to the user's input, displaying an error message if the input does not pass the requirements.
31/// - **Formatter**: Custom formatter in case you need to pre-process the user input before showing it as the final answer.
32/// - **Suggester**: Custom function that returns a list of input suggestions based on the current text input. See more on "Autocomplete" below.
33///
34/// ## Default behaviors
35///
36/// Default behaviors for each one of `Text` configuration options:
37///
38/// - The input formatter just echoes back the given input.
39/// - No validators are called, accepting any sort of input including empty ones.
40/// - No default values or help messages.
41/// - No autocompletion features set-up.
42/// - Prompt messages are always required when instantiating via `new()`.
43///
44/// ## Autocomplete
45///
46/// With `Text` inputs, it is also possible to set-up an autocompletion system to provide a better UX when necessary.
47///
48/// You can call `with_autocomplete()` and provide a value that implements the `Autocomplete` trait. The `Autocomplete` trait has two provided methods: `get_suggestions` and `get_completion`.
49///
50/// - `get_suggestions` is called whenever the user's text input is modified, e.g. a new letter is typed, returning a `Vec<String>`. The `Vec<String>` is the list of suggestions that the prompt displays to the user according to their text input. The user can then navigate through the list and if they submit while highlighting one of these suggestions, the suggestion is treated as the final answer.
51/// - `get_completion` is called whenever the user presses the autocompletion hotkey (`tab` by default), with the current text input and the text of the currently highlighted suggestion, if any, as parameters. This method should return whether any text replacement (an autocompletion) should be made. If the prompt receives a replacement to be made, it substitutes the current text input for the string received from the `get_completion` call.
52///
53/// For example, in the `complex_autocompletion.rs` example file, the `FilePathCompleter` scans the file system based on the current text input, storing a list of paths that match the current text input.
54///
55/// Everytime `get_suggestions` is called, the method returns the list of paths that match the user input. When the user presses the autocompletion hotkey, the `FilePathCompleter` checks whether there is any path selected from the list, if there is, it decides to replace the current text input for it. The interesting piece of functionality is that if there isn't a path selected from the list, the `FilePathCompleter` calculates the longest common prefix amongst all scanned paths and updates the text input to an unambiguous new value. Similar to how terminals work when traversing paths.
56///
57/// # Example
58///
59/// ```no_run
60/// use inquire::Text;
61///
62/// let name = Text::new("What is your name?").prompt();
63///
64/// match name {
65///     Ok(name) => println!("Hello {}", name),
66///     Err(_) => println!("An error happened when asking for your name, try again later."),
67/// }
68/// ```
69#[derive(Clone)]
70pub struct Text<'a> {
71    /// Message to be presented to the user.
72    pub message: &'a str,
73
74    /// Initial value of the prompt's text input.
75    ///
76    /// If you want to set a default value for the prompt, returned when the user's submission is empty, see [`default`].
77    ///
78    /// [`default`]: Self::default
79    pub initial_value: Option<&'a str>,
80
81    /// Default value, returned when the user input is empty.
82    pub default: Option<&'a str>,
83
84    /// Short hint that describes the expected value of the input.
85    pub placeholder: Option<&'a str>,
86
87    /// Help message to be presented to the user.
88    pub help_message: Option<&'a str>,
89
90    /// Function that formats the user input and presents it to the user as the final rendering of the prompt.
91    pub formatter: StringFormatter<'a>,
92
93    /// Autocompleter responsible for handling suggestions and input completions.
94    pub autocompleter: Option<Box<dyn Autocomplete>>,
95
96    /// Collection of validators to apply to the user input.
97    ///
98    /// Validators are executed in the order they are stored, stopping at and displaying to the user
99    /// only the first validation error that might appear.
100    ///
101    /// The possible error is displayed to the user one line above the prompt.
102    pub validators: Vec<Box<dyn StringValidator>>,
103
104    /// Page size of the suggestions displayed to the user, when applicable.
105    pub page_size: usize,
106
107    /// RenderConfig to apply to the rendered interface.
108    ///
109    /// Note: The default render config considers if the NO_COLOR environment variable
110    /// is set to decide whether to render the colored config or the empty one.
111    ///
112    /// When overriding the config in a prompt, NO_COLOR is no longer considered and your
113    /// config is treated as the only source of truth. If you want to customize colors
114    /// and still suport NO_COLOR, you will have to do this on your end.
115    pub render_config: RenderConfig,
116}
117
118impl<'a> Text<'a> {
119    /// Default formatter, set to [DEFAULT_STRING_FORMATTER](crate::formatter::DEFAULT_STRING_FORMATTER)
120    pub const DEFAULT_FORMATTER: StringFormatter<'a> = DEFAULT_STRING_FORMATTER;
121
122    /// Default page size, equal to the global default page size [config::DEFAULT_PAGE_SIZE]
123    pub const DEFAULT_PAGE_SIZE: usize = config::DEFAULT_PAGE_SIZE;
124
125    /// Default validators added to the [Text] prompt, none.
126    pub const DEFAULT_VALIDATORS: Vec<Box<dyn StringValidator>> = vec![];
127
128    /// Default help message.
129    pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = None;
130
131    /// Creates a [Text] with the provided message and default options.
132    pub fn new(message: &'a str) -> Self {
133        Self {
134            message,
135            placeholder: None,
136            initial_value: None,
137            default: None,
138            help_message: Self::DEFAULT_HELP_MESSAGE,
139            validators: Self::DEFAULT_VALIDATORS,
140            formatter: Self::DEFAULT_FORMATTER,
141            page_size: Self::DEFAULT_PAGE_SIZE,
142            autocompleter: None,
143            render_config: get_configuration(),
144        }
145    }
146
147    /// Sets the help message of the prompt.
148    pub fn with_help_message(mut self, message: &'a str) -> Self {
149        self.help_message = Some(message);
150        self
151    }
152
153    /// Sets the initial value of the prompt's text input.
154    ///
155    /// If you want to set a default value for the prompt, returned when the user's submission is empty, see [`with_default`].
156    ///
157    /// [`with_default`]: Self::with_default
158    pub fn with_initial_value(mut self, message: &'a str) -> Self {
159        self.initial_value = Some(message);
160        self
161    }
162
163    /// Sets the default input.
164    pub fn with_default(mut self, message: &'a str) -> Self {
165        self.default = Some(message);
166        self
167    }
168
169    /// Sets the placeholder.
170    pub fn with_placeholder(mut self, placeholder: &'a str) -> Self {
171        self.placeholder = Some(placeholder);
172        self
173    }
174
175    /// Sets a new autocompleter
176    pub fn with_autocomplete<AC>(mut self, ac: AC) -> Self
177    where
178        AC: Autocomplete + 'static,
179    {
180        self.autocompleter = Some(Box::new(ac));
181        self
182    }
183
184    /// Sets the formatter.
185    pub fn with_formatter(mut self, formatter: StringFormatter<'a>) -> Self {
186        self.formatter = formatter;
187        self
188    }
189
190    /// Sets the page size
191    pub fn with_page_size(mut self, page_size: usize) -> Self {
192        self.page_size = page_size;
193        self
194    }
195
196    /// Adds a validator to the collection of validators. You might want to use this feature
197    /// in case you need to require certain features from the user's answer, such as
198    /// defining a limit of characters.
199    ///
200    /// Validators are executed in the order they are stored, stopping at and displaying to the user
201    /// only the first validation error that might appear.
202    ///
203    /// The possible error is displayed to the user one line above the prompt.
204    pub fn with_validator<V>(mut self, validator: V) -> Self
205    where
206        V: StringValidator + 'static,
207    {
208        // Directly make space for at least 5 elements, so we won't to re-allocate too often when
209        // calling this function repeatedly.
210        if self.validators.capacity() == 0 {
211            self.validators.reserve(5);
212        }
213
214        self.validators.push(Box::new(validator));
215        self
216    }
217
218    /// Adds the validators to the collection of validators in the order they are given.
219    /// You might want to use this feature in case you need to require certain features
220    /// from the user's answer, such as defining a limit of characters.
221    ///
222    /// Validators are executed in the order they are stored, stopping at and displaying to the user
223    /// only the first validation error that might appear.
224    ///
225    /// The possible error is displayed to the user one line above the prompt.
226    pub fn with_validators(mut self, validators: &[Box<dyn StringValidator>]) -> Self {
227        for validator in validators {
228            #[allow(clippy::clone_double_ref)]
229            self.validators.push(validator.clone());
230        }
231        self
232    }
233
234    /// Sets the provided color theme to this prompt.
235    ///
236    /// Note: The default render config considers if the NO_COLOR environment variable
237    /// is set to decide whether to render the colored config or the empty one.
238    ///
239    /// When overriding the config in a prompt, NO_COLOR is no longer considered and your
240    /// config is treated as the only source of truth. If you want to customize colors
241    /// and still suport NO_COLOR, you will have to do this on your end.
242    pub fn with_render_config(mut self, render_config: RenderConfig) -> Self {
243        self.render_config = render_config;
244        self
245    }
246
247    /// Parses the provided behavioral and rendering options and prompts
248    /// the CLI user for input according to the defined rules.
249    ///
250    /// This method is intended for flows where the user skipping/cancelling
251    /// the prompt - by pressing ESC - is considered normal behavior. In this case,
252    /// it does not return `Err(InquireError::OperationCanceled)`, but `Ok(None)`.
253    ///
254    /// Meanwhile, if the user does submit an answer, the method wraps the return
255    /// type with `Some`.
256    pub fn prompt_skippable(self) -> InquireResult<Option<String>> {
257        match self.prompt() {
258            Ok(answer) => Ok(Some(answer)),
259            Err(InquireError::OperationCanceled) => Ok(None),
260            Err(err) => Err(err),
261        }
262    }
263
264    /// Parses the provided behavioral and rendering options and prompts
265    /// the CLI user for input according to the defined rules.
266    pub fn prompt(self) -> InquireResult<String> {
267        let terminal = get_default_terminal()?;
268        let mut backend = Backend::new(terminal, self.render_config)?;
269        self.prompt_with_backend(&mut backend)
270    }
271
272    pub(crate) fn prompt_with_backend<B: TextBackend>(
273        self,
274        backend: &mut B,
275    ) -> InquireResult<String> {
276        TextPrompt::from(self).prompt(backend)
277    }
278}
279
280struct TextPrompt<'a> {
281    message: &'a str,
282    default: Option<&'a str>,
283    help_message: Option<&'a str>,
284    input: Input,
285    formatter: StringFormatter<'a>,
286    validators: Vec<Box<dyn StringValidator>>,
287    error: Option<ErrorMessage>,
288    autocompleter: Box<dyn Autocomplete>,
289    suggested_options: Vec<String>,
290    suggestion_cursor_index: Option<usize>,
291    page_size: usize,
292}
293
294impl<'a> From<Text<'a>> for TextPrompt<'a> {
295    fn from(so: Text<'a>) -> Self {
296        let input = Input::new_with(so.initial_value.unwrap_or_default());
297        let input = if let Some(placeholder) = so.placeholder {
298            input.with_placeholder(placeholder)
299        } else {
300            input
301        };
302
303        Self {
304            message: so.message,
305            default: so.default,
306            help_message: so.help_message,
307            formatter: so.formatter,
308            autocompleter: so
309                .autocompleter
310                .unwrap_or_else(|| Box::<NoAutoCompletion>::default()),
311            input,
312            error: None,
313            suggestion_cursor_index: None,
314            page_size: so.page_size,
315            suggested_options: vec![],
316            validators: so.validators,
317        }
318    }
319}
320
321impl<'a> From<&'a str> for Text<'a> {
322    fn from(val: &'a str) -> Self {
323        Text::new(val)
324    }
325}
326
327impl<'a> TextPrompt<'a> {
328    fn update_suggestions(&mut self) -> InquireResult<()> {
329        self.suggested_options = self.autocompleter.get_suggestions(self.input.content())?;
330        self.suggestion_cursor_index = None;
331
332        Ok(())
333    }
334
335    fn get_highlighted_suggestion(&self) -> Option<&str> {
336        if let Some(cursor) = self.suggestion_cursor_index {
337            let suggestion = self.suggested_options.get(cursor).unwrap().as_ref();
338            Some(suggestion)
339        } else {
340            None
341        }
342    }
343
344    fn move_cursor_up(&mut self, qty: usize) -> bool {
345        self.suggestion_cursor_index = match self.suggestion_cursor_index {
346            None => None,
347            Some(index) if index < qty => None,
348            Some(index) => Some(index.saturating_sub(qty)),
349        };
350
351        false
352    }
353
354    fn move_cursor_down(&mut self, qty: usize) -> bool {
355        self.suggestion_cursor_index = match self.suggested_options.is_empty() {
356            true => None,
357            false => match self.suggestion_cursor_index {
358                None if qty == 0 => None,
359                None => Some(min(
360                    qty.saturating_sub(1),
361                    self.suggested_options.len().saturating_sub(1),
362                )),
363                Some(index) => Some(min(
364                    index.saturating_add(qty),
365                    self.suggested_options.len().saturating_sub(1),
366                )),
367            },
368        };
369
370        false
371    }
372
373    fn handle_tab_key(&mut self) -> InquireResult<bool> {
374        let suggestion = self.get_highlighted_suggestion().map(|s| s.to_owned());
375        match self
376            .autocompleter
377            .get_completion(self.input.content(), suggestion)?
378        {
379            Replacement::Some(value) => {
380                self.input = Input::new_with(value);
381                Ok(true)
382            }
383            Replacement::None => Ok(false),
384        }
385    }
386
387    fn on_change(&mut self, key: Key) -> InquireResult<()> {
388        let dirty = match key {
389            Key::Up(KeyModifiers::NONE) => self.move_cursor_up(1),
390            Key::PageUp => self.move_cursor_up(self.page_size),
391
392            Key::Down(KeyModifiers::NONE) => self.move_cursor_down(1),
393            Key::PageDown => self.move_cursor_down(self.page_size),
394
395            Key::Tab => self.handle_tab_key()?,
396
397            key => self.input.handle_key(key),
398        };
399
400        if dirty {
401            self.update_suggestions()?;
402        }
403
404        Ok(())
405    }
406
407    fn get_current_answer(&self) -> &str {
408        // If there is a highlighted suggestion, assume user wanted it as
409        // the answer.
410        if let Some(suggestion) = self.get_highlighted_suggestion() {
411            return suggestion;
412        }
413
414        // Empty input with default values override any validators.
415        if self.input.content().is_empty() {
416            if let Some(val) = self.default {
417                return val;
418            }
419        }
420
421        self.input.content()
422    }
423
424    fn validate_current_answer(&self) -> InquireResult<Validation> {
425        for validator in &self.validators {
426            match validator.validate(self.get_current_answer()) {
427                Ok(Validation::Valid) => {}
428                Ok(Validation::Invalid(msg)) => return Ok(Validation::Invalid(msg)),
429                Err(err) => return Err(InquireError::Custom(err)),
430            }
431        }
432
433        Ok(Validation::Valid)
434    }
435
436    fn render<B: TextBackend>(&mut self, backend: &mut B) -> InquireResult<()> {
437        let prompt = &self.message;
438
439        backend.frame_setup()?;
440
441        if let Some(err) = &self.error {
442            backend.render_error_message(err)?;
443        }
444
445        backend.render_prompt(prompt, self.default, &self.input)?;
446
447        let choices = self
448            .suggested_options
449            .iter()
450            .enumerate()
451            .map(|(i, val)| ListOption::new(i, val.as_ref()))
452            .collect::<Vec<ListOption<&str>>>();
453
454        let page = paginate(self.page_size, &choices, self.suggestion_cursor_index);
455
456        backend.render_suggestions(page)?;
457
458        if let Some(message) = self.help_message {
459            backend.render_help_message(message)?;
460        } else if !choices.is_empty() {
461            backend.render_help_message(DEFAULT_HELP_MESSAGE_WITH_AC)?;
462        }
463
464        backend.frame_finish()?;
465
466        Ok(())
467    }
468
469    fn prompt<B: TextBackend>(mut self, backend: &mut B) -> InquireResult<String> {
470        let final_answer: String;
471        self.update_suggestions()?;
472
473        loop {
474            self.render(backend)?;
475
476            let key = backend.read_key()?;
477
478            match key {
479                Key::Interrupt => interrupt_prompt!(),
480                Key::Cancel => cancel_prompt!(backend, self.message),
481                Key::Submit => match self.validate_current_answer()? {
482                    Validation::Valid => {
483                        final_answer = self.get_current_answer().to_owned();
484                        break;
485                    }
486                    Validation::Invalid(msg) => self.error = Some(msg),
487                },
488                key => self.on_change(key)?,
489            }
490        }
491
492        let formatted = (self.formatter)(&final_answer);
493
494        finish_prompt_with_answer!(backend, self.message, &formatted, final_answer);
495    }
496}
497
498#[cfg(test)]
499#[cfg(feature = "crossterm")]
500mod test {
501    use super::Text;
502    use crate::{
503        terminal::crossterm::CrosstermTerminal,
504        ui::{Backend, RenderConfig},
505        validator::{ErrorMessage, Validation},
506    };
507    use crossterm::event::{KeyCode, KeyEvent};
508
509    fn default<'a>() -> Text<'a> {
510        Text::new("Question?")
511    }
512
513    macro_rules! text_to_events {
514        ($text:expr) => {{
515            $text.chars().map(KeyCode::Char)
516        }};
517    }
518
519    macro_rules! text_test {
520        ($name:ident,$input:expr,$output:expr) => {
521            text_test! {$name, $input, $output, default()}
522        };
523
524        ($name:ident,$input:expr,$output:expr,$prompt:expr) => {
525            #[test]
526            fn $name() {
527                let read: Vec<KeyEvent> = $input.into_iter().map(KeyEvent::from).collect();
528                let mut read = read.iter();
529
530                let mut write: Vec<u8> = Vec::new();
531
532                let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read);
533                let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap();
534
535                let ans = $prompt.prompt_with_backend(&mut backend).unwrap();
536
537                assert_eq!($output, ans);
538            }
539        };
540    }
541
542    text_test!(empty, vec![KeyCode::Enter], "");
543
544    text_test!(single_letter, vec![KeyCode::Char('b'), KeyCode::Enter], "b");
545
546    text_test!(
547        letters_and_enter,
548        text_to_events!("normal input\n"),
549        "normal input"
550    );
551
552    text_test!(
553        letters_and_enter_with_emoji,
554        text_to_events!("with emoji 🧘🏻‍♂️, 🌍, 🍞, 🚗, 📞\n"),
555        "with emoji 🧘🏻‍♂️, 🌍, 🍞, 🚗, 📞"
556    );
557
558    text_test!(
559        input_and_correction,
560        {
561            let mut events = vec![];
562            events.append(&mut text_to_events!("anor").collect());
563            events.push(KeyCode::Backspace);
564            events.push(KeyCode::Backspace);
565            events.push(KeyCode::Backspace);
566            events.push(KeyCode::Backspace);
567            events.append(&mut text_to_events!("normal input").collect());
568            events.push(KeyCode::Enter);
569            events
570        },
571        "normal input"
572    );
573
574    text_test!(
575        input_and_excessive_correction,
576        {
577            let mut events = vec![];
578            events.append(&mut text_to_events!("anor").collect());
579            events.push(KeyCode::Backspace);
580            events.push(KeyCode::Backspace);
581            events.push(KeyCode::Backspace);
582            events.push(KeyCode::Backspace);
583            events.push(KeyCode::Backspace);
584            events.push(KeyCode::Backspace);
585            events.push(KeyCode::Backspace);
586            events.push(KeyCode::Backspace);
587            events.push(KeyCode::Backspace);
588            events.push(KeyCode::Backspace);
589            events.append(&mut text_to_events!("normal input").collect());
590            events.push(KeyCode::Enter);
591            events
592        },
593        "normal input"
594    );
595
596    text_test!(
597        input_correction_after_validation,
598        {
599            let mut events = vec![];
600            events.append(&mut text_to_events!("1234567890").collect());
601            events.push(KeyCode::Enter);
602            events.push(KeyCode::Backspace);
603            events.push(KeyCode::Backspace);
604            events.push(KeyCode::Backspace);
605            events.push(KeyCode::Backspace);
606            events.push(KeyCode::Backspace);
607            events.append(&mut text_to_events!("yes").collect());
608            events.push(KeyCode::Enter);
609            events
610        },
611        "12345yes",
612        Text::new("").with_validator(|ans: &str| match ans.len() {
613            len if len > 5 && len < 10 => Ok(Validation::Valid),
614            _ => Ok(Validation::Invalid(ErrorMessage::Default)),
615        })
616    );
617}