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::{stdout, Write},
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 if key.kind == KeyEventKind::Press {
418 match (key.code, key.modifiers) {
419 (KeyCode::Up | KeyCode::Left, _) => {
420 if let Some(less) = is_less {
421 let prev_less = less_idx;
422
423 if idx > 0 {
424 idx -= 1;
425 less_idx = less_idx.saturating_sub(1);
426 } else {
427 idx = max - 1;
428 less_idx = less - 1;
429 }
430
431 self.draw_less(less, idx, less_idx, prev_less);
432 } else {
433 self.draw_unfocus(idx);
434 let mut stdout = stdout();
435
436 if idx > 0 {
437 idx -= 1;
438 let _ = execute!(stdout, cursor::MoveUp(1));
439 } else if max > 1 {
440 idx = max - 1;
441 let _ = execute!(stdout, cursor::MoveDown(max as u16 - 1));
442 }
443
444 self.draw_focus(idx);
445 }
446 }
447 (KeyCode::Down | KeyCode::Right, _) => {
448 if let Some(less) = is_less {
449 let prev_less = less_idx;
450
451 if idx < max - 1 {
452 idx += 1;
453 if less_idx < less - 1 {
454 less_idx += 1;
455 }
456 } else {
457 idx = 0;
458 less_idx = 0;
459 }
460
461 self.draw_less(less, idx, less_idx, prev_less);
462 } else {
463 self.draw_unfocus(idx);
464 let mut stdout = stdout();
465
466 if idx < max - 1 {
467 idx += 1;
468 let _ = execute!(stdout, cursor::MoveDown(1));
469 } else if idx > 0 {
470 idx = 0;
471 let _ = execute!(stdout, cursor::MoveUp(max as u16 - 1));
472 }
473
474 self.draw_focus(idx);
475 }
476 }
477 (KeyCode::PageDown, _) => {
478 if let Some(less) = is_less {
479 let prev_less = less_idx;
480
481 if idx + less as usize >= max - 1 {
482 less_idx = less - 1;
483 idx = max - 1;
484 } else {
485 idx += less as usize;
486
487 if max - idx < (less - less_idx) as usize {
488 less_idx = less - (max - idx) as u16;
489 }
490 }
491
492 self.draw_less(less, idx, less_idx, prev_less);
493 }
494 }
495 (KeyCode::PageUp, _) if idx != 0 => {
496 if let Some(less) = is_less {
497 let prev_less = less_idx;
498
499 if idx <= less as usize {
500 less_idx = 0;
501 idx = 0;
502 } else {
503 idx -= less as usize;
504 less_idx = prev_less.min(idx as u16);
505 }
506
507 self.draw_less(less, idx, less_idx, prev_less);
508 }
509 }
510 (KeyCode::Home, _) if idx != 0 => {
511 if let Some(less) = is_less {
512 let prev_less = less_idx;
513
514 idx = 0;
515 less_idx = 0;
516
517 self.draw_less(less, idx, less_idx, prev_less);
518 } else {
519 self.draw_unfocus(idx);
520
521 let mut stdout = stdout();
522 let _ = execute!(stdout, cursor::MoveUp(idx as u16));
523
524 idx = 0;
525 self.draw_focus(0);
526 }
527 }
528 (KeyCode::End, _) if idx != max - 1 => {
529 if let Some(less) = is_less {
530 let prev_less = less_idx;
531
532 idx = max - 1;
533 less_idx = less - 1;
534
535 self.draw_less(less, idx, less_idx, prev_less);
536 } else {
537 self.draw_unfocus(idx);
538
539 let mut stdout = stdout();
540 let diff = max - idx - 1;
541 let _ = execute!(stdout, cursor::MoveDown(diff as u16));
542
543 idx = max - 1;
544
545 self.draw_focus(idx);
546 }
547 }
548 (KeyCode::Enter, _) => {
549 terminal::disable_raw_mode()?;
550
551 if let Some(less) = is_less {
552 self.w_out_less(less, idx, less_idx);
553 } else {
554 self.w_out(idx);
555 }
556
557 let opt = self
558 .options
559 .get(idx)
560 .expect("idx should always be in bound");
561 let value = opt.value.clone();
562 return Ok(value);
563 }
564 (KeyCode::Char('c' | 'd'), KeyModifiers::CONTROL) => {
565 terminal::disable_raw_mode()?;
566
567 if let Some(less) = is_less {
568 self.w_cancel_less(less, idx, less_idx);
569 } else {
570 self.w_cancel(idx);
571 }
572
573 if let Some(cancel) = self.cancel.as_deref() {
574 cancel();
575 }
576
577 return Err(ClackError::Cancelled);
578 }
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}