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}