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