cli_prompts/prompts/options/
multiselect.rs1use 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
13pub 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 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 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 pub fn help_message<S: Into<String>>(mut self, message: S) -> Self {
89 self.help_message = Some(message.into());
90 self
91 }
92
93 pub fn dont_display_help_message(mut self) -> Self {
95 self.help_message = None;
96 self
97 }
98
99 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}