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
103        self
104    }
105
106    /// Adds multiple items to the selector.
107    pub fn items<T, I>(mut self, items: I) -> Self
108    where
109        T: ToString,
110        I: IntoIterator<Item = T>,
111    {
112        self.items
113            .extend(items.into_iter().map(|item| item.to_string()));
114
115        self
116    }
117
118    /// Sets the select prompt.
119    ///
120    /// By default, when a prompt is set the system also prints out a confirmation after
121    /// the selection. You can opt-out of this with [`report`](Self::report).
122    pub fn with_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
123        self.prompt = Some(prompt.into());
124        self.report = true;
125        self
126    }
127
128    /// Indicates whether to report the selected value after interaction.
129    ///
130    /// The default is to report the selection.
131    pub fn report(mut self, val: bool) -> Self {
132        self.report = val;
133        self
134    }
135
136    /// Enables user interaction and returns the result.
137    ///
138    /// The user can select the items with the 'Space' bar or 'Enter' and the index of selected item will be returned.
139    /// The dialog is rendered on stderr.
140    /// Result contains `index` if user selected one of items using 'Enter'.
141    /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'.
142    #[inline]
143    pub fn interact(self) -> Result<usize> {
144        self.interact_on(&Term::stderr())
145    }
146
147    /// Enables user interaction and returns the result.
148    ///
149    /// The user can select the items with the 'Space' bar or 'Enter' and the index of selected item will be returned.
150    /// The dialog is rendered on stderr.
151    /// Result contains `Some(index)` if user selected one of items using 'Enter' or `None` if user cancelled with 'Esc' or 'q'.
152    ///
153    /// ## Example
154    ///
155    ///```rust,no_run
156    /// use dialoguer::Select;
157    ///
158    /// fn main() {
159    ///     let items = vec!["foo", "bar", "baz"];
160    ///
161    ///     let selection = Select::new()
162    ///         .with_prompt("What do you choose?")
163    ///         .items(&items)
164    ///         .interact_opt()
165    ///         .unwrap();
166    ///
167    ///     match selection {
168    ///         Some(index) => println!("You chose: {}", items[index]),
169    ///         None => println!("You did not choose anything.")
170    ///     }
171    /// }
172    ///```
173    #[inline]
174    pub fn interact_opt(self) -> Result<Option<usize>> {
175        self.interact_on_opt(&Term::stderr())
176    }
177
178    /// Like [`interact`](Self::interact) but allows a specific terminal to be set.
179    #[inline]
180    pub fn interact_on(self, term: &Term) -> Result<usize> {
181        Ok(self
182            ._interact_on(term, false)?
183            .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))?)
184    }
185
186    /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set.
187    #[inline]
188    pub fn interact_on_opt(self, term: &Term) -> Result<Option<usize>> {
189        self._interact_on(term, true)
190    }
191
192    /// Like `interact` but allows a specific terminal to be set.
193    fn _interact_on(self, term: &Term, allow_quit: bool) -> Result<Option<usize>> {
194        if !term.is_term() {
195            return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into());
196        }
197
198        if self.items.is_empty() {
199            return Err(io::Error::new(
200                io::ErrorKind::Other,
201                "Empty list of items given to `Select`",
202            ))?;
203        }
204
205        let mut paging = Paging::new(term, self.items.len(), self.max_length);
206        let mut render = TermThemeRenderer::new(term, self.theme);
207        let mut sel = self.default;
208
209        let mut size_vec = Vec::new();
210
211        for items in self
212            .items
213            .iter()
214            .flat_map(|i| i.split('\n'))
215            .collect::<Vec<_>>()
216        {
217            let size = &items.len();
218            size_vec.push(*size);
219        }
220
221        term.hide_cursor()?;
222        paging.update_page(sel);
223
224        loop {
225            if let Some(ref prompt) = self.prompt {
226                paging.render_prompt(|paging_info| render.select_prompt(prompt, paging_info))?;
227            }
228
229            for (idx, item) in self
230                .items
231                .iter()
232                .enumerate()
233                .skip(paging.current_page * paging.capacity)
234                .take(paging.capacity)
235            {
236                render.select_prompt_item(item, sel == idx)?;
237            }
238
239            term.flush()?;
240
241            match term.read_key()? {
242                Key::ArrowDown | Key::Tab | Key::Char('j') => {
243                    if sel == !0 {
244                        sel = 0;
245                    } else {
246                        sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize;
247                    }
248                }
249                Key::Escape | Key::Char('q') => {
250                    if allow_quit {
251                        if self.clear {
252                            render.clear()?;
253                        } else {
254                            term.clear_last_lines(paging.capacity)?;
255                        }
256
257                        term.show_cursor()?;
258                        term.flush()?;
259
260                        return Ok(None);
261                    }
262                }
263                Key::ArrowUp | Key::BackTab | Key::Char('k') => {
264                    if sel == !0 {
265                        sel = self.items.len() - 1;
266                    } else {
267                        sel = ((sel as i64 - 1 + self.items.len() as i64)
268                            % (self.items.len() as i64)) as usize;
269                    }
270                }
271                Key::ArrowLeft | Key::Char('h') => {
272                    if paging.active {
273                        sel = paging.previous_page();
274                    }
275                }
276                Key::ArrowRight | Key::Char('l') => {
277                    if paging.active {
278                        sel = paging.next_page();
279                    }
280                }
281
282                Key::Enter | Key::Char(' ') if sel != !0 => {
283                    if self.clear {
284                        render.clear()?;
285                    }
286
287                    if let Some(ref prompt) = self.prompt {
288                        if self.report {
289                            render.select_prompt_selection(prompt, &self.items[sel])?;
290                        }
291                    }
292
293                    term.show_cursor()?;
294                    term.flush()?;
295
296                    return Ok(Some(sel));
297                }
298                _ => {}
299            }
300
301            paging.update(sel)?;
302
303            if paging.active {
304                render.clear()?;
305            } else {
306                render.clear_preserve_prompt(&size_vec)?;
307            }
308        }
309    }
310}
311
312impl<'a> Select<'a> {
313    /// Creates a select prompt with a specific theme.
314    ///
315    /// ## Example
316    ///
317    /// ```rust,no_run
318    /// use dialoguer::{theme::ColorfulTheme, Select};
319    ///
320    /// fn main() {
321    ///     let selection = Select::with_theme(&ColorfulTheme::default())
322    ///         .items(&["foo", "bar", "baz"])
323    ///         .interact()
324    ///         .unwrap();
325    /// }
326    /// ```
327    pub fn with_theme(theme: &'a dyn Theme) -> Self {
328        Self {
329            default: !0,
330            items: vec![],
331            prompt: None,
332            report: false,
333            clear: true,
334            max_length: None,
335            theme,
336        }
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn test_clone() {
346        let select = Select::new().with_prompt("Do you want to continue?");
347
348        let _ = select.clone();
349    }
350
351    #[test]
352    fn test_str() {
353        let selections = &[
354            "Ice Cream",
355            "Vanilla Cupcake",
356            "Chocolate Muffin",
357            "A Pile of sweet, sweet mustard",
358        ];
359
360        assert_eq!(
361            Select::new().default(0).items(&selections[..]).items,
362            selections
363        );
364    }
365
366    #[test]
367    fn test_string() {
368        let selections = vec!["a".to_string(), "b".to_string()];
369
370        assert_eq!(
371            Select::new().default(0).items(&selections).items,
372            selections
373        );
374    }
375
376    #[test]
377    fn test_ref_str() {
378        let a = "a";
379        let b = "b";
380
381        let selections = &[a, b];
382
383        assert_eq!(Select::new().default(0).items(selections).items, selections);
384    }
385
386    #[test]
387    fn test_iterator() {
388        let items = ["First", "Second", "Third"];
389        let iterator = items.iter().skip(1);
390
391        assert_eq!(Select::new().default(0).items(iterator).items, &items[1..]);
392    }
393}