use std::{error, io, sync::Arc};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use nucleo::{pattern::CaseMatching, Config, Injector, Nucleo, Snapshot};
use ratatui::{prelude::CrosstermBackend, Terminal};
use crate::{selectable::Selectable, ui::ui};
pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;
pub struct Picker<T: std::marker::Sync + std::marker::Send + 'static> {
pub matcher: Nucleo<Selectable<T>>,
pub current_index: u32,
pub query: String,
}
impl<T: std::marker::Sync + std::marker::Send + std::fmt::Display> Picker<T> {
pub fn new() -> Self {
let matcher = Nucleo::new(Config::DEFAULT, Arc::new(|| {}), None, 1);
Picker {
matcher,
current_index: 0,
query: String::new(),
}
}
pub fn inject_items<F>(&self, f: F)
where
F: FnOnce(&Injector<Selectable<T>>),
{
let injector = self.matcher.injector();
f(&injector);
}
pub fn tick(&mut self, timeout: u64) {
self.matcher.tick(timeout);
}
pub fn snapshot(&self) -> &Snapshot<Selectable<T>> {
self.matcher.snapshot()
}
pub fn items(&self) -> Vec<&Selectable<T>> {
self.snapshot().matched_items(..).map(|i| i.data).collect()
}
pub(crate) fn selected_items(&self) -> Vec<&T> {
let selected_items: Vec<&T> = self
.snapshot()
.matched_items(..)
.filter(|i| i.data.is_selected())
.map(|i| i.data.value())
.collect();
if !selected_items.is_empty() {
selected_items
} else {
self.snapshot()
.matched_items(..)
.nth(self.current_index as usize)
.map(|i| vec![i.data.value()])
.unwrap_or(vec![])
}
}
pub fn next(&mut self) {
let indices = self.snapshot().matched_item_count();
if indices == 0 {
return;
}
self.current_index = (self.current_index + 1) % indices;
}
pub fn previous(&mut self) {
let indices = self.snapshot().matched_item_count();
if self.snapshot().matched_item_count() == 0 {
return;
}
self.current_index = if self.current_index == 0 {
indices - 1
} else {
self.current_index.saturating_sub(1)
};
}
pub fn toggle_selected(&mut self) {
let snapshot = self.snapshot();
if snapshot.matched_item_count() == 0 {
return;
}
if let Some(i) = snapshot.get_matched_item(self.current_index) {
i.data.toggle_selected();
};
}
pub(crate) fn append_to_query(&mut self, key: char) {
self.query.push(key);
self.matcher
.pattern
.reparse(0, &self.query, CaseMatching::Smart, true);
}
pub(crate) fn delete_from_query(&mut self) {
self.query.pop();
self.matcher
.pattern
.reparse(0, &self.query, CaseMatching::Smart, false);
}
pub(crate) fn clear_query(&mut self) {
self.query.clear();
self.matcher
.pattern
.reparse(0, &self.query, CaseMatching::Smart, false);
}
pub fn run(&mut self) -> AppResult<Vec<&T>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let result = self.run_loop(&mut terminal);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
result
}
pub(crate) fn run_loop<B: ratatui::backend::Backend>(
&mut self,
terminal: &mut Terminal<B>,
) -> AppResult<Vec<&T>> {
loop {
self.tick(10);
terminal.draw(|f| ui(f, self))?;
if let Ok(Event::Key(key)) = event::read() {
match (key.code, key.modifiers) {
(KeyCode::Char(key), KeyModifiers::NONE) => {
self.append_to_query(key);
}
(KeyCode::Backspace, KeyModifiers::NONE) => {
self.delete_from_query();
}
(KeyCode::Esc, KeyModifiers::NONE) => {
return Ok(vec![]);
}
(KeyCode::Char('u'), KeyModifiers::CONTROL) => {
self.clear_query();
}
(KeyCode::Char('c'), KeyModifiers::CONTROL) => {
return Ok(vec![]);
}
(KeyCode::Enter, KeyModifiers::NONE) => {
return Ok(self.selected_items());
}
(KeyCode::Down, KeyModifiers::NONE) => {
self.next();
}
(KeyCode::Up, KeyModifiers::NONE) => {
self.previous();
}
(KeyCode::Tab, KeyModifiers::NONE) => {
self.toggle_selected();
}
_ => {}
}
};
}
}
}