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}