cli_prompts/prompts/options/
multiselect.rs

1use crate::{
2    engine::CommandBuffer,
3    input::Key,
4    prompts::{options::Options, AbortReason, EventOutcome, Prompt},
5    style::MultiselectionStyle,
6};
7
8use super::multioption_prompt::MultiOptionPrompt;
9
10const DEFAUTL_MAX_OPTIONS: u16 = 5;
11const DEFAULT_HELP_MESSAGE: &str = "Space to select, enter to submit";
12
13/// Prompt that allows to select multiple options from the given list.
14/// Supports filtering and moving the selection with arrow keys.
15///
16/// ```rust
17/// use cli_prompts::{
18///     prompts::{Multiselect, AbortReason},
19///     DisplayPrompt,
20/// };
21///
22/// fn main() {
23///     let files = [
24///         "hello.txt",
25///         "image.jpg",
26///         "music.mp3",
27///         "document.pdf"
28///     ];
29///
30///     let prompt = Multiselect::new("Select files to copy", files.into_iter())
31///                     .dont_display_help_message()
32///                     .max_displayed_options(3);
33///     let selection : Result<Vec<&str>, AbortReason> = prompt.display();
34///     match selection {
35///         Ok(selected_files) => {
36///             for file in selected_files {
37///                 // Copy the file
38///             }
39///         },
40///         Err(abort_reason) => println!("Prompt is aborted because of {:?}", abort_reason),
41///     }
42/// }
43/// ```
44pub struct Multiselect<T> {
45    label: String,
46    options: Options<T>,
47    selected_options: Vec<usize>,
48    help_message: Option<String>,
49    max_displayed_options: u16,
50    currently_selected_index: usize,
51    is_submitted: bool,
52    filter: String,
53    style: MultiselectionStyle,
54}
55
56impl<T> Multiselect<T>
57where
58    T: Into<String> + Clone,
59{
60
61    /// Create new prompt with the given label and the iterator over a type that is convertable to
62    /// `String`
63    pub fn new<S, I>(label: S, options: I) -> Self
64    where
65        S: Into<String>,
66        I: Iterator<Item = T>,
67    {
68        let options = Options::from_iter(options);
69        Self::new_internal(label.into(), options)
70    }
71}
72
73impl<T> Multiselect<T> {
74    
75    /// Create new prompt with the given label and a transformation function that will convert the
76    /// iterator items to strings
77    pub fn new_transformed<S, I, F>(label: S, options: I, transformation: F) -> Self
78    where
79        S: Into<String>,
80        I: Iterator<Item = T>,
81        F: Fn(&T) -> String,
82    {
83        let options = Options::from_iter_transformed(options, transformation);
84        Self::new_internal(label.into(), options)
85    }
86
87    /// Set help message to be displayed after the filter string
88    pub fn help_message<S: Into<String>>(mut self, message: S) -> Self {
89        self.help_message = Some(message.into());
90        self
91    }
92
93    /// Makes prompt not to display the help message
94    pub fn dont_display_help_message(mut self) -> Self {
95        self.help_message = None;
96        self
97    }
98
99    /// Sets the maximum number of options that can be displayed on the screen
100    pub fn max_displayed_options(mut self, max_options: u16) -> Self {
101        self.max_displayed_options = max_options;
102        self
103    }
104}
105
106impl<T> MultiOptionPrompt<T> for Multiselect<T> {
107    fn max_options_count(&self) -> u16 {
108        self.max_displayed_options
109    }
110
111    fn options(&self) -> &Options<T> {
112        &self.options
113    }
114
115    fn currently_selected_index(&self) -> usize {
116        self.currently_selected_index
117    }
118
119    fn draw_option(
120        &self,
121        option_index: usize,
122        option_label: &str,
123        is_selected: bool,
124        commands: &mut impl CommandBuffer,
125    ) {
126        let is_option_selected = self.selected_options.contains(&option_index);
127        self.style
128            .print_option(option_label, is_option_selected, is_selected, commands);
129    }
130
131    fn draw_header(&self, commands: &mut impl CommandBuffer, is_submitted: bool) {
132        if is_submitted {
133            commands.set_formatting(&self.style.submitted_formatting);
134            for (i, selected_index) in self.selected_options.iter().enumerate() {
135                let selected_option = &self.options.transformed_options()[*selected_index];
136                commands.print(selected_option);
137
138                if i < self.selected_options.len() - 1 {
139                    commands.print(", ");
140                }
141            }
142            commands.reset_formatting();
143        } else {
144            commands.print(&self.filter);
145            commands.print(" ");
146            if let Some(help_message) = self.help_message.as_ref() {
147                commands.set_formatting(&self.style.help_message_formatting);
148                commands.print("[");
149                commands.print(help_message);
150                commands.print("]");
151                commands.reset_formatting();
152            }
153        }
154    }
155}
156
157impl<T> Prompt<Vec<T>> for Multiselect<T> {
158    fn draw(&self, commands: &mut impl CommandBuffer) {
159        self.draw_multioption(
160            &self.label,
161            self.is_submitted,
162            &self.style.label_style,
163            commands,
164        );
165    }
166
167    fn on_key_pressed(&mut self, key: Key) -> EventOutcome<Vec<T>> {
168        match key {
169            Key::Up if self.currently_selected_index > 0 => {
170                self.currently_selected_index -= 1;
171                EventOutcome::Continue
172            }
173            Key::Down
174                if self.currently_selected_index < self.options.filtered_options().len() - 1 =>
175            {
176                self.currently_selected_index += 1;
177                EventOutcome::Continue
178            }
179            Key::Char(c) => {
180                if c == ' ' {
181                    let selected_option_index =
182                        self.options.filtered_options()[self.currently_selected_index];
183                    let existing_value_index = self
184                        .selected_options
185                        .iter()
186                        .enumerate()
187                        .find(|&x| *x.1 == selected_option_index)
188                        .map(|x| x.0);
189
190                    if let Some(i) = existing_value_index {
191                        self.selected_options.remove(i);
192                    } else {
193                        self.selected_options.push(selected_option_index);
194                    }
195
196                    if self.filter.len() > 0 {
197                        self.filter.clear();
198                        self.options.filter(&self.filter);
199                        self.currently_selected_index = 0;
200                    }
201                    EventOutcome::Continue
202                } else {
203                    self.filter.push(c);
204                    self.options.filter(&self.filter);
205                    self.currently_selected_index = 0;
206                    EventOutcome::Continue
207                }
208            }
209            Key::Backspace if self.filter.len() > 0 => {
210                self.filter.pop();
211                self.options.filter(&self.filter);
212                self.currently_selected_index = 0;
213                EventOutcome::Continue
214            }
215            Key::Enter if self.selected_options.len() > 0 => {
216                self.is_submitted = true;
217                self.selected_options.sort();
218
219                let mut result = vec![];
220                for selected_option_index in self.selected_options.iter().rev() {
221                    let selected_option = self
222                        .options
223                        .all_options_mut()
224                        .remove(*selected_option_index);
225                    result.push(selected_option);
226                }
227
228                EventOutcome::Done(result)
229            }
230            Key::Esc => EventOutcome::Abort(AbortReason::Interrupt),
231            _ => EventOutcome::Continue,
232        }
233    }
234}
235
236impl<T> Multiselect<T> {
237    fn new_internal(label: String, options: Options<T>) -> Self {
238        Multiselect {
239            label,
240            options,
241            selected_options: vec![],
242            help_message: Some(DEFAULT_HELP_MESSAGE.into()),
243            max_displayed_options: DEFAUTL_MAX_OPTIONS,
244            currently_selected_index: 0,
245            is_submitted: false,
246            filter: String::new(),
247            style: MultiselectionStyle::default(),
248        }
249    }
250}