1use crossterm::event::KeyEventKind;
2use crossterm::{
3 cursor,
4 event::{read, Event, KeyCode, KeyEvent},
5 execute,
6 style::{Color, Print, Stylize},
7 terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
8};
9use std::io::{stdout, Write};
10
11pub struct Prompt {
12 prompt: String,
13 input_options: Vec<String>,
14 selected_index: usize,
15}
16
17impl Prompt {
18 pub fn new(prompt: String, options: Vec<String>) -> Self {
19 Self {
20 prompt,
21 input_options: options,
22 selected_index: 0,
23 }
24 }
25
26 pub fn get_selected_option(&self) -> Option<&str> {
27 self.input_options
28 .get(self.selected_index)
29 .map(String::as_str)
30 }
31
32 pub fn render(&self) {
33 let mut stdout = stdout();
34 execute!(stdout, cursor::MoveTo(0, 0), Clear(ClearType::All)).unwrap();
35
36 self.render_bordered_box();
37
38 let mut x = 2;
39 for (i, option) in self.input_options.iter().enumerate() {
40 execute!(stdout, cursor::MoveTo(x, 3)).unwrap();
41 if i == self.selected_index {
42 execute!(stdout, Print("> ".with(Color::Yellow))).unwrap();
43 execute!(stdout, Print(option.clone().with(Color::Yellow).bold())).unwrap();
44 } else {
45 execute!(stdout, Print(" ".with(Color::White))).unwrap();
46 execute!(stdout, Print(option.clone().with(Color::White))).unwrap();
47 }
48 x += option.len() as u16 + 4; }
50
51 execute!(stdout, cursor::MoveTo(1, 5)).unwrap();
52 execute!(
53 stdout,
54 Print("Use ←/→ to navigate, Enter to select".with(Color::DarkGrey))
55 )
56 .unwrap();
57
58 stdout.flush().unwrap();
59 }
60
61 fn calculate_border_width(&self) -> u16 {
62 let total_options_width: u16 = self
63 .input_options
64 .iter()
65 .map(|option| option.len() as u16 + 4)
66 .sum();
67 let prompt_width = self.prompt.len() as u16 + 2;
68 std::cmp::max(total_options_width, prompt_width) }
70
71 fn render_bordered_box(&self) {
72 let mut stdout = stdout();
73 let border_width = self.calculate_border_width();
74
75 execute!(stdout, Print("╭".with(Color::Blue))).unwrap();
77 for _ in 0..border_width {
78 execute!(stdout, Print("─".with(Color::Blue))).unwrap();
79 }
80 execute!(stdout, Print("╮".with(Color::Blue))).unwrap();
81
82 execute!(
84 stdout,
85 cursor::MoveTo(1, 1),
86 Print(format!(" {} ", self.prompt).with(Color::Yellow))
87 )
88 .unwrap();
89
90 execute!(stdout, cursor::MoveTo(0, 2), Print("├".with(Color::Blue))).unwrap();
92 for _ in 0..border_width {
93 execute!(stdout, Print("─".with(Color::Blue))).unwrap();
94 }
95 execute!(stdout, Print("┤".with(Color::Blue))).unwrap();
96
97 execute!(stdout, cursor::MoveTo(0, 4), Print("╰".with(Color::Blue))).unwrap();
99 for _ in 0..border_width {
100 execute!(stdout, Print("─".with(Color::Blue))).unwrap();
101 }
102 execute!(stdout, Print("╯".with(Color::Blue))).unwrap();
103 }
104
105 pub fn run(&mut self) -> Result<Option<&str>, Box<dyn std::error::Error>> {
106 let mut stdout = stdout();
107 execute!(
108 stdout,
109 EnterAlternateScreen,
110 cursor::Hide,
111 Clear(ClearType::All)
112 )?;
113
114 self.render();
115
116 loop {
117 match read()? {
118 Event::Key(KeyEvent {
119 code,
120 kind: KeyEventKind::Press,
121 ..
122 }) => match code {
123 KeyCode::Char('\n') | KeyCode::Enter => {
124 execute!(stdout, LeaveAlternateScreen, cursor::Show)?;
125 return Ok(self.get_selected_option().map(|s| s));
126 }
127 KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('H') => {
128 if self.selected_index > 0 {
129 self.selected_index -= 1;
130 }
131 }
132 KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('L') => {
133 if self.selected_index < self.input_options.len() - 1 {
134 self.selected_index += 1;
135 }
136 }
137 _ => {}
138 },
139 _ => {}
140 }
141 self.render();
142 }
143 }
144}