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