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; 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}