diff options
| -rw-r--r-- | Cargo.lock | 13 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | src/backend.rs | 1 | ||||
| -rw-r--r-- | src/backend/search.rs | 40 | ||||
| -rw-r--r-- | src/frontend/app.rs | 38 | ||||
| -rw-r--r-- | src/frontend/handler.rs | 12 | ||||
| -rw-r--r-- | src/frontend/ui.rs | 113 |
7 files changed, 167 insertions, 51 deletions
@@ -99,6 +99,7 @@ dependencies = [ "crossterm", "futures", "itertools", + "nucleo-matcher", "ratatui", "regex", "sarge", @@ -606,7 +607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -717,6 +718,16 @@ dependencies = [ ] [[package]] +name = "nucleo-matcher" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" +dependencies = [ + "memchr", + "unicode-segmentation", +] + +[[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -11,6 +11,7 @@ biblatex = "0.9.3" crossterm = { version = "0.28.1", features = ["event-stream"] } futures = "0.3.30" itertools = "0.13.0" +nucleo-matcher = "0.3.1" # ratatui = "0.28.1" ratatui = { version = "0.28.1", features = ["unstable-rendered-line-info"]} regex = "1.10.6" diff --git a/src/backend.rs b/src/backend.rs index 586496b..75adb9f 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -17,3 +17,4 @@ pub mod bib; pub mod cliargs; +pub mod search; diff --git a/src/backend/search.rs b/src/backend/search.rs new file mode 100644 index 0000000..ae874db --- /dev/null +++ b/src/backend/search.rs @@ -0,0 +1,40 @@ +use nucleo_matcher::{ + pattern::{CaseMatching, Normalization, Pattern}, + Config, Matcher, +}; +use std::collections::HashMap; + +// Stringify inner Vec<String> by joining/concat +fn convert_to_string(inner_vec: &Vec<String>) -> String { + inner_vec.join(" ") +} + +// Return a filtered entry list +pub fn search_entry_list(search_pattern: &str, orig_list: Vec<Vec<String>>) -> Vec<Vec<String>> { + // Create a hashmap to connect stingified entry with entry vec + let mut entry_string_hm: HashMap<String, Vec<String>> = HashMap::new(); + + // Convert all entries to string and insert them into the hashmap + // next to the original inner Vec<String> of the entry list + for entry in orig_list { + entry_string_hm.insert(convert_to_string(&entry), entry); + } + + // Set up matcher (TODO: One time needed only, move to higher level) + let mut matcher = Matcher::new(Config::DEFAULT); + + // Filter the stringified entries and collect them into a vec + let filtered_matches: Vec<String> = { + let matches = Pattern::parse(search_pattern, CaseMatching::Ignore, Normalization::Smart) + .match_list(entry_string_hm.keys(), &mut matcher); + matches.into_iter().map(|f| f.0.to_string()).collect() + }; + + // Create filtered entry list and push the inner entry vec's to it + // Use the filtered stringified hm-key as index + let mut filtered_list: Vec<Vec<String>> = Vec::new(); + for m in filtered_matches { + filtered_list.push(entry_string_hm[&m].to_owned()); + } + filtered_list +} diff --git a/src/frontend/app.rs b/src/frontend/app.rs index 75a6ede..970a064 100644 --- a/src/frontend/app.rs +++ b/src/frontend/app.rs @@ -15,7 +15,7 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. ///// -use crate::backend::bib::*; +use crate::backend::{bib::*, search}; use std::error; use arboard::Clipboard; @@ -39,6 +39,7 @@ pub enum CurrentArea { pub enum FormerArea { EntryArea, TagArea, + SearchArea, } // Application. @@ -211,13 +212,20 @@ impl App { // Toggle moveable list between entries and tags pub fn toggle_area(&mut self) { match self.current_area { - CurrentArea::EntryArea => self.current_area = CurrentArea::TagArea, - CurrentArea::TagArea => self.current_area = CurrentArea::EntryArea, + CurrentArea::EntryArea => { + self.current_area = CurrentArea::TagArea; + self.tag_list.tag_list_state.select(Some(0)) + } + CurrentArea::TagArea => { + self.current_area = CurrentArea::EntryArea; + self.tag_list.tag_list_state.select(None) + } CurrentArea::SearchArea => { if let Some(former_area) = &self.former_area { match former_area { FormerArea::EntryArea => self.current_area = CurrentArea::EntryArea, FormerArea::TagArea => self.current_area = CurrentArea::TagArea, + _ => {} } } } @@ -226,6 +234,7 @@ impl App { match former_area { FormerArea::EntryArea => self.current_area = CurrentArea::EntryArea, FormerArea::TagArea => self.current_area = CurrentArea::TagArea, + FormerArea::SearchArea => self.current_area = CurrentArea::SearchArea, } } } @@ -273,9 +282,11 @@ impl App { } pub fn select_first(&mut self) { - self.scroll_info = 0; match self.current_area { - CurrentArea::EntryArea => self.entry_table.entry_table_state.select_first(), + CurrentArea::EntryArea => { + self.scroll_info = 0; + self.entry_table.entry_table_state.select_first(); + } CurrentArea::TagArea => self.tag_list.tag_list_state.select_first(), _ => {} } @@ -283,9 +294,11 @@ impl App { } pub fn select_last(&mut self) { - self.scroll_info = 0; match self.current_area { - CurrentArea::EntryArea => self.entry_table.entry_table_state.select_last(), + CurrentArea::EntryArea => { + self.scroll_info = 0; + self.entry_table.entry_table_state.select_last(); + } CurrentArea::TagArea => self.tag_list.tag_list_state.select_last(), _ => {} } @@ -305,4 +318,15 @@ impl App { let yanked_text = selection.to_string(); clipboard.set_text(yanked_text).unwrap(); } + + // Search entry list + pub fn search_entries(&mut self) { + let orig_list = &self.biblio_data.entry_list.bibentries; + let filtered_list = search::search_entry_list(&self.search_string, orig_list.clone()); + self.entry_table = EntryTable::from_iter(filtered_list) + } + + pub fn reset_entry_table(&mut self) { + self.entry_table = EntryTable::from_iter(self.biblio_data.entry_list.bibentries.clone()) + } } diff --git a/src/frontend/handler.rs b/src/frontend/handler.rs index 27aa8de..573795c 100644 --- a/src/frontend/handler.rs +++ b/src/frontend/handler.rs @@ -18,7 +18,7 @@ use crate::frontend::app::{App, AppResult}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use super::app::{CurrentArea, FormerArea}; +use super::app::{CurrentArea, EntryTable, FormerArea}; /// Handles the key events and updates the state of [`App`]. pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { @@ -97,6 +97,11 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { KeyCode::Tab | KeyCode::BackTab => { app.toggle_area(); } + KeyCode::Esc => { + if let Some(FormerArea::SearchArea) = app.former_area { + app.reset_entry_table(); + } + } _ => {} }, // Keycodes for the search area (popup) @@ -105,18 +110,21 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { app.toggle_area(); app.former_area = None; app.search_string.clear(); + app.reset_entry_table(); } KeyCode::Enter => { // TODO: run function for filtering the list app.toggle_area(); - app.former_area = None; + app.former_area = Some(FormerArea::SearchArea); app.search_string.clear(); } KeyCode::Backspace => { app.search_string.pop(); + app.search_entries(); } KeyCode::Char(search_pattern) => { app.search_string.push(search_pattern); + app.search_entries(); } _ => {} }, diff --git a/src/frontend/ui.rs b/src/frontend/ui.rs index 0e44e5c..5ca7100 100644 --- a/src/frontend/ui.rs +++ b/src/frontend/ui.rs @@ -15,6 +15,7 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. ///// +use futures::SinkExt; use ratatui::{ buffer::Buffer, layout::{Constraint, Layout, Rect}, @@ -35,7 +36,7 @@ use crate::{ frontend::app::{App, TagListItem}, }; -use super::app::CurrentArea; +use super::app::{CurrentArea, FormerArea}; const MAIN_BLUE_COLOR: Color = Color::Indexed(39); const MAIN_PURPLE_COLOR: Color = Color::Indexed(129); @@ -47,6 +48,7 @@ const SELECTED_STYLE: Style = Style::new() .add_modifier(Modifier::BOLD) .add_modifier(Modifier::REVERSED); const TEXT_FG_COLOR: Color = SLATE.c200; +const TEXT_CONFIRMED: Style = Style::new().fg(Color::Green); pub const fn alternate_colors(i: usize) -> Color { if i % 2 == 0 { @@ -109,8 +111,25 @@ impl App { pub fn render_footer(&mut self, area: Rect, buf: &mut Buffer) { match &self.current_area { CurrentArea::SearchArea => { + let search_title = { + match self.former_area { + Some(FormerArea::EntryArea) => { + let search_title = " Search Entries ".to_string(); + search_title + } + Some(FormerArea::TagArea) => { + let search_title = " Search Keywords ".to_string(); + search_title + } + _ => { + let search_title = " Search ".to_string(); + search_title + } + } + }; + let block = Block::bordered() - .title(" Search Entries ") + .title(search_title) .border_set(symbols::border::ROUNDED); Paragraph::new(self.search_string.clone()) .block(block) @@ -196,45 +215,57 @@ impl App { // We get the info depending on the item's state. // TODO: Implement logic showin informations for selected entry: let style_value = Style::new().bold(); - let mut lines = vec![]; - lines.push(Line::from(vec![ - Span::styled("Authors: ", style_value), - Span::styled( - String::from(BibiEntry::get_authors( - &self.get_selected_citekey(), - &self.main_biblio.bibliography, - )), - Style::new().green(), - ), - ])); - lines.push(Line::from(vec![ - Span::styled("Title: ", style_value), - Span::styled( - String::from(BibiEntry::get_title( - &self.get_selected_citekey(), - &self.main_biblio.bibliography, - )), - Style::new().magenta(), - ), - ])); - lines.push(Line::from(vec![ - Span::styled("Year: ", style_value), - Span::styled( - String::from(BibiEntry::get_year( - &self.get_selected_citekey(), - &self.main_biblio.bibliography, - )), - Style::new().light_magenta(), - ), - ])); - lines.push(Line::from("")); - lines.push(Line::from(vec![Span::styled( - String::from(BibiEntry::get_abstract( - &self.get_selected_citekey(), - &self.main_biblio.bibliography, - )), - Style::default(), - )])); + let lines = { + // if self.entry_table.entry_table_items.len() > 0 { + if self.entry_table.entry_table_state.selected().is_some() { + let mut lines = vec![]; + lines.push(Line::from(vec![ + Span::styled("Authors: ", style_value), + Span::styled( + String::from(BibiEntry::get_authors( + &self.get_selected_citekey(), + &self.main_biblio.bibliography, + )), + Style::new().green(), + ), + ])); + lines.push(Line::from(vec![ + Span::styled("Title: ", style_value), + Span::styled( + String::from(BibiEntry::get_title( + &self.get_selected_citekey(), + &self.main_biblio.bibliography, + )), + Style::new().magenta(), + ), + ])); + lines.push(Line::from(vec![ + Span::styled("Year: ", style_value), + Span::styled( + String::from(BibiEntry::get_year( + &self.get_selected_citekey(), + &self.main_biblio.bibliography, + )), + Style::new().light_magenta(), + ), + ])); + lines.push(Line::from("")); + lines.push(Line::from(vec![Span::styled( + String::from(BibiEntry::get_abstract( + &self.get_selected_citekey(), + &self.main_biblio.bibliography, + )), + Style::default(), + )])); + lines + } else { + let lines = vec![ + Line::from(" "), + Line::from("No entry selected".bold().into_centered_line().red()), + ]; + lines + } + }; let info = Text::from(lines); // We show the list item's info under the list in this paragraph |
