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