term_kit/
prompt.rs

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; // Add spacing between options
51        }
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) // Use the larger of the two
71    }
72
73    fn render_bordered_box(&self) {
74        let mut stdout = stdout();
75        let border_width = self.calculate_border_width();
76
77        // Top border
78        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        // Prompt line
85        execute!(
86            stdout,
87            cursor::MoveTo(1, 1),
88            Print(format!(" {} ", self.prompt).with(Color::Yellow))
89        )
90        .unwrap();
91
92        // Middle border (below the prompt)
93        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        // Bottom border (below the options)
100        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}