use crossterm::event::KeyCode; use crossterm::event::KeyEventKind; use crossterm::terminal::disable_raw_mode; use crossterm::terminal::enable_raw_mode; use ratatui::prelude::Backend; use ratatui::prelude::CrosstermBackend; use ratatui::prelude::Rect; use ratatui::style::Modifier; use ratatui::style::Style; use ratatui::widgets::List; use ratatui::widgets::ListItem; use ratatui::widgets::ListState; use ratatui::Frame; use ratatui::Terminal; use std::error::Error; struct Selector { pub state: ListState, pub values: Vec, pub selected: Vec, } impl Selector { pub fn new(values: Vec) -> Self { Self { state: ListState::default().with_selected(Some(0)), values, selected: Vec::new(), } } pub fn down(&mut self) { let current_selected = self.selected(); if current_selected >= self.values.len() - 1 { self.state.select(Some(0)) } else { self.state.select(Some(current_selected + 1)); } } pub fn up(&mut self) { let current_selected = self.selected(); if current_selected <= 0 { self.state.select(Some(self.values.len() - 1)); } else { self.state.select(Some(current_selected - 1)) } } fn selected(&self) -> usize { match self.state.selected() { Some(i) => i, None => 0, } } fn mark(&mut self) { let current_selected = self.selected(); match self .selected .iter() .position(|&value| value == current_selected) { Some(x) => _ = self.selected.remove(x), None => self.selected.push(current_selected), }; } fn marked(&self) -> Vec<&str> { self.selected .iter() .map(|p| self.values[*p].as_str()) .collect() } } fn main() -> Result<(), Box> { enable_raw_mode()?; let stdout = std::io::stdout(); let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let terminal_rect = terminal.size().unwrap(); println!("---"); // since we need 5 lines... for _ in 0..5 { println!(""); } let cursor_pos = terminal.get_cursor().unwrap(); let mut options = Selector::new(vec![ "Option 1".into(), "Option 2".into(), "Option 3".into(), "Option 4".into(), "Option 5".into(), "Option 6".into(), ]); let lower_rect = Rect::new(0, cursor_pos.1 - 5, terminal_rect.width, 5); run_app(&mut terminal, &lower_rect, &mut options)?; terminal.set_cursor(0, cursor_pos.1)?; // finish disable_raw_mode()?; println!("Selected: {:?}", options.marked()); Ok(()) } fn run_app( terminal: &mut Terminal, pos: &Rect, values: &mut Selector, ) -> std::io::Result<()> { loop { terminal.draw(|f| ui(f, pos, values))?; match crossterm::event::read()? { crossterm::event::Event::Key(key) => { if key.kind == KeyEventKind::Press { match key.code { KeyCode::Esc => break, KeyCode::Enter => break, KeyCode::Down => values.down(), KeyCode::Up => values.up(), KeyCode::Char(' ') => values.mark(), _ => (), } } } _ => (), } } Ok(()) } fn ui(f: &mut Frame, pos: &Rect, cursor: &mut Selector) { let items = cursor .values .iter() .enumerate() .map(|(pos, desc)| { ListItem::new(format!( "{} {}", if cursor.selected.contains(&pos) { "[x]" } else { "[ ]" }, desc.to_string() )) }) .collect::>(); let list = List::new(items) .highlight_style(Style::default().add_modifier(Modifier::BOLD)) .highlight_symbol("> "); f.render_stateful_widget(list, *pos, &mut cursor.state); }