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