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