diff options
| -rw-r--r-- | Cargo.lock | 2 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | README.md | 39 | ||||
| -rw-r--r-- | src/frontend/entries.rs | 88 | ||||
| -rw-r--r-- | src/frontend/handler.rs | 49 | ||||
| -rw-r--r-- | src/frontend/keywords.rs | 8 | ||||
| -rw-r--r-- | src/frontend/ui.rs | 101 |
7 files changed, 217 insertions, 72 deletions
@@ -71,7 +71,7 @@ dependencies = [ [[package]] name = "bibiman" -version = "0.3.1" +version = "0.4.2" dependencies = [ "arboard", "biblatex", @@ -1,6 +1,6 @@ [package] name = "bibiman" -version = "0.4.0" +version = "0.4.2" authors = ["lukeflo <lukeflo@some.email.not>"] license = "GPL-3.0-or-later" edition = "2021" @@ -60,30 +60,36 @@ updated: - [x] **Open related PDF** file (`file` BibLaTeX key) with keypress. - [x] **Open related URL/DOI** with keypress. - [x] **Scrollbar** for better navigating. +- [x] **Sort Entries** by different each column (`Authors`, `Title`, `Year`, + `Pubtype`) - [ ] **Open related notes file** for specific entry. - [ ] **Add Entry via DOI** as formatted code. -- [ ] **Sort Entries** by different values (partly possible for author column) -- [ ] **Support Hayagriva(`.yaml.`)** format as input. - [ ] **Implement config file** for setting some default values like main bibfile, PDF-opener, or editor +- [ ] **Support Hayagriva(`.yaml`)** format as input (_on hold for now_, because + the Hayagriva Yaml style doesn't offer keywords; s. issue in + [Hayagriva repo](https://github.com/typst/hayagriva/issues/240)). ## Keybindings Use the following keybindings to manage the TUI: -| Key | Action | -| -------------------------------------------------------------------------------- | ------------------------------------------- | -| **<kbd>j</kbd><kbd>k</kbd>** \| **<kbd>Down</kbd><kbd>Up</kbd>** | Move selected list | -| **<kbd>g</kbd><kbd>G</kbd>** | Go to first/last entry | -| **<kbd>PageDown</kbd><kbd>PageUp</kbd>** \| **<kbd>Alt-j</kbd><kbd>Alt-k</kbd>** | Scroll Info window | -| **<kbd>y</kbd>** | Yank/copy citekey of selected entry | -| **<kbd>e</kbd>** | Open editor at selected entry | -| **<kbd>o</kbd>** \| **<kbd>u</kbd>** | Open related PDF \| URL/DOI | -| **<kbd>TAB</kbd>** | Switch between entries and keywords | -| **<kbd>/</kbd>** \| **<kbd>Ctrl-f</kbd>** | Enter search mode | -| **<kbd>Enter</kbd>** | Filter by selected keyword / Confirm search | -| **<kbd>ESC</kbd>** | Abort search / Reset current list | -| **<kbd>q</kbd>** \| **<kbd>Ctrl-c</kbd>** | Quit TUI | +| Key | Action | +| -------------------------------------- | ------------------------------------------- | +| `j`,`k` \| `Down`,`Up` | Move down/up by 1 | +| `Ctrl-d` \| `Ctrl-u` | Move down/up by 5 | +| `g`,`G` | Go to first/last entry | +| `h`,`k` | Select previous/next entry column | +| `s` | Sort current column (toggles) | +| `PageDown`,`PageUp` \| `Alt-j`,`Alt-k` | Scroll Info window | +| `y` | Yank/copy citekey of selected entry | +| `e` | Open editor at selected entry | +| `o` \| `u` | Open related PDF \| URL/DOI | +| `TAB` | Switch between entries and keywords | +| `/` \| `Ctrl-f` | Enter search mode | +| `Enter` | Filter by selected keyword / Confirm search | +| `ESC` | Abort search / Reset current list | +| `q` \| `Ctrl-c` | Quit TUI | ## Search @@ -122,7 +128,8 @@ Now, `bibiman` also provides the possibility to open PDFs (as value of the `file` BibLaTeX field), as well as DOIs and URLs. For selecting the right program, it uses `xdg-open` on Linux, `open` on MacOS, -and `start` on Windows. _MacOS and Windows are untested right now!_ +and `start` on Windows. _MacOS is untested right now! Windows does not work, +have to figure this out_ Furhtermore, DOIs have to begin with either `https://doi...` as full URL or `10.(...)` as regular DOI style. URLs work if they begin with either `http...` diff --git a/src/frontend/entries.rs b/src/frontend/entries.rs index 6c227df..d5b0d8c 100644 --- a/src/frontend/entries.rs +++ b/src/frontend/entries.rs @@ -24,11 +24,20 @@ use editor_command::EditorBuilder; use ratatui::widgets::{ScrollbarState, TableState}; use std::process::{Command, Stdio}; +#[derive(Debug)] +pub enum EntryTableColumn { + Authors, + Title, + Year, + Pubtype, +} + // Define list containing entries as table #[derive(Debug)] pub struct EntryTable { pub entry_table_items: Vec<EntryTableItem>, pub entry_table_at_search_start: Vec<EntryTableItem>, + pub entry_table_selected_column: EntryTableColumn, pub entry_table_reversed_sort: bool, pub entry_table_state: TableState, pub entry_scroll_state: ScrollbarState, @@ -45,6 +54,7 @@ impl EntryTable { Self { entry_table_items, entry_table_at_search_start: Vec::new(), + entry_table_selected_column: EntryTableColumn::Authors, entry_table_reversed_sort: false, entry_table_state, entry_scroll_state, @@ -76,35 +86,39 @@ impl EntryTable { // Sort entry table by specific column. // Toggle sorting by hitting same key again - pub fn sort_entry_table(&mut self, sorting: &str, toggle: bool) { + pub fn sort_entry_table(&mut self, toggle: bool) { if toggle { self.entry_table_reversed_sort = !self.entry_table_reversed_sort; } if self.entry_table_reversed_sort { - match sorting { - "author" => self + match self.entry_table_selected_column { + EntryTableColumn::Authors => self .entry_table_items .sort_by(|a, b| b.authors.to_lowercase().cmp(&a.authors.to_lowercase())), - "title" => self + EntryTableColumn::Title => self .entry_table_items .sort_by(|a, b| b.title.to_lowercase().cmp(&a.title.to_lowercase())), - "year" => self + EntryTableColumn::Year => self .entry_table_items .sort_by(|a, b| b.year.to_lowercase().cmp(&a.year.to_lowercase())), - _ => {} + EntryTableColumn::Pubtype => self + .entry_table_items + .sort_by(|a, b| b.pubtype.to_lowercase().cmp(&a.pubtype.to_lowercase())), } } else if !self.entry_table_reversed_sort { - match sorting { - "author" => self + match self.entry_table_selected_column { + EntryTableColumn::Authors => self .entry_table_items .sort_by(|a, b| a.authors.to_lowercase().cmp(&b.authors.to_lowercase())), - "title" => self + EntryTableColumn::Title => self .entry_table_items .sort_by(|a, b| a.title.to_lowercase().cmp(&b.title.to_lowercase())), - "year" => self + EntryTableColumn::Year => self .entry_table_items .sort_by(|a, b| a.year.to_lowercase().cmp(&b.year.to_lowercase())), - _ => {} + EntryTableColumn::Pubtype => self + .entry_table_items + .sort_by(|a, b| a.pubtype.to_lowercase().cmp(&b.pubtype.to_lowercase())), } } } @@ -189,22 +203,22 @@ impl App { // Entry Table commands // Movement - pub fn select_next_entry(&mut self) { + pub fn select_next_entry(&mut self, entries: u16) { self.entry_table.entry_info_scroll = 0; self.entry_table.entry_info_scroll_state = self.entry_table.entry_info_scroll_state.position(0); - self.entry_table.entry_table_state.select_next(); + self.entry_table.entry_table_state.scroll_down_by(entries); self.entry_table.entry_scroll_state = self .entry_table .entry_scroll_state .position(self.entry_table.entry_table_state.selected().unwrap()); } - pub fn select_previous_entry(&mut self) { + pub fn select_previous_entry(&mut self, entries: u16) { self.entry_table.entry_info_scroll = 0; self.entry_table.entry_info_scroll_state = self.entry_table.entry_info_scroll_state.position(0); - self.entry_table.entry_table_state.select_previous(); + self.entry_table.entry_table_state.scroll_up_by(entries); self.entry_table.entry_scroll_state = self .entry_table .entry_scroll_state @@ -230,6 +244,48 @@ impl App { .position(self.entry_table.entry_table_items.len()); } + pub fn select_next_column(&mut self) { + match self.entry_table.entry_table_selected_column { + EntryTableColumn::Authors => { + self.entry_table.entry_table_selected_column = EntryTableColumn::Title; + self.entry_table.sort_entry_table(false); + } + EntryTableColumn::Title => { + self.entry_table.entry_table_selected_column = EntryTableColumn::Year; + self.entry_table.sort_entry_table(false); + } + EntryTableColumn::Year => { + self.entry_table.entry_table_selected_column = EntryTableColumn::Pubtype; + self.entry_table.sort_entry_table(false); + } + EntryTableColumn::Pubtype => { + self.entry_table.entry_table_selected_column = EntryTableColumn::Authors; + self.entry_table.sort_entry_table(false); + } + } + } + + pub fn select_prev_column(&mut self) { + match self.entry_table.entry_table_selected_column { + EntryTableColumn::Authors => { + self.entry_table.entry_table_selected_column = EntryTableColumn::Pubtype; + self.entry_table.sort_entry_table(false); + } + EntryTableColumn::Title => { + self.entry_table.entry_table_selected_column = EntryTableColumn::Authors; + self.entry_table.sort_entry_table(false); + } + EntryTableColumn::Year => { + self.entry_table.entry_table_selected_column = EntryTableColumn::Title; + self.entry_table.sort_entry_table(false); + } + EntryTableColumn::Pubtype => { + self.entry_table.entry_table_selected_column = EntryTableColumn::Year; + self.entry_table.sort_entry_table(false); + } + } + } + // Get the citekey of the selected entry pub fn get_selected_citekey(&self) -> &str { let idx = self.entry_table.entry_table_state.selected().unwrap(); @@ -312,7 +368,7 @@ impl App { 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.sort_entry_table(false); } self.entry_table.entry_scroll_state = ScrollbarState::content_length( self.entry_table.entry_scroll_state, diff --git a/src/frontend/handler.rs b/src/frontend/handler.rs index ec1647e..39ec7a2 100644 --- a/src/frontend/handler.rs +++ b/src/frontend/handler.rs @@ -49,23 +49,33 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App, tui: &mut Tui) -> R // Keycodes for the tag area CurrentArea::TagArea => match key_event.code { KeyCode::Down => { - app.select_next_tag(); + app.select_next_tag(1); } KeyCode::Up => { - app.select_previous_tag(); + app.select_previous_tag(1); } KeyCode::Char('j') => { if key_event.modifiers == KeyModifiers::ALT { app.scroll_info_down(); } else { - app.select_next_tag(); + app.select_next_tag(1); } } KeyCode::Char('k') => { if key_event.modifiers == KeyModifiers::ALT { app.scroll_info_up(); } else { - app.select_previous_tag(); + app.select_previous_tag(1); + } + } + KeyCode::Char('d') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.select_next_tag(5) + } + } + KeyCode::Char('u') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.select_previous_tag(5) } } KeyCode::Char('g') | KeyCode::Home => { @@ -96,23 +106,35 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App, tui: &mut Tui) -> R // Keycodes for the entry area CurrentArea::EntryArea => match key_event.code { KeyCode::Down => { - app.select_next_entry(); + app.select_next_entry(1); } KeyCode::Up => { - app.select_previous_entry(); + app.select_previous_entry(1); } KeyCode::Char('j') => { if key_event.modifiers == KeyModifiers::ALT { app.scroll_info_down(); } else { - app.select_next_entry(); + app.select_next_entry(1); } } KeyCode::Char('k') => { if key_event.modifiers == KeyModifiers::ALT { app.scroll_info_up(); } else { - app.select_previous_entry(); + app.select_previous_entry(1); + } + } + KeyCode::Char('d') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.select_next_entry(5); + } + } + KeyCode::Char('u') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.select_previous_entry(5); + } else { + app.open_doi_url()?; } } KeyCode::Char('g') | KeyCode::Home => { @@ -121,8 +143,14 @@ 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('h') => { + app.select_prev_column(); + } + KeyCode::Char('l') => { + app.select_next_column(); + } KeyCode::Char('s') => { - app.entry_table.sort_entry_table("author", true); + app.entry_table.sort_entry_table(true); } KeyCode::Char('y') => { App::yank_text(&app.get_selected_citekey()); @@ -133,9 +161,6 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App, tui: &mut Tui) -> R KeyCode::Char('o') => { app.open_connected_file()?; } - KeyCode::Char('u') => { - app.open_doi_url()?; - } KeyCode::Char('/') => { app.enter_search_area(); } diff --git a/src/frontend/keywords.rs b/src/frontend/keywords.rs index ba74b02..8f13230 100644 --- a/src/frontend/keywords.rs +++ b/src/frontend/keywords.rs @@ -60,16 +60,16 @@ impl App { // Tag List commands // Movement - pub fn select_next_tag(&mut self) { - self.tag_list.tag_list_state.select_next(); + pub fn select_next_tag(&mut self, keywords: u16) { + self.tag_list.tag_list_state.scroll_down_by(keywords); self.tag_list.tag_scroll_state = self .tag_list .tag_scroll_state .position(self.tag_list.tag_list_state.selected().unwrap()); } - pub fn select_previous_tag(&mut self) { - self.tag_list.tag_list_state.select_previous(); + pub fn select_previous_tag(&mut self, keywords: u16) { + self.tag_list.tag_list_state.scroll_up_by(keywords); self.tag_list.tag_scroll_state = self .tag_list .tag_scroll_state diff --git a/src/frontend/ui.rs b/src/frontend/ui.rs index 4ea275d..d4fbba1 100644 --- a/src/frontend/ui.rs +++ b/src/frontend/ui.rs @@ -30,7 +30,10 @@ use ratatui::{ use crate::frontend::{app::App, keywords::TagListItem}; -use super::app::{CurrentArea, FormerArea}; +use super::{ + app::{CurrentArea, FormerArea}, + entries::EntryTableColumn, +}; const MAIN_BLUE_COLOR: Color = Color::Indexed(39); // const MAIN_PURPLE_COLOR: Color = Color::Indexed(129); @@ -49,6 +52,7 @@ 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 HEADER_FOOTER_BG: Color = Color::Indexed(235); const SCROLLBAR_UPPER_CORNER: Option<&str> = Some("┓"); const SCROLLBAR_LOWER_CORNER: Option<&str> = Some("┛"); @@ -183,8 +187,6 @@ impl App { BOX_UNSELECTED_BORDER_STYLE }); - let background_style = Color::Indexed(235); - let [file_area, keyword_area, count_area] = Layout::horizontal([ Constraint::Fill(4), Constraint::Fill(2), @@ -197,7 +199,7 @@ impl App { Span::raw("File: ").bold(), Span::raw(self.main_bibfile.file_name().unwrap().to_string_lossy()).bold(), ]) - .bg(background_style) + .bg(HEADER_FOOTER_BG) .render(file_area, buf); Line::from(if !self.tag_list.selected_keyword.is_empty() { @@ -210,7 +212,7 @@ impl App { } else { vec![Span::raw(" ")] }) - .bg(background_style) + .bg(HEADER_FOOTER_BG) .render(keyword_area, buf); Line::from(if self.entry_table.entry_table_state.selected().is_some() { @@ -224,7 +226,7 @@ impl App { vec![Span::raw("No entries")] }) .right_aligned() - .bg(background_style) + .bg(HEADER_FOOTER_BG) .render(count_area, buf); // Paragraph::new(Line::from(vec![Span::raw( // self.main_bibfile.display().to_string(), @@ -259,23 +261,72 @@ impl App { BOX_UNSELECTED_BORDER_STYLE }); - let header_style = Style::default().bold().fg(TEXT_FG_COLOR); + let header_style = Style::default() + .bold() + .fg(TEXT_FG_COLOR) + .bg(HEADER_FOOTER_BG); 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()), + if let EntryTableColumn::Authors = self.entry_table.entry_table_selected_column { + 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 + } + )), + ])) + } else { + Cell::from("Author".to_string()) + }, + if let EntryTableColumn::Title = self.entry_table.entry_table_selected_column { + Cell::from(Line::from(vec![ + Span::raw("Title").underlined(), + Span::raw(format!( + " {}", + if self.entry_table.entry_table_reversed_sort { + SORTED_ENTRIES_REVERSED + } else { + SORTED_ENTRIES + } + )), + ])) + } else { + Cell::from("Title".to_string()) + }, + if let EntryTableColumn::Year = self.entry_table.entry_table_selected_column { + Cell::from(Line::from(vec![ + Span::raw("Year").underlined(), + Span::raw(format!( + " {}", + if self.entry_table.entry_table_reversed_sort { + SORTED_ENTRIES_REVERSED + } else { + SORTED_ENTRIES + } + )), + ])) + } else { + Cell::from("Year".to_string()) + }, + if let EntryTableColumn::Pubtype = self.entry_table.entry_table_selected_column { + Cell::from(Line::from(vec![ + Span::raw("Pubtype").underlined(), + Span::raw(format!( + " {}", + if self.entry_table.entry_table_reversed_sort { + SORTED_ENTRIES_REVERSED + } else { + SORTED_ENTRIES + } + )), + ])) + } else { + Cell::from("Pubtype".to_string()) + }, ]) .style(header_style) .height(1); @@ -299,7 +350,13 @@ impl App { [ Constraint::Percentage(20), Constraint::Fill(1), - Constraint::Length(4), + Constraint::Length( + if let EntryTableColumn::Year = self.entry_table.entry_table_selected_column { + 6 + } else { + 4 + }, + ), Constraint::Percentage(10), ], ) |
