1use crate::event::*;
2use crate::pagination::paginate;
3use crate::prompts::{DefaultSelectFormatter, SelectFormatter};
4use crate::{Error, Prompt, PromptBody, PromptInput, PromptState, RenderPayload};
5
6#[derive(Debug, Clone)]
8pub struct MultiSelectOption<T: Default + Clone> {
9 pub label: String,
11 pub value: T,
13 pub hint: Option<String>,
15 pub selected: bool,
17}
18
19impl<T: Default + Clone> MultiSelectOption<T> {
20 pub fn new(label: impl std::fmt::Display, value: T) -> Self {
22 Self {
23 label: label.to_string(),
24 value,
25 hint: None,
26 selected: false,
27 }
28 }
29
30 pub fn with_hint(mut self, hint: impl std::fmt::Display) -> Self {
32 self.hint = Some(hint.to_string());
33 self
34 }
35}
36
37pub trait MultiSelectFormatter {
82 fn option_icon(&self, active: bool, selected: bool) -> String;
84 fn option_label(&self, label: String, active: bool, selected: bool) -> String;
86 fn option_hint(&self, hint: Option<String>, active: bool, selected: bool) -> String;
88 fn option(
90 &self,
91 icon: String,
92 label: String,
93 hint: String,
94 active: bool,
95 selected: bool,
96 ) -> String;
97
98 fn submit(&self, labels: Vec<String>) -> String {
100 labels.join(", ")
101 }
102
103 fn err_required(&self) -> String {
105 "This field is required.".into()
106 }
107
108 fn err_min(&self, min: usize) -> String {
110 format!("Please select at least {} options.", min)
111 }
112
113 fn err_max(&self, max: usize) -> String {
115 format!("Please select no more than {} options.", max)
116 }
117}
118
119pub struct DefaultMultiSelectFormatter {
121 inner: DefaultSelectFormatter,
122}
123
124impl DefaultMultiSelectFormatter {
125 #[allow(clippy::new_without_default)]
126 pub fn new() -> Self {
127 Self {
128 inner: DefaultSelectFormatter::new(),
129 }
130 }
131}
132
133impl MultiSelectFormatter for DefaultMultiSelectFormatter {
134 fn option_icon(&self, _active: bool, selected: bool) -> String {
135 self.inner.option_icon(selected)
136 }
137
138 fn option_label(&self, label: String, active: bool, _selected: bool) -> String {
139 self.inner.option_label(label, active)
140 }
141
142 fn option_hint(&self, hint: Option<String>, active: bool, _selected: bool) -> String {
143 self.inner.option_hint(hint, active)
144 }
145
146 fn option(
147 &self,
148 icon: String,
149 label: String,
150 hint: String,
151 active: bool,
152 _selected: bool,
153 ) -> String {
154 self.inner.option(icon, label, hint, active)
155 }
156}
157
158pub struct MultiSelect<T: Default + Clone> {
182 formatter: Box<dyn MultiSelectFormatter>,
183 message: String,
184 hint: Option<String>,
185 required: bool,
186 min: usize,
187 max: usize,
188 page_size: usize,
189 options: Vec<MultiSelectOption<T>>,
190 index: usize,
191}
192
193impl<T: Default + Clone> MultiSelect<T> {
194 pub fn new(message: impl std::fmt::Display, options: Vec<MultiSelectOption<T>>) -> Self {
196 Self {
197 formatter: Box::new(DefaultMultiSelectFormatter::new()),
198 message: message.to_string(),
199 hint: None,
200 required: true,
201 min: 0,
202 max: usize::MAX,
203 page_size: 8,
204 options,
205 index: 0,
206 }
207 }
208
209 pub fn with_formatter(&mut self, formatter: impl MultiSelectFormatter + 'static) -> &mut Self {
211 self.formatter = Box::new(formatter);
212 self
213 }
214
215 pub fn with_hint(&mut self, hint: impl std::fmt::Display) -> &mut Self {
217 self.hint = Some(hint.to_string());
218 self
219 }
220
221 pub fn with_required(&mut self, required: bool) -> &mut Self {
223 self.required = required;
224 self
225 }
226
227 pub fn with_min(&mut self, value: usize) -> &mut Self {
229 self.min = value;
230 self
231 }
232
233 pub fn with_max(&mut self, value: usize) -> &mut Self {
235 self.max = value;
236 self
237 }
238
239 pub fn with_page_size(&mut self, page_size: usize) -> &mut Self {
241 self.page_size = page_size;
242 self
243 }
244
245 fn values(&mut self) -> Vec<T> {
246 self.options
247 .iter()
248 .filter_map(|option| {
249 if option.selected {
250 Some(option.value.clone())
251 } else {
252 None
253 }
254 })
255 .collect::<Vec<_>>()
256 }
257
258 fn map_options<F>(&mut self, f: F) -> Vec<MultiSelectOption<T>>
259 where
260 F: Fn(&MultiSelectOption<T>) -> MultiSelectOption<T>,
261 {
262 self.options.iter().map(f).collect::<Vec<_>>()
263 }
264}
265
266impl<T: Default + Clone> AsMut<MultiSelect<T>> for MultiSelect<T> {
267 fn as_mut(&mut self) -> &mut MultiSelect<T> {
268 self
269 }
270}
271
272impl<T: Default + Clone> Prompt for MultiSelect<T> {
273 type Output = Vec<T>;
274
275 fn setup(&mut self) -> Result<(), Error> {
276 if self.options.is_empty() {
277 return Err(Error::Config("options cannot be empty.".into()));
278 }
279
280 if self.min > self.max {
281 return Err(Error::Config(format!(
282 "min cannot be greater than max (min={}, max={})",
283 self.min, self.max
284 )));
285 }
286
287 Ok(())
288 }
289
290 fn handle(&mut self, code: KeyCode, modifiers: KeyModifiers) -> crate::PromptState {
291 match (code, modifiers) {
292 (KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => PromptState::Cancel,
293 (KeyCode::Enter, _) => {
294 let values = self.values();
295 if values.is_empty() && self.required {
296 PromptState::Error(self.formatter.err_required())
297 } else if values.len() < self.min {
298 PromptState::Error(self.formatter.err_min(self.min))
299 } else if values.len() > self.max {
300 PromptState::Error(self.formatter.err_max(self.max))
301 } else {
302 PromptState::Submit
303 }
304 }
305 (KeyCode::Up, _)
306 | (KeyCode::Char('k'), _)
307 | (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
308 self.index = self.index.saturating_sub(1);
309 PromptState::Active
310 }
311 (KeyCode::Down, _)
312 | (KeyCode::Char('j'), _)
313 | (KeyCode::Char('n'), KeyModifiers::CONTROL) => {
314 self.index = std::cmp::min(
315 self.options.len().saturating_sub(1),
316 self.index.saturating_add(1),
317 );
318 PromptState::Active
319 }
320 (KeyCode::Char(' '), KeyModifiers::NONE) => {
321 let mut option = self.options.get(self.index).unwrap().clone();
322 option.selected = !option.selected;
323 self.options[self.index] = option;
324 PromptState::Active
325 }
326 (KeyCode::Char('a'), KeyModifiers::NONE) => {
327 self.options = self.map_options(|option| {
328 let mut option = option.clone();
329 option.selected = true;
330 option
331 });
332 PromptState::Active
333 }
334 (KeyCode::Char('i'), KeyModifiers::NONE) => {
335 self.options = self.map_options(|option| {
336 let mut option = option.clone();
337 option.selected = !option.selected;
338 option
339 });
340 PromptState::Active
341 }
342 _ => PromptState::Active,
343 }
344 }
345
346 fn submit(&mut self) -> Self::Output {
347 self.values()
348 }
349
350 fn render(&mut self, state: &PromptState) -> Result<RenderPayload, String> {
351 let payload = RenderPayload::new(self.message.clone(), self.hint.clone(), None);
352
353 match state {
354 PromptState::Submit => {
355 let raw = self.formatter.submit(
356 self.options
357 .iter()
358 .filter_map(|option| {
359 if option.selected {
360 Some(option.label.clone())
361 } else {
362 None
363 }
364 })
365 .collect::<Vec<_>>(),
366 );
367 Ok(payload.input(PromptInput::Raw(raw)))
368 }
369
370 _ => {
371 let page = paginate(self.page_size, &self.options, self.index);
372 let options = page
373 .items
374 .iter()
375 .enumerate()
376 .map(|(i, option)| {
377 let active = i == page.cursor;
378 let selected = option.selected;
379 self.formatter.option(
380 self.formatter.option_icon(active, selected),
381 self.formatter
382 .option_label(option.label.clone(), active, selected),
383 self.formatter
384 .option_hint(option.hint.clone(), active, selected),
385 active,
386 selected,
387 )
388 })
389 .collect::<Vec<_>>()
390 .join("\n");
391
392 Ok(payload.body(PromptBody::Raw(options)))
393 }
394 }
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use crate::test_prompt;
402
403 macro_rules! options {
404 ($count: expr) => {{
405 let mut options = Vec::new();
406 for i in 1..=$count {
407 options.push(MultiSelectOption::new(
408 format!("Value{}", i),
409 format!("value{}", i),
410 ));
411 }
412 options
413 }};
414 }
415
416 test_prompt!(
417 test_hint,
418 MultiSelect::new("test message", options!(3)).with_hint("hint message"),
419 vec![]
420 );
421
422 test_prompt!(
423 test_10_items_with_5_page_size,
424 MultiSelect::new("test message", options!(10)).with_page_size(5),
425 vec![]
426 );
427
428 test_prompt!(
429 test_option_hint,
430 MultiSelect::new(
431 "test message",
432 vec![
433 MultiSelectOption::new("Value1", "value1".to_string()).with_hint("hint1"),
434 MultiSelectOption::new("Value2", "value2".to_string()),
435 MultiSelectOption::new("Value3", "value3".to_string()).with_hint("hint3"),
436 ]
437 )
438 .with_page_size(5),
439 vec![]
440 );
441
442 test_prompt!(
443 test_move,
444 MultiSelect::new("test message", options!(10)).with_page_size(5),
445 vec![
446 (KeyCode::Char('j'), KeyModifiers::NONE),
447 (KeyCode::Char('n'), KeyModifiers::CONTROL),
448 (KeyCode::Char('k'), KeyModifiers::NONE),
449 (KeyCode::Char('p'), KeyModifiers::CONTROL),
450 (KeyCode::Down, KeyModifiers::NONE),
451 (KeyCode::Down, KeyModifiers::NONE),
452 (KeyCode::Down, KeyModifiers::NONE),
453 (KeyCode::Down, KeyModifiers::NONE),
454 (KeyCode::Down, KeyModifiers::NONE),
455 (KeyCode::Down, KeyModifiers::NONE),
456 (KeyCode::Down, KeyModifiers::NONE),
457 (KeyCode::Down, KeyModifiers::NONE),
458 (KeyCode::Down, KeyModifiers::NONE), (KeyCode::Down, KeyModifiers::NONE),
460 (KeyCode::Up, KeyModifiers::NONE),
461 (KeyCode::Up, KeyModifiers::NONE),
462 (KeyCode::Up, KeyModifiers::NONE),
463 (KeyCode::Up, KeyModifiers::NONE),
464 (KeyCode::Up, KeyModifiers::NONE),
465 (KeyCode::Up, KeyModifiers::NONE),
466 (KeyCode::Up, KeyModifiers::NONE),
467 (KeyCode::Up, KeyModifiers::NONE),
468 (KeyCode::Up, KeyModifiers::NONE), (KeyCode::Up, KeyModifiers::NONE),
470 ]
471 );
472
473 test_prompt!(
474 test_select_2_and_5,
475 MultiSelect::new("test message", options!(10)).with_page_size(5),
476 vec![
477 (KeyCode::Down, KeyModifiers::NONE),
478 (KeyCode::Char(' '), KeyModifiers::NONE),
479 (KeyCode::Down, KeyModifiers::NONE),
480 (KeyCode::Down, KeyModifiers::NONE),
481 (KeyCode::Down, KeyModifiers::NONE),
482 (KeyCode::Char(' '), KeyModifiers::NONE),
483 (KeyCode::Enter, KeyModifiers::NONE),
484 ]
485 );
486
487 test_prompt!(
488 test_select_all_and_inverse,
489 MultiSelect::new("test message", options!(5)).as_mut(),
490 vec![
491 (KeyCode::Char('a'), KeyModifiers::NONE),
492 (KeyCode::Char('i'), KeyModifiers::NONE),
493 (KeyCode::Down, KeyModifiers::NONE),
494 (KeyCode::Char(' '), KeyModifiers::NONE),
495 (KeyCode::Down, KeyModifiers::NONE),
496 (KeyCode::Char(' '), KeyModifiers::NONE),
497 (KeyCode::Char('i'), KeyModifiers::NONE),
498 (KeyCode::Enter, KeyModifiers::NONE),
499 ]
500 );
501
502 test_prompt!(
503 test_required_error,
504 MultiSelect::new("test message", options!(5)).with_required(true),
505 vec![(KeyCode::Enter, KeyModifiers::NONE)]
506 );
507
508 test_prompt!(
509 test_non_required_empty_submit,
510 MultiSelect::new("test message", options!(5)).with_required(false),
511 vec![(KeyCode::Enter, KeyModifiers::NONE)]
512 );
513
514 test_prompt!(
515 test_min_error,
516 MultiSelect::new("test message", options!(5)).with_min(2),
517 vec![
518 (KeyCode::Enter, KeyModifiers::NONE),
519 (KeyCode::Char(' '), KeyModifiers::NONE),
520 (KeyCode::Enter, KeyModifiers::NONE),
521 (KeyCode::Down, KeyModifiers::NONE),
522 (KeyCode::Char(' '), KeyModifiers::NONE),
523 (KeyCode::Enter, KeyModifiers::NONE),
524 ]
525 );
526
527 test_prompt!(
528 test_max_error,
529 MultiSelect::new("test message", options!(5)).with_max(3),
530 vec![
531 (KeyCode::Enter, KeyModifiers::NONE),
532 (KeyCode::Char(' '), KeyModifiers::NONE),
533 (KeyCode::Down, KeyModifiers::NONE),
534 (KeyCode::Char(' '), KeyModifiers::NONE),
535 (KeyCode::Down, KeyModifiers::NONE),
536 (KeyCode::Char(' '), KeyModifiers::NONE),
537 (KeyCode::Down, KeyModifiers::NONE),
538 (KeyCode::Char(' '), KeyModifiers::NONE),
539 (KeyCode::Enter, KeyModifiers::NONE),
540 (KeyCode::Char(' '), KeyModifiers::NONE),
541 (KeyCode::Enter, KeyModifiers::NONE),
542 ]
543 );
544}