1use crate::{
4 error::ClackError,
5 style::{IS_UNICODE, ansi, chars},
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::{Write, stdout},
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 && key.kind == KeyEventKind::Press
438 {
439 match (key.code, key.modifiers) {
440 (KeyCode::Up | KeyCode::Left, _) => {
441 if let Some(less) = is_less {
442 let prev_less = less_idx;
443
444 if idx > 0 {
445 idx -= 1;
446 less_idx = less_idx.saturating_sub(1);
447 } else {
448 idx = max - 1;
449 less_idx = less - 1;
450 }
451
452 self.draw_less(&options, less, idx, less_idx, prev_less);
453 } else {
454 self.draw_unfocus(&options, idx);
455 let mut stdout = stdout();
456
457 if idx > 0 {
458 idx -= 1;
459 let _ = execute!(stdout, cursor::MoveUp(1));
460 } else if max > 1 {
461 idx = max - 1;
462 let _ = execute!(stdout, cursor::MoveDown(max as u16 - 1));
463 }
464
465 self.draw_focus(&options, idx);
466 }
467 }
468 (KeyCode::Down | KeyCode::Right, _) => {
469 if let Some(less) = is_less {
470 let prev_less = less_idx;
471
472 if idx < max - 1 {
473 idx += 1;
474 if less_idx < less - 1 {
475 less_idx += 1;
476 }
477 } else {
478 idx = 0;
479 less_idx = 0;
480 }
481
482 self.draw_less(&options, less, idx, less_idx, prev_less);
483 } else {
484 self.draw_unfocus(&options, idx);
485 let mut stdout = stdout();
486
487 if idx < max - 1 {
488 idx += 1;
489 let _ = execute!(stdout, cursor::MoveDown(1));
490 } else if idx > 0 {
491 idx = 0;
492 let _ = execute!(stdout, cursor::MoveUp(max as u16 - 1));
493 }
494
495 self.draw_focus(&options, idx);
496 }
497 }
498 (KeyCode::PageDown, _) => {
499 if let Some(less) = is_less {
500 let prev_less = less_idx;
501
502 if idx + less as usize >= max - 1 {
503 less_idx = less - 1;
504 idx = max - 1;
505 } else {
506 idx += less as usize;
507
508 if max - idx < (less - less_idx) as usize {
509 less_idx = less - (max - idx) as u16;
510 }
511 }
512
513 self.draw_less(&options, less, idx, less_idx, prev_less);
514 }
515 }
516 (KeyCode::PageUp, _) if idx != 0 => {
517 if let Some(less) = is_less {
518 let prev_less = less_idx;
519
520 if idx <= less as usize {
521 less_idx = 0;
522 idx = 0;
523 } else {
524 idx -= less as usize;
525 less_idx = prev_less.min(idx as u16);
526 }
527
528 self.draw_less(&options, less, idx, less_idx, prev_less);
529 }
530 }
531 (KeyCode::Home, _) if idx != 0 => {
532 if let Some(less) = is_less {
533 let prev_less = less_idx;
534
535 idx = 0;
536 less_idx = 0;
537
538 self.draw_less(&options, less, idx, less_idx, prev_less);
539 } else {
540 self.draw_unfocus(&options, idx);
541
542 let mut stdout = stdout();
543 let _ = execute!(stdout, cursor::MoveUp(idx as u16));
544
545 idx = 0;
546 self.draw_focus(&options, 0);
547 }
548 }
549 (KeyCode::End, _) if idx != max - 1 => {
550 if let Some(less) = is_less {
551 let prev_less = less_idx;
552
553 idx = max - 1;
554 less_idx = less - 1;
555
556 self.draw_less(&options, less, idx, less_idx, prev_less);
557 } else {
558 self.draw_unfocus(&options, idx);
559
560 let mut stdout = stdout();
561 let diff = max - idx - 1;
562 let _ = execute!(stdout, cursor::MoveDown(diff as u16));
563
564 idx = max - 1;
565
566 self.draw_focus(&options, idx);
567 }
568 }
569 (KeyCode::Char(' '), _) => {
570 let opt = options.get_mut(idx).expect("idx should always be in bound");
571 opt.toggle();
572 self.draw_focus(&options, idx);
573 }
574 (KeyCode::Enter, _) => {
575 terminal::disable_raw_mode()?;
576
577 let selected_opts =
578 options.iter().filter(|opt| opt.active).collect::<Vec<_>>();
579
580 if let Some(less) = is_less {
581 self.w_out_less(less, less_idx, &selected_opts);
582 } else {
583 self.w_out(idx, &selected_opts);
584 }
585
586 let all = options
587 .into_iter()
588 .filter(|opt| opt.active)
589 .map(|opt| opt.value)
590 .collect();
591
592 return Ok(all);
593 }
594 (KeyCode::Char('c' | 'd'), KeyModifiers::CONTROL) => {
595 terminal::disable_raw_mode()?;
596
597 if let Some(less) = is_less {
598 self.w_cancel_less(less, idx, less_idx);
599 } else {
600 self.w_cancel(idx);
601 }
602
603 if let Some(cancel) = self.cancel.as_deref() {
604 cancel();
605 }
606
607 return Err(ClackError::Cancelled);
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}