1use crate::{
4 error::ClackError,
5 outro,
6 style::{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)]
22pub struct Opt<T: Clone, O: Display> {
23 value: T,
24 label: O,
25 hint: Option<String>,
26}
27
28impl<T: Clone, O: Display> 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 }
44 }
45
46 pub fn simple(value: T, label: O) -> Self {
56 Opt::new(value, label, None::<String>)
57 }
58
59 pub fn hint<S: ToString>(value: T, label: O, hint: S) -> Self {
69 Opt::new(value, label, Some(hint))
70 }
71
72 fn trunc(&self, hint: usize) -> String {
73 let size = crossterm::terminal::size();
74 let label = format!("{}", self.label);
75
76 match size {
77 Ok((width, _height)) => label
78 .unicode_truncate(width as usize - 5 - hint)
79 .0
80 .to_owned(),
81 Err(_) => label,
82 }
83 }
84
85 fn focus(&self) -> String {
86 let hint_len = self.hint.as_deref().map_or(0, |hint| hint.len() + 3);
87 let label = self.trunc(hint_len);
88
89 let fmt = format!("{} {}", (*chars::RADIO_ACTIVE).green(), label);
90
91 if let Some(hint) = &self.hint {
92 let hint = format!("({hint})");
93 format!("{} {}", fmt, hint.dimmed())
94 } else {
95 fmt
96 }
97 }
98
99 fn unfocus(&self) -> String {
100 let label = self.trunc(0);
101 format!("{} {}", (*chars::RADIO_INACTIVE).dimmed(), label.dimmed())
102 }
103}
104
105pub struct Select<M: Display, T: Clone, O: Display> {
126 message: M,
127 less: bool,
128 less_amt: Option<u16>,
129 less_max: Option<u16>,
130 cancel: Option<Box<dyn Fn()>>,
131 options: Vec<Opt<T, O>>,
132}
133
134impl<M: Display, T: Clone, O: Display> Select<M, T, O> {
135 pub fn new(message: M) -> Self {
152 Select {
153 message,
154 less: false,
155 less_amt: None,
156 less_max: None,
157 cancel: None,
158 options: vec![],
159 }
160 }
161
162 pub fn option(&mut self, value: T, label: O) -> &mut Self {
179 let opt = Opt::new(value, label, None::<String>);
180 self.options.push(opt);
181 self
182 }
183
184 pub fn option_hint<S: ToString>(&mut self, value: T, label: O, hint: S) -> &mut Self {
202 let opt = Opt::new(value, label, Some(hint));
203 self.options.push(opt);
204 self
205 }
206
207 pub fn options(&mut self, options: Vec<Opt<T, O>>) -> &mut Self {
227 self.options = options;
228 self
229 }
230
231 pub fn less(&mut self) -> &mut Self {
252 self.less = true;
253 self
254 }
255
256 pub fn less_max(&mut self, max: u16) -> &mut Self {
282 assert!(max > 0, "less max value has to be greater than zero");
283 assert!(
284 self.less_amt.is_none(),
285 "cannot set both less_amt and less_max"
286 );
287 self.less = true;
288 self.less_max = Some(max);
289 self
290 }
291
292 pub fn less_amt(&mut self, less: u16) -> &mut Self {
318 assert!(less > 0, "less value has to be greater than zero");
319 assert!(
320 self.less_max.is_none(),
321 "cannot set both less_amt and less_max"
322 );
323 self.less = true;
324 self.less_amt = Some(less);
325 self
326 }
327
328 pub fn cancel<F>(&mut self, cancel: F) -> &mut Self
351 where
352 F: Fn() + 'static,
353 {
354 let cancel = Box::new(cancel);
355 self.cancel = Some(cancel);
356
357 self
358 }
359
360 fn mk_less(&self) -> Option<u16> {
361 if !self.less {
362 return None;
363 }
364
365 if let Some(less) = self.less_amt {
366 let is_less = self.options.len() > less as usize;
367 is_less.then_some(less)
368 } else if let Ok((_, rows)) = crossterm::terminal::size() {
369 let len = self.options.len();
370 let rows = rows.saturating_sub(4);
371 let rows = self.less_max.map_or(rows, |max| u16::min(rows, max));
372
373 let is_less = rows > 0 && len > rows as usize;
374 is_less.then_some(rows)
375 } else {
376 None
377 }
378 }
379
380 pub fn interact(&self) -> Result<T, ClackError> {
398 if self.options.is_empty() {
399 return Err(ClackError::NoOptions);
400 }
401
402 let max = self.options.len();
403 let is_less = self.mk_less();
404
405 let mut idx = 0;
406 let mut less_idx: u16 = 0;
407
408 if let Some(less) = is_less {
409 self.w_init_less(less);
410 } else {
411 self.w_init();
412 }
413
414 terminal::enable_raw_mode()?;
415
416 loop {
417 if let Event::Key(key) = event::read()?
418 && key.kind == KeyEventKind::Press
419 {
420 match (key.code, key.modifiers) {
421 (KeyCode::Up | KeyCode::Left, _) => {
422 if let Some(less) = is_less {
423 let prev_less = less_idx;
424
425 if idx > 0 {
426 idx -= 1;
427 less_idx = less_idx.saturating_sub(1);
428 } else {
429 idx = max - 1;
430 less_idx = less - 1;
431 }
432
433 self.draw_less(less, idx, less_idx, prev_less);
434 } else {
435 self.draw_unfocus(idx);
436 let mut stdout = stdout();
437
438 if idx > 0 {
439 idx -= 1;
440 let _ = execute!(stdout, cursor::MoveUp(1));
441 } else if max > 1 {
442 idx = max - 1;
443 let _ = execute!(stdout, cursor::MoveDown(max as u16 - 1));
444 }
445
446 self.draw_focus(idx);
447 }
448 }
449 (KeyCode::Down | KeyCode::Right, _) => {
450 if let Some(less) = is_less {
451 let prev_less = less_idx;
452
453 if idx < max - 1 {
454 idx += 1;
455 if less_idx < less - 1 {
456 less_idx += 1;
457 }
458 } else {
459 idx = 0;
460 less_idx = 0;
461 }
462
463 self.draw_less(less, idx, less_idx, prev_less);
464 } else {
465 self.draw_unfocus(idx);
466 let mut stdout = stdout();
467
468 if idx < max - 1 {
469 idx += 1;
470 let _ = execute!(stdout, cursor::MoveDown(1));
471 } else if idx > 0 {
472 idx = 0;
473 let _ = execute!(stdout, cursor::MoveUp(max as u16 - 1));
474 }
475
476 self.draw_focus(idx);
477 }
478 }
479 (KeyCode::PageDown, _) => {
480 if let Some(less) = is_less {
481 let prev_less = less_idx;
482
483 if idx + less as usize >= max - 1 {
484 less_idx = less - 1;
485 idx = max - 1;
486 } else {
487 idx += less as usize;
488
489 if max - idx < (less - less_idx) as usize {
490 less_idx = less - (max - idx) as u16;
491 }
492 }
493
494 self.draw_less(less, idx, less_idx, prev_less);
495 }
496 }
497 (KeyCode::PageUp, _) if idx != 0 => {
498 if let Some(less) = is_less {
499 let prev_less = less_idx;
500
501 if idx <= less as usize {
502 less_idx = 0;
503 idx = 0;
504 } else {
505 idx -= less as usize;
506 less_idx = prev_less.min(idx as u16);
507 }
508
509 self.draw_less(less, idx, less_idx, prev_less);
510 }
511 }
512 (KeyCode::Home, _) if idx != 0 => {
513 if let Some(less) = is_less {
514 let prev_less = less_idx;
515
516 idx = 0;
517 less_idx = 0;
518
519 self.draw_less(less, idx, less_idx, prev_less);
520 } else {
521 self.draw_unfocus(idx);
522
523 let mut stdout = stdout();
524 let _ = execute!(stdout, cursor::MoveUp(idx as u16));
525
526 idx = 0;
527 self.draw_focus(0);
528 }
529 }
530 (KeyCode::End, _) if idx != max - 1 => {
531 if let Some(less) = is_less {
532 let prev_less = less_idx;
533
534 idx = max - 1;
535 less_idx = less - 1;
536
537 self.draw_less(less, idx, less_idx, prev_less);
538 } else {
539 self.draw_unfocus(idx);
540
541 let mut stdout = stdout();
542 let diff = max - idx - 1;
543 let _ = execute!(stdout, cursor::MoveDown(diff as u16));
544
545 idx = max - 1;
546
547 self.draw_focus(idx);
548 }
549 }
550 (KeyCode::Enter, _) => {
551 terminal::disable_raw_mode()?;
552
553 if let Some(less) = is_less {
554 self.w_out_less(less, idx, less_idx);
555 } else {
556 self.w_out(idx);
557 }
558
559 let opt = self
560 .options
561 .get(idx)
562 .expect("idx should always be in bound");
563 let value = opt.value.clone();
564 return Ok(value);
565 }
566 (KeyCode::Char('c' | 'd'), KeyModifiers::CONTROL) => {
567 terminal::disable_raw_mode()?;
568
569 if let Some(less) = is_less {
570 self.w_cancel_less(less, idx, less_idx);
571 } else {
572 self.w_cancel(idx);
573 }
574
575 if let Some(cancel) = self.cancel.as_deref() {
576 cancel();
577 } else {
578 outro!();
579 }
580
581 return Err(ClackError::Cancelled);
582 }
583 _ => {}
584 }
585 }
586 }
587 }
588}
589
590impl<M: Display, T: Clone, O: Display> Select<M, T, O> {
591 fn draw_focus(&self, idx: usize) {
592 let opt = self
593 .options
594 .get(idx)
595 .expect("idx should always be in bound");
596 let line = opt.focus();
597 self.draw(&line);
598 }
599
600 fn draw_unfocus(&self, idx: usize) {
601 let opt = self
602 .options
603 .get(idx)
604 .expect("idx should always be in bound");
605 let line = opt.unfocus();
606 self.draw(&line);
607 }
608
609 fn draw(&self, line: &str) {
610 let mut stdout = stdout();
611 let _ = execute!(stdout, cursor::MoveToColumn(0));
612
613 print!("{}", ansi::CLEAR_LINE);
614 print!("{} {}", (*chars::BAR).cyan(), line);
615 let _ = stdout.flush();
616 }
617
618 fn draw_less(&self, less: u16, idx: usize, less_idx: u16, prev_less: u16) {
619 let mut stdout = stdout();
620 if prev_less > 0 {
621 let _ = execute!(stdout, cursor::MoveToPreviousLine(prev_less));
622 } else {
623 let _ = execute!(stdout, cursor::MoveToColumn(0));
624 }
625
626 for i in 0..less.into() {
627 let i_idx = idx + i - less_idx as usize;
628 let opt = self
629 .options
630 .get(i_idx)
631 .expect("i_idx should always be in bound");
632 let line = opt.unfocus();
633
634 print!("{}", ansi::CLEAR_LINE);
635 println!("{} {}\r", (*chars::BAR).cyan(), line);
636
637 let _ = execute!(stdout, cursor::MoveToColumn(0));
638 }
639
640 let max = self.options.len();
641 let amt = max.to_string().len();
642 print!("{}", ansi::CLEAR_LINE);
643 println!(
644 "{} ......... ({:#0amt$}/{})",
645 (*chars::BAR).cyan(),
646 idx + 1,
647 max,
648 amt = amt
649 );
650
651 let _ = execute!(stdout, cursor::MoveToPreviousLine(less + 1));
652 if less_idx > 0 {
653 let _ = execute!(stdout, cursor::MoveToNextLine(less_idx));
654 }
655
656 self.draw_focus(idx);
657 }
658}
659
660impl<M: Display, T: Clone, O: Display> Select<M, T, O> {
661 fn w_init(&self) {
662 let mut stdout = stdout();
663
664 println!("{}", *chars::BAR);
665 println!("{} {}", (*chars::STEP_ACTIVE).cyan(), self.message);
666
667 for opt in &self.options {
668 let line = opt.unfocus();
669 println!("{} {}", (*chars::BAR).cyan(), line);
670 }
671
672 print!("{}", (*chars::BAR_END).cyan());
673
674 let len = self.options.len() as u16;
675 let _ = execute!(stdout, cursor::MoveToPreviousLine(len));
676
677 self.draw_focus(0);
678 }
679
680 fn w_init_less(&self, less: u16) {
681 println!("{}", *chars::BAR);
682 println!("{} {}", (*chars::STEP_ACTIVE).cyan(), self.message);
683
684 self.draw_less(less, 0, 0, 0);
685
686 let mut stdout = stdout();
687 let _ = execute!(stdout, cursor::MoveToNextLine(less));
688
689 println!();
690 print!("{}", (*chars::BAR_END).cyan());
691
692 let _ = execute!(stdout, cursor::MoveToPreviousLine(less + 1));
693
694 self.draw_focus(0);
695 }
696
697 fn w_cancel(&self, idx: usize) {
698 let mut stdout = stdout();
699 let _ = execute!(stdout, cursor::MoveToPreviousLine(idx as u16 + 1));
700
701 println!("{} {}", (*chars::STEP_CANCEL).red(), self.message);
702
703 for _ in &self.options {
704 println!("{}", ansi::CLEAR_LINE);
705 }
706 print!("{}", ansi::CLEAR_LINE);
707
708 let len = self.options.len() as u16;
709 let _ = execute!(stdout, cursor::MoveToPreviousLine(len));
710
711 let label = &self
712 .options
713 .get(idx)
714 .expect("idx should always be in bound")
715 .label;
716 println!("{} {}", *chars::BAR, label.strikethrough().dimmed());
717 }
718
719 fn w_cancel_less(&self, less: u16, idx: usize, less_idx: u16) {
720 let mut stdout = stdout();
721 if less_idx > 0 {
722 let _ = execute!(stdout, cursor::MoveToPreviousLine(less_idx + 1));
723 } else {
724 let _ = execute!(stdout, cursor::MoveToPreviousLine(1));
725 }
726
727 println!("{} {}", (*chars::STEP_CANCEL).red(), self.message);
728
729 for _ in 0..less.into() {
730 println!("{}", ansi::CLEAR_LINE);
731 }
732
733 println!("{}", ansi::CLEAR_LINE);
734 println!("{}", ansi::CLEAR_LINE);
735
736 let mv = less + 2;
737 let _ = execute!(stdout, cursor::MoveToPreviousLine(mv));
738
739 let label = &self
740 .options
741 .get(idx)
742 .expect("idx should always be in bound")
743 .label;
744 println!("{} {}", *chars::BAR, label.strikethrough().dimmed());
745 }
746
747 fn w_out(&self, idx: usize) {
748 let mut stdout = stdout();
749 let _ = execute!(stdout, cursor::MoveToPreviousLine(idx as u16 + 1));
750
751 println!("{} {}", (*chars::STEP_SUBMIT).green(), self.message);
752
753 for _ in &self.options {
754 println!("{}", ansi::CLEAR_LINE);
755 }
756 print!("{}", ansi::CLEAR_LINE);
757
758 let len = self.options.len() as u16;
759 let _ = execute!(stdout, cursor::MoveToPreviousLine(len));
760
761 let label = &self
762 .options
763 .get(idx)
764 .expect("idx should always be in bound")
765 .label;
766 println!("{} {}", *chars::BAR, label.dimmed());
767 }
768
769 fn w_out_less(&self, less: u16, idx: usize, less_idx: u16) {
770 let mut stdout = stdout();
771 if less_idx > 0 {
772 let _ = execute!(stdout, cursor::MoveToPreviousLine(less_idx + 1));
773 } else {
774 let _ = execute!(stdout, cursor::MoveToPreviousLine(1));
775 }
776
777 println!("{} {}", (*chars::STEP_SUBMIT).green(), self.message);
778
779 for _ in 0..less.into() {
780 println!("{}", ansi::CLEAR_LINE);
781 }
782
783 println!("{}", ansi::CLEAR_LINE);
784 println!("{}", ansi::CLEAR_LINE);
785
786 let mv = less + 2;
787 let _ = execute!(stdout, cursor::MoveToPreviousLine(mv));
788
789 let label = &self
790 .options
791 .get(idx)
792 .expect("idx should always be in bound")
793 .label;
794 println!("{} {}", *chars::BAR, label.dimmed());
795 }
796}
797
798pub fn select<M: Display, T: Clone, O: Display>(message: M) -> Select<M, T, O> {
800 Select::new(message)
801}