1use crate::{
4 error::ClackError,
5 outro,
6 style::{IS_UNICODE, ansi, chars},
7};
8use crossterm::{
9 cursor,
10 event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
11 execute, terminal,
12};
13use owo_colors::OwoColorize;
14use std::{
15 fmt::Display,
16 io::{Write, stdout},
17};
18use unicode_truncate::UnicodeTruncateStr;
19
20#[derive(Debug, Clone)]
22pub struct Opt<T: Clone, O: Display + Clone> {
23 value: T,
24 label: O,
25 hint: Option<String>,
26 active: bool,
27}
28
29impl<T: Clone, O: Display + Clone> Opt<T, O> {
30 pub fn new<S: ToString>(value: T, label: O, hint: Option<S>) -> Self {
40 Opt {
41 value,
42 label,
43 hint: hint.map(|hint| hint.to_string()),
44 active: false,
45 }
46 }
47
48 pub fn simple(value: T, label: O) -> Self {
58 Opt::new(value, label, None::<String>)
59 }
60
61 pub fn hint<S: ToString>(value: T, label: O, hint: S) -> Self {
71 Opt::new(value, label, Some(hint))
72 }
73
74 fn toggle(&mut self) {
75 self.active = !self.active;
76 }
77
78 fn trunc(&self, hint: usize) -> String {
79 let size = crossterm::terminal::size();
80 let label = format!("{}", self.label);
81
82 let one_three = if *IS_UNICODE { 1 } else { 3 };
83
84 match size {
85 Ok((width, _height)) => label
86 .unicode_truncate(width as usize - 4 - one_three - hint)
87 .0
88 .to_owned(),
89 Err(_) => label,
90 }
91 }
92
93 fn focus(&self) -> String {
94 let hint_len = self.hint.as_deref().map_or(0, |hint| hint.len() + 3);
95 let label = self.trunc(hint_len);
96
97 let fmt = if self.active {
98 format!("{} {}", (*chars::CHECKBOX_SELECTED).green(), label)
99 } else {
100 format!("{} {}", (*chars::CHECKBOX_ACTIVE).cyan(), label)
101 };
102
103 if let Some(hint) = &self.hint {
104 let hint = format!("({hint})");
105 format!("{} {}", fmt, hint.dimmed())
106 } else {
107 fmt
108 }
109 }
110
111 fn unfocus(&self) -> String {
112 let label = self.trunc(0);
113
114 if self.active {
115 format!("{} {}", (*chars::CHECKBOX_SELECTED).green(), label.dimmed())
116 } else {
117 format!(
118 "{} {}",
119 (*chars::CHECKBOX_INACTIVE).dimmed(),
120 label.dimmed()
121 )
122 }
123 }
124}
125
126pub struct MultiSelect<M: Display, T: Clone, O: Display + Clone> {
144 message: M,
145 less: bool,
146 less_amt: Option<u16>,
147 less_max: Option<u16>,
148 cancel: Option<Box<dyn Fn()>>,
149 options: Vec<Opt<T, O>>,
150}
151
152impl<M: Display, T: Clone, O: Display + Clone> MultiSelect<M, T, O> {
153 pub fn new(message: M) -> Self {
170 MultiSelect {
171 message,
172 less: false,
173 less_amt: None,
174 less_max: None,
175 cancel: None,
176 options: vec![],
177 }
178 }
179
180 pub fn option(&mut self, val: T, label: O) -> &mut Self {
197 let opt = Opt::new(val, label, None::<String>);
198 self.options.push(opt);
199 self
200 }
201
202 pub fn option_hint<S: ToString>(&mut self, val: T, label: O, hint: S) -> &mut Self {
220 let opt = Opt::new(val, label, Some(hint));
221 self.options.push(opt);
222 self
223 }
224
225 pub fn options(&mut self, options: Vec<Opt<T, O>>) -> &mut Self {
245 self.options = options;
246 self
247 }
248
249 pub fn less(&mut self) -> &mut Self {
270 self.less = true;
271 self
272 }
273
274 pub fn less_max(&mut self, max: u16) -> &mut Self {
300 assert!(max > 0, "less max value has to be greater than zero");
301 assert!(
302 self.less_amt.is_none(),
303 "cannot set both less_amt and less_max"
304 );
305 self.less = true;
306 self.less_max = Some(max);
307 self
308 }
309
310 pub fn less_amt(&mut self, less: u16) -> &mut Self {
336 assert!(less > 0, "less value has to be greater than zero");
337 assert!(
338 self.less_amt.is_none(),
339 "cannot set both less_amt and less_max"
340 );
341 self.less = true;
342 self.less_amt = Some(less);
343 self
344 }
345
346 pub fn cancel<F>(&mut self, cancel: F) -> &mut Self
369 where
370 F: Fn() + 'static,
371 {
372 let cancel = Box::new(cancel);
373 self.cancel = Some(cancel);
374
375 self
376 }
377
378 fn mk_less(&self) -> Option<u16> {
379 if !self.less {
380 return None;
381 }
382
383 if let Some(less) = self.less_amt {
384 let is_less = self.options.len() > less as usize;
385 is_less.then_some(less)
386 } else if let Ok((_, rows)) = crossterm::terminal::size() {
387 let len = self.options.len();
388 let rows = rows.saturating_sub(4);
389 let rows = self.less_max.map_or(rows, |max| u16::min(rows, max));
390
391 let is_less = rows > 0 && len > rows as usize;
392 is_less.then_some(rows)
393 } else {
394 None
395 }
396 }
397
398 pub fn interact(&self) -> Result<Vec<T>, ClackError> {
416 if self.options.is_empty() {
417 return Err(ClackError::NoOptions);
418 }
419
420 let mut options = self.options.clone();
421
422 let max = self.options.len();
423 let is_less = self.mk_less();
424
425 let mut idx = 0;
426 let mut less_idx: u16 = 0;
427
428 if let Some(less) = is_less {
429 self.w_init_less(less);
430 } else {
431 self.w_init();
432 }
433
434 terminal::enable_raw_mode()?;
435
436 loop {
437 if let Event::Key(key) = event::read()?
438 && key.kind == KeyEventKind::Press
439 {
440 match (key.code, key.modifiers) {
441 (KeyCode::Up | KeyCode::Left, _) => {
442 if let Some(less) = is_less {
443 let prev_less = less_idx;
444
445 if idx > 0 {
446 idx -= 1;
447 less_idx = less_idx.saturating_sub(1);
448 } else {
449 idx = max - 1;
450 less_idx = less - 1;
451 }
452
453 self.draw_less(&options, less, idx, less_idx, prev_less);
454 } else {
455 self.draw_unfocus(&options, idx);
456 let mut stdout = stdout();
457
458 if idx > 0 {
459 idx -= 1;
460 let _ = execute!(stdout, cursor::MoveUp(1));
461 } else if max > 1 {
462 idx = max - 1;
463 let _ = execute!(stdout, cursor::MoveDown(max as u16 - 1));
464 }
465
466 self.draw_focus(&options, idx);
467 }
468 }
469 (KeyCode::Down | KeyCode::Right, _) => {
470 if let Some(less) = is_less {
471 let prev_less = less_idx;
472
473 if idx < max - 1 {
474 idx += 1;
475 if less_idx < less - 1 {
476 less_idx += 1;
477 }
478 } else {
479 idx = 0;
480 less_idx = 0;
481 }
482
483 self.draw_less(&options, less, idx, less_idx, prev_less);
484 } else {
485 self.draw_unfocus(&options, idx);
486 let mut stdout = stdout();
487
488 if idx < max - 1 {
489 idx += 1;
490 let _ = execute!(stdout, cursor::MoveDown(1));
491 } else if idx > 0 {
492 idx = 0;
493 let _ = execute!(stdout, cursor::MoveUp(max as u16 - 1));
494 }
495
496 self.draw_focus(&options, idx);
497 }
498 }
499 (KeyCode::PageDown, _) => {
500 if let Some(less) = is_less {
501 let prev_less = less_idx;
502
503 if idx + less as usize >= max - 1 {
504 less_idx = less - 1;
505 idx = max - 1;
506 } else {
507 idx += less as usize;
508
509 if max - idx < (less - less_idx) as usize {
510 less_idx = less - (max - idx) as u16;
511 }
512 }
513
514 self.draw_less(&options, less, idx, less_idx, prev_less);
515 }
516 }
517 (KeyCode::PageUp, _) if idx != 0 => {
518 if let Some(less) = is_less {
519 let prev_less = less_idx;
520
521 if idx <= less as usize {
522 less_idx = 0;
523 idx = 0;
524 } else {
525 idx -= less as usize;
526 less_idx = prev_less.min(idx as u16);
527 }
528
529 self.draw_less(&options, less, idx, less_idx, prev_less);
530 }
531 }
532 (KeyCode::Home, _) if idx != 0 => {
533 if let Some(less) = is_less {
534 let prev_less = less_idx;
535
536 idx = 0;
537 less_idx = 0;
538
539 self.draw_less(&options, less, idx, less_idx, prev_less);
540 } else {
541 self.draw_unfocus(&options, idx);
542
543 let mut stdout = stdout();
544 let _ = execute!(stdout, cursor::MoveUp(idx as u16));
545
546 idx = 0;
547 self.draw_focus(&options, 0);
548 }
549 }
550 (KeyCode::End, _) if idx != max - 1 => {
551 if let Some(less) = is_less {
552 let prev_less = less_idx;
553
554 idx = max - 1;
555 less_idx = less - 1;
556
557 self.draw_less(&options, less, idx, less_idx, prev_less);
558 } else {
559 self.draw_unfocus(&options, idx);
560
561 let mut stdout = stdout();
562 let diff = max - idx - 1;
563 let _ = execute!(stdout, cursor::MoveDown(diff as u16));
564
565 idx = max - 1;
566
567 self.draw_focus(&options, idx);
568 }
569 }
570 (KeyCode::Char(' '), _) => {
571 let opt = options.get_mut(idx).expect("idx should always be in bound");
572 opt.toggle();
573 self.draw_focus(&options, idx);
574 }
575 (KeyCode::Enter, _) => {
576 terminal::disable_raw_mode()?;
577
578 let selected_opts =
579 options.iter().filter(|opt| opt.active).collect::<Vec<_>>();
580
581 if let Some(less) = is_less {
582 self.w_out_less(less, less_idx, &selected_opts);
583 } else {
584 self.w_out(idx, &selected_opts);
585 }
586
587 let all = options
588 .into_iter()
589 .filter(|opt| opt.active)
590 .map(|opt| opt.value)
591 .collect();
592
593 return Ok(all);
594 }
595 (KeyCode::Char('c' | 'd'), KeyModifiers::CONTROL) => {
596 terminal::disable_raw_mode()?;
597
598 if let Some(less) = is_less {
599 self.w_cancel_less(less, idx, less_idx);
600 } else {
601 self.w_cancel(idx);
602 }
603
604 if let Some(cancel) = self.cancel.as_deref() {
605 cancel();
606 } else {
607 outro!();
608 }
609
610 return Err(ClackError::Cancelled);
611 }
612 _ => {}
613 }
614 }
615 }
616 }
617}
618
619impl<M: Display, T: Clone, O: Display + Clone> MultiSelect<M, T, O> {
620 fn draw_focus(&self, options: &[Opt<T, O>], idx: usize) {
621 let opt = options.get(idx).expect("idx should always be in bound");
622 let line = opt.focus();
623 self.draw(&line);
624 }
625
626 fn draw_unfocus(&self, options: &[Opt<T, O>], idx: usize) {
627 let opt = options.get(idx).expect("idx should always be in bound");
628 let line = opt.unfocus();
629 self.draw(&line);
630 }
631
632 fn draw(&self, line: &str) {
633 let mut stdout = stdout();
634 let _ = execute!(stdout, cursor::MoveToColumn(0));
635
636 print!("{}", ansi::CLEAR_LINE);
637 print!("{} {}", (*chars::BAR).cyan(), line);
638 let _ = stdout.flush();
639 }
640
641 fn draw_less(&self, opts: &[Opt<T, O>], less: u16, idx: usize, less_idx: u16, prev_less: u16) {
642 let mut stdout = stdout();
643 if prev_less > 0 {
644 let _ = execute!(stdout, cursor::MoveToPreviousLine(prev_less));
645 } else {
646 let _ = execute!(stdout, cursor::MoveToColumn(0));
647 }
648
649 for i in 0..less.into() {
650 let i_idx = idx + i - less_idx as usize;
651 let opt = opts.get(i_idx).expect("i_idx should always be in bound");
652 let line = opt.unfocus();
653
654 print!("{}", ansi::CLEAR_LINE);
655 println!("{} {}\r", (*chars::BAR).cyan(), line);
656
657 let _ = execute!(stdout, cursor::MoveToColumn(0));
658 }
659
660 let max = self.options.len();
661 let amt = max.to_string().len();
662 print!("{}", ansi::CLEAR_LINE);
663 println!(
664 "{} ......... ({:#0amt$}/{})",
665 (*chars::BAR).cyan(),
666 idx + 1,
667 max,
668 amt = amt
669 );
670
671 let _ = execute!(stdout, cursor::MoveToPreviousLine(less + 1));
672 if less_idx > 0 {
673 let _ = execute!(stdout, cursor::MoveToNextLine(less_idx));
674 }
675
676 self.draw_focus(opts, idx);
677 }
678}
679
680impl<M: Display, T: Clone, O: Display + Clone> MultiSelect<M, T, O> {
681 fn w_init(&self) {
682 let mut stdout = stdout();
683
684 println!("{}", *chars::BAR);
685 println!("{} {}", (*chars::STEP_ACTIVE).cyan(), self.message);
686
687 for opt in &self.options {
688 let line = opt.unfocus();
689 println!("{} {}", (*chars::BAR).cyan(), line);
690 }
691
692 print!("{}", (*chars::BAR_END).cyan());
693
694 let len = self.options.len() as u16;
695 let _ = execute!(stdout, cursor::MoveToPreviousLine(len));
696
697 self.draw_focus(&self.options, 0);
698 }
699
700 fn w_init_less(&self, less: u16) {
701 println!("{}", *chars::BAR);
702 println!("{} {}", (*chars::STEP_ACTIVE).cyan(), self.message);
703
704 self.draw_less(&self.options, less, 0, 0, 0);
705
706 let mut stdout = stdout();
707 let _ = execute!(stdout, cursor::MoveToNextLine(less));
708
709 println!();
710 print!("{}", (*chars::BAR_END).cyan());
711
712 let _ = execute!(stdout, cursor::MoveToPreviousLine(less + 1));
713
714 self.draw_focus(&self.options, 0);
715 }
716
717 fn w_cancel(&self, idx: usize) {
718 let mut stdout = stdout();
719 let _ = execute!(stdout, cursor::MoveToPreviousLine(idx as u16 + 1));
720
721 println!("{} {}", (*chars::STEP_CANCEL).red(), self.message);
722
723 for _ in &self.options {
724 println!("{}", ansi::CLEAR_LINE);
725 }
726 print!("{}", ansi::CLEAR_LINE);
727
728 let len = self.options.len() as u16;
729 let _ = execute!(stdout, cursor::MoveToPreviousLine(len));
730
731 let label = &self
732 .options
733 .get(idx)
734 .expect("idx should always be in bound")
735 .label;
736 println!("{} {}", *chars::BAR, label.strikethrough().dimmed());
737 }
738
739 fn w_cancel_less(&self, less: u16, idx: usize, less_idx: u16) {
740 let mut stdout = stdout();
741 if less_idx > 0 {
742 let _ = execute!(stdout, cursor::MoveToPreviousLine(less_idx + 1));
743 } else {
744 let _ = execute!(stdout, cursor::MoveToPreviousLine(1));
745 }
746
747 println!("{} {}", (*chars::STEP_CANCEL).red(), self.message);
748
749 for _ in 0..less.into() {
750 println!("{}", ansi::CLEAR_LINE);
751 }
752
753 println!("{}", ansi::CLEAR_LINE);
754 println!("{}", ansi::CLEAR_LINE);
755
756 let mv = less + 2;
757 let _ = execute!(stdout, cursor::MoveToPreviousLine(mv));
758
759 let label = &self
760 .options
761 .get(idx)
762 .expect("idx should always be in bound")
763 .label;
764 println!("{} {}", *chars::BAR, label.strikethrough().dimmed());
765 }
766
767 fn w_out(&self, idx: usize, selected: &[&Opt<T, O>]) {
768 let mut stdout = stdout();
769 let _ = execute!(stdout, cursor::MoveToPreviousLine(idx as u16 + 1));
770
771 println!("{} {}", (*chars::STEP_SUBMIT).green(), self.message);
772
773 for _ in &self.options {
774 println!("{}", ansi::CLEAR_LINE);
775 }
776 println!("{}", ansi::CLEAR_LINE);
777
778 let mv = self.options.len() as u16 + 1;
779 let _ = execute!(stdout, cursor::MoveToPreviousLine(mv));
780
781 let vals = selected.iter().map(|&opt| &opt.label).collect::<Vec<_>>();
782
783 if vals.is_empty() {
784 println!("{} {}", *chars::BAR, "none".dimmed().italic());
785 } else {
786 let vals = self.join(&vals);
787 println!("{} {}", *chars::BAR, vals.dimmed());
788 };
789 }
790
791 fn w_out_less(&self, less: u16, less_idx: u16, selected: &[&Opt<T, O>]) {
792 let mut stdout = stdout();
793 if less_idx > 0 {
794 let _ = execute!(stdout, cursor::MoveToPreviousLine(less_idx + 1));
795 } else {
796 let _ = execute!(stdout, cursor::MoveToPreviousLine(1));
797 }
798
799 println!("{} {}", (*chars::STEP_SUBMIT).green(), self.message);
800
801 for _ in 0..less.into() {
802 println!("{}", ansi::CLEAR_LINE);
803 }
804 println!("{}", ansi::CLEAR_LINE);
805 println!("{}", ansi::CLEAR_LINE);
806
807 let mv = less + 2;
808 let _ = execute!(stdout, cursor::MoveToPreviousLine(mv));
809
810 let vals = selected.iter().map(|&opt| &opt.label).collect::<Vec<_>>();
811
812 if vals.is_empty() {
813 println!("{} {}", *chars::BAR, "none".dimmed().italic());
814 } else {
815 let vals = self.join(&vals);
816 println!("{} {}", *chars::BAR, vals.dimmed());
817 };
818 }
819
820 fn join(&self, v: &[&O]) -> String {
821 v.iter()
822 .map(|val| val.to_string())
823 .collect::<Vec<_>>()
824 .join(", ")
825 }
826}
827
828pub fn multi_select<M: Display, T: Clone, O: Display + Clone>(message: M) -> MultiSelect<M, T, O> {
830 MultiSelect::new(message)
831}