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