diff options
Diffstat (limited to 'src/frontend')
| -rw-r--r-- | src/frontend/app.rs | 12 | ||||
| -rw-r--r-- | src/frontend/entries.rs | 190 | ||||
| -rw-r--r-- | src/frontend/handler.rs | 3 | ||||
| -rw-r--r-- | src/frontend/keywords.rs | 32 | ||||
| -rw-r--r-- | src/frontend/tui.rs | 71 | ||||
| -rw-r--r-- | src/frontend/ui.rs | 45 |
6 files changed, 201 insertions, 152 deletions
diff --git a/src/frontend/app.rs b/src/frontend/app.rs index 8d6454d..822c6f0 100644 --- a/src/frontend/app.rs +++ b/src/frontend/app.rs @@ -53,8 +53,6 @@ pub struct App { pub main_bibfile: PathBuf, // main bibliography pub main_biblio: BibiMain, - // bibliographic data - pub biblio_data: BibiData, // search struct: pub search_struct: BibiSearch, // tag list @@ -76,16 +74,14 @@ impl App { let running = true; let main_bibfile = args.bibfilearg; let main_biblio = BibiMain::new(main_bibfile.clone()); - let biblio_data = BibiData::new(&main_biblio.bibliography, &main_biblio.citekeys); let tag_list = TagList::new(main_biblio.keyword_list.clone()); let search_struct = BibiSearch::default(); - let entry_table = EntryTable::from_iter(biblio_data.entry_list.bibentries.clone()); + let entry_table = EntryTable::new(main_biblio.entry_list.clone()); let current_area = CurrentArea::EntryArea; Ok(Self { running, main_bibfile, main_biblio, - biblio_data, tag_list, search_struct, entry_table, @@ -129,11 +125,9 @@ impl App { pub fn update_lists(&mut self) { self.main_biblio = BibiMain::new(self.main_bibfile.clone()); - self.biblio_data = - BibiData::new(&self.main_biblio.bibliography, &self.main_biblio.citekeys); // self.tag_list = TagList::from_iter(self.main_biblio.keyword_list.clone()); self.tag_list = TagList::new(self.main_biblio.keyword_list.clone()); - self.entry_table = EntryTable::from_iter(self.biblio_data.entry_list.bibentries.clone()); + self.entry_table = EntryTable::new(self.main_biblio.entry_list.clone()); } // Toggle moveable list between entries and tags @@ -157,7 +151,7 @@ impl App { } pub fn reset_current_list(&mut self) { - self.entry_table = EntryTable::from_iter(self.biblio_data.entry_list.bibentries.clone()); + self.entry_table = EntryTable::new(self.main_biblio.entry_list.clone()); self.tag_list = TagList::new(self.main_biblio.keyword_list.clone()); if let CurrentArea::TagArea = self.current_area { self.tag_list.tag_list_state.select(Some(0)) diff --git a/src/frontend/entries.rs b/src/frontend/entries.rs index 98604f9..6c227df 100644 --- a/src/frontend/entries.rs +++ b/src/frontend/entries.rs @@ -17,11 +17,10 @@ use super::app::App; use super::tui::Tui; -use crate::backend::search::BibiSearch; +use crate::backend::{bib::BibiData, search::BibiSearch}; use color_eyre::eyre::{Context, Ok, Result}; use core::panic; use editor_command::EditorBuilder; -use itertools::Itertools; use ratatui::widgets::{ScrollbarState, TableState}; use std::process::{Command, Stdio}; @@ -30,44 +29,92 @@ use std::process::{Command, Stdio}; pub struct EntryTable { pub entry_table_items: Vec<EntryTableItem>, pub entry_table_at_search_start: Vec<EntryTableItem>, + pub entry_table_reversed_sort: bool, pub entry_table_state: TableState, pub entry_scroll_state: ScrollbarState, pub entry_info_scroll: u16, pub entry_info_scroll_state: ScrollbarState, } -impl FromIterator<Vec<String>> for EntryTable { - fn from_iter<T: IntoIterator<Item = Vec<String>>>(iter: T) -> Self { - let entry_table_items: Vec<EntryTableItem> = iter - .into_iter() - .sorted() - // 0: authors, 1: title, 2: date, 3: pubtype, 4: keywords, 5: citekey - // 6: abstract, 7: doi/url, 8: pdf filepath - // See backend/bib.rs BibiEntry impl - .map(|i| { - EntryTableItem::new( - &i[0], &i[1], &i[2], &i[3], &i[4], &i[5], &i[6], &i[7], &i[8], - ) - }) - .collect(); +impl EntryTable { + pub fn new(entry_list: Vec<BibiData>) -> Self { + let entry_table_items = Self::set_entry_table(entry_list); let entry_table_state = TableState::default().with_selected(0); let entry_scroll_state = ScrollbarState::new(entry_table_items.len()); let entry_info_scroll_state = ScrollbarState::default(); Self { entry_table_items, entry_table_at_search_start: Vec::new(), + entry_table_reversed_sort: false, entry_table_state, entry_scroll_state, entry_info_scroll: 0, entry_info_scroll_state, } } + + pub fn set_entry_table(entry_list: Vec<BibiData>) -> Vec<EntryTableItem> { + let mut entry_table: Vec<EntryTableItem> = entry_list + .into_iter() + .map(|e| EntryTableItem { + authors: e.authors, + short_author: String::new(), + title: e.title, + year: e.year, + pubtype: e.pubtype, + keywords: e.keywords, + citekey: e.citekey, + abstract_text: e.abstract_text, + doi_url: e.doi_url, + filepath: e.filepath, + }) + .collect(); + + entry_table.sort_by(|a, b| a.authors.to_lowercase().cmp(&b.authors.to_lowercase())); + entry_table + } + + // Sort entry table by specific column. + // Toggle sorting by hitting same key again + pub fn sort_entry_table(&mut self, sorting: &str, toggle: bool) { + if toggle { + self.entry_table_reversed_sort = !self.entry_table_reversed_sort; + } + if self.entry_table_reversed_sort { + match sorting { + "author" => self + .entry_table_items + .sort_by(|a, b| b.authors.to_lowercase().cmp(&a.authors.to_lowercase())), + "title" => self + .entry_table_items + .sort_by(|a, b| b.title.to_lowercase().cmp(&a.title.to_lowercase())), + "year" => self + .entry_table_items + .sort_by(|a, b| b.year.to_lowercase().cmp(&a.year.to_lowercase())), + _ => {} + } + } else if !self.entry_table_reversed_sort { + match sorting { + "author" => self + .entry_table_items + .sort_by(|a, b| a.authors.to_lowercase().cmp(&b.authors.to_lowercase())), + "title" => self + .entry_table_items + .sort_by(|a, b| a.title.to_lowercase().cmp(&b.title.to_lowercase())), + "year" => self + .entry_table_items + .sort_by(|a, b| a.year.to_lowercase().cmp(&b.year.to_lowercase())), + _ => {} + } + } + } } // Define contents of each entry table row #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct EntryTableItem { pub authors: String, + pub short_author: String, pub title: String, pub year: String, pub pubtype: String, @@ -79,34 +126,34 @@ pub struct EntryTableItem { } impl EntryTableItem { - pub fn new( - authors: &str, - title: &str, - year: &str, - pubtype: &str, - keywords: &str, - citekey: &str, - abstract_text: &str, - doi_url: &str, - filepath: &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(), - abstract_text: abstract_text.to_string(), - doi_url: doi_url.to_string(), - filepath: filepath.to_string(), - } - } - // This functions decides which fields are rendered in the entry table // Fields which should be usable but not visible can be left out - pub fn ref_vec(&self) -> Vec<&String> { - vec![&self.authors, &self.title, &self.year, &self.pubtype] + pub fn ref_vec(&mut self) -> Vec<&str> { + self.short_author = match self.authors.split_once(",") { + Some((first, _rest)) => { + if self.authors.contains("(ed.)") { + let first_author = format!("{} et al. (ed.)", first); + first_author + } else { + let first_author = format!("{} et al.", first); + first_author + } + } + None => String::from(""), + }; + + vec![ + { + if self.short_author.is_empty() { + &self.authors + } else { + &self.short_author + } + }, + &self.title, + &self.year, + &self.pubtype, + ] } pub fn authors(&self) -> &str { @@ -128,6 +175,14 @@ impl EntryTableItem { pub fn citekey(&self) -> &str { &self.citekey } + + pub fn doi_url(&self) -> &str { + &self.doi_url + } + + pub fn filepath(&self) -> &str { + &self.filepath + } } impl App { @@ -256,6 +311,13 @@ impl App { let filtered_list = BibiSearch::search_entry_list(&mut self.search_struct.search_string, orig_list.clone()); self.entry_table.entry_table_items = filtered_list; + if self.entry_table.entry_table_reversed_sort { + self.entry_table.sort_entry_table("author", false); + } + self.entry_table.entry_scroll_state = ScrollbarState::content_length( + self.entry_table.entry_scroll_state, + self.entry_table.entry_table_items.len(), + ); } // Open file connected with entry through 'file' or 'pdf' field @@ -326,6 +388,8 @@ impl App { #[cfg(test)] mod tests { + use super::EntryTableItem; + #[test] fn check_os() { let os = std::env::consts::OS; @@ -336,4 +400,46 @@ mod tests { std::env::consts::OS ) } + + #[test] + fn shorten_authors() { + let mut entry: EntryTableItem = EntryTableItem { + authors: "Miller, Schmitz, Bernard".to_string(), + short_author: "".to_string(), + title: "A title".to_string(), + year: "2000".to_string(), + pubtype: "article".to_string(), + keywords: "key1, key2".to_string(), + citekey: "miller_2000".to_string(), + abstract_text: "An abstract".to_string(), + doi_url: "www.text.org".to_string(), + filepath: "/home/test".to_string(), + }; + + let entry_vec = EntryTableItem::ref_vec(&mut entry); + + let mut entry_editors: EntryTableItem = EntryTableItem { + authors: "Miller, Schmitz, Bernard (ed.)".to_string(), + short_author: "".to_string(), + title: "A title".to_string(), + year: "2000".to_string(), + pubtype: "article".to_string(), + keywords: "key1, key2".to_string(), + citekey: "miller_2000".to_string(), + abstract_text: "An abstract".to_string(), + doi_url: "www.text.org".to_string(), + filepath: "/home/test".to_string(), + }; + + let entry_vec_editors = EntryTableItem::ref_vec(&mut entry_editors); + + assert_eq!( + entry_vec, + vec!["Miller et al.", "A title", "2000", "article"] + ); + assert_eq!( + entry_vec_editors, + vec!["Miller et al. (ed.)", "A title", "2000", "article"] + ) + } } diff --git a/src/frontend/handler.rs b/src/frontend/handler.rs index c2cacf5..ec1647e 100644 --- a/src/frontend/handler.rs +++ b/src/frontend/handler.rs @@ -121,6 +121,9 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App, tui: &mut Tui) -> R KeyCode::Char('G') | KeyCode::End => { app.select_last_entry(); } + KeyCode::Char('s') => { + app.entry_table.sort_entry_table("author", true); + } KeyCode::Char('y') => { App::yank_text(&app.get_selected_citekey()); } diff --git a/src/frontend/keywords.rs b/src/frontend/keywords.rs index 9432752..ba74b02 100644 --- a/src/frontend/keywords.rs +++ b/src/frontend/keywords.rs @@ -42,23 +42,6 @@ impl TagListItem { } } -// impl FromIterator<String> for TagList { -// fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self { -// let tag_list_items: Vec<TagListItem> = iter -// .into_iter() -// .map(|info| TagListItem::new(&info)) -// .collect(); -// let tag_list_state = ListState::default(); // for preselection: .with_selected(Some(0)); -// let tag_scroll_state = ScrollbarState::new(tag_list_items.len()); -// Self { -// tag_list_items, -// tag_list_state, -// tag_scroll_state, -// selected_keyword: String::new(), -// } -// } -// } - impl TagList { pub fn new(keyword_list: Vec<String>) -> Self { let tag_list_items = keyword_list; @@ -118,7 +101,11 @@ impl App { let filtered_list = BibiSearch::search_tag_list(&self.search_struct.search_string, orig_list.clone()); self.tag_list.tag_list_items = filtered_list; - // self.tag_list = TagList::from_iter(filtered_list) + // Update scrollbar length after filtering list + self.tag_list.tag_scroll_state = ScrollbarState::content_length( + self.tag_list.tag_scroll_state, + self.tag_list.tag_list_items.len(), + ); } pub fn filter_tags_by_entries(&mut self) { @@ -143,6 +130,10 @@ impl App { self.search_struct.filtered_tag_list = filtered_keywords.clone(); self.tag_list.tag_list_items = filtered_keywords; + self.tag_list.tag_scroll_state = ScrollbarState::content_length( + self.tag_list.tag_scroll_state, + self.tag_list.tag_list_items.len(), + ); } // Filter the entry list by tags when hitting enter @@ -154,6 +145,11 @@ impl App { let filtered_list = BibiSearch::filter_entries_by_tag(&keyword, &orig_list); self.tag_list.selected_keyword = keyword.to_string(); self.entry_table.entry_table_items = filtered_list; + // Update scrollbar state with new lenght of itemlist + self.entry_table.entry_scroll_state = ScrollbarState::content_length( + self.entry_table.entry_scroll_state, + self.entry_table.entry_table_items.len(), + ); self.filter_tags_by_entries(); self.toggle_area(); self.entry_table.entry_table_state.select(Some(0)); diff --git a/src/frontend/tui.rs b/src/frontend/tui.rs index 698407d..e3c9c1a 100644 --- a/src/frontend/tui.rs +++ b/src/frontend/tui.rs @@ -24,6 +24,8 @@ use crossterm::{ terminal::{EnterAlternateScreen, LeaveAlternateScreen}, }; // use ratatui::backend::{Backend, CrosstermBackend}; +use color_eyre::eyre::{OptionExt, Result}; +use futures::{FutureExt, StreamExt}; use ratatui::backend::CrosstermBackend as Backend; use std::io::{stdout, Stdout}; use std::panic; @@ -31,10 +33,6 @@ use std::{ ops::{Deref, DerefMut}, time::Duration, }; - -use color_eyre::config::HookBuilder; -use color_eyre::eyre::{OptionExt, Result}; -use futures::{FutureExt, StreamExt}; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; @@ -51,14 +49,6 @@ pub enum Event { Resize(u16, u16), } -// pub type IO = std::io::{{crossterm_io | title_case}}; -// pub fn io() -> IO { -// std::io::{{crossterm_io}}() -// } -/// Representation of a terminal user interface. -/// -/// It is responsible for setting up the terminal, -/// initializing the interface and handling the draw events. #[derive(Debug)] pub struct Tui { /// Interface to the Terminal. @@ -73,7 +63,7 @@ pub struct Tui { } impl Tui { - /// Constructs a new instance of [`Tui`]. + // Constructs a new instance of [`Tui`]. pub fn new() -> Result<Self> { let terminal = ratatui::Terminal::new(Backend::new(stdout()))?; let (sender, receiver) = mpsc::unbounded_channel(); @@ -150,26 +140,6 @@ impl Tui { cancellation_token.cancel(); } - /// Initializes the terminal interface. - /// - /// It enables the raw mode and sets terminal properties. - // pub fn init(&mut self) -> Result<()> { - // terminal::enable_raw_mode()?; - // crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?; - - // // Define a custom panic hook to reset the terminal properties. - // // This way, you won't have your terminal messed up if an unexpected error happens. - // let panic_hook = panic::take_hook(); - // panic::set_hook(Box::new(move |panic| { - // Self::reset().expect("failed to reset the terminal"); - // panic_hook(panic); - // })); - - // self.terminal.hide_cursor()?; - // self.terminal.clear()?; - // Ok(()) - // } - pub fn enter(&mut self) -> Result<()> { crossterm::terminal::enable_raw_mode()?; crossterm::execute!(stdout(), EnterAlternateScreen, cursor::Hide)?; @@ -216,10 +186,10 @@ impl Tui { Ok(()) } - /// [`Draw`] the terminal interface by [`rendering`] the widgets. - /// - /// [`Draw`]: ratatui::Terminal::draw - /// [`rendering`]: crate::ui::render + // [`Draw`] the terminal interface by [`rendering`] the widgets. + // + // [`Draw`]: ratatui::Terminal::draw + // [`rendering`]: crate::ui::render pub fn draw(&mut self, app: &mut App) -> Result<()> { // self.terminal.draw(|frame| ui::render(app, frame))?; self.terminal @@ -230,33 +200,6 @@ impl Tui { pub async fn next(&mut self) -> Result<Event> { self.receiver.recv().await.ok_or_eyre("This is an IO error") } - - pub fn init_error_hooks() -> Result<()> { - let (panic, error) = HookBuilder::default().into_hooks(); - let panic = panic.into_panic_hook(); - let error = error.into_eyre_hook(); - color_eyre::eyre::set_hook(Box::new(move |e| { - let _ = crossterm::execute!( - stdout(), - DisableMouseCapture, - LeaveAlternateScreen, - cursor::Show - ); - let _ = crossterm::terminal::disable_raw_mode(); - error(e) - }))?; - std::panic::set_hook(Box::new(move |info| { - let _ = crossterm::execute!( - stdout(), - DisableMouseCapture, - LeaveAlternateScreen, - cursor::Show - ); - let _ = crossterm::terminal::disable_raw_mode(); - panic(info) - })); - Ok(()) - } } impl Deref for Tui { diff --git a/src/frontend/ui.rs b/src/frontend/ui.rs index 08cebcb..4ea275d 100644 --- a/src/frontend/ui.rs +++ b/src/frontend/ui.rs @@ -28,7 +28,7 @@ use ratatui::{ }, }; -use crate::{frontend::app::App, frontend::keywords::TagListItem}; +use crate::frontend::{app::App, keywords::TagListItem}; use super::app::{CurrentArea, FormerArea}; @@ -47,6 +47,8 @@ const SELECTED_STYLE: Style = Style::new() .add_modifier(Modifier::REVERSED); const TEXT_FG_COLOR: Color = Color::Indexed(252); const TEXT_UNSELECTED_FG_COLOR: Color = Color::Indexed(245); +const SORTED_ENTRIES: &str = "▼"; +const SORTED_ENTRIES_REVERSED: &str = "▲"; const SCROLLBAR_UPPER_CORNER: Option<&str> = Some("┓"); const SCROLLBAR_LOWER_CORNER: Option<&str> = Some("┛"); @@ -259,15 +261,22 @@ impl App { let header_style = Style::default().bold().fg(TEXT_FG_COLOR); - let header = [ - "Authors".underlined(), - "Title".underlined(), - "Year".underlined(), - "Type".underlined(), - ] - .into_iter() - .map(Cell::from) - .collect::<Row>() + let header = Row::new(vec![ + Cell::from(Line::from(vec![ + Span::raw("Author").underlined(), + Span::raw(format!( + " {}", + if self.entry_table.entry_table_reversed_sort { + SORTED_ENTRIES_REVERSED + } else { + SORTED_ENTRIES + } + )), + ])), + Cell::from("Title".to_string().underlined()), + Cell::from("Year".to_string().underlined()), + Cell::from("Type".to_string().underlined()), + ]) .style(header_style) .height(1); @@ -275,7 +284,7 @@ impl App { let rows = self .entry_table .entry_table_items - .iter() + .iter_mut() .enumerate() .map(|(_i, data)| { let item = data.ref_vec(); @@ -340,15 +349,16 @@ impl App { let mut lines = vec![]; lines.push(Line::from(vec![ Span::styled("Authors: ", style_value), - Span::styled(cur_entry.authors.clone(), Style::new().green()), + // Span::styled(cur_entry.authors.clone(), Style::new().green()), + Span::styled(cur_entry.authors(), Style::new().green()), ])); lines.push(Line::from(vec![ Span::styled("Title: ", style_value), - Span::styled(cur_entry.title.clone(), Style::new().magenta()), + Span::styled(cur_entry.title(), Style::new().magenta()), ])); lines.push(Line::from(vec![ Span::styled("Year: ", style_value), - Span::styled(cur_entry.year.clone(), Style::new().light_magenta()), + Span::styled(cur_entry.year(), Style::new().light_magenta()), ])); if !cur_entry.doi_url.is_empty() || !cur_entry.filepath.is_empty() { lines.push(Line::raw("")); @@ -357,7 +367,7 @@ impl App { lines.push(Line::from(vec![ Span::styled("DOI/URL: ", style_value_sec), Span::styled( - cur_entry.doi_url.clone(), + cur_entry.doi_url(), Style::default().fg(TEXT_FG_COLOR).underlined(), ), ])); @@ -365,10 +375,7 @@ impl App { if !cur_entry.filepath.is_empty() { lines.push(Line::from(vec![ Span::styled("File: ", style_value_sec), - Span::styled( - cur_entry.filepath.clone(), - Style::default().fg(TEXT_FG_COLOR), - ), + Span::styled(cur_entry.filepath(), Style::default().fg(TEXT_FG_COLOR)), ])); } lines.push(Line::from("")); |
