term_kit/
listselector.rs

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