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