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};
9
10pub struct ListSelector {
11 options: Vec<String>,
12 selected_index: usize,
13 top_visible_index: usize,
14}
15
16impl ListSelector {
17 pub fn new(options: Vec<String>) -> Self {
18 Self {
19 options,
20 selected_index: 0,
21 top_visible_index: 0,
22 }
23 }
24
25 pub fn get_selected_option(&self) -> Option<&str> {
26 self.options.get(self.selected_index).map(String::as_str)
27 }
28
29 pub fn render(&self) {
30 let mut stdout = stdout();
31 let (_cols, rows) = size().unwrap();
32
33 let num_visible_options = (rows - 1) as usize; let start_index = self.top_visible_index;
35 let end_index = (start_index + num_visible_options).min(self.options.len());
36
37 for i in start_index..end_index {
38 let y = (i - start_index) as u16;
39 execute!(stdout, cursor::MoveTo(0, y), Clear(ClearType::CurrentLine),).unwrap();
40 if i == self.selected_index {
41 execute!(stdout, Print(format!("> {}", self.options[i]).reverse()),).unwrap();
42 } else {
43 execute!(stdout, Print(self.options[i].clone()),).unwrap();
44 }
45 }
46 stdout.flush().unwrap();
47 }
48
49 pub fn run(&mut self) -> Result<Option<&str>, Box<dyn std::error::Error>> {
50 let mut stdout = stdout();
51 execute!(
52 stdout,
53 EnterAlternateScreen,
54 cursor::Hide,
55 Clear(ClearType::All)
56 )?;
57 self.render();
58
59 loop {
60 match read()? {
61 Event::Key(KeyEvent {
62 code,
63 kind: KeyEventKind::Press,
64 ..
65 }) => 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 Ok(self.get_selected_option())
96 }
97}