inquire/prompts/select.rs
1use std::fmt::Display;
2
3use crate::{
4 config::{self, get_configuration},
5 error::{InquireError, InquireResult},
6 formatter::OptionFormatter,
7 input::Input,
8 list_option::ListOption,
9 terminal::get_default_terminal,
10 type_aliases::Filter,
11 ui::{Backend, Key, KeyModifiers, RenderConfig, SelectBackend},
12 utils::paginate,
13};
14
15/// Prompt suitable for when you need the user to select one option among many.
16///
17/// The user can select and submit the current highlighted option by pressing enter.
18///
19/// 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 selected option (`Vec` element) after the user submits.
20/// - If the list is empty, the prompt operation will fail with an `InquireError::InvalidConfiguration` error.
21///
22/// This prompt does not support custom validators because of its nature. A submission always selects exactly one of the options. If this option was not supposed to be selected or is invalid in some way, it probably should not be included in the options list.
23///
24/// 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).
25///
26/// Like all others, this prompt also allows you to customize several aspects of it:
27///
28/// - **Prompt message**: Required when creating the prompt.
29/// - **Options list**: Options displayed to the user. Must be **non-empty**.
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 option string value by default.
34/// - **Page size**: Number of options displayed at once, 7 by default.
35/// - **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.
36/// - **Filter function**: Function that defines if an option is displayed or not based on the current filter input.
37///
38/// # Example
39///
40/// ```no_run
41/// use inquire::{error::InquireError, Select};
42///
43/// let options: Vec<&str> = vec!["Banana", "Apple", "Strawberry", "Grapes",
44/// "Lemon", "Tangerine", "Watermelon", "Orange", "Pear", "Avocado", "Pineapple",
45/// ];
46///
47/// let ans: Result<&str, InquireError> = Select::new("What's your favorite fruit?", options).prompt();
48///
49/// match ans {
50/// Ok(choice) => println!("{}! That's mine too!", choice),
51/// Err(_) => println!("There was an error, please try again"),
52/// }
53/// ```
54///
55/// [`InquireError::InvalidConfiguration`]: crate::error::InquireError::InvalidConfiguration
56#[derive(Clone)]
57pub struct Select<'a, T> {
58 /// Message to be presented to the user.
59 pub message: &'a str,
60
61 /// Options displayed to the user.
62 pub options: Vec<T>,
63
64 /// Help message to be presented to the user.
65 pub help_message: Option<&'a str>,
66
67 /// Page size of the options displayed to the user.
68 pub page_size: usize,
69
70 /// Whether vim mode is enabled. When enabled, the user can
71 /// navigate through the options using hjkl.
72 pub vim_mode: bool,
73
74 /// Starting cursor index of the selection.
75 pub starting_cursor: usize,
76
77 /// Function called with the current user input to filter the provided
78 /// options.
79 pub filter: Filter<'a, T>,
80
81 /// Function that formats the user input and presents it to the user as the final rendering of the prompt.
82 pub formatter: OptionFormatter<'a, T>,
83
84 /// RenderConfig to apply to the rendered interface.
85 ///
86 /// Note: The default render config considers if the NO_COLOR environment variable
87 /// is set to decide whether to render the colored config or the empty one.
88 ///
89 /// When overriding the config in a prompt, NO_COLOR is no longer considered and your
90 /// config is treated as the only source of truth. If you want to customize colors
91 /// and still suport NO_COLOR, you will have to do this on your end.
92 pub render_config: RenderConfig,
93}
94
95impl<'a, T> Select<'a, T>
96where
97 T: Display,
98{
99 /// String formatter used by default in [Select](crate::Select) prompts.
100 /// Simply prints the string value contained in the selected option.
101 ///
102 /// # Examples
103 ///
104 /// ```
105 /// use inquire::list_option::ListOption;
106 /// use inquire::Select;
107 ///
108 /// let formatter = Select::<&str>::DEFAULT_FORMATTER;
109 /// assert_eq!(String::from("First option"), formatter(ListOption::new(0, &"First option")));
110 /// assert_eq!(String::from("First option"), formatter(ListOption::new(11, &"First option")));
111 /// ```
112 pub const DEFAULT_FORMATTER: OptionFormatter<'a, T> = &|ans| ans.to_string();
113
114 /// Default filter function, which checks if the current filter value is a substring of the option value.
115 /// If it is, the option is displayed.
116 ///
117 /// # Examples
118 ///
119 /// ```
120 /// use inquire::Select;
121 ///
122 /// let filter = Select::<&str>::DEFAULT_FILTER;
123 /// assert_eq!(false, filter("sa", &"New York", "New York", 0));
124 /// assert_eq!(true, filter("sa", &"Sacramento", "Sacramento", 1));
125 /// assert_eq!(true, filter("sa", &"Kansas", "Kansas", 2));
126 /// assert_eq!(true, filter("sa", &"Mesa", "Mesa", 3));
127 /// assert_eq!(false, filter("sa", &"Phoenix", "Phoenix", 4));
128 /// assert_eq!(false, filter("sa", &"Philadelphia", "Philadelphia", 5));
129 /// assert_eq!(true, filter("sa", &"San Antonio", "San Antonio", 6));
130 /// assert_eq!(true, filter("sa", &"San Diego", "San Diego", 7));
131 /// assert_eq!(false, filter("sa", &"Dallas", "Dallas", 8));
132 /// assert_eq!(true, filter("sa", &"San Francisco", "San Francisco", 9));
133 /// assert_eq!(false, filter("sa", &"Austin", "Austin", 10));
134 /// assert_eq!(false, filter("sa", &"Jacksonville", "Jacksonville", 11));
135 /// assert_eq!(true, filter("sa", &"San Jose", "San Jose", 12));
136 /// ```
137 pub const DEFAULT_FILTER: Filter<'a, T> = &|filter, _, string_value, _| -> bool {
138 let filter = filter.to_lowercase();
139
140 string_value.to_lowercase().contains(&filter)
141 };
142
143 /// Default page size.
144 pub const DEFAULT_PAGE_SIZE: usize = config::DEFAULT_PAGE_SIZE;
145
146 /// Default value of vim mode.
147 pub const DEFAULT_VIM_MODE: bool = config::DEFAULT_VIM_MODE;
148
149 /// Default starting cursor index.
150 pub const DEFAULT_STARTING_CURSOR: usize = 0;
151
152 /// Default help message.
153 pub const DEFAULT_HELP_MESSAGE: Option<&'a str> =
154 Some("↑↓ to move, enter to select, type to filter");
155
156 /// Creates a [Select] with the provided message and options, along with default configuration values.
157 pub fn new(message: &'a str, options: Vec<T>) -> Self {
158 Self {
159 message,
160 options,
161 help_message: Self::DEFAULT_HELP_MESSAGE,
162 page_size: Self::DEFAULT_PAGE_SIZE,
163 vim_mode: Self::DEFAULT_VIM_MODE,
164 starting_cursor: Self::DEFAULT_STARTING_CURSOR,
165 filter: Self::DEFAULT_FILTER,
166 formatter: Self::DEFAULT_FORMATTER,
167 render_config: get_configuration(),
168 }
169 }
170
171 /// Sets the help message of the prompt.
172 pub fn with_help_message(mut self, message: &'a str) -> Self {
173 self.help_message = Some(message);
174 self
175 }
176
177 /// Removes the set help message.
178 pub fn without_help_message(mut self) -> Self {
179 self.help_message = None;
180 self
181 }
182
183 /// Sets the page size.
184 pub fn with_page_size(mut self, page_size: usize) -> Self {
185 self.page_size = page_size;
186 self
187 }
188
189 /// Enables or disables vim_mode.
190 pub fn with_vim_mode(mut self, vim_mode: bool) -> Self {
191 self.vim_mode = vim_mode;
192 self
193 }
194
195 /// Sets the filter function.
196 pub fn with_filter(mut self, filter: Filter<'a, T>) -> Self {
197 self.filter = filter;
198 self
199 }
200
201 /// Sets the formatter.
202 pub fn with_formatter(mut self, formatter: OptionFormatter<'a, T>) -> Self {
203 self.formatter = formatter;
204 self
205 }
206
207 /// Sets the starting cursor index.
208 pub fn with_starting_cursor(mut self, starting_cursor: usize) -> Self {
209 self.starting_cursor = starting_cursor;
210 self
211 }
212
213 /// Sets the provided color theme to this prompt.
214 ///
215 /// Note: The default render config considers if the NO_COLOR environment variable
216 /// is set to decide whether to render the colored config or the empty one.
217 ///
218 /// When overriding the config in a prompt, NO_COLOR is no longer considered and your
219 /// config is treated as the only source of truth. If you want to customize colors
220 /// and still suport NO_COLOR, you will have to do this on your end.
221 pub fn with_render_config(mut self, render_config: RenderConfig) -> Self {
222 self.render_config = render_config;
223 self
224 }
225
226 /// Parses the provided behavioral and rendering options and prompts
227 /// the CLI user for input according to the defined rules.
228 ///
229 /// Returns the owned object selected by the user.
230 pub fn prompt(self) -> InquireResult<T> {
231 self.raw_prompt().map(|op| op.value)
232 }
233
234 /// Parses the provided behavioral and rendering options and prompts
235 /// the CLI user for input according to the defined rules.
236 ///
237 /// This method is intended for flows where the user skipping/cancelling
238 /// the prompt - by pressing ESC - is considered normal behavior. In this case,
239 /// it does not return `Err(InquireError::OperationCanceled)`, but `Ok(None)`.
240 ///
241 /// Meanwhile, if the user does submit an answer, the method wraps the return
242 /// type with `Some`.
243 pub fn prompt_skippable(self) -> InquireResult<Option<T>> {
244 match self.prompt() {
245 Ok(answer) => Ok(Some(answer)),
246 Err(InquireError::OperationCanceled) => Ok(None),
247 Err(err) => Err(err),
248 }
249 }
250
251 /// Parses the provided behavioral and rendering options and prompts
252 /// the CLI user for input according to the defined rules.
253 ///
254 /// Returns a [`ListOption`](crate::list_option::ListOption) containing
255 /// the index of the selection and the owned object selected by the user.
256 pub fn raw_prompt(self) -> InquireResult<ListOption<T>> {
257 let terminal = get_default_terminal()?;
258 let mut backend = Backend::new(terminal, self.render_config)?;
259 self.prompt_with_backend(&mut backend)
260 }
261
262 pub(crate) fn prompt_with_backend<B: SelectBackend>(
263 self,
264 backend: &mut B,
265 ) -> InquireResult<ListOption<T>> {
266 SelectPrompt::new(self)?.prompt(backend)
267 }
268}
269
270struct SelectPrompt<'a, T> {
271 message: &'a str,
272 options: Vec<T>,
273 string_options: Vec<String>,
274 filtered_options: Vec<usize>,
275 help_message: Option<&'a str>,
276 vim_mode: bool,
277 cursor_index: usize,
278 page_size: usize,
279 input: Input,
280 filter: Filter<'a, T>,
281 formatter: OptionFormatter<'a, T>,
282}
283
284impl<'a, T> SelectPrompt<'a, T>
285where
286 T: Display,
287{
288 fn new(so: Select<'a, T>) -> InquireResult<Self> {
289 if so.options.is_empty() {
290 return Err(InquireError::InvalidConfiguration(
291 "Available options can not be empty".into(),
292 ));
293 }
294
295 if so.starting_cursor >= so.options.len() {
296 return Err(InquireError::InvalidConfiguration(format!(
297 "Starting cursor index {} is out-of-bounds for length {} of options",
298 so.starting_cursor,
299 &so.options.len()
300 )));
301 }
302
303 let string_options = so.options.iter().map(T::to_string).collect();
304 let filtered_options = (0..so.options.len()).collect();
305
306 Ok(Self {
307 message: so.message,
308 options: so.options,
309 string_options,
310 filtered_options,
311 help_message: so.help_message,
312 vim_mode: so.vim_mode,
313 cursor_index: so.starting_cursor,
314 page_size: so.page_size,
315 input: Input::new(),
316 filter: so.filter,
317 formatter: so.formatter,
318 })
319 }
320
321 fn filter_options(&self) -> Vec<usize> {
322 self.options
323 .iter()
324 .enumerate()
325 .filter_map(|(i, opt)| match self.input.content() {
326 val if val.is_empty() => Some(i),
327 val if (self.filter)(val, opt, self.string_options.get(i).unwrap(), i) => Some(i),
328 _ => None,
329 })
330 .collect()
331 }
332
333 fn move_cursor_up(&mut self, qty: usize, wrap: bool) {
334 if wrap {
335 let after_wrap = qty.saturating_sub(self.cursor_index);
336 self.cursor_index = self
337 .cursor_index
338 .checked_sub(qty)
339 .unwrap_or_else(|| self.filtered_options.len().saturating_sub(after_wrap))
340 } else {
341 self.cursor_index = self.cursor_index.saturating_sub(qty);
342 }
343 }
344
345 fn move_cursor_down(&mut self, qty: usize, wrap: bool) {
346 self.cursor_index = self.cursor_index.saturating_add(qty);
347
348 if self.cursor_index >= self.filtered_options.len() {
349 self.cursor_index = if self.filtered_options.is_empty() {
350 0
351 } else if wrap {
352 self.cursor_index % self.filtered_options.len()
353 } else {
354 self.filtered_options.len().saturating_sub(1)
355 }
356 }
357 }
358
359 fn on_change(&mut self, key: Key) {
360 match key {
361 Key::Up(KeyModifiers::NONE) => self.move_cursor_up(1, true),
362 Key::Char('k', KeyModifiers::NONE) if self.vim_mode => self.move_cursor_up(1, true),
363 Key::PageUp => self.move_cursor_up(self.page_size, false),
364 Key::Home => self.move_cursor_up(usize::MAX, false),
365
366 Key::Down(KeyModifiers::NONE) => self.move_cursor_down(1, true),
367 Key::Char('j', KeyModifiers::NONE) if self.vim_mode => self.move_cursor_down(1, true),
368 Key::PageDown => self.move_cursor_down(self.page_size, false),
369 Key::End => self.move_cursor_down(usize::MAX, false),
370
371 key => {
372 let dirty = self.input.handle_key(key);
373
374 if dirty {
375 let options = self.filter_options();
376 if options.len() <= self.cursor_index {
377 self.cursor_index = options.len().saturating_sub(1);
378 }
379 self.filtered_options = options;
380 }
381 }
382 };
383 }
384
385 fn has_answer_highlighted(&mut self) -> bool {
386 self.filtered_options.get(self.cursor_index).is_some()
387 }
388
389 fn get_final_answer(&mut self) -> ListOption<T> {
390 // should only be called after current cursor index is validated
391 // on has_answer_highlighted
392
393 let index = *self.filtered_options.get(self.cursor_index).unwrap();
394 let value = self.options.swap_remove(index);
395
396 ListOption::new(index, value)
397 }
398
399 fn render<B: SelectBackend>(&mut self, backend: &mut B) -> InquireResult<()> {
400 let prompt = &self.message;
401
402 backend.frame_setup()?;
403
404 backend.render_select_prompt(prompt, &self.input)?;
405
406 let choices = self
407 .filtered_options
408 .iter()
409 .cloned()
410 .map(|i| ListOption::new(i, self.options.get(i).unwrap()))
411 .collect::<Vec<ListOption<&T>>>();
412
413 let page = paginate(self.page_size, &choices, Some(self.cursor_index));
414
415 backend.render_options(page)?;
416
417 if let Some(help_message) = self.help_message {
418 backend.render_help_message(help_message)?;
419 }
420
421 backend.frame_finish()?;
422
423 Ok(())
424 }
425
426 fn prompt<B: SelectBackend>(mut self, backend: &mut B) -> InquireResult<ListOption<T>> {
427 loop {
428 self.render(backend)?;
429
430 let key = backend.read_key()?;
431
432 match key {
433 Key::Interrupt => interrupt_prompt!(),
434 Key::Cancel => cancel_prompt!(backend, self.message),
435 Key::Submit => match self.has_answer_highlighted() {
436 true => break,
437 false => {}
438 },
439 key => self.on_change(key),
440 }
441 }
442
443 let final_answer = self.get_final_answer();
444 let formatted = (self.formatter)(final_answer.as_ref());
445
446 finish_prompt_with_answer!(backend, self.message, &formatted, final_answer);
447 }
448}
449
450#[cfg(test)]
451#[cfg(feature = "crossterm")]
452mod test {
453 use crate::{
454 formatter::OptionFormatter,
455 list_option::ListOption,
456 terminal::crossterm::CrosstermTerminal,
457 ui::{Backend, RenderConfig},
458 Select,
459 };
460 use crossterm::event::{KeyCode, KeyEvent};
461
462 #[test]
463 /// Tests that a closure that actually closes on a variable can be used
464 /// as a Select formatter.
465 fn closure_formatter() {
466 let read: Vec<KeyEvent> = vec![KeyCode::Down, KeyCode::Enter]
467 .into_iter()
468 .map(KeyEvent::from)
469 .collect();
470 let mut read = read.iter();
471
472 let formatted = String::from("Thanks!");
473 let formatter: OptionFormatter<i32> = &|_| formatted.clone();
474
475 let options = vec![1, 2, 3];
476
477 let mut write: Vec<u8> = Vec::new();
478 let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read);
479 let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap();
480
481 let ans = Select::new("Question", options)
482 .with_formatter(formatter)
483 .prompt_with_backend(&mut backend)
484 .unwrap();
485
486 assert_eq!(ListOption::new(1, 2), ans);
487 }
488
489 #[test]
490 // Anti-regression test: https://github.com/mikaelmello/inquire/issues/29
491 fn enter_arrow_on_empty_list_does_not_panic() {
492 let read: Vec<KeyEvent> = [
493 KeyCode::Char('9'),
494 KeyCode::Enter,
495 KeyCode::Backspace,
496 KeyCode::Char('3'),
497 KeyCode::Enter,
498 ]
499 .iter()
500 .map(|c| KeyEvent::from(*c))
501 .collect();
502
503 let mut read = read.iter();
504
505 let options = vec![1, 2, 3];
506
507 let mut write: Vec<u8> = Vec::new();
508 let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read);
509 let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap();
510
511 let ans = Select::new("Question", options)
512 .prompt_with_backend(&mut backend)
513 .unwrap();
514
515 assert_eq!(ListOption::new(2, 3), ans);
516 }
517
518 #[test]
519 // Anti-regression test: https://github.com/mikaelmello/inquire/issues/30
520 fn down_arrow_on_empty_list_does_not_panic() {
521 let read: Vec<KeyEvent> = [
522 KeyCode::Char('9'),
523 KeyCode::Down,
524 KeyCode::Backspace,
525 KeyCode::Char('3'),
526 KeyCode::Down,
527 KeyCode::Backspace,
528 KeyCode::Enter,
529 ]
530 .iter()
531 .map(|c| KeyEvent::from(*c))
532 .collect();
533
534 let mut read = read.iter();
535
536 let options = vec![1, 2, 3];
537
538 let mut write: Vec<u8> = Vec::new();
539 let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read);
540 let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap();
541
542 let ans = Select::new("Question", options)
543 .prompt_with_backend(&mut backend)
544 .unwrap();
545
546 assert_eq!(ListOption::new(0, 1), ans);
547 }
548}