dialoguer/prompts/
select.rs

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