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