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}