inquire/prompts/
multiselect.rs

1use std::{collections::BTreeSet, fmt::Display};
2
3use crate::{
4    config::{self, get_configuration},
5    error::{InquireError, InquireResult},
6    formatter::MultiOptionFormatter,
7    input::Input,
8    list_option::ListOption,
9    terminal::get_default_terminal,
10    type_aliases::Filter,
11    ui::{Backend, Key, KeyModifiers, MultiSelectBackend, RenderConfig},
12    utils::paginate,
13    validator::{ErrorMessage, MultiOptionValidator, Validation},
14};
15
16/// Prompt suitable for when you need the user to select many options (including none if applicable) among a list of them.
17///
18/// The user can select (or deselect) the current highlighted option by pressing space, clean all selections by pressing the left arrow and select all options by pressing the right arrow.
19///
20/// This prompt requires a prompt message and a **non-empty** `Vec` of options to be displayed to the user. The options can be of any type as long as they implement the `Display` trait. It is required that the `Vec` is moved to the prompt, as the prompt will return the ownership of the `Vec` after the user submits, with only the selected options inside it.
21/// - If the list is empty, the prompt operation will fail with an `InquireError::InvalidConfiguration` error.
22///
23/// The options are paginated in order to provide a smooth experience to the user, with the default page size being 7. The user can move from the options and the pages will be updated accordingly, including moving from the last to the first options (or vice-versa).
24///
25/// Customizable options:
26///
27/// - **Prompt message**: Required when creating the prompt.
28/// - **Options list**: Options displayed to the user. Must be **non-empty**.
29/// - **Default selections**: Options that are selected by default when the prompt is first rendered. The user can unselect them. If any of the indices is out-of-range of the option list, the prompt will fail with an [`InquireError::InvalidConfiguration`] error.
30/// - **Starting cursor**: Index of the cursor when the prompt is first rendered. Default is 0 (first option). If the index is out-of-range of the option list, the prompt will fail with an [`InquireError::InvalidConfiguration`] error.
31/// - **Help message**: Message displayed at the line below the prompt.
32/// - **Formatter**: Custom formatter in case you need to pre-process the user input before showing it as the final answer.
33///   - Prints the selected options string value, joined using a comma as the separator, by default.
34/// - **Validator**: Custom validator to make sure a given submitted input pass the specified requirements, e.g. not allowing 0 selected options or limiting the number of options that the user is allowed to select.
35///   - No validators are on by default.
36/// - **Page size**: Number of options displayed at once, 7 by default.
37/// - **Display option indexes**: On long lists, it might be helpful to display the indexes of the options to the user. Via the `RenderConfig`, you can set the display mode of the indexes as a prefix of an option. The default configuration is `None`, to not render any index when displaying the options.
38/// - **Filter function**: Function that defines if an option is displayed or not based on the current filter input.
39/// - **Keep filter flag**: Whether the current filter input should be cleared or not after a selection is made. Defaults to true.
40///
41/// # Example
42///
43/// For a full-featured example, check the [GitHub repository](https://github.com/mikaelmello/inquire/blob/main/examples/multiselect.rs).
44///
45/// [`InquireError::InvalidConfiguration`]: crate::error::InquireError::InvalidConfiguration
46#[derive(Clone)]
47pub struct MultiSelect<'a, T> {
48    /// Message to be presented to the user.
49    pub message: &'a str,
50
51    /// Options displayed to the user.
52    pub options: Vec<T>,
53
54    /// Default indexes of options to be selected from the start.
55    pub default: Option<&'a [usize]>,
56
57    /// Help message to be presented to the user.
58    pub help_message: Option<&'a str>,
59
60    /// Page size of the options displayed to the user.
61    pub page_size: usize,
62
63    /// Whether vim mode is enabled. When enabled, the user can
64    /// navigate through the options using hjkl.
65    pub vim_mode: bool,
66
67    /// Starting cursor index of the selection.
68    pub starting_cursor: usize,
69
70    /// Function called with the current user input to filter the provided
71    /// options.
72    pub filter: Filter<'a, T>,
73
74    /// Whether the current filter typed by the user is kept or cleaned after a selection is made.
75    pub keep_filter: bool,
76
77    /// Function that formats the user input and presents it to the user as the final rendering of the prompt.
78    pub formatter: MultiOptionFormatter<'a, T>,
79
80    /// Validator to apply to the user input.
81    ///
82    /// In case of error, the message is displayed one line above the prompt.
83    pub validator: Option<Box<dyn MultiOptionValidator<T>>>,
84
85    /// RenderConfig to apply to the rendered interface.
86    ///
87    /// Note: The default render config considers if the NO_COLOR environment variable
88    /// is set to decide whether to render the colored config or the empty one.
89    ///
90    /// When overriding the config in a prompt, NO_COLOR is no longer considered and your
91    /// config is treated as the only source of truth. If you want to customize colors
92    /// and still suport NO_COLOR, you will have to do this on your end.
93    pub render_config: RenderConfig,
94}
95
96impl<'a, T> MultiSelect<'a, T>
97where
98    T: Display,
99{
100    /// String formatter used by default in [MultiSelect](crate::MultiSelect) prompts.
101    /// Prints the string value of all selected options, separated by commas.
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use inquire::list_option::ListOption;
107    /// use inquire::MultiSelect;
108    ///
109    /// let formatter = MultiSelect::<&str>::DEFAULT_FORMATTER;
110    ///
111    /// let mut ans = vec![ListOption::new(0, &"New York")];
112    /// assert_eq!(String::from("New York"), formatter(&ans));
113    ///
114    /// ans.push(ListOption::new(3, &"Seattle"));
115    /// assert_eq!(String::from("New York, Seattle"), formatter(&ans));
116    ///
117    /// ans.push(ListOption::new(7, &"Vancouver"));
118    /// assert_eq!(String::from("New York, Seattle, Vancouver"), formatter(&ans));
119    /// ```
120    pub const DEFAULT_FORMATTER: MultiOptionFormatter<'a, T> = &|ans| {
121        ans.iter()
122            .map(|opt| opt.to_string())
123            .collect::<Vec<String>>()
124            .join(", ")
125    };
126
127    /// Default filter function, which checks if the current filter value is a substring of the option value.
128    /// If it is, the option is displayed.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use inquire::MultiSelect;
134    ///
135    /// let filter = MultiSelect::<&str>::DEFAULT_FILTER;
136    /// assert_eq!(false, filter("sa", &"New York",      "New York",      0));
137    /// assert_eq!(true,  filter("sa", &"Sacramento",    "Sacramento",    1));
138    /// assert_eq!(true,  filter("sa", &"Kansas",        "Kansas",        2));
139    /// assert_eq!(true,  filter("sa", &"Mesa",          "Mesa",          3));
140    /// assert_eq!(false, filter("sa", &"Phoenix",       "Phoenix",       4));
141    /// assert_eq!(false, filter("sa", &"Philadelphia",  "Philadelphia",  5));
142    /// assert_eq!(true,  filter("sa", &"San Antonio",   "San Antonio",   6));
143    /// assert_eq!(true,  filter("sa", &"San Diego",     "San Diego",     7));
144    /// assert_eq!(false, filter("sa", &"Dallas",        "Dallas",        8));
145    /// assert_eq!(true,  filter("sa", &"San Francisco", "San Francisco", 9));
146    /// assert_eq!(false, filter("sa", &"Austin",        "Austin",       10));
147    /// assert_eq!(false, filter("sa", &"Jacksonville",  "Jacksonville", 11));
148    /// assert_eq!(true,  filter("sa", &"San Jose",      "San Jose",     12));
149    /// ```
150    pub const DEFAULT_FILTER: Filter<'a, T> = &|filter, _, string_value, _| -> bool {
151        let filter = filter.to_lowercase();
152
153        string_value.to_lowercase().contains(&filter)
154    };
155
156    /// Default page size, equal to the global default page size [config::DEFAULT_PAGE_SIZE]
157    pub const DEFAULT_PAGE_SIZE: usize = config::DEFAULT_PAGE_SIZE;
158
159    /// Default value of vim mode, equal to the global default value [config::DEFAULT_PAGE_SIZE]
160    pub const DEFAULT_VIM_MODE: bool = config::DEFAULT_VIM_MODE;
161
162    /// Default starting cursor index.
163    pub const DEFAULT_STARTING_CURSOR: usize = 0;
164
165    /// Default behavior of keeping or cleaning the current filter value.
166    pub const DEFAULT_KEEP_FILTER: bool = true;
167
168    /// Default help message.
169    pub const DEFAULT_HELP_MESSAGE: Option<&'a str> =
170        Some("↑↓ to move, space to select one, → to all, ← to none, type to filter");
171
172    /// Creates a [MultiSelect] with the provided message and options, along with default configuration values.
173    pub fn new(message: &'a str, options: Vec<T>) -> Self {
174        Self {
175            message,
176            options,
177            default: None,
178            help_message: Self::DEFAULT_HELP_MESSAGE,
179            page_size: Self::DEFAULT_PAGE_SIZE,
180            vim_mode: Self::DEFAULT_VIM_MODE,
181            starting_cursor: Self::DEFAULT_STARTING_CURSOR,
182            keep_filter: Self::DEFAULT_KEEP_FILTER,
183            filter: Self::DEFAULT_FILTER,
184            formatter: Self::DEFAULT_FORMATTER,
185            validator: None,
186            render_config: get_configuration(),
187        }
188    }
189
190    /// Sets the help message of the prompt.
191    pub fn with_help_message(mut self, message: &'a str) -> Self {
192        self.help_message = Some(message);
193        self
194    }
195
196    /// Removes the set help message.
197    pub fn without_help_message(mut self) -> Self {
198        self.help_message = None;
199        self
200    }
201
202    /// Sets the page size.
203    pub fn with_page_size(mut self, page_size: usize) -> Self {
204        self.page_size = page_size;
205        self
206    }
207
208    /// Enables or disables vim_mode.
209    pub fn with_vim_mode(mut self, vim_mode: bool) -> Self {
210        self.vim_mode = vim_mode;
211        self
212    }
213
214    /// Sets the keep filter behavior.
215    pub fn with_keep_filter(mut self, keep_filter: bool) -> Self {
216        self.keep_filter = keep_filter;
217        self
218    }
219
220    /// Sets the filter function.
221    pub fn with_filter(mut self, filter: Filter<'a, T>) -> Self {
222        self.filter = filter;
223        self
224    }
225
226    /// Sets the formatter.
227    pub fn with_formatter(mut self, formatter: MultiOptionFormatter<'a, T>) -> Self {
228        self.formatter = formatter;
229        self
230    }
231
232    /// Sets the validator to apply to the user input. You might want to use this feature
233    /// in case you need to limit the user to specific choices, such as limiting the number
234    /// of selections.
235    ///
236    /// In case of error, the message is displayed one line above the prompt.
237    pub fn with_validator<V>(mut self, validator: V) -> Self
238    where
239        V: MultiOptionValidator<T> + 'static,
240    {
241        self.validator = Some(Box::new(validator));
242        self
243    }
244
245    /// Sets the indexes to be selected by the default.
246    pub fn with_default(mut self, default: &'a [usize]) -> Self {
247        self.default = Some(default);
248        self
249    }
250
251    /// Sets the starting cursor index.
252    pub fn with_starting_cursor(mut self, starting_cursor: usize) -> Self {
253        self.starting_cursor = starting_cursor;
254        self
255    }
256
257    /// Sets the provided color theme to this prompt.
258    ///
259    /// Note: The default render config considers if the NO_COLOR environment variable
260    /// is set to decide whether to render the colored config or the empty one.
261    ///
262    /// When overriding the config in a prompt, NO_COLOR is no longer considered and your
263    /// config is treated as the only source of truth. If you want to customize colors
264    /// and still suport NO_COLOR, you will have to do this on your end.
265    pub fn with_render_config(mut self, render_config: RenderConfig) -> Self {
266        self.render_config = render_config;
267        self
268    }
269
270    /// Parses the provided behavioral and rendering options and prompts
271    /// the CLI user for input according to the defined rules.
272    ///
273    /// Returns the owned objects selected by the user.
274    ///
275    /// This method is intended for flows where the user skipping/cancelling
276    /// the prompt - by pressing ESC - is considered normal behavior. In this case,
277    /// it does not return `Err(InquireError::OperationCanceled)`, but `Ok(None)`.
278    ///
279    /// Meanwhile, if the user does submit an answer, the method wraps the return
280    /// type with `Some`.
281    pub fn prompt_skippable(self) -> InquireResult<Option<Vec<T>>> {
282        match self.prompt() {
283            Ok(answer) => Ok(Some(answer)),
284            Err(InquireError::OperationCanceled) => Ok(None),
285            Err(err) => Err(err),
286        }
287    }
288
289    /// Parses the provided behavioral and rendering options and prompts
290    /// the CLI user for input according to the defined rules.
291    ///
292    /// Returns the owned objects selected by the user.
293    pub fn prompt(self) -> InquireResult<Vec<T>> {
294        self.raw_prompt()
295            .map(|op| op.into_iter().map(|o| o.value).collect())
296    }
297
298    /// Parses the provided behavioral and rendering options and prompts
299    /// the CLI user for input according to the defined rules.
300    ///
301    /// Returns a vector of [`ListOption`](crate::list_option::ListOption)s containing
302    /// the index of the selections and the owned objects selected by the user.
303    ///
304    /// This method is intended for flows where the user skipping/cancelling
305    /// the prompt - by pressing ESC - is considered normal behavior. In this case,
306    /// it does not return `Err(InquireError::OperationCanceled)`, but `Ok(None)`.
307    ///
308    /// Meanwhile, if the user does submit an answer, the method wraps the return
309    /// type with `Some`.
310    pub fn raw_prompt_skippable(self) -> InquireResult<Option<Vec<ListOption<T>>>> {
311        match self.raw_prompt() {
312            Ok(answer) => Ok(Some(answer)),
313            Err(InquireError::OperationCanceled) => Ok(None),
314            Err(err) => Err(err),
315        }
316    }
317
318    /// Parses the provided behavioral and rendering options and prompts
319    /// the CLI user for input according to the defined rules.
320    ///
321    /// Returns a [`ListOption`](crate::list_option::ListOption) containing
322    /// the index of the selection and the owned object selected by the user.
323    pub fn raw_prompt(self) -> InquireResult<Vec<ListOption<T>>> {
324        let terminal = get_default_terminal()?;
325        let mut backend = Backend::new(terminal, self.render_config)?;
326        self.prompt_with_backend(&mut backend)
327    }
328
329    pub(crate) fn prompt_with_backend<B: MultiSelectBackend>(
330        self,
331        backend: &mut B,
332    ) -> InquireResult<Vec<ListOption<T>>> {
333        MultiSelectPrompt::new(self)?.prompt(backend)
334    }
335}
336
337struct MultiSelectPrompt<'a, T> {
338    message: &'a str,
339    options: Vec<T>,
340    string_options: Vec<String>,
341    help_message: Option<&'a str>,
342    vim_mode: bool,
343    cursor_index: usize,
344    checked: BTreeSet<usize>,
345    page_size: usize,
346    keep_filter: bool,
347    input: Input,
348    filtered_options: Vec<usize>,
349    filter: Filter<'a, T>,
350    formatter: MultiOptionFormatter<'a, T>,
351    validator: Option<Box<dyn MultiOptionValidator<T>>>,
352    error: Option<ErrorMessage>,
353}
354
355impl<'a, T> MultiSelectPrompt<'a, T>
356where
357    T: Display,
358{
359    fn new(mso: MultiSelect<'a, T>) -> InquireResult<Self> {
360        if mso.options.is_empty() {
361            return Err(InquireError::InvalidConfiguration(
362                "Available options can not be empty".into(),
363            ));
364        }
365        if let Some(default) = mso.default {
366            for i in default {
367                if i >= &mso.options.len() {
368                    return Err(InquireError::InvalidConfiguration(format!(
369                        "Index {} is out-of-bounds for length {} of options",
370                        i,
371                        &mso.options.len()
372                    )));
373                }
374            }
375        }
376
377        let string_options = mso.options.iter().map(T::to_string).collect();
378        let filtered_options = (0..mso.options.len()).collect();
379        let checked_options = mso
380            .default
381            .map_or_else(BTreeSet::new, |d| d.iter().cloned().collect());
382
383        Ok(Self {
384            message: mso.message,
385            options: mso.options,
386            string_options,
387            filtered_options,
388            help_message: mso.help_message,
389            vim_mode: mso.vim_mode,
390            cursor_index: mso.starting_cursor,
391            page_size: mso.page_size,
392            keep_filter: mso.keep_filter,
393            input: Input::new(),
394            filter: mso.filter,
395            formatter: mso.formatter,
396            validator: mso.validator,
397            error: None,
398            checked: checked_options,
399        })
400    }
401
402    fn filter_options(&self) -> Vec<usize> {
403        self.options
404            .iter()
405            .enumerate()
406            .filter_map(|(i, opt)| match self.input.content() {
407                val if val.is_empty() => Some(i),
408                val if (self.filter)(val, opt, self.string_options.get(i).unwrap(), i) => Some(i),
409                _ => None,
410            })
411            .collect()
412    }
413
414    fn move_cursor_up(&mut self, qty: usize, wrap: bool) {
415        if wrap {
416            let after_wrap = qty.saturating_sub(self.cursor_index);
417            self.cursor_index = self
418                .cursor_index
419                .checked_sub(qty)
420                .unwrap_or_else(|| self.filtered_options.len().saturating_sub(after_wrap))
421        } else {
422            self.cursor_index = self.cursor_index.saturating_sub(qty);
423        }
424    }
425
426    fn move_cursor_down(&mut self, qty: usize, wrap: bool) {
427        self.cursor_index = self.cursor_index.saturating_add(qty);
428
429        if self.cursor_index >= self.filtered_options.len() {
430            self.cursor_index = if self.filtered_options.is_empty() {
431                0
432            } else if wrap {
433                self.cursor_index % self.filtered_options.len()
434            } else {
435                self.filtered_options.len().saturating_sub(1)
436            }
437        }
438    }
439
440    fn toggle_cursor_selection(&mut self) {
441        let idx = match self.filtered_options.get(self.cursor_index) {
442            Some(val) => val,
443            None => return,
444        };
445
446        if self.checked.contains(idx) {
447            self.checked.remove(idx);
448        } else {
449            self.checked.insert(*idx);
450        }
451
452        if !self.keep_filter {
453            self.input.clear();
454        }
455    }
456
457    fn on_change(&mut self, key: Key) {
458        match key {
459            Key::Up(KeyModifiers::NONE) => self.move_cursor_up(1, true),
460            Key::Char('k', KeyModifiers::NONE) if self.vim_mode => self.move_cursor_up(1, true),
461            Key::PageUp => self.move_cursor_up(self.page_size, false),
462            Key::Home => self.move_cursor_up(usize::MAX, false),
463
464            Key::Down(KeyModifiers::NONE) => self.move_cursor_down(1, true),
465            Key::Char('j', KeyModifiers::NONE) if self.vim_mode => self.move_cursor_down(1, true),
466            Key::PageDown => self.move_cursor_down(self.page_size, false),
467            Key::End => self.move_cursor_down(usize::MAX, false),
468
469            Key::Char(' ', KeyModifiers::NONE) => self.toggle_cursor_selection(),
470            Key::Right(KeyModifiers::NONE) => {
471                self.checked.clear();
472                for idx in &self.filtered_options {
473                    self.checked.insert(*idx);
474                }
475
476                if !self.keep_filter {
477                    self.input.clear();
478                }
479            }
480            Key::Left(KeyModifiers::NONE) => {
481                self.checked.clear();
482
483                if !self.keep_filter {
484                    self.input.clear();
485                }
486            }
487            key => {
488                let dirty = self.input.handle_key(key);
489
490                if dirty {
491                    let options = self.filter_options();
492                    if options.len() <= self.cursor_index {
493                        self.cursor_index = options.len().saturating_sub(1);
494                    }
495                    self.filtered_options = options;
496                }
497            }
498        };
499    }
500
501    fn validate_current_answer(&self) -> InquireResult<Validation> {
502        if let Some(validator) = &self.validator {
503            let selected_options = self
504                .options
505                .iter()
506                .enumerate()
507                .filter_map(|(idx, opt)| match &self.checked.contains(&idx) {
508                    true => Some(ListOption::new(idx, opt)),
509                    false => None,
510                })
511                .collect::<Vec<_>>();
512
513            let res = validator.validate(&selected_options)?;
514            Ok(res)
515        } else {
516            Ok(Validation::Valid)
517        }
518    }
519
520    fn get_final_answer(&mut self) -> Vec<ListOption<T>> {
521        let mut answer = vec![];
522
523        // by iterating in descending order, we can safely
524        // swap remove because the elements to the right
525        // that we did not remove will not matter anymore.
526        for index in self.checked.iter().rev() {
527            let index = *index;
528            let value = self.options.swap_remove(index);
529            let lo = ListOption::new(index, value);
530            answer.push(lo);
531        }
532        answer.reverse();
533
534        answer
535    }
536
537    fn render<B: MultiSelectBackend>(&mut self, backend: &mut B) -> InquireResult<()> {
538        let prompt = &self.message;
539
540        backend.frame_setup()?;
541
542        if let Some(err) = &self.error {
543            backend.render_error_message(err)?;
544        }
545
546        backend.render_multiselect_prompt(prompt, &self.input)?;
547
548        let choices = self
549            .filtered_options
550            .iter()
551            .cloned()
552            .map(|i| ListOption::new(i, self.options.get(i).unwrap()))
553            .collect::<Vec<ListOption<&T>>>();
554
555        let page = paginate(self.page_size, &choices, Some(self.cursor_index));
556
557        backend.render_options(page, &self.checked)?;
558
559        if let Some(help_message) = self.help_message {
560            backend.render_help_message(help_message)?;
561        }
562
563        backend.frame_finish()?;
564
565        Ok(())
566    }
567
568    fn prompt<B: MultiSelectBackend>(
569        mut self,
570        backend: &mut B,
571    ) -> InquireResult<Vec<ListOption<T>>> {
572        loop {
573            self.render(backend)?;
574
575            let key = backend.read_key()?;
576
577            match key {
578                Key::Interrupt => interrupt_prompt!(),
579                Key::Cancel => cancel_prompt!(backend, self.message),
580                Key::Submit => match self.validate_current_answer()? {
581                    Validation::Valid => break,
582                    Validation::Invalid(msg) => self.error = Some(msg),
583                },
584                key => self.on_change(key),
585            }
586        }
587
588        let final_answer = self.get_final_answer();
589        let refs: Vec<ListOption<&T>> = final_answer.iter().map(ListOption::as_ref).collect();
590        let formatted = (self.formatter)(&refs);
591
592        finish_prompt_with_answer!(backend, self.message, &formatted, final_answer);
593    }
594}
595
596#[cfg(test)]
597#[cfg(feature = "crossterm")]
598mod test {
599    use crate::{
600        formatter::MultiOptionFormatter,
601        list_option::ListOption,
602        terminal::crossterm::CrosstermTerminal,
603        ui::{Backend, RenderConfig},
604        MultiSelect,
605    };
606    use crossterm::event::{KeyCode, KeyEvent};
607
608    #[test]
609    /// Tests that a closure that actually closes on a variable can be used
610    /// as a Select formatter.
611    fn closure_formatter() {
612        let read: Vec<KeyEvent> = vec![KeyCode::Char(' '), KeyCode::Enter]
613            .into_iter()
614            .map(KeyEvent::from)
615            .collect();
616        let mut read = read.iter();
617
618        let formatted = String::from("Thanks!");
619        let formatter: MultiOptionFormatter<i32> = &|_| formatted.clone();
620
621        let options = vec![1, 2, 3];
622
623        let mut write: Vec<u8> = Vec::new();
624        let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read);
625        let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap();
626
627        let ans = MultiSelect::new("Question", options)
628            .with_formatter(formatter)
629            .prompt_with_backend(&mut backend)
630            .unwrap();
631
632        assert_eq!(vec![ListOption::new(0, 1)], ans);
633    }
634
635    #[test]
636    // Anti-regression test: https://github.com/mikaelmello/inquire/issues/30
637    fn down_arrow_on_empty_list_does_not_panic() {
638        let read: Vec<KeyEvent> = [
639            KeyCode::Char('9'),
640            KeyCode::Down,
641            KeyCode::Backspace,
642            KeyCode::Char('3'),
643            KeyCode::Down,
644            KeyCode::Backspace,
645            KeyCode::Enter,
646        ]
647        .iter()
648        .map(|c| KeyEvent::from(*c))
649        .collect();
650
651        let mut read = read.iter();
652
653        let options = vec![1, 2, 3];
654
655        let mut write: Vec<u8> = Vec::new();
656        let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read);
657        let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap();
658
659        let ans = MultiSelect::new("Question", options)
660            .prompt_with_backend(&mut backend)
661            .unwrap();
662
663        assert_eq!(Vec::<ListOption<i32>>::new(), ans);
664    }
665
666    #[test]
667    // Anti-regression test: https://github.com/mikaelmello/inquire/issues/31
668    fn list_option_indexes_are_relative_to_input_vec() {
669        let read: Vec<KeyEvent> = vec![
670            KeyCode::Down,
671            KeyCode::Char(' '),
672            KeyCode::Down,
673            KeyCode::Char(' '),
674            KeyCode::Enter,
675        ]
676        .into_iter()
677        .map(KeyEvent::from)
678        .collect();
679        let mut read = read.iter();
680
681        let options = vec![1, 2, 3];
682
683        let mut write: Vec<u8> = Vec::new();
684        let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read);
685        let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap();
686
687        let ans = MultiSelect::new("Question", options)
688            .prompt_with_backend(&mut backend)
689            .unwrap();
690
691        assert_eq!(vec![ListOption::new(1, 2), ListOption::new(2, 3)], ans);
692    }
693}