term_kit/
listselector.rs

1use crossterm::{
2    cursor,
3    event::{read, Event, KeyCode, KeyEvent, KeyEventKind},
4    execute,
5    style::{Print, Stylize},
6    terminal::{size, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
7};
8use std::io::{stdout, Write};
9use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
10
11pub struct ListSelector {
12    options: Vec<String>,
13    selected_index: usize,
14    top_visible_index: usize,
15}
16
17impl ListSelector {
18    pub fn new(options: Vec<String>) -> Self {
19        Self {
20            options,
21            selected_index: 0,
22            top_visible_index: 0,
23        }
24    }
25
26    pub fn get_selected_option(&self) -> Option<&str> {
27        self.options.get(self.selected_index).map(String::as_str)
28    }
29
30    pub fn render(&self) {
31        let mut stdout = stdout();
32        let (_cols, rows) = size().unwrap();
33
34        let num_visible_options = (rows - 1) as usize; // Leave a line for the cursor
35        let start_index = self.top_visible_index;
36        let end_index = (start_index + num_visible_options).min(self.options.len());
37
38        for i in start_index..end_index {
39            let y = (i - start_index) as u16;
40            execute!(stdout, cursor::MoveTo(0, y), Clear(ClearType::CurrentLine),).unwrap();
41            if i == self.selected_index {
42                execute!(stdout, Print(format!("> {}", self.options[i]).reverse()),).unwrap();
43            } else {
44                execute!(stdout, Print(self.options[i].clone()),).unwrap();
45            }
46        }
47        stdout.flush().unwrap();
48    }
49
50    pub fn run(&mut self) -> Result<Option<&str>, Box<dyn std::error::Error>> {
51        let mut stdout = stdout();
52        enable_raw_mode()?;
53        execute!(
54            stdout,
55            EnterAlternateScreen,
56            cursor::Hide,
57            Clear(ClearType::All)
58        )?;
59        self.render();
60
61        loop {
62            match read()? {
63                Event::Key(KeyEvent {
64                    code,
65                    kind: KeyEventKind::Press,
66                    ..
67                }) => match code {
68                    KeyCode::Up | KeyCode::Char('j') | KeyCode::Char('J') => {
69                        if self.selected_index > 0 {
70                            self.selected_index -= 1;
71                            if self.selected_index < self.top_visible_index {
72                                self.top_visible_index -= 1;
73                            }
74                        }
75                        self.render();
76                    }
77                    KeyCode::Down | KeyCode::Char('k') | KeyCode::Char('K') => {
78                        if self.selected_index < self.options.len() - 1 {
79                            self.selected_index += 1;
80                            let (_, rows) = size().unwrap();
81                            let max_visible_index = (self.top_visible_index + (rows - 2) as usize)
82                                .min(self.options.len() - 1);
83                            if self.selected_index > max_visible_index {
84                                self.top_visible_index += 1;
85                            }
86                        }
87                        self.render();
88                    }
89                    KeyCode::Enter => break,
90                    _ => {}
91                },
92                _ => {}
93            }
94        }
95
96        disable_raw_mode()?;
97        execute!(stdout, LeaveAlternateScreen, cursor::Show)?;
98        Ok(self.get_selected_option())
99    }
100}