term_kit/
prompt.rs

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