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