use crate::fuzzy::{create_matcher, match_items_with_history};
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use nucleo_matcher::Matcher;
use ratatui::{
DefaultTerminal, Frame,
layout::{Constraint, Layout, Margin, Position},
style::{Style, Stylize},
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
};
struct App<'a> {
input: String,
cursor: usize,
title: String,
matcher: Matcher,
items: Vec<String>,
matched: Vec<usize>,
list_state: ListState,
history: &'a [String],
}
pub fn search(items: Vec<String>, title: &str, history: &[String]) -> Option<String> {
let mut terminal = ratatui::init();
let mut app = App {
input: String::new(),
cursor: 0,
title: title.to_string(),
matcher: create_matcher(),
items,
matched: Vec::new(),
list_state: ListState::default(),
history,
};
app.list_state.select(Some(0));
update_matches(&mut app);
let res = event_loop(&mut terminal, &mut app);
ratatui::restore();
match res {
Ok(Some(selected)) => Some(selected),
_ => None,
}
}
fn update_matches(app: &mut App) {
app.matched = match_items_with_history(&mut app.matcher, &app.items, &app.input, app.history);
let sel = app
.list_state
.selected()
.unwrap_or(0)
.min(app.matched.len().saturating_sub(1));
app.list_state.select(Some(sel));
}
fn event_loop(terminal: &mut DefaultTerminal, app: &mut App) -> std::io::Result<Option<String>> {
loop {
terminal.draw(|frame| render(frame, app))?;
if let Event::Key(key) = event::read()?
&& key.kind == KeyEventKind::Press
{
match key.code {
KeyCode::Esc => break Ok(None),
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
break Ok(None);
}
KeyCode::Enter => {
let sel = app.list_state.selected().and_then(|s| app.matched.get(s));
if let Some(&idx) = sel {
break Ok(Some(app.items[idx].clone()));
}
}
KeyCode::Backspace => {
if app.cursor > 0 {
app.input.remove(app.cursor - 1);
app.cursor -= 1;
update_matches(app);
}
}
KeyCode::Delete => {
if app.cursor < app.input.len() {
app.input.remove(app.cursor);
update_matches(app);
}
}
KeyCode::Left => {
app.cursor = app.cursor.saturating_sub(1);
}
KeyCode::Right => {
if app.cursor < app.input.len() {
app.cursor += 1;
}
}
KeyCode::Down | KeyCode::Char('j')
if key.code == KeyCode::Down
|| key.modifiers.contains(KeyModifiers::CONTROL) =>
{
let next = app
.list_state
.selected()
.unwrap_or(0)
.saturating_add(1)
.min(app.matched.len().saturating_sub(1));
app.list_state.select(Some(next));
}
KeyCode::Up | KeyCode::Char('k')
if key.code == KeyCode::Up || key.modifiers.contains(KeyModifiers::CONTROL) =>
{
let prev = app.list_state.selected().unwrap_or(0).saturating_sub(1);
app.list_state.select(Some(prev));
}
KeyCode::Char(c) => {
app.input.insert(app.cursor, c);
app.cursor += 1;
update_matches(app);
}
_ => {}
}
}
}
}
fn render(frame: &mut Frame, app: &App) {
let [top, sep, bottom] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(1),
])
.areas(frame.area());
frame.render_widget(
Paragraph::new(format!("> {}", app.input)).style(Style::new().bold()),
top,
);
frame.set_cursor_position(Position {
x: top.x + 2 + app.cursor as u16,
y: top.y,
});
let sep_inner = sep.inner(Margin {
horizontal: 1,
vertical: 0,
});
frame.render_widget(
Block::default()
.borders(Borders::TOP)
.title(format!(" {} ", app.title))
.dim(),
sep_inner,
);
let list_items: Vec<ListItem> = app
.matched
.iter()
.map(|&idx| ListItem::new(app.items[idx].as_str()))
.collect();
let mut list_state = app.list_state;
frame.render_stateful_widget(
List::new(list_items)
.highlight_symbol("▓ ")
.scroll_padding(2)
.highlight_style(Style::new().bg(ratatui::style::Color::DarkGray)),
bottom,
&mut list_state,
);
}