diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/backend/bib.rs | 61 | ||||
| -rw-r--r-- | src/backend/cliargs.rs | 2 | ||||
| -rw-r--r-- | src/backend/search.rs | 173 | ||||
| -rw-r--r-- | src/frontend/app.rs | 86 | ||||
| -rw-r--r-- | src/frontend/handler.rs | 52 | ||||
| -rw-r--r-- | src/frontend/ui.rs | 30 |
6 files changed, 300 insertions, 104 deletions
diff --git a/src/backend/bib.rs b/src/backend/bib.rs index cbaa00e..c897099 100644 --- a/src/backend/bib.rs +++ b/src/backend/bib.rs @@ -29,7 +29,7 @@ pub struct BibiMain { pub bibfilestring: String, // content of bibfile as string pub bibliography: Bibliography, // parsed bibliography pub citekeys: Vec<String>, // list of all citekeys - // pub bibentries: BibiDataSets, + pub keyword_list: Vec<String>, // list of all available keywords } impl BibiMain { @@ -39,11 +39,13 @@ impl BibiMain { let bibfilestring = fs::read_to_string(&bibfile).unwrap(); let bibliography = biblatex::Bibliography::parse(&bibfilestring).unwrap(); let citekeys = Self::get_citekeys(&bibliography); + let keyword_list = Self::collect_tag_list(&citekeys, &bibliography); Self { bibfile, bibfilestring, bibliography, citekeys, + keyword_list, } } @@ -56,6 +58,40 @@ impl BibiMain { citekeys.sort_by_key(|name| name.to_lowercase()); citekeys } + + // collect all keywords present in the bibliography + // sort them and remove duplicates + // this list is for fast filtering entries by topics/keyowrds + pub fn collect_tag_list(citekeys: &Vec<String>, biblio: &Bibliography) -> Vec<String> { + // Initialize vector collecting all keywords + let mut keyword_list = vec![]; + + // Loop over entries and collect all keywords + for i in citekeys { + if biblio.get(&i).unwrap().keywords().is_ok() { + let items = biblio + .get(&i) + .unwrap() + .keywords() + .unwrap() + .format_verbatim(); + // Split keyword string into slices, trim leading and trailing + // whitespaces, remove empty slices, and collect them + let mut key_vec: Vec<String> = items + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + // Append keywords to vector + keyword_list.append(&mut key_vec); + } + } + + // Sort the vector and remove duplicates + keyword_list.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); + keyword_list.dedup(); + keyword_list + } } #[derive(Debug)] @@ -91,7 +127,7 @@ pub struct BibiEntry { pub title: String, pub year: String, pub pubtype: String, - // pub keywords: Vec<String>, + pub keywords: String, pub citekey: String, } @@ -102,6 +138,7 @@ impl BibiEntry { Self::get_title(&citekey, &biblio), Self::get_year(&citekey, &biblio), Self::get_pubtype(&citekey, &biblio), + Self::get_keywords(&citekey, &biblio), citekey.to_string(), ] } @@ -181,12 +218,20 @@ impl BibiEntry { } pub fn get_keywords(citekey: &str, biblio: &Bibliography) -> String { - let keywords = biblio - .get(&citekey) - .unwrap() - .keywords() - .unwrap() - .format_verbatim(); + let keywords = { + if biblio.get(&citekey).unwrap().keywords().is_ok() { + let keywords = biblio + .get(&citekey) + .unwrap() + .keywords() + .unwrap() + .format_verbatim(); + keywords + } else { + let keywords = String::from(""); + keywords + } + }; keywords } diff --git a/src/backend/cliargs.rs b/src/backend/cliargs.rs index 32972e0..516de66 100644 --- a/src/backend/cliargs.rs +++ b/src/backend/cliargs.rs @@ -16,7 +16,7 @@ ///// use core::panic; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use sarge::prelude::*; diff --git a/src/backend/search.rs b/src/backend/search.rs index c3b5816..d09dd04 100644 --- a/src/backend/search.rs +++ b/src/backend/search.rs @@ -4,50 +4,145 @@ use nucleo_matcher::{ }; use std::collections::HashMap; -// Stringify inner Vec<String> by joining/concat -fn convert_to_string(inner_vec: &Vec<String>) -> String { - inner_vec.join(" ") +#[derive(Debug)] +pub struct BibiSearch { + pub search_string: String, // Search string show in footer, used for search + pub inner_search: bool, // True, if we trigger a search for already filtered list + pub filtered_entry_list: Vec<Vec<String>>, // Temporary holds filtered entry list to refilter it } -// 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(); +impl Default for BibiSearch { + fn default() -> Self { + Self { + search_string: String::new(), + inner_search: false, + filtered_entry_list: Vec::new(), + } + } +} + +impl BibiSearch { + // 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(Self::convert_to_string(&entry), entry); + } - // 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 } - // 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()); + pub fn search_tag_list(search_pattern: &str, orig_list: Vec<String>) -> Vec<String> { + // Set up matcher (TODO: One time needed only) + let mut matcher = Matcher::new(Config::DEFAULT); + + // Filter the list items by search pattern + let filtered_matches: Vec<String> = { + let matches = + Pattern::parse(search_pattern, CaseMatching::Ignore, Normalization::Smart) + .match_list(orig_list, &mut matcher); + matches.into_iter().map(|f| f.0.to_string()).collect() + }; + filtered_matches } - filtered_list -} -pub fn search_tag_list(search_pattern: &str, orig_list: Vec<String>) -> Vec<String> { - // Set up matcher (TODO: One time needed only) - let mut matcher = Matcher::new(Config::DEFAULT); - - // Filter the list items by search pattern - let filtered_matches: Vec<String> = { - let matches = Pattern::parse(search_pattern, CaseMatching::Ignore, Normalization::Smart) - .match_list(orig_list, &mut matcher); - matches.into_iter().map(|f| f.0.to_string()).collect() - }; - filtered_matches + pub fn filter_entries_by_tag(keyword: &str, orig_list: &Vec<Vec<String>>) -> Vec<Vec<String>> { + let mut filtered_list: Vec<Vec<String>> = Vec::new(); + + for e in orig_list { + if e[4].contains(keyword) { + filtered_list.push(e.to_owned()); + } + } + + filtered_list + } } +// // 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 +// } + +// pub fn search_tag_list(search_pattern: &str, orig_list: Vec<String>) -> Vec<String> { +// // Set up matcher (TODO: One time needed only) +// let mut matcher = Matcher::new(Config::DEFAULT); + +// // Filter the list items by search pattern +// let filtered_matches: Vec<String> = { +// let matches = Pattern::parse(search_pattern, CaseMatching::Ignore, Normalization::Smart) +// .match_list(orig_list, &mut matcher); +// matches.into_iter().map(|f| f.0.to_string()).collect() +// }; +// filtered_matches +// } + +// pub fn filter_entries_by_tag(keyword: &str, orig_list: &Vec<Vec<String>>) -> Vec<Vec<String>> { +// let mut filtered_list: Vec<Vec<String>> = Vec::new(); + +// for e in orig_list { +// if e[4].contains(keyword) { +// filtered_list.push(e.to_owned()); +// } +// } + +// filtered_list +// } diff --git a/src/frontend/app.rs b/src/frontend/app.rs index 1105901..ed0318f 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::*, search}; +use crate::backend::{bib::*, search::BibiSearch}; use std::error; use arboard::Clipboard; @@ -51,6 +51,8 @@ pub struct App { pub main_biblio: BibiMain, // bibliographic data pub biblio_data: BibiData, + // search struct: + pub search_struct: BibiSearch, // tag list pub tag_list: TagList, // table items @@ -62,7 +64,7 @@ pub struct App { // mode for popup window pub former_area: Option<FormerArea>, // search string - pub search_string: String, + // pub search_string: String, } // Define the fundamental List @@ -75,14 +77,14 @@ pub struct TagList { // Structure of the list items. #[derive(Debug)] pub struct TagListItem { - pub info: String, + pub keyword: String, } // Function to process inputed characters and convert them (to string, or more complex function) impl TagListItem { pub fn new(info: &str) -> Self { Self { - info: info.to_string(), + keyword: info.to_string(), } } } @@ -107,7 +109,7 @@ impl FromIterator<Vec<String>> for EntryTable { let entry_table_items = iter .into_iter() .sorted() - .map(|i| EntryTableItem::new(&i[0], &i[1], &i[2], &i[3], &i[4])) + .map(|i| EntryTableItem::new(&i[0], &i[1], &i[2], &i[3], &i[4], &i[5])) .collect(); let entry_table_state = TableState::default().with_selected(0); Self { @@ -131,17 +133,26 @@ pub struct EntryTableItem { pub title: String, pub year: String, pub pubtype: String, + pub keywords: String, pub citekey: String, // pub year: u16, } impl EntryTableItem { - pub fn new(authors: &str, title: &str, year: &str, pubtype: &str, citekey: &str) -> Self { + pub fn new( + authors: &str, + title: &str, + year: &str, + pubtype: &str, + keywords: &str, + citekey: &str, + ) -> Self { Self { authors: authors.to_string(), title: title.to_string(), year: year.to_string(), pubtype: pubtype.to_string(), + keywords: keywords.to_string(), citekey: citekey.to_string(), } } @@ -178,7 +189,8 @@ impl Default for App { let running = true; let main_biblio = BibiMain::new(); let biblio_data = BibiData::new(&main_biblio.bibliography, &main_biblio.citekeys); - let tag_list = TagList::from_iter(main_biblio.citekeys.clone()); + let tag_list = TagList::from_iter(main_biblio.keyword_list.clone()); + let search_struct = BibiSearch::default(); let entry_table = EntryTable::from_iter(biblio_data.entry_list.bibentries.clone()); let current_area = CurrentArea::EntryArea; Self { @@ -186,11 +198,12 @@ impl Default for App { main_biblio, biblio_data, tag_list, + search_struct, entry_table, scroll_info: 0, current_area, former_area: None, - search_string: String::new(), + // search_string: String::new(), } } } @@ -290,8 +303,40 @@ impl App { self.tag_list.tag_list_state.select_last(); } + pub fn get_selected_tag(&self) -> &str { + let idx = self.tag_list.tag_list_state.selected().unwrap(); + let keyword = &self.tag_list.tag_list_items[idx].keyword; + keyword + } + + pub fn search_tags(&mut self) { + let orig_list = &self.main_biblio.keyword_list; + let filtered_list = + BibiSearch::search_tag_list(&self.search_struct.search_string, orig_list.clone()); + self.tag_list = TagList::from_iter(filtered_list) + } + pub fn reset_taglist(&mut self) { - self.tag_list = TagList::from_iter(self.main_biblio.citekeys.clone()) + self.tag_list = TagList::from_iter(self.main_biblio.keyword_list.clone()) + } + + // Filter the entry list by tags + // If already inside a filtered tag or entry list, apply the filtering + // to the already filtered list only + pub fn filter_for_tags(&mut self) { + let orig_list = { + if self.search_struct.inner_search { + let orig_list = &self.search_struct.filtered_entry_list; + orig_list + } else { + let orig_list = &self.biblio_data.entry_list.bibentries; + orig_list + } + }; + let keyword = self.get_selected_tag(); + let filtered_list = BibiSearch::filter_entries_by_tag(&keyword, &orig_list); + self.search_struct.filtered_entry_list = filtered_list; + self.entry_table = EntryTable::from_iter(self.search_struct.filtered_entry_list.clone()); } // Entry Table commands @@ -331,19 +376,18 @@ impl App { // Search entry list pub fn search_entries(&mut self) { - match self.former_area { - Some(FormerArea::EntryArea) => { + let orig_list = { + if self.search_struct.inner_search { + let orig_list = &self.search_struct.filtered_entry_list; + orig_list + } else { 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) - } - Some(FormerArea::TagArea) => { - let orig_list = &self.main_biblio.citekeys; - let filtered_list = search::search_tag_list(&self.search_string, orig_list.clone()); - self.tag_list = TagList::from_iter(filtered_list) + orig_list } - _ => {} - } + }; + let filtered_list = + BibiSearch::search_entry_list(&mut self.search_struct.search_string, orig_list.clone()); + //search::search_entry_list(&self.search_string, orig_list.clone()); + self.entry_table = EntryTable::from_iter(filtered_list) } } diff --git a/src/frontend/handler.rs b/src/frontend/handler.rs index c3eeed5..cdbd981 100644 --- a/src/frontend/handler.rs +++ b/src/frontend/handler.rs @@ -15,10 +15,13 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. ///// -use crate::frontend::app::{App, AppResult}; +use crate::{ + backend::search::BibiSearch, + frontend::app::{App, AppResult}, +}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use super::app::{CurrentArea, EntryTable, FormerArea}; +use super::app::{CurrentArea, FormerArea}; /// Handles the key events and updates the state of [`App`]. pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { @@ -61,14 +64,19 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { KeyCode::Char('/') => { app.former_area = Some(FormerArea::TagArea); app.current_area = CurrentArea::SearchArea; + // app.search_struct.is_search = true; } KeyCode::Tab | KeyCode::BackTab => { app.toggle_area(); } KeyCode::Esc => { - if let Some(FormerArea::SearchArea) = app.former_area { - app.reset_taglist(); - } + app.reset_taglist(); + } + KeyCode::Enter => { + app.filter_for_tags(); + app.toggle_area(); + // app.reset_taglist(); + app.former_area = Some(FormerArea::TagArea); } _ => {} }, @@ -90,6 +98,9 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { App::yank_text(&app.get_selected_citekey()); } KeyCode::Char('/') => { + if let Some(FormerArea::TagArea) = app.former_area { + app.search_struct.inner_search = true; + } app.former_area = Some(FormerArea::EntryArea); app.current_area = CurrentArea::SearchArea; } @@ -97,9 +108,11 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { app.toggle_area(); } KeyCode::Esc => { - if let Some(FormerArea::SearchArea) = app.former_area { - app.reset_entry_table(); + app.reset_entry_table(); + if app.search_struct.inner_search { + app.reset_taglist(); } + app.former_area = None; } _ => {} }, @@ -113,21 +126,34 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { app.reset_taglist(); } app.former_area = None; - app.search_string.clear(); + // If search is canceled, reset default status of struct + BibiSearch::default(); + // app.search_struct.search_string.clear(); + // app.search_struct.is_search = false; + // app.search_struct.filtered_entry_list.clear(); } KeyCode::Enter => { // TODO: run function for filtering the list app.toggle_area(); app.former_area = Some(FormerArea::SearchArea); - app.search_string.clear(); + // app.search_string.clear(); + app.search_struct.search_string.clear(); } KeyCode::Backspace => { - app.search_string.pop(); - app.search_entries(); + app.search_struct.search_string.pop(); + if let Some(FormerArea::EntryArea) = app.former_area { + app.search_entries(); + } else if let Some(FormerArea::TagArea) = app.former_area { + app.search_tags(); + } } KeyCode::Char(search_pattern) => { - app.search_string.push(search_pattern); - app.search_entries(); + app.search_struct.search_string.push(search_pattern); + if let Some(FormerArea::EntryArea) = app.former_area { + app.search_entries(); + } else if let Some(FormerArea::TagArea) = app.former_area { + app.search_tags(); + } } _ => {} }, diff --git a/src/frontend/ui.rs b/src/frontend/ui.rs index 0829227..7323917 100644 --- a/src/frontend/ui.rs +++ b/src/frontend/ui.rs @@ -18,15 +18,12 @@ use ratatui::{ buffer::Buffer, layout::{Constraint, Layout, Rect}, - style::{ - palette::tailwind::{GRAY, SLATE}, - Color, Modifier, Style, Stylize, - }, + style::{palette::tailwind::SLATE, Color, Modifier, Style, Stylize}, symbols, text::{Line, Span, Text}, widgets::{ - Block, Borders, Cell, HighlightSpacing, List, ListItem, Padding, Paragraph, Row, - StatefulWidget, Table, Widget, Wrap, + Block, Cell, HighlightSpacing, List, ListItem, Padding, Paragraph, Row, StatefulWidget, + Table, Widget, Wrap, }, }; @@ -38,7 +35,7 @@ use crate::{ use super::app::{CurrentArea, FormerArea}; const MAIN_BLUE_COLOR: Color = Color::Indexed(39); -const MAIN_PURPLE_COLOR: Color = Color::Indexed(129); +// const MAIN_PURPLE_COLOR: Color = Color::Indexed(129); const BOX_BORDER_STYLE_MAIN: Style = Style::new().fg(Color::White).bg(Color::Black); const NORMAL_ROW_BG: Color = Color::Black; const ALT_ROW_BG_COLOR: Color = Color::Indexed(234); @@ -47,7 +44,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); +// const TEXT_CONFIRMED: Style = Style::new().fg(Color::Green); pub const fn alternate_colors(i: usize) -> Color { if i % 2 == 0 { @@ -59,13 +56,7 @@ pub const fn alternate_colors(i: usize) -> Color { impl From<&TagListItem> for ListItem<'_> { fn from(value: &TagListItem) -> Self { - let line = Line::styled(format!("{}", value.info), TEXT_FG_COLOR); - // match value.status { - // Status::Todo => Line::styled(format!(" ☐ {}", value.todo), TEXT_FG_COLOR), - // Status::Completed => { - // Line::styled(format!(" ✓ {}", value.todo), COMPLETED_TEXT_FG_COLOR) - // } - // }; + let line = Line::styled(format!("{}", value.keyword), TEXT_FG_COLOR); ListItem::new(line) } } @@ -129,7 +120,7 @@ impl App { let block = Block::bordered() .title(search_title) .border_set(symbols::border::ROUNDED); - Paragraph::new(self.search_string.clone()) + Paragraph::new(self.search_struct.search_string.clone()) .block(block) .render(area, buf); } @@ -149,12 +140,7 @@ impl App { pub fn render_entrytable(&mut self, area: Rect, buf: &mut Buffer) { let block = Block::bordered() // can also be Block::new - .title( - Line::raw(" Bibliographic Entries ") - .centered() - .bold() - .fg(Color::Indexed(39)), - ) + .title(Line::raw(" Bibliographic Entries ").centered().bold()) .border_set(symbols::border::ROUNDED) .border_style(BOX_BORDER_STYLE_MAIN) .bg(Color::Black); // .bg(NORMAL_ROW_BG); |
