|
|
@ -3,40 +3,129 @@ use crossterm::event::KeyEventKind; |
|
|
|
use crossterm::terminal::disable_raw_mode; |
|
|
|
use crossterm::terminal::disable_raw_mode; |
|
|
|
use crossterm::terminal::enable_raw_mode; |
|
|
|
use crossterm::terminal::enable_raw_mode; |
|
|
|
use ratatui::prelude::Backend; |
|
|
|
use ratatui::prelude::Backend; |
|
|
|
use ratatui::prelude::Constraint; |
|
|
|
|
|
|
|
use ratatui::prelude::CrosstermBackend; |
|
|
|
use ratatui::prelude::CrosstermBackend; |
|
|
|
use ratatui::prelude::Direction; |
|
|
|
use ratatui::prelude::Rect; |
|
|
|
use ratatui::prelude::Layout; |
|
|
|
|
|
|
|
use ratatui::style::Color; |
|
|
|
|
|
|
|
use ratatui::style::Modifier; |
|
|
|
use ratatui::style::Modifier; |
|
|
|
use ratatui::style::Style; |
|
|
|
use ratatui::style::Style; |
|
|
|
use ratatui::widgets::List; |
|
|
|
use ratatui::widgets::List; |
|
|
|
use ratatui::widgets::ListItem; |
|
|
|
use ratatui::widgets::ListItem; |
|
|
|
|
|
|
|
use ratatui::widgets::ListState; |
|
|
|
use ratatui::Frame; |
|
|
|
use ratatui::Frame; |
|
|
|
use ratatui::Terminal; |
|
|
|
use ratatui::Terminal; |
|
|
|
use std::error::Error; |
|
|
|
use std::error::Error; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct Cursor { |
|
|
|
|
|
|
|
pub state: ListState, |
|
|
|
|
|
|
|
pub values: Vec<String>, |
|
|
|
|
|
|
|
pub selected: Vec<usize>, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl Cursor { |
|
|
|
|
|
|
|
pub fn new(values: Vec<String>) -> 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<dyn Error>> { |
|
|
|
fn main() -> Result<(), Box<dyn Error>> { |
|
|
|
enable_raw_mode()?; |
|
|
|
enable_raw_mode()?; |
|
|
|
let mut stdout = std::io::stdout(); |
|
|
|
let stdout = std::io::stdout(); |
|
|
|
let backend = CrosstermBackend::new(stdout); |
|
|
|
let backend = CrosstermBackend::new(stdout); |
|
|
|
let mut terminal = Terminal::new(backend)?; |
|
|
|
let mut terminal = Terminal::new(backend)?; |
|
|
|
let res = run_app(&mut terminal); |
|
|
|
|
|
|
|
|
|
|
|
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 = Cursor::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
|
|
|
|
// finish
|
|
|
|
disable_raw_mode()?; |
|
|
|
disable_raw_mode()?; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
println!("Selected: {:?}", options.marked()); |
|
|
|
Ok(()) |
|
|
|
Ok(()) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> std::io::Result<()> { |
|
|
|
fn run_app<B: Backend>( |
|
|
|
|
|
|
|
terminal: &mut Terminal<B>, |
|
|
|
|
|
|
|
pos: &Rect, |
|
|
|
|
|
|
|
values: &mut Cursor, |
|
|
|
|
|
|
|
) -> std::io::Result<()> { |
|
|
|
loop { |
|
|
|
loop { |
|
|
|
terminal.draw(|f| ui(f))?; |
|
|
|
terminal.draw(|f| ui(f, pos, values))?; |
|
|
|
|
|
|
|
|
|
|
|
match crossterm::event::read()? { |
|
|
|
match crossterm::event::read()? { |
|
|
|
crossterm::event::Event::Key(key) => { |
|
|
|
crossterm::event::Event::Key(key) => { |
|
|
|
if key.kind == KeyEventKind::Press { |
|
|
|
if key.kind == KeyEventKind::Press { |
|
|
|
match key.code { |
|
|
|
match key.code { |
|
|
|
KeyCode::Esc => break, |
|
|
|
KeyCode::Esc => break, |
|
|
|
|
|
|
|
KeyCode::Enter => break, |
|
|
|
|
|
|
|
KeyCode::Down => values.down(), |
|
|
|
|
|
|
|
KeyCode::Up => values.up(), |
|
|
|
|
|
|
|
KeyCode::Char(' ') => values.mark(), |
|
|
|
_ => (), |
|
|
|
_ => (), |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -47,23 +136,26 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> std::io::Result<()> { |
|
|
|
Ok(()) |
|
|
|
Ok(()) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fn ui<B: Backend>(f: &mut Frame<B>) { |
|
|
|
fn ui<B: Backend>(f: &mut Frame<B>, pos: &Rect, cursor: &mut Cursor) { |
|
|
|
let items = [ |
|
|
|
let items = cursor |
|
|
|
ListItem::new("Item 1"), |
|
|
|
.values |
|
|
|
ListItem::new("Item 2"), |
|
|
|
.iter() |
|
|
|
ListItem::new("Item 3"), |
|
|
|
.enumerate() |
|
|
|
]; |
|
|
|
.map(|(pos, desc)| { |
|
|
|
let block = Layout::default() |
|
|
|
ListItem::new(format!( |
|
|
|
.direction(Direction::Horizontal) |
|
|
|
"{} {}", |
|
|
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) |
|
|
|
if cursor.selected.contains(&pos) { |
|
|
|
.split(f.size()); |
|
|
|
"✔" |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
"✕" |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
desc.to_string() |
|
|
|
|
|
|
|
)) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
.collect::<Vec<ListItem>>(); |
|
|
|
let list = List::new(items) |
|
|
|
let list = List::new(items) |
|
|
|
.highlight_style( |
|
|
|
// .block(Block::default().borders(Borders::ALL))
|
|
|
|
Style::default() |
|
|
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD)) |
|
|
|
.bg(Color::LightGreen) |
|
|
|
|
|
|
|
.add_modifier(Modifier::BOLD), |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
.highlight_symbol("> "); |
|
|
|
.highlight_symbol("> "); |
|
|
|
f.render_widget(list, block[0]); |
|
|
|
f.render_stateful_widget(list, *pos, &mut cursor.state); |
|
|
|
} |
|
|
|
} |
|
|
|