dialoguer/prompts/
multi_select.rs

1use std::{io, iter::repeat, ops::Rem};
2
3use console::{Key, Term};
4
5use crate::{
6    theme::{render::TermThemeRenderer, SimpleTheme, Theme},
7    Paging, Result,
8};
9
10/// Renders a multi select prompt.
11///
12/// ## Example
13///
14/// ```rust,no_run
15/// use dialoguer::MultiSelect;
16///
17/// fn main() {
18///     let items = vec!["foo", "bar", "baz"];
19///
20///     let selection = MultiSelect::new()
21///         .with_prompt("What do you choose?")
22///         .items(&items)
23///         .interact()
24///         .unwrap();
25///
26///     println!("You chose:");
27///
28///     for i in selection {
29///         println!("{}", items[i]);
30///     }
31/// }
32/// ```
33#[derive(Clone)]
34pub struct MultiSelect<'a> {
35    defaults: Vec<bool>,
36    items: Vec<String>,
37    prompt: Option<String>,
38    report: bool,
39    clear: bool,
40    max_length: Option<usize>,
41    theme: &'a dyn Theme,
42}
43
44impl Default for MultiSelect<'static> {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50impl MultiSelect<'static> {
51    /// Creates a multi select prompt with default theme.
52    pub fn new() -> Self {
53        Self::with_theme(&SimpleTheme)
54    }
55}
56
57impl MultiSelect<'_> {
58    /// Sets the clear behavior of the menu.
59    ///
60    /// The default is to clear the menu.
61    pub fn clear(mut self, val: bool) -> Self {
62        self.clear = val;
63        self
64    }
65
66    /// Sets a defaults for the menu.
67    pub fn defaults(mut self, val: &[bool]) -> Self {
68        self.defaults = val
69            .to_vec()
70            .iter()
71            .copied()
72            .chain(repeat(false))
73            .take(self.items.len())
74            .collect();
75        self
76    }
77
78    /// Sets an optional max length for a page
79    ///
80    /// Max length is disabled by None
81    pub fn max_length(mut self, val: usize) -> Self {
82        // Paging subtracts two from the capacity, paging does this to
83        // make an offset for the page indicator. So to make sure that
84        // we can show the intended amount of items we need to add two
85        // to our value.
86        self.max_length = Some(val + 2);
87        self
88    }
89
90    /// Add a single item to the selector.
91    #[inline]
92    pub fn item<T: ToString>(self, item: T) -> Self {
93        self.item_checked(item, false)
94    }
95
96    /// Add a single item to the selector with a default checked state.
97    pub fn item_checked<T: ToString>(mut self, item: T, checked: bool) -> Self {
98        self.items.push(item.to_string());
99        self.defaults.push(checked);
100        self
101    }
102
103    /// Adds multiple items to the selector.
104    pub fn items<T, I>(self, items: I) -> Self
105    where
106        T: ToString,
107        I: IntoIterator<Item = T>,
108    {
109        self.items_checked(items.into_iter().map(|item| (item, false)))
110    }
111
112    /// Adds multiple items to the selector with checked state
113    pub fn items_checked<T, I>(mut self, items: I) -> Self
114    where
115        T: ToString,
116        I: IntoIterator<Item = (T, bool)>,
117    {
118        for (item, checked) in items.into_iter() {
119            self.items.push(item.to_string());
120            self.defaults.push(checked);
121        }
122        self
123    }
124
125    /// Prefaces the menu with a prompt.
126    ///
127    /// By default, when a prompt is set the system also prints out a confirmation after
128    /// the selection. You can opt-out of this with [`report`](Self::report).
129    pub fn with_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
130        self.prompt = Some(prompt.into());
131        self
132    }
133
134    /// Indicates whether to report the selected values after interaction.
135    ///
136    /// The default is to report the selections.
137    pub fn report(mut self, val: bool) -> Self {
138        self.report = val;
139        self
140    }
141
142    /// Enables user interaction and returns the result.
143    ///
144    /// The user can select the items with the 'Space' bar and on 'Enter' the indices of selected items will be returned.
145    /// The dialog is rendered on stderr.
146    /// Result contains `Vec<index>` if user hit 'Enter'.
147    /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'.
148    #[inline]
149    pub fn interact(self) -> Result<Vec<usize>> {
150        self.interact_on(&Term::stderr())
151    }
152
153    /// Enables user interaction and returns the result.
154    ///
155    /// The user can select the items with the 'Space' bar and on 'Enter' the indices of selected items will be returned.
156    /// The dialog is rendered on stderr.
157    /// Result contains `Some(Vec<index>)` if user hit 'Enter' or `None` if user cancelled with 'Esc' or 'q'.
158    ///
159    /// ## Example
160    ///
161    /// ```rust,no_run
162    /// use dialoguer::MultiSelect;
163    ///
164    /// fn main() {
165    ///     let items = vec!["foo", "bar", "baz"];
166    ///
167    ///     let ordered = MultiSelect::new()
168    ///         .items(&items)
169    ///         .interact_opt()
170    ///         .unwrap();
171    ///
172    ///     match ordered {
173    ///         Some(positions) => {
174    ///             println!("You chose:");
175    ///
176    ///             for i in positions {
177    ///                 println!("{}", items[i]);
178    ///             }
179    ///         },
180    ///         None => println!("You did not choose anything.")
181    ///     }
182    /// }
183    /// ```
184    #[inline]
185    pub fn interact_opt(self) -> Result<Option<Vec<usize>>> {
186        self.interact_on_opt(&Term::stderr())
187    }
188
189    /// Like [`interact`](Self::interact) but allows a specific terminal to be set.
190    #[inline]
191    pub fn interact_on(self, term: &Term) -> Result<Vec<usize>> {
192        Ok(self
193            ._interact_on(term, false)?
194            .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))?)
195    }
196
197    /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set.
198    #[inline]
199    pub fn interact_on_opt(self, term: &Term) -> Result<Option<Vec<usize>>> {
200        self._interact_on(term, true)
201    }
202
203    fn _interact_on(self, term: &Term, allow_quit: bool) -> Result<Option<Vec<usize>>> {
204        if !term.is_term() {
205            return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into());
206        }
207
208        if self.items.is_empty() {
209            return Err(io::Error::new(
210                io::ErrorKind::Other,
211                "Empty list of items given to `MultiSelect`",
212            ))?;
213        }
214
215        let mut paging = Paging::new(term, self.items.len(), self.max_length);
216        let mut render = TermThemeRenderer::new(term, self.theme);
217        let mut sel = 0;
218
219        let mut size_vec = Vec::new();
220
221        for items in self
222            .items
223            .iter()
224            .flat_map(|i| i.split('\n'))
225            .collect::<Vec<_>>()
226        {
227            let size = &items.len();
228            size_vec.push(*size);
229        }
230
231        let mut checked: Vec<bool> = self.defaults.clone();
232
233        term.hide_cursor()?;
234
235        loop {
236            if let Some(ref prompt) = self.prompt {
237                paging
238                    .render_prompt(|paging_info| render.multi_select_prompt(prompt, paging_info))?;
239            }
240
241            for (idx, item) in self
242                .items
243                .iter()
244                .enumerate()
245                .skip(paging.current_page * paging.capacity)
246                .take(paging.capacity)
247            {
248                render.multi_select_prompt_item(item, checked[idx], sel == idx)?;
249            }
250
251            term.flush()?;
252
253            match term.read_key()? {
254                Key::ArrowDown | Key::Tab | Key::Char('j') => {
255                    if sel == !0 {
256                        sel = 0;
257                    } else {
258                        sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize;
259                    }
260                }
261                Key::ArrowUp | Key::BackTab | Key::Char('k') => {
262                    if sel == !0 {
263                        sel = self.items.len() - 1;
264                    } else {
265                        sel = ((sel as i64 - 1 + self.items.len() as i64)
266                            % (self.items.len() as i64)) as usize;
267                    }
268                }
269                Key::ArrowLeft | Key::Char('h') => {
270                    if paging.active {
271                        sel = paging.previous_page();
272                    }
273                }
274                Key::ArrowRight | Key::Char('l') => {
275                    if paging.active {
276                        sel = paging.next_page();
277                    }
278                }
279                Key::Char(' ') => {
280                    checked[sel] = !checked[sel];
281                }
282                Key::Char('a') => {
283                    if checked.iter().all(|&item_checked| item_checked) {
284                        checked.fill(false);
285                    } else {
286                        checked.fill(true);
287                    }
288                }
289                Key::Escape | Key::Char('q') => {
290                    if allow_quit {
291                        if self.clear {
292                            render.clear()?;
293                        } else {
294                            term.clear_last_lines(paging.capacity)?;
295                        }
296
297                        term.show_cursor()?;
298                        term.flush()?;
299
300                        return Ok(None);
301                    }
302                }
303                Key::Enter => {
304                    if self.clear {
305                        render.clear()?;
306                    }
307
308                    if let Some(ref prompt) = self.prompt {
309                        if self.report {
310                            let selections: Vec<_> = checked
311                                .iter()
312                                .enumerate()
313                                .filter_map(|(idx, &checked)| {
314                                    if checked {
315                                        Some(self.items[idx].as_str())
316                                    } else {
317                                        None
318                                    }
319                                })
320                                .collect();
321
322                            render.multi_select_prompt_selection(prompt, &selections[..])?;
323                        }
324                    }
325
326                    term.show_cursor()?;
327                    term.flush()?;
328
329                    return Ok(Some(
330                        checked
331                            .into_iter()
332                            .enumerate()
333                            .filter_map(|(idx, checked)| if checked { Some(idx) } else { None })
334                            .collect(),
335                    ));
336                }
337                _ => {}
338            }
339
340            paging.update(sel)?;
341
342            if paging.active {
343                render.clear()?;
344            } else {
345                render.clear_preserve_prompt(&size_vec)?;
346            }
347        }
348    }
349}
350
351impl<'a> MultiSelect<'a> {
352    /// Creates a multi select prompt with a specific theme.
353    ///
354    /// ## Example
355    ///
356    /// ```rust,no_run
357    /// use dialoguer::{theme::ColorfulTheme, MultiSelect};
358    ///
359    /// fn main() {
360    ///     let selection = MultiSelect::with_theme(&ColorfulTheme::default())
361    ///         .items(&["foo", "bar", "baz"])
362    ///         .interact()
363    ///         .unwrap();
364    /// }
365    /// ```
366    pub fn with_theme(theme: &'a dyn Theme) -> Self {
367        Self {
368            items: vec![],
369            defaults: vec![],
370            clear: true,
371            prompt: None,
372            report: true,
373            max_length: None,
374            theme,
375        }
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    #[test]
384    fn test_clone() {
385        let multi_select = MultiSelect::new().with_prompt("Select your favorite(s)");
386
387        let _ = multi_select.clone();
388    }
389
390    #[test]
391    fn test_iterator() {
392        let items = ["First", "Second", "Third"];
393        let iterator = items.iter().skip(1);
394
395        assert_eq!(MultiSelect::new().items(iterator).items, &items[1..]);
396    }
397}