feat(tui): Implement simple search function
This commit is contained in:
parent
53b1a5edda
commit
5da0795103
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -443,6 +443,7 @@ dependencies = [
|
||||
"crossterm",
|
||||
"ratatui",
|
||||
"serde",
|
||||
"tui-input",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -616,6 +617,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.14"
|
||||
|
@ -10,6 +10,7 @@ anyhow = "1.0"
|
||||
bincode = "1.3"
|
||||
ratatui = "0.29"
|
||||
crossterm = "0.28"
|
||||
tui-input = "0.11"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
|
133
src/tui.rs
133
src/tui.rs
@ -4,10 +4,11 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent};
|
||||
use ratatui::layout::Constraint;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
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 tui_input::{Input, InputRequest};
|
||||
|
||||
use crate::state::{clear_selected_project_file, write_selected_project_file};
|
||||
use crate::Projects;
|
||||
@ -20,13 +21,27 @@ enum Message {
|
||||
MoveDown,
|
||||
MoveUp,
|
||||
Confirm,
|
||||
Search,
|
||||
SearchUpdate,
|
||||
ExitSearch,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
Normal,
|
||||
Search,
|
||||
}
|
||||
|
||||
|
||||
struct State {
|
||||
projects: Projects,
|
||||
search: Input,
|
||||
mode: Mode,
|
||||
should_exit: bool,
|
||||
project_table: TableState,
|
||||
filtered_projects: Vec<String>,
|
||||
selected_project: Option<PathBuf>,
|
||||
}
|
||||
|
||||
@ -36,8 +51,11 @@ impl State {
|
||||
|
||||
Self {
|
||||
projects,
|
||||
search: Input::default(),
|
||||
mode: Mode::default(),
|
||||
should_exit: false,
|
||||
project_table,
|
||||
filtered_projects: Vec::new(),
|
||||
selected_project: None,
|
||||
}
|
||||
}
|
||||
@ -66,7 +84,7 @@ pub fn run(projects: Projects) -> Result<()> {
|
||||
}
|
||||
|
||||
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) {
|
||||
let msg = match event.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => Message::Exit,
|
||||
KeyCode::Char(' ') | KeyCode::Enter => Message::Confirm,
|
||||
KeyCode::Char('j') | KeyCode::Down => Message::MoveDown,
|
||||
KeyCode::Char('k') | KeyCode::Up => Message::MoveUp,
|
||||
fn handle_event(state: &mut State, tx: &mut mpsc::Sender<Message>, event: KeyEvent) {
|
||||
let msg = match (event.code, &state.mode) {
|
||||
(KeyCode::Char('q') | KeyCode::Esc, Mode::Normal) => Message::Exit,
|
||||
(KeyCode::Char('j') | KeyCode::Down, Mode::Normal) => Message::MoveDown,
|
||||
(KeyCode::Char('k') | KeyCode::Up, Mode::Normal) => 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,
|
||||
};
|
||||
|
||||
@ -118,12 +149,22 @@ fn handle_messages(state: &mut State, rx: &mut mpsc::Receiver<Message>) -> Resul
|
||||
}
|
||||
Message::Confirm => {
|
||||
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.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) {
|
||||
let block = Block::default().borders(Borders::ALL);
|
||||
frame.render_widget(&block, frame.area());
|
||||
|
||||
let rows = state.projects.list.iter().map(|project| {
|
||||
let project_path = project.to_str().unwrap();
|
||||
Row::new([Cell::new(project_path)])
|
||||
});
|
||||
match state.mode {
|
||||
Mode::Normal => {
|
||||
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 table_highlight = Style::default().fg(Color::Blue);
|
||||
|
||||
let table = Table::new(rows, widths)
|
||||
.block(block)
|
||||
.row_highlight_style(table_highlight);
|
||||
let table = Table::new(rows, widths).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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user