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