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