feat(tui): Implement simple search function

This commit is contained in:
Patrick Auernig 2024-11-27 18:08:04 +01:00
parent 53b1a5edda
commit 5da0795103
3 changed files with 126 additions and 19 deletions

11
Cargo.lock generated
View File

@ -443,6 +443,7 @@ dependencies = [
"crossterm", "crossterm",
"ratatui", "ratatui",
"serde", "serde",
"tui-input",
] ]
[[package]] [[package]]
@ -616,6 +617,16 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "tui-input"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d1733c47f1a217b7deff18730ff7ca4ecafc5771368f715ab072d679a36114"
dependencies = [
"ratatui",
"unicode-width 0.2.0",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.14" version = "1.0.14"

View File

@ -10,6 +10,7 @@ anyhow = "1.0"
bincode = "1.3" bincode = "1.3"
ratatui = "0.29" ratatui = "0.29"
crossterm = "0.28" crossterm = "0.28"
tui-input = "0.11"
[dependencies.serde] [dependencies.serde]
version = "1.0" version = "1.0"

View File

@ -4,10 +4,11 @@ use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEvent}; use crossterm::event::{self, Event, KeyCode, KeyEvent};
use ratatui::layout::Constraint; use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style}; use ratatui::style::{Color, Style};
use ratatui::widgets::{Block, Borders, Cell, Row, Table, TableState}; use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState};
use ratatui::Frame; use ratatui::Frame;
use tui_input::{Input, InputRequest};
use crate::state::{clear_selected_project_file, write_selected_project_file}; use crate::state::{clear_selected_project_file, write_selected_project_file};
use crate::Projects; use crate::Projects;
@ -20,13 +21,27 @@ enum Message {
MoveDown, MoveDown,
MoveUp, MoveUp,
Confirm, Confirm,
Search,
SearchUpdate,
ExitSearch,
}
#[derive(Debug, Default)]
enum Mode {
#[default]
Normal,
Search,
} }
struct State { struct State {
projects: Projects, projects: Projects,
search: Input,
mode: Mode,
should_exit: bool, should_exit: bool,
project_table: TableState, project_table: TableState,
filtered_projects: Vec<String>,
selected_project: Option<PathBuf>, selected_project: Option<PathBuf>,
} }
@ -36,8 +51,11 @@ impl State {
Self { Self {
projects, projects,
search: Input::default(),
mode: Mode::default(),
should_exit: false, should_exit: false,
project_table, project_table,
filtered_projects: Vec::new(),
selected_project: None, selected_project: None,
} }
} }
@ -66,7 +84,7 @@ pub fn run(projects: Projects) -> Result<()> {
} }
if let Event::Key(key_event) = event::read()? { if let Event::Key(key_event) = event::read()? {
handle_event(&mut msg_tx, key_event); handle_event(&mut state, &mut msg_tx, key_event);
} }
} }
@ -80,12 +98,25 @@ pub fn run(projects: Projects) -> Result<()> {
} }
fn handle_event(tx: &mut mpsc::Sender<Message>, event: KeyEvent) { fn handle_event(state: &mut State, tx: &mut mpsc::Sender<Message>, event: KeyEvent) {
let msg = match event.code { let msg = match (event.code, &state.mode) {
KeyCode::Char('q') | KeyCode::Esc => Message::Exit, (KeyCode::Char('q') | KeyCode::Esc, Mode::Normal) => Message::Exit,
KeyCode::Char(' ') | KeyCode::Enter => Message::Confirm, (KeyCode::Char('j') | KeyCode::Down, Mode::Normal) => Message::MoveDown,
KeyCode::Char('j') | KeyCode::Down => Message::MoveDown, (KeyCode::Char('k') | KeyCode::Up, Mode::Normal) => Message::MoveUp,
KeyCode::Char('k') | KeyCode::Up => Message::MoveUp, (KeyCode::Char('/'), Mode::Normal) => Message::Search,
(KeyCode::Esc, Mode::Search) => Message::ExitSearch,
(KeyCode::Char(c), Mode::Search) => {
state.search.handle(InputRequest::InsertChar(c));
Message::SearchUpdate
}
(KeyCode::Backspace, Mode::Search) => {
state.search.handle(InputRequest::DeletePrevChar);
Message::SearchUpdate
}
(KeyCode::Enter, _) => Message::Confirm,
_ => Message::Noop, _ => Message::Noop,
}; };
@ -118,12 +149,22 @@ fn handle_messages(state: &mut State, rx: &mut mpsc::Receiver<Message>) -> Resul
} }
Message::Confirm => { Message::Confirm => {
if let Some(selected) = state.project_table.selected() { if let Some(selected) = state.project_table.selected() {
if let Some(project_path) = state.projects.list.get(selected) { if let Some(project_path) = state.filtered_projects.get(selected) {
state.should_exit = true; state.should_exit = true;
state.selected_project = Some(project_path.clone()); state.selected_project = Some(PathBuf::from(project_path));
}; };
} }
} }
Message::Search => {
state.mode = Mode::Search;
}
Message::ExitSearch => {
state.search = Input::default();
state.mode = Mode::Normal;
}
Message::SearchUpdate => {
state.project_table.select_first();
}
_ => (), _ => (),
} }
@ -133,19 +174,73 @@ fn handle_messages(state: &mut State, rx: &mut mpsc::Receiver<Message>) -> Resul
fn draw(state: &mut State, frame: &mut Frame) { fn draw(state: &mut State, frame: &mut Frame) {
let block = Block::default().borders(Borders::ALL); let block = Block::default().borders(Borders::ALL);
frame.render_widget(&block, frame.area());
let rows = state.projects.list.iter().map(|project| { match state.mode {
let project_path = project.to_str().unwrap(); Mode::Normal => {
Row::new([Cell::new(project_path)]) let layout = Layout::vertical([Constraint::Fill(1)]);
}); let inner_area = block.inner(frame.area());
let layout_rects = layout.split(inner_area);
draw_list(state, frame, layout_rects[0]);
}
Mode::Search => {
let layout = Layout::vertical([Constraint::Fill(1), Constraint::Max(2)]);
let inner_area = block.inner(frame.area());
let layout_rects = layout.split(inner_area);
draw_list(state, frame, layout_rects[0]);
draw_search(state, frame, layout_rects[1]);
}
}
}
fn draw_search(state: &mut State, frame: &mut Frame, area: Rect) {
const PROMPT_PREFIX: &str = "Search: ";
let search_input = &state.search;
let scroll_offset = (0, search_input.visual_scroll(area.width as usize) as u16);
let prompt = format!("{PROMPT_PREFIX}{}", search_input.value());
let search = Paragraph::new(prompt).scroll(scroll_offset);
frame.render_widget(search, area);
let cursor_pos = (
area.x + (search_input.visual_cursor() + PROMPT_PREFIX.len()) as u16,
area.y,
);
frame.set_cursor_position(cursor_pos)
}
fn draw_list(state: &mut State, frame: &mut Frame, area: Rect) {
state.filtered_projects = state
.projects
.list
.iter()
.filter_map(|project| {
let path_str = project.to_str().expect("invalid path string");
let search_value = state.search.value();
if path_str.contains(search_value) {
Some(path_str.to_string())
} else {
None
}
})
.collect();
let rows = state
.filtered_projects
.iter()
.map(|path| Row::new([Cell::new(path.as_str())]));
let widths = [Constraint::Min(20)]; let widths = [Constraint::Min(20)];
let table_highlight = Style::default().fg(Color::Blue); let table_highlight = Style::default().fg(Color::Blue);
let table = Table::new(rows, widths) let table = Table::new(rows, widths).row_highlight_style(table_highlight);
.block(block)
.row_highlight_style(table_highlight);
frame.render_stateful_widget(table, frame.area(), &mut state.project_table); frame.render_stateful_widget(table, area, &mut state.project_table);
} }