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