diff --git a/Cargo.lock b/Cargo.lock index 777cee7d..6885a282 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -452,6 +452,7 @@ version = "0.8.0" dependencies = [ "colored", "crossterm", + "fuzzy-matcher", "ratatui", "regex", "serde", diff --git a/Cargo.toml b/Cargo.toml index 946e6def..b39629a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ colored = "2" ratatui = "0.23.0" crossterm = "0.27.0" serde = { version = "1.0.181", features = ["derive"] } +fuzzy-matcher = "0.3.7" diff --git a/src/usecases/fzf_make_ratatui/app.rs b/src/usecases/fzf_make_ratatui/app.rs index 4fdeb218..011325a3 100644 --- a/src/usecases/fzf_make_ratatui/app.rs +++ b/src/usecases/fzf_make_ratatui/app.rs @@ -1,3 +1,5 @@ +use crate::models::makefile::Makefile; + use super::ui::ui; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture, KeyCode}, @@ -8,7 +10,7 @@ use ratatui::{ backend::{Backend, CrosstermBackend}, Terminal, }; -use std::{error::Error, io}; +use std::{error::Error, io, process}; pub enum CurrentPain { Main, @@ -28,21 +30,42 @@ impl CurrentPain { enum Message { MoveToNextPain, Quit, + KeyInput(String), + Backspace, // TODO: Delegate to rhysd/tui-textarea } pub struct Model { pub key_input: String, pub current_pain: CurrentPain, // the current screen the user is looking at, and will later determine what is rendered. pub should_quit: bool, + pub makefile: Makefile, } impl Model { - pub fn new() -> Model { - Model { + pub fn new() -> Result> { + let makefile = match Makefile::create_makefile() { + Err(e) => { + println!("[ERR] {}", e); + process::exit(1) + } + Ok(f) => f, + }; + + Ok(Model { key_input: String::new(), current_pain: CurrentPain::Main, should_quit: false, - } + makefile, + }) + } + + pub fn update_key_input(&mut self, key_input: String) -> String { + self.key_input.clone() + &key_input + } + pub fn pop(&mut self) -> String { + let mut origin = self.key_input.clone(); + origin.pop(); + origin } } @@ -54,8 +77,9 @@ pub fn main() -> Result<(), Box> { let backend = CrosstermBackend::new(stderr); let mut terminal = Terminal::new(backend)?; - let mut model = Model::new(); - let _ = run(&mut terminal, &mut model); // TODO: error handling + if let Ok(mut model) = Model::new() { + let _ = run(&mut terminal, &mut model); // TODO: error handling + } disable_raw_mode()?; execute!( @@ -93,8 +117,8 @@ fn handle_event(model: &Model) -> io::Result> { KeyCode::Esc => Some(Message::Quit), _ => match model.current_pain { CurrentPain::Main => match key.code { - KeyCode::Char('e') => Some(Message::MoveToNextPain), - KeyCode::Tab => Some(Message::MoveToNextPain), + KeyCode::Backspace => Some(Message::Backspace), + KeyCode::Char(char) => Some(Message::KeyInput(char.to_string())), _ => None, }, CurrentPain::History => match key.code { @@ -121,6 +145,12 @@ fn update(model: &mut Model, message: Option) -> &mut Model { Some(Message::Quit) => { model.should_quit = true; } + Some(Message::KeyInput(key_input)) => { + model.key_input = model.update_key_input(key_input); + } + Some(Message::Backspace) => { + model.key_input = model.pop(); + } None => {} } model diff --git a/src/usecases/fzf_make_ratatui/ui.rs b/src/usecases/fzf_make_ratatui/ui.rs index 86f4bfca..08d64bed 100644 --- a/src/usecases/fzf_make_ratatui/ui.rs +++ b/src/usecases/fzf_make_ratatui/ui.rs @@ -1,12 +1,16 @@ +use fuzzy_matcher::skim::SkimMatcherV2; +use fuzzy_matcher::FuzzyMatcher; use ratatui::{ backend::Backend, layout::{Constraint, Direction, Layout}, style::{Color, Style}, text::{Line, Span}, - widgets::{Block, Borders, Paragraph}, + widgets::{Block, Borders, List, ListItem, Paragraph}, Frame, }; +use crate::models::makefile::Makefile; + use super::app::Model; pub fn ui(f: &mut Frame, model: &Model) { @@ -28,19 +32,35 @@ pub fn ui(f: &mut Frame, model: &Model) { .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .split(fzf_make_chunks[0]); - let list = rounded_border_block("Preview", model.current_pain.is_main()); - f.render_widget(list, fzf_make_preview_chunks[0]); - let list = rounded_border_block("Targets", model.current_pain.is_main()); - f.render_widget(list, fzf_make_preview_chunks[1]); - let list = rounded_border_block("Input", model.current_pain.is_main()); - f.render_widget(list, fzf_make_chunks[1]); - - let title_block = rounded_border_block("History", model.current_pain.is_history()); - f.render_widget(title_block, fzf_preview_and_history_chunks[1]); + f.render_widget( + // TODO: ハイライトしてtargetの内容を表示する + rounded_border_block("Preview", model.current_pain.is_main()), + fzf_make_preview_chunks[0], + ); + f.render_widget( + targets_block( + "Targets", + model.key_input.clone(), + model.makefile.clone(), + model.current_pain.is_main(), + ), + fzf_make_preview_chunks[1], + ); + f.render_widget( + // NOTE: To show cursor, use rhysd/tui-textarea + input_block("Input", &model.key_input, model.current_pain.is_main()), + fzf_make_chunks[1], + ); + f.render_widget( + rounded_border_block("History", model.current_pain.is_history()), + fzf_preview_and_history_chunks[1], + ); let hint_text = match model.current_pain { - super::app::CurrentPain::Main => ": to quit, move to next tab", - super::app::CurrentPain::History => "q / : to quit, move to next tab", + super::app::CurrentPain::Main => { + "(Any key except the following): Narrow down targets, : Quit, Move to next tab" + } + super::app::CurrentPain::History => "q / : Quit, Move to next tab", }; let current_keys_hint = { Span::styled(hint_text, Style::default()) }; @@ -49,19 +69,77 @@ pub fn ui(f: &mut Frame, model: &Model) { f.render_widget(key_notes_footer, main_chunks[1]); } +fn input_block<'a>(title: &'a str, target_input: &'a str, is_current: bool) -> Paragraph<'a> { + let fg_color = if is_current { + Color::Yellow + } else { + Color::default() + }; + + Paragraph::new(Line::from(target_input)) + .block( + Block::default() + .title(title) + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(fg_color)) + .style(Style::default()) + .padding(ratatui::widgets::Padding::new(2, 0, 0, 0)), + ) + .style(Style::default()) +} +fn targets_block(title: &str, key_input: String, makefile: Makefile, is_current: bool) -> List<'_> { + // TODO: 選択する + let fg_color = if is_current { + Color::Yellow + } else { + Color::default() + }; + + let matcher = SkimMatcherV2::default(); + let mut filtered_list: Vec<(Option, String)> = makefile + .to_targets_string() + .into_iter() + .map(|target| match matcher.fuzzy_indices(&target, &key_input) { + Some((score, _)) => (Some(score), target), // TODO: highligh matched part + None => (None, target), + }) + .filter(|(score, _)| score.is_some()) + .collect(); + + filtered_list.sort_by(|(score1, _), (score2, _)| score1.cmp(score2)); + filtered_list.reverse(); + + // Sort filtered_list by first element of tuple + let list: Vec = filtered_list + .into_iter() + .map(|(_, target)| ListItem::new(target).style(Style::default().fg(Color::Yellow))) + .collect(); + + List::new(list) + .block( + Block::default() + .title(title) + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(fg_color)) + .style(Style::default()) + .padding(ratatui::widgets::Padding::new(2, 0, 0, 0)), + ) + .style(Style::default()) +} + fn rounded_border_block(title: &str, is_current: bool) -> Block { - if is_current { - Block::default() - .title(title) - .borders(Borders::ALL) - .border_type(ratatui::widgets::BorderType::Rounded) - .border_style(Style::default().fg(Color::Yellow)) - .style(Style::default()) + let fg_color = if is_current { + Color::Yellow } else { - Block::default() - .title(title) - .borders(Borders::ALL) - .border_type(ratatui::widgets::BorderType::Rounded) - .style(Style::default()) - } + Color::default() + }; + + Block::default() + .title(title) + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(fg_color)) + .style(Style::default()) }