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