diff options
25 files changed, 936 insertions, 251 deletions
@@ -104,6 +104,7 @@ dependencies = [ "itertools", "lexopt", "nucleo-matcher", + "owo-colors", "rand", "ratatui", "regex", @@ -38,3 +38,4 @@ ureq = "2.12.1" # config = { version = "0.15.8", default-features = false, features = ["async", "async-trait", "convert-case", "convert_case", "toml"] } serde = { version = "1.0.217", features = ["serde_derive"] } figment = { version = "0.10.19", features = [ "toml", "test" ]} +owo-colors = "4.2.2" @@ -30,6 +30,7 @@ - [Search](#search) - [Edit bib entry](#edit-bib-entry) - [Open connected files or links](#open-connected-files-or-links) + - [Note file creation](#note-file-creation) - [Issues and code improvement](#issues-and-code-improvement) - [Alternatives](#alternatives) - [Comparison](#comparison) @@ -45,6 +46,8 @@ Here's a small impression how it looks and works: [](https://postimg.cc/ct0W0mK4) + + ## Installation<a name="installation"></a> ### Crates.io<a name="cratesio"></a> @@ -191,6 +194,8 @@ set through the CLI, `bibiman` will offer to create a default config file at the standard location. This will very likely happen on the first run of `bibiman` after installation. If rejected, you probably will be asked again next time. +The created config contains all values which are set as default by `bibiman`. + ### General Configuration<a name="general-configuration"></a> The following general values can be set through the config file: @@ -200,19 +205,35 @@ The following general values can be set through the config file: # Default files/dirs which are loaded on startup # Use absolute paths (~ for HOME works). Otherwise, loading might not work. bibfiles = [ "/path/to/bibfile", "path/to/dir/with/bibfiles" ] + # Default editor to use when editing files. Arguments are possible editor = "vim" # with args: "vim -y" + # Default app to open PDFs/Epubs pdf_opener = "xdg-open" + # Default app to open URLs/DOIs url_opener = "xdg-open" + # Prefix which is prepended to the filepath from the `file` field # Use absolute paths (~ for HOME works). Otherwise, loading might not work. file_prefix = "/some/path/prefix" + # Path to folder (with subfolders) containing PDF files with the basename # of the format "citekey.pdf". Other PDF basenames are not accepted. # Use absolute paths (~ for HOME works). Otherwise, loading might not work. pdf_path = "/path/to/pdf-folder" + +## Path to folder (with subfolders) containing note files with the basename of +## the format "citekey.extension". Other basenames are not accepted. The possible +## extensions can be set through the "note_extensions" array. +note_path = "path/to/note-files" +note_extensions = [ "md", "txt" ] + +## Symbols/chars to show if not has specific attachement +file_symbol = " " +link_symbol = " " +note_symbol = "" ``` `bibfiles` @@ -246,13 +267,14 @@ pdf_path = "/path/to/pdf-folder" created through the `pdf_path` variable. Thus, it is safe to mix both approaches if wanted! -`pdf_path` +`pdf_path` and `note_path` -: The `pdf_path` is used as path wich is recursivley searched for files which - basename consists of the an entrys `citekey` plus a `.pdf` ending - (case-insensitive). Every file which matches this pattern for an existing - `citekey` is associated with the particular entry for the current `bibiman` - session and can be opened from within. +: The `pdf_path`/`note_path` is used as path wich is recursivley searched for + files which basename consists of the an entrys `citekey` plus a `.pdf` ending + or one of the specified note endinfs (case-insensitive). Every file which + matches this pattern for an existing `citekey` is associated with the + particular entry for the current `bibiman` session and can be opened from + within. ### Color Configuration<a name="color-configuration"></a> @@ -270,8 +292,15 @@ info_color = "99" confirm_color = "47" warn_color = "124" bar_bg_color = "234" +popup_fg_color = "43" popup_bg_color = "234" selected_row_bg_color = "237" +note_color = "123" +file_color = "209" +link_color = "27" +author_color = "38" +title_color = "37" +year_color = "135" ``` Colors can be set through three different methods: @@ -309,7 +338,8 @@ These are the current features, the list will be updated: - [x] **Add Entry via DOI**. - [x] **Implement config file** for setting some default values like main bibfile, PDF-opener, or editor -- [ ] **Open related notes file** for specific entry. +- [x] **Open related notes file** for specific entry. +- [x] **Create note file** for bib entries. - [ ] **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)). @@ -335,6 +365,7 @@ Use the following keybindings to manage the TUI: | `e` | Open editor at selected entry | | `a` | Add entry through DOI | | `o` | Open related PDF or URL/DOI | +| `n` | Create new note file for selected entry | | `TAB` | Switch between entries and keywords | | `/`, `Ctrl-f` | Enter search mode | | `Enter` | Filter by selected keyword / Confirm search or selection | @@ -346,7 +377,8 @@ Use the following keybindings to manage the TUI: There are some shortcuts to select an item from the opening/yanking popup without navigating the list: -- `o-o`|`o-l`: directly opens the first file/link for the selected entry. +- `o-o`|`o-l`|`o-n`: directly opens the first file|link|note for the selected + entry. - `y-y`: directly yanks the citekey of the selected entry to the clipboard. ## Search<a name="search"></a> @@ -384,12 +416,12 @@ thus, there might be unexpected errors with it. ## Open connected files or links<a name="open-connected-files-or-links"></a> -`bibiman` also provides the possibility to open PDFs (as value of the `file` -BibLaTeX field), as well as DOIs and URLs. +`bibiman` also provides the possibility to open PDFs , note files, as well as +DOIs and URLs connected with the different entries of the bibfile. For selecting the right program, it uses `xdg-open` on Linux, `open` on MacOS, -and `start` on Windows. Thanks to the report from @bastislack in #2 MacOS seems -to work. +and `start` on Windows by default. Thanks to the report from @bastislack in #2 +MacOS seems to work. _However, Windows does not work. Have to figure this out. Reports from some Windows users are very welcome._ @@ -398,6 +430,25 @@ Furthermore, 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...` or with `www...`. +## Note file creation<a name="note-file-creation"></a> + +It is possible to create notes for an entry missing such a file. The `note_path` +and `note_extensions` values need to be set in the config file or it will fail. + +The notes basename is *always* the citekey of the selected entry and the +directory is set to the value of the `note_path` variable. The extension can be +choosen from one of the file format extension set in the `note_extensions` +array. + +**Be aware**: The operation of creating new notes is not permitted if the +citekey contains some special chars which could cause problems with Unixish +shell commands and file operations. Currently, the following chars are not +allowed as part of the citekey: `/` | `|` | `#` | `*` | `\` | `"` | `'` | `;` | +`!` + +The bibfile itself will *not be edited*. Therefore, you can't break anything in +your bibfile with this operation! + ## Issues and code improvement<a name="issues-and-code-improvement"></a> This is my first Rust project and, thus, also a learning process. If you find @@ -16,14 +16,14 @@ ///// use crate::bibiman::CurrentArea; -use crate::config::BibiConfig; -use color_eyre::eyre::{Context, Ok, Result}; -// use super::Event; use crate::cliargs::CLIArgs; +use crate::config::BibiConfig; use crate::tui::commands::InputCmdAction; -use crate::tui::popup::PopupKind; +use crate::tui::popup::{PopupItem, PopupKind}; use crate::tui::{self, Tui}; use crate::{bibiman::Bibiman, tui::commands::CmdAction}; +use color_eyre::eyre::{Context, Ok, Result}; +use crossterm::event::KeyCode; use std::ffi::OsStr; use std::path::PathBuf; use std::process::{Command, Stdio}; @@ -82,7 +82,13 @@ impl App { } else if let Some(PopupKind::YankItem) | Some(PopupKind::OpenRes) = self.bibiman.popup_area.popup_kind { - self.bibiman.fast_selection(cfg, key_event.code)?; + self.bibiman.fast_selection(cfg, &mut tui, key_event.code)?; + // if a fast match char was used, restart event-loop. + // otherwise, the fast match char will be executed as command + match key_event.code { + KeyCode::Char('o' | 'l' | 'n' | 'y') => continue, + _ => {} + } } let command = if self.input_mode { CmdAction::Input(InputCmdAction::parse(key_event, &self.input)) @@ -186,7 +192,8 @@ impl App { } Some(PopupKind::OpenRes) | Some(PopupKind::AppendToFile) - | Some(PopupKind::YankItem) => { + | Some(PopupKind::YankItem) + | Some(PopupKind::CreateNote) => { self.bibiman.popup_area.popup_state.scroll_down_by(1) } _ => {} @@ -207,7 +214,8 @@ impl App { } Some(PopupKind::OpenRes) | Some(PopupKind::AppendToFile) - | Some(PopupKind::YankItem) => { + | Some(PopupKind::YankItem) + | Some(PopupKind::CreateNote) => { self.bibiman.popup_area.popup_state.scroll_up_by(1) } _ => {} @@ -267,6 +275,8 @@ impl App { self.bibiman.close_popup(); } else if let Some(PopupKind::YankItem) = self.bibiman.popup_area.popup_kind { self.bibiman.close_popup(); + } else if let Some(PopupKind::CreateNote) = self.bibiman.popup_area.popup_kind { + self.bibiman.close_popup(); } } else { self.bibiman.reset_current_list(); @@ -279,12 +289,14 @@ impl App { if let Some(PopupKind::Help) = self.bibiman.popup_area.popup_kind { self.bibiman.close_popup(); } else if let Some(PopupKind::OpenRes) = self.bibiman.popup_area.popup_kind { - self.bibiman.open_connected_res(cfg)?; + self.bibiman.open_connected_res(cfg, tui)?; } else if let Some(PopupKind::AppendToFile) = self.bibiman.popup_area.popup_kind { self.bibiman.append_entry_to_file(cfg)? } else if let Some(PopupKind::YankItem) = self.bibiman.popup_area.popup_kind { self.bibiman.yank_entry_field()? + } else if let Some(PopupKind::CreateNote) = self.bibiman.popup_area.popup_kind { + self.bibiman.create_note(cfg)? } } } @@ -305,13 +317,25 @@ impl App { .selected() .unwrap(); let entry = self.bibiman.entry_table.entry_table_items[idx].clone(); - let mut items = vec![("Citekey: ".to_string(), entry.citekey.clone())]; + let mut items = vec![( + "Citekey: ".to_string(), + entry.citekey.clone(), + PopupItem::Citekey, + )]; if entry.doi_url.is_some() { - items.push(("Weblink: ".into(), entry.doi_url.unwrap().clone())) + items.push(( + "Weblink: ".into(), + entry.doi_url.unwrap().clone(), + PopupItem::Link, + )) } if entry.filepath.is_some() { entry.filepath.unwrap().iter().for_each(|p| { - items.push(("Filepath: ".into(), p.clone().into_string().unwrap())) + items.push(( + "Filepath: ".into(), + p.clone().into_string().unwrap(), + PopupItem::Entryfile, + )) }); // items.push(( // "Filepath: ".into(), @@ -342,18 +366,20 @@ impl App { .selected() .unwrap(); let entry = self.bibiman.entry_table.entry_table_items[idx].clone(); - let mut items: Vec<(String, String)> = vec![]; - if entry.filepath.is_some() || entry.doi_url.is_some() { + let mut items: Vec<(String, String, PopupItem)> = vec![]; + if entry.filepath.is_some() || entry.doi_url.is_some() || entry.notes.is_some() + { if entry.doi_url.is_some() { items.push(( - "Weblink (DOI/URL): ".into(), + "Link: ".into(), entry.doi_url.unwrap().clone(), + PopupItem::Link, )) } if entry.filepath.is_some() { entry.filepath.unwrap().iter().for_each(|p| { items.push(( - "File (PDF/EPUB): ".into(), + "File: ".into(), // p.clone().into_string().unwrap(), if entry.file_field && cfg.general.file_prefix.is_some() { cfg.general @@ -367,9 +393,19 @@ impl App { } else { p.clone().into_string().unwrap() }, + PopupItem::Entryfile, )) }); } + if entry.notes.is_some() { + entry.notes.unwrap().iter().for_each(|n| { + items.push(( + "Note: ".into(), + n.clone().into_string().unwrap(), + PopupItem::Notefile, + )); + }); + } self.bibiman .open_popup(PopupKind::OpenRes, None, None, Some(items))?; @@ -389,6 +425,87 @@ impl App { self.bibiman.add_entry(); } } + CmdAction::CreateNote => { + if let CurrentArea::EntryArea = self.bibiman.current_area { + let citekey = self.bibiman.entry_table.entry_table_items[self + .bibiman + .entry_table + .entry_table_state + .selected() + .unwrap()] + .citekey + .clone(); + // disallow chars which can cause other shell executions + if citekey.contains("/") + | citekey.contains("|") + | citekey.contains("#") + | citekey.contains("\\") + | citekey.contains("*") + | citekey.contains("\"") + | citekey.contains(";") + | citekey.contains("!") + | citekey.contains("\'") + { + self.bibiman.open_popup( + PopupKind::MessageError, + Some("Selected entrys citekey contains special char: "), + Some(&citekey), + None, + )?; + } else if cfg.general.note_path.is_some() + && cfg.general.note_extensions.is_some() + && self.bibiman.entry_table.entry_table_items[self + .bibiman + .entry_table + .entry_table_state + .selected() + .unwrap()] + .notes + .is_none() + { + let mut items = vec![]; + for ex in cfg.general.note_extensions.as_ref().unwrap() { + items.push(( + self.bibiman.entry_table.entry_table_items[self + .bibiman + .entry_table + .entry_table_state + .selected() + .unwrap()] + .citekey() + .to_string(), + ex.clone(), + PopupItem::Notefile, + )); + } + self.bibiman + .open_popup(PopupKind::CreateNote, None, None, Some(items))?; + } else if cfg.general.note_path.is_some() + && self.bibiman.entry_table.entry_table_items[self + .bibiman + .entry_table + .entry_table_state + .selected() + .unwrap()] + .notes + .is_some() + { + self.bibiman.open_popup( + PopupKind::MessageError, + Some("Selected entry already has a connected note"), + None, + None, + )?; + } else { + self.bibiman.open_popup( + PopupKind::MessageError, + Some("No note path found. Set it in config file."), + None, + None, + )?; + } + } + } CmdAction::ShowHelp => { self.bibiman.open_popup(PopupKind::Help, None, None, None)?; } @@ -404,15 +521,6 @@ impl App { pub fn open_connected_file(cfg: &BibiConfig, file: &OsStr) -> Result<()> { // Build command to execute pdf-reader. 'xdg-open' is Linux standard let cmd = &cfg.general.pdf_opener; - // If necessary, replace ~ with /home dir - // let file = if cfg.general.file_prefix.is_some() { - // cfg.general.file_prefix.clone().unwrap().join(file) - // } else { - // PathBuf::from(file) - // }; - // let file = PathBuf::from(file); - - // let file = expand_home(&file).into_os_string(); // Pass filepath as argument, pipe stdout and stderr to /dev/null // to keep the TUI clean (where is it piped on Windows???) diff --git a/src/bibiman.rs b/src/bibiman.rs index 21601e3..6d21f8c 100644 --- a/src/bibiman.rs +++ b/src/bibiman.rs @@ -20,21 +20,22 @@ use crate::bibiman::entries::EntryTableColumn; use crate::bibiman::{bibisetup::*, search::BibiSearch}; use crate::cliargs::CLIArgs; use crate::config::BibiConfig; -use crate::tui::popup::{PopupArea, PopupKind}; +use crate::tui::popup::{PopupArea, PopupItem, PopupKind}; use crate::tui::Tui; use crate::{app, cliargs}; use crate::{bibiman::entries::EntryTable, bibiman::keywords::TagList}; use arboard::Clipboard; -use color_eyre::eyre::{Error, Result}; +use color_eyre::eyre::{Context, Error, Result}; use crossterm::event::KeyCode; use editor_command::EditorBuilder; use ratatui::widgets::ScrollbarState; use regex::Regex; +use std::ffi::OsStr; use std::fs::{self, read_to_string}; use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::PathBuf; -use std::process::Command; +use std::process::{Command, Stdio}; use std::result::Result::Ok; use tui_input::Input; @@ -160,7 +161,7 @@ impl Bibiman { popup_kind: PopupKind, message: Option<&str>, object: Option<&str>, - items: Option<Vec<(String, String)>>, + items: Option<Vec<(String, String, PopupItem)>>, ) -> Result<()> { if let CurrentArea::EntryArea = self.current_area { self.former_area = Some(FormerArea::EntryArea); @@ -237,6 +238,18 @@ impl Bibiman { )) } } + PopupKind::CreateNote => { + if items.is_some() { + self.popup_area.popup_kind = Some(PopupKind::CreateNote); + self.popup_area.popup_selection(items.unwrap()); + self.popup_area.popup_state.select(Some(0)); + Ok(()) + } else { + Err(Error::msg( + "No Vec<(String, String)> passed as argument to generate the items list", + )) + } + } } } @@ -366,9 +379,9 @@ impl Bibiman { .entry_table_state .selected_column() .unwrap() - == 3 + == 4 { - self.entry_table.entry_table_state.select_first_column(); + self.entry_table.entry_table_state.select_column(Some(1)); } else { self.entry_table.entry_table_state.select_next_column(); } @@ -395,7 +408,7 @@ impl Bibiman { .entry_table_state .selected_column() .unwrap() - == 0 + == 1 { self.entry_table.entry_table_state.select_last_column(); } else { @@ -512,6 +525,74 @@ impl Bibiman { Ok(()) } + pub fn open_connected_note( + &mut self, + cfg: &BibiConfig, + tui: &mut Tui, + file: &OsStr, + ) -> Result<()> { + // get filecontent and citekey for calculating line number + + match std::env::var("TERM") { + Ok(sh) => { + let editor = if let Some(e) = cfg.general.editor.clone() { + e + } else if let Ok(e) = std::env::var("VISUAL") { + e + } else if let Ok(e) = std::env::var("EDITOR") { + e + } else { + String::from("vi") + }; + let _ = Command::new(sh) + .arg("-e") + .arg(editor) + .arg(file) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .wrap_err("Couldn't run editor"); + // Prepare arguments to open file at specific line + // let status = note_cmd.status()?; + // if !status.success() { + // eprintln!("Spawning editor failed with status {}", status); + // } + } + Err(_e) => { + let citekey: &str = &self.entry_table.entry_table_items + [self.entry_table.entry_table_state.selected().unwrap()] + .citekey + .clone(); + // Exit TUI to enter editor + tui.exit()?; + // Use VISUAL or EDITOR. Set "vi" as last fallback + let mut note_cmd: Command = EditorBuilder::new() + .source(cfg.general.editor.clone()) + .environment() + .source(Some("vi")) + .build() + .unwrap(); + // Prepare arguments to open file at specific line + let status = note_cmd.arg(file).status()?; + if !status.success() { + eprintln!("Spawning editor failed with status {}", status); + } + + // Enter TUI again + tui.enter()?; + tui.terminal.clear()?; + + // Update the database and the lists to show changes + // Self::update_lists(self, cfg); + + // Select entry which was selected before entering editor + self.select_entry_by_citekey(citekey); + } + } + + Ok(()) + } + pub fn add_entry(&mut self) { if let CurrentArea::EntryArea = self.current_area { self.former_area = Some(FormerArea::EntryArea); @@ -562,10 +643,18 @@ impl Bibiman { } pub fn append_to_file(&mut self) { - let mut items = vec![("Create new file".to_owned(), "".to_string())]; + let mut items = vec![( + "Create new file".to_owned(), + "".to_string(), + PopupItem::Default, + )]; if self.main_bibfiles.len() > 1 { for f in self.main_bibfiles.clone() { - items.push(("File: ".into(), f.to_str().unwrap().to_owned())); + items.push(( + "File: ".into(), + f.to_str().unwrap().to_owned(), + PopupItem::Bibfile, + )); } } else { items.push(( @@ -576,6 +665,7 @@ impl Bibiman { .to_str() .unwrap() .to_owned(), + PopupItem::Bibfile, )); } self.popup_area.popup_selection(items); @@ -662,7 +752,36 @@ impl Bibiman { Ok(()) } - pub fn open_connected_res(&mut self, cfg: &BibiConfig) -> Result<()> { + pub fn create_note(&mut self, cfg: &BibiConfig) -> Result<()> { + // Index of selected entry + let entry_idx = self.entry_table.entry_table_state.selected().unwrap(); + let citekey = self.entry_table.entry_table_items[entry_idx] + .citekey + .clone(); + + // Index of selected popup field + let popup_idx = self.popup_area.popup_state.selected().unwrap(); + let ext = self.popup_area.popup_list[popup_idx].1.clone(); + + let basename = PathBuf::from(&citekey).with_extension(ext); + let path = cfg.general.note_path.as_ref().unwrap(); + + let new_file = path.join(basename); + + let new_file = if new_file.starts_with("~") { + expand_home(&new_file) + } else { + new_file + }; + + File::create_new(new_file).unwrap(); + self.close_popup(); + self.update_lists(cfg); + self.select_entry_by_citekey(&citekey); + Ok(()) + } + + pub fn open_connected_res(&mut self, cfg: &BibiConfig, tui: &mut Tui) -> Result<()> { // Index of selected entry let entry_idx = self.entry_table.entry_table_state.selected().unwrap(); @@ -671,12 +790,12 @@ impl Bibiman { let popup_entry = self.popup_area.popup_list[popup_idx].1.clone(); // Choose ressource depending an selected popup field - if self.popup_area.popup_list[popup_idx].0.contains("Weblink") { + if let PopupItem::Link = self.popup_area.popup_list[popup_idx].2 { let object = self.entry_table.entry_table_items[entry_idx].doi_url(); let url = app::prepare_weblink(object); app::open_connected_link(cfg, &url)?; self.close_popup(); - } else if self.popup_area.popup_list[popup_idx].0.contains("File") { + } else if let PopupItem::Entryfile = self.popup_area.popup_list[popup_idx].2 { // TODO: Selection for multiple files // let object = self.entry_table.entry_table_items[entry_idx].filepath()[0]; let file = expand_home(&PathBuf::from(popup_entry.clone())); @@ -692,6 +811,20 @@ impl Bibiman { None, )?; } + } else if let PopupItem::Notefile = self.popup_area.popup_list[popup_idx].2 { + let file = expand_home(&PathBuf::from(popup_entry.clone())); + // let object: OsString = popup_entry.into(); + if file.is_file() { + self.open_connected_note(cfg, tui, &file.into_os_string())?; + self.close_popup(); + } else { + self.open_popup( + PopupKind::MessageError, + Some("No valid file path: "), + Some(file.to_str().unwrap()), + None, + )?; + } } else { eprintln!("Unable to find ressource to open"); }; @@ -733,11 +866,17 @@ impl Bibiman { /// /// `o` -> opens the first file of the `filepath` `Vec` for the current entry /// `l` -> opens the link of the current entry + /// `n` -> opens the first note /// /// **Yanking popup** /// /// `y` -> yanks the citekey for the current entry - pub fn fast_selection(&mut self, cfg: &BibiConfig, key_code: KeyCode) -> Result<()> { + pub fn fast_selection( + &mut self, + cfg: &BibiConfig, + tui: &mut Tui, + key_code: KeyCode, + ) -> Result<()> { if let CurrentArea::PopupArea = self.current_area { let entry_idx = self.entry_table.entry_table_state.selected().unwrap(); match self.popup_area.popup_kind { @@ -747,7 +886,19 @@ impl Bibiman { .filepath .clone(); if file.is_some() { - let file = expand_home(&PathBuf::from(file.unwrap()[0].clone())); + let file = if self.entry_table.entry_table_items[entry_idx].file_field + && cfg.general.file_prefix.is_some() + { + cfg.general + .file_prefix + .clone() + .unwrap() + .join(&file.unwrap()[0]) + .into_os_string() + } else { + file.unwrap()[0].clone() + }; + let file = expand_home(&PathBuf::from(file)); // let object: OsString = popup_entry.into(); if file.is_file() { app::open_connected_file(cfg, &file.into_os_string())?; @@ -762,6 +913,24 @@ impl Bibiman { } } } + KeyCode::Char('n') => { + let file = self.entry_table.entry_table_items[entry_idx].notes.clone(); + if file.is_some() { + let file = expand_home(&PathBuf::from(file.unwrap()[0].clone())); + // let object: OsString = popup_entry.into(); + if file.is_file() { + self.open_connected_note(cfg, tui, &file.into_os_string())?; + self.close_popup(); + } else { + self.open_popup( + PopupKind::MessageError, + Some("No valid file path: "), + Some(file.to_str().unwrap()), + None, + )?; + } + } + } KeyCode::Char('l') => { if self.entry_table.entry_table_items[entry_idx] .doi_url diff --git a/src/bibiman/bibisetup.rs b/src/bibiman/bibisetup.rs index bf5baf5..1f8a912 100644 --- a/src/bibiman/bibisetup.rs +++ b/src/bibiman/bibisetup.rs @@ -55,12 +55,23 @@ pub struct BibiData { pub filepath: Option<Vec<OsString>>, pub file_field: bool, pub subtitle: Option<String>, + pub notes: Option<Vec<OsString>>, + pub symbols: [Option<String>; 3], +} + +#[derive(Debug, Clone, PartialEq)] +pub struct BibiRow<'a> { + pub authors: &'a str, + pub title: &'a str, + pub year: &'a str, + pub pubtype: &'a str, + pub symbols: &'a [Option<String>; 3], } impl BibiData { // 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(&mut self) -> Vec<&str> { + pub fn ref_vec(&mut self, cfg: &BibiConfig) -> BibiRow { self.short_author = match self.authors.split_once(",") { Some((first, _rest)) => { if self.authors().contains("(ed.)") { @@ -74,18 +85,35 @@ impl BibiData { None => String::from(""), }; - vec![ - { + self.symbols = self.create_symbols(cfg); + + // vec![ + // { + // if self.short_author.is_empty() { + // self.authors() + // } else { + // &self.short_author + // } + // }, + // self.title(), + // self.year(), + // self.pubtype(), + // &self.symbols, + // ] + + BibiRow { + authors: { if self.short_author.is_empty() { self.authors() } else { &self.short_author } }, - self.title(), - self.year(), - self.pubtype(), - ] + title: self.title(), + year: self.year(), + pubtype: self.pubtype(), + symbols: &self.symbols, + } } pub fn entry_id(&self) -> &u32 { @@ -131,6 +159,26 @@ impl BibiData { pub fn subtitle(&self) -> &str { self.subtitle.as_ref().unwrap() } + + fn create_symbols(&self, cfg: &BibiConfig) -> [Option<String>; 3] { + [ + if self.file_field || self.filepath.is_some() { + Some(cfg.general.file_symbol.clone()) + } else { + None + }, + if self.doi_url.is_some() { + Some(cfg.general.link_symbol.clone()) + } else { + None + }, + if self.notes.is_some() { + Some(cfg.general.note_symbol.clone()) + } else { + None + }, + ] + } } impl BibiSetup { @@ -200,10 +248,26 @@ impl BibiSetup { cfg: &BibiConfig, ) -> Vec<BibiData> { let mut pdf_files = if cfg.general.pdf_path.is_some() { - collect_pdf_file_paths(cfg.general.pdf_path.as_ref().unwrap()) + collect_file_paths(cfg.general.pdf_path.as_ref().unwrap(), &Some(vec!["pdf"])) } else { None }; + let ext: Option<Vec<&str>> = + if cfg.general.note_path.is_some() && cfg.general.note_extensions.is_some() { + let mut ext: Vec<&str> = Vec::new(); + for e in cfg.general.note_extensions.as_ref().unwrap().iter() { + ext.push(e); + } + Some(ext) + } else { + None + }; + let mut note_files = + if cfg.general.note_path.is_some() && cfg.general.note_extensions.is_some() { + collect_file_paths(cfg.general.note_path.as_ref().unwrap(), &ext) + } else { + None + }; citekeys .iter() .enumerate() @@ -225,6 +289,12 @@ impl BibiSetup { filepath: filepaths.0, file_field: filepaths.1, subtitle: Self::get_subtitle(k, bibliography), + notes: if note_files.is_some() { + Self::get_notepath(k, &mut note_files, &ext) + } else { + None + }, + symbols: [None, None, None], } }) .collect() @@ -373,12 +443,27 @@ impl BibiSetup { true, ) } else if pdf_files.is_some() { - (Self::merge_filepath_or_none(&citekey, pdf_files), false) + ( + Self::merge_filepath_or_none_two(&citekey, pdf_files, vec!["pdf"]), + false, + ) } else { (None, false) } } + pub fn get_notepath( + citekey: &str, + note_files: &mut Option<HashMap<String, Vec<PathBuf>>>, + ext: &Option<Vec<&str>>, + ) -> Option<Vec<OsString>> { + if let Some(e) = ext { + Self::merge_filepath_or_none_two(citekey, note_files, e.to_vec()) + } else { + None + } + } + pub fn get_subtitle(citekey: &str, biblio: &Bibliography) -> Option<String> { if biblio.get(citekey).unwrap().subtitle().is_ok() { Some( @@ -394,99 +479,76 @@ impl BibiSetup { } } - fn merge_filepath_or_none( + /// Check if there exists files with the basename of the format + /// "citekey.extension" in the passed hashmap. If so, return all matches + /// as `Option<Vec>`, otherwise return `None` + fn merge_filepath_or_none_two( citekey: &str, - pdf_files: &mut Option<HashMap<String, Vec<PathBuf>>>, + files: &mut Option<HashMap<String, Vec<PathBuf>>>, + extensions: Vec<&str>, ) -> Option<Vec<OsString>> { - let pdf_file = { - // let mut idx = 0; - let citekey = citekey.to_owned().to_ascii_lowercase() + ".pdf"; - // let filename = citekey.to_owned() + ".pdf"; - // for f in args.pdf_files.unwrap().iter() { - // if f.file_name().unwrap().to_str().unwrap() == &filename { - // break f; - // } - // } - - // loop { - // if idx + 1 > pdf_files.as_ref().unwrap().len() { - // break None; - // } - // let cur_entry = pdf_files.as_ref().unwrap()[idx].clone(); - // if cur_entry.is_file() - // && cur_entry - // .file_name() - // .unwrap() - // .to_ascii_lowercase() - // .to_str() - // .unwrap() - // == citekey - // { - // let path = cur_entry.to_owned().into_os_string(); - // pdf_files.as_mut().unwrap().swap_remove(idx); - // break Some(path); - // } else { - // idx += 1 - // } - // } - - // for file in pdf_files.as_ref().unwrap().iter() { - // let filename = file.file_name().unwrap().to_ascii_lowercase(); - // if filename.to_str().unwrap() == citekey { - // break; - // } else if pdf_files.as_ref().unwrap().len() > idx { - // break; - // } else { - // idx += 1; - // } - // } - - // if pdf_files.as_ref().unwrap()[idx].is_file() { - // Some(pdf_files.as_ref().unwrap()[idx].to_owned().into_os_string()) - // } else { - // None - // } - - if pdf_files.as_ref().unwrap().contains_key(&citekey) { - let path_vec = pdf_files + let mut file = Vec::new(); + + for e in extensions.iter() { + let basename = citekey.to_owned().to_ascii_lowercase() + "." + e; + if files.as_ref().unwrap().contains_key(&basename) { + let _ = files .as_ref() .unwrap() - .get(&citekey) + .get(&basename) .unwrap() - .to_owned(); - Some(path_vec.into_iter().map(|p| p.into_os_string()).collect()) - } else { - None + .to_owned() + .into_iter() + .for_each(|p| file.push(p.into_os_string())); } - }; + } - pdf_file + if file.is_empty() { + None + } else { + Some(file) + } } } -/// This function walks the given dir and collects all pdf files into a `HashMap` -/// of the format `[String, Vec<PathBuf>]`, where `String` represents the basename -/// of the file and the `Vec<PathBuf>` holds all filepaths ending with this basename. +/// This function walks the given dir and collects all files matching one of the +/// passed extensions into a `HashMap` of the format `[String, Vec<PathBuf>]`, +/// where `String` represents the basename of the file and the `Vec<PathBuf>` holds +/// all filepaths ending with this basename. /// /// In most cases the latter is only a single path, but there might be some concepts /// with subdirs were some entries have multiple files associated with them. -pub fn collect_pdf_file_paths(pdf_dir: &PathBuf) -> Option<HashMap<String, Vec<PathBuf>>> { +/// +/// Passing [`None`] as argument for extensions will result in collecting all files +/// from the given directory and its subdirectories! +pub fn collect_file_paths( + file_dir: &PathBuf, + extensions: &Option<Vec<&str>>, +) -> Option<HashMap<String, Vec<PathBuf>>> { let mut files: HashMap<String, Vec<PathBuf>> = HashMap::new(); // Expand tilde to /home/user - let pdf_dir = if pdf_dir.starts_with("~") { - &app::expand_home(&pdf_dir) + let file_dir = if file_dir.starts_with("~") { + &app::expand_home(&file_dir) } else { - pdf_dir + file_dir }; // Walk the passed dir and collect all pdf files into hashmap - if pdf_dir.is_dir() { - for file in WalkDir::new(pdf_dir) { + if file_dir.is_dir() { + for file in WalkDir::new(file_dir) { let f = file.unwrap().into_path(); if f.is_file() && f.extension().is_some() - && f.extension().unwrap_or_default().to_ascii_lowercase() == "pdf" + && extensions.as_ref().is_some_and(|v| { + v.contains( + &f.extension() + .unwrap_or_default() + .to_ascii_lowercase() + .to_str() + .unwrap_or_default(), + ) + }) { let filename = f .file_name() @@ -500,6 +562,19 @@ pub fn collect_pdf_file_paths(pdf_dir: &PathBuf) -> Option<HashMap<String, Vec<P } else { files.insert(filename, vec![f]); } + } else if f.is_file() && extensions.is_none() { + let filename = f + .file_name() + .unwrap() + .to_ascii_lowercase() + .into_string() + .unwrap(); + + if let Some(paths) = files.get_mut(&filename) { + paths.push(f); + } else { + files.insert(filename, vec![f]); + } } } } @@ -510,3 +585,52 @@ pub fn collect_pdf_file_paths(pdf_dir: &PathBuf) -> Option<HashMap<String, Vec<P Some(files) } } + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, ffi::OsString, path::PathBuf}; + + use super::BibiSetup; + + #[test] + fn check_file_matching() { + let mut files: HashMap<String, Vec<PathBuf>> = HashMap::new(); + files.insert( + "citekey.md".to_string(), + vec![ + PathBuf::from("/one/note/citekey.md"), + PathBuf::from("/one/other/citekey.md"), + ], + ); + files.insert( + "citekey.pdf".to_string(), + vec![ + PathBuf::from("/one/note/citekey.pdf"), + PathBuf::from("/one/other/citekey.pdf"), + ], + ); + files.insert( + "citekey2.pdf".to_string(), + vec![ + PathBuf::from("/one/note/citekey2.pdf"), + PathBuf::from("/one/other/citekey2.pdf"), + ], + ); + + let matches = + BibiSetup::merge_filepath_or_none_two("citekey", &mut Some(files), vec!["md", "pdf"]); + + assert_eq!( + matches.clone().unwrap().iter().next().unwrap().to_owned(), + OsString::from("/one/note/citekey.md") + ); + assert_eq!( + matches.clone().unwrap().last().unwrap().to_owned(), + OsString::from("/one/other/citekey.pdf") + ); + assert!(!matches + .clone() + .unwrap() + .contains(&OsString::from("/one/other/citekey2.pdf"))); + } +} diff --git a/src/bibiman/entries.rs b/src/bibiman/entries.rs index 88a1583..9b536fd 100644 --- a/src/bibiman/entries.rs +++ b/src/bibiman/entries.rs @@ -47,8 +47,9 @@ impl EntryTable { // entry_table let entry_table_state = TableState::default() .with_selected(0) - .with_selected_column(0) - .with_selected_cell(Some((0, 0))); + .with_selected_column(1) + // other two values above are ignored, if selected cell isn't fitting + .with_selected_cell(Some((0, 1))); let entry_scroll_state = ScrollbarState::new(entry_table_items.len()); let entry_info_scroll_state = ScrollbarState::default(); Self { @@ -159,6 +160,7 @@ mod tests { filepath: None, file_field: false, subtitle: None, + notes: None, }; let entry_vec = BibiData::ref_vec(&mut entry); @@ -177,6 +179,7 @@ mod tests { filepath: None, file_field: false, subtitle: None, + notes: None, }; let entry_vec_editors = BibiData::ref_vec(&mut entry_editors); diff --git a/src/bibiman/search.rs b/src/bibiman/search.rs index f391aed..0e32f63 100644 --- a/src/bibiman/search.rs +++ b/src/bibiman/search.rs @@ -137,6 +137,7 @@ mod tests { filepath: Some(vec![OsString::from("/home/file/path.pdf")]), file_field: true, subtitle: None, + notes: None, }; let joined_vec = BibiSearch::convert_to_string(&bibvec); diff --git a/src/cliargs.rs b/src/cliargs.rs index 114b15a..082ecda 100644 --- a/src/cliargs.rs +++ b/src/cliargs.rs @@ -16,9 +16,11 @@ ///// use color_eyre::eyre::Result; -use color_eyre::owo_colors::OwoColorize; use dirs::{config_dir, home_dir}; use lexopt::prelude::*; +use owo_colors::colors::css::LightGreen; +use owo_colors::colors::*; +use owo_colors::OwoColorize; use std::env; use std::path::PathBuf; use walkdir::WalkDir; @@ -112,49 +114,82 @@ pub fn parse_files(args: Vec<PathBuf>) -> Vec<PathBuf> { } pub fn help_func() -> String { - let help = format!( - "\ -{} {} - -USAGE: - bibiman [FLAGS] [files/dirs] - -POSITIONAL ARGS: - <file> Path to .bib file - <dir> Path to directory containing .bib files - - Both can be passed multiple times - -FLAGS: - -h, --help Show this help and exit - -v, --version Show the version and exit - -c, --config-file=<value> Path to config file used for current session. - Takes precedence over standard config file. - --light-terminal Enable color mode for light terminal background - --pdf-path=<value> Use PDF files named by citekey at the given path and its - subdirs as value for the `file` field of the entry matching - the citekey for the current session. - Does not overwrite or change the original file. - (might not work with citekeys containing special chars)", - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION"), - ); + let help = vec![ + format!( + "{} {}\n", + env!("CARGO_PKG_NAME").fg::<Green>().bold(), + env!("CARGO_PKG_VERSION").fg::<LightGreen>(), + ), + format!( + "{}:\n\t{} [Flags] [files/dirs]\n", + "USAGE".bold(), + "bibiman".bold() + ), + format!( + "{}:\n\t{}\t\tPath to {} file", + "POSITIONAL ARGUMENTS".bold(), + "<file>".fg::<BrightMagenta>().bold(), + ".bib".fg::<BrightBlack>().bold() + ), + format!( + "\t{}\tPath to directory containing {} files", + "<directory>".fg::<BrightBlue>().bold(), + ".bib".fg::<BrightBlack>().bold() + ), + format!("\n\t{}", "Both can be passed multiple times".italic()), + format!("\n{}:", "FLAGS".bold()), + format!("\t{}", "-h, --help".bold().fg::<BrightCyan>()), + format!("\t\t{}", "Show this help and exit"), + format!("\t{}", "-v, --version".bold().fg::<BrightCyan>()), + format!("\t\t{}", "Show the version and exit"), + format!("\t{}", "--light-terminal".bold().fg::<BrightCyan>()), + format!( + "\t\t{}", + "Enable default colors for light terminal background" + ), + format!( + "\t{}{}", + "-c, --config-file=".bold().fg::<BrightCyan>(), + "<value>".bold().italic().fg::<BrightCyan>() + ), + format!("\t\t{}", "Path to config file used for current session."), + format!("\t\t{}", "Takes precedence over standard config file."), + format!( + "\t{}{}", + "--pdf-path=".bold().fg::<BrightCyan>(), + "<value>".bold().italic().fg::<BrightCyan>() + ), + format!("\t\t{}", "Path to directory containing PDF files."), + format!( + "\t\t{}", + "If the pdf files basename matches an entrys citekey," + ), + format!( + "\t\t{}", + "its attached as connected PDF file for the current session." + ), + format!("\t\t{}", "Does not edit the bibfile itself!"), + ]; + let help = help.join("\n"); help } pub fn version_func() -> String { - let version = format!( - "\ -{} {} -{} -{} - -Target Triple: {}", - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION"), - env!("CARGO_PKG_AUTHORS"), - env!("CARGO_PKG_LICENSE"), - env!("TARGET") - ); + let version: Vec<String> = vec![ + format!( + "{} {}", + env!("CARGO_PKG_NAME").fg::<Green>().bold(), + env!("CARGO_PKG_VERSION").fg::<LightGreen>() + ), + format!("{}", env!("CARGO_PKG_AUTHORS").bold()), + format!("{}", env!("CARGO_PKG_LICENSE")), + format!("\n"), + format!( + "{} {}", + "Target Triple:".bold(), + env!("TARGET").fg::<BrightRed>() + ), + ]; + let version = version.join("\n"); version } diff --git a/src/config.rs b/src/config.rs index f5f2dd0..1f6c619 100644 --- a/src/config.rs +++ b/src/config.rs @@ -56,6 +56,17 @@ const DEFAULT_CONFIG: &str = r##" ## Use absolute paths (~ for HOME works). Otherwise, loading might not work. # pdf_path = "/path/to/pdf/folder" +## Path to folder (with subfolders) containing note files with the basename of +## the format "citekey.extension". Other basenames are not accepted. The possible +## extensions can be set through the "note_extensions" array. +# note_path = "/path/to/notes/folder" +# note_extensions = [ "md", "txt", "org" ] + +## Symbols/chars to show if not has specific attachement +# note_symbol = "N" +# file_symbol = "F" +# link_symbol = "L" + # [colors] ## Default values for dark-themed terminal ## Possible values are: @@ -70,8 +81,15 @@ const DEFAULT_CONFIG: &str = r##" # confirm_color = "47" # warn_color = "124" # bar_bg_color = "234" +# popup_fg_color = "43" # popup_bg_color = "234" # selected_row_bg_color = "237" +# note_color = "123" +# file_color = "209" +# link_color = "39" +# author_color = "38" +# title_color = "37" +# year_color = "135" "##; /// Main struct of the config file. Contains substructs/headings in toml @@ -90,6 +108,11 @@ pub struct General { pub url_opener: String, pub file_prefix: Option<PathBuf>, pub pdf_path: Option<PathBuf>, + pub note_path: Option<PathBuf>, + pub note_extensions: Option<Vec<String>>, + pub note_symbol: String, + pub file_symbol: String, + pub link_symbol: String, } /// Substruct [colors] in config.toml @@ -103,8 +126,15 @@ pub struct Colors { pub confirm_color: Color, pub warn_color: Color, pub bar_bg_color: Color, + pub popup_fg_color: Color, pub popup_bg_color: Color, pub selected_row_bg_color: Color, + pub note_color: Color, + pub file_color: Color, + pub link_color: Color, + pub author_color: Color, + pub title_color: Color, + pub year_color: Color, } impl Default for BibiConfig { @@ -117,6 +147,11 @@ impl Default for BibiConfig { url_opener: select_opener(), file_prefix: None, pdf_path: None, + note_path: None, + note_extensions: None, + note_symbol: String::from("N"), + file_symbol: String::from("F"), + link_symbol: String::from("L"), }, colors: Self::dark_colors(), } @@ -133,6 +168,11 @@ impl BibiConfig { url_opener: select_opener(), file_prefix: None, pdf_path: None, + note_path: None, + note_extensions: None, + note_symbol: String::from("N"), + file_symbol: String::from("F"), + link_symbol: String::from("L"), }, colors: if args.light_theme { Self::light_colors() @@ -173,8 +213,15 @@ impl BibiConfig { confirm_color: Color::Indexed(47), warn_color: Color::Indexed(124), bar_bg_color: Color::Indexed(235), + popup_fg_color: Color::Indexed(43), popup_bg_color: Color::Indexed(234), selected_row_bg_color: Color::Indexed(237), + note_color: Color::Indexed(123), + file_color: Color::Indexed(209), + link_color: Color::Indexed(39), + author_color: Color::Indexed(38), + title_color: Color::Indexed(37), + year_color: Color::Indexed(135), } } @@ -187,10 +234,17 @@ impl BibiConfig { keyword_color: Color::Indexed(58), info_color: Color::Indexed(57), bar_bg_color: Color::Indexed(144), + popup_fg_color: Color::Indexed(43), popup_bg_color: Color::Indexed(187), confirm_color: Color::Indexed(22), warn_color: Color::Indexed(124), selected_row_bg_color: Color::Indexed(107), + note_color: Color::Indexed(123), + file_color: Color::Indexed(209), + link_color: Color::Indexed(27), + author_color: Color::Indexed(38), + title_color: Color::Indexed(37), + year_color: Color::Indexed(135), } } diff --git a/src/tui/commands.rs b/src/tui/commands.rs index 08ee677..89fcf44 100644 --- a/src/tui/commands.rs +++ b/src/tui/commands.rs @@ -73,6 +73,8 @@ pub enum CmdAction { ShowHelp, // Add new entry AddEntry, + // Create note + CreateNote, // Do nothing. Nothing, } @@ -149,6 +151,8 @@ impl From<KeyEvent> for CmdAction { // Open linked ressource KeyCode::Char('o') => Self::Open, // KeyCode::Char('u') => Self::Open(OpenRessource::WebLink), + // Create note file + KeyCode::Char('n') => Self::CreateNote, // Edit currently selected entry KeyCode::Char('e') => Self::EditFile, // Yank selected item/value diff --git a/src/tui/popup.rs b/src/tui/popup.rs index 93b01c3..46e4792 100644 --- a/src/tui/popup.rs +++ b/src/tui/popup.rs @@ -38,6 +38,19 @@ pub enum PopupKind { AddEntry, /// select an item of the current entry to yank to clipboard YankItem, + /// Create a new note, select extension + CreateNote, +} + +#[derive(Debug)] +pub enum PopupItem { + Bibfile, + Entryfile, + Notefile, + Citekey, + Link, + Default, + None, } #[derive(Debug, Default)] @@ -46,7 +59,7 @@ pub struct PopupArea { pub popup_kind: Option<PopupKind>, pub popup_message: String, pub popup_scroll_pos: u16, - pub popup_list: Vec<(String, String)>, + pub popup_list: Vec<(String, String, PopupItem)>, pub popup_state: ListState, pub popup_sel_item: String, // pub add_entry_input: String, @@ -116,8 +129,8 @@ impl PopupArea { /// Opens a popup with a selectable list /// - /// The list items are passed as argument of the kind `Vec<(String, String)>`. - pub fn popup_selection(&mut self, items: Vec<(String, String)>) { + /// The list items are passed as argument of the kind `Vec<(String, String, PopupItem)>`. + pub fn popup_selection(&mut self, items: Vec<(String, String, PopupItem)>) { self.popup_list = items; // self.popup_kind = Some(PopupKind::SelectRes); self.is_popup = true; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index ebebe4c..69ca058 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -17,7 +17,7 @@ use std::path::PathBuf; -use super::popup::PopupArea; +use super::popup::{PopupArea, PopupItem}; use crate::bibiman::entries::EntryTableColumn; use crate::bibiman::{CurrentArea, FormerArea}; use crate::cliargs::CLIArgs; @@ -273,21 +273,49 @@ pub fn render_popup(app: &mut App, cfg: &BibiConfig, frame: &mut Frame) { frame.render_widget(Clear, popup_area); frame.render_widget(&content, popup_area) } - Some(PopupKind::OpenRes) | Some(PopupKind::AppendToFile) | Some(PopupKind::YankItem) => { - let list_items: Vec<ListItem> = app - .bibiman - .popup_area - .popup_list - .iter() - .map( - |(mes, obj)| { - ListItem::from(Line::from(vec![ - Span::styled(mes, Style::new().bold()), - Span::raw(obj), - ])) - }, // ListItem::from(mes.to_owned() + obj) - ) - .collect(); + Some(PopupKind::OpenRes) + | Some(PopupKind::AppendToFile) + | Some(PopupKind::YankItem) + | Some(PopupKind::CreateNote) => { + let list_items: Vec<ListItem> = if let Some(PopupKind::CreateNote) = + app.bibiman.popup_area.popup_kind + { + app.bibiman + .popup_area + .popup_list + .iter() + .map(|(m, o, _i)| { + ListItem::from(Line::from(vec![Span::raw(m), Span::raw("."), Span::raw(o)])) + .fg(cfg.colors.note_color) + }) + .collect() + } else { + app.bibiman + .popup_area + .popup_list + .iter() + .map( + |(mes, obj, i)| { + let style: Color = match i { + PopupItem::Bibfile => cfg.colors.entry_color, + PopupItem::Citekey => cfg.colors.entry_color, + PopupItem::Entryfile => cfg.colors.file_color, + PopupItem::Notefile => cfg.colors.note_color, + PopupItem::Link => cfg.colors.link_color, + PopupItem::Default => cfg.colors.main_text_color, + PopupItem::None => cfg.colors.main_text_color, + }; + ListItem::from( + Line::from(vec![ + Span::styled(mes, Style::new().bold()), + Span::raw(obj), + ]) + .fg(style), + ) + }, // ListItem::from(mes.to_owned() + obj) + ) + .collect() + }; let title = if let Some(PopupKind::OpenRes) = app.bibiman.popup_area.popup_kind { " Open " @@ -295,21 +323,23 @@ pub fn render_popup(app: &mut App, cfg: &BibiConfig, frame: &mut Frame) { " Select file to append entry " } else if let Some(PopupKind::YankItem) = app.bibiman.popup_area.popup_kind { " Yank to clipboard " + } else if let Some(PopupKind::CreateNote) = app.bibiman.popup_area.popup_kind { + " Create Note with extension " } else { " Select " }; let bottom_info = if let Some(PopupKind::OpenRes) = app.bibiman.popup_area.popup_kind { - " (j,k|↓,↑) ━ (o,l) ━ (ENTER) ━ (ESC) ".bold() + " (j,k|↓,↑) ━ (o,l,n) ━ (ENTER) ━ (ESC) " } else if let Some(PopupKind::YankItem) = app.bibiman.popup_area.popup_kind { - " (j,k|↓,↑) ━ (y) ━ (ENTER) ━ (ESC) ".bold() + " (j,k|↓,↑) ━ (y) ━ (ENTER) ━ (ESC) " } else { - " (j,k|↓,↑) ━ (ENTER) ━ (ESC) ".bold() + " (j,k|↓,↑) ━ (ENTER) ━ (ESC) " }; let block = Block::bordered() .title_top(title.bold()) - .title_bottom(bottom_info) + .title_bottom(bottom_info.bold()) .title_alignment(Alignment::Center) .style( Style::new() @@ -317,7 +347,7 @@ pub fn render_popup(app: &mut App, cfg: &BibiConfig, frame: &mut Frame) { .bg(cfg.colors.popup_bg_color), ) .border_set(symbols::border::THICK) - .border_style(Style::new().fg(cfg.colors.keyword_color)); + .border_style(Style::new().fg(cfg.colors.popup_fg_color)); let list = List::new(list_items).block(block).highlight_style( Style::new() @@ -333,7 +363,7 @@ pub fn render_popup(app: &mut App, cfg: &BibiConfig, frame: &mut Frame) { .popup_area .popup_list .iter() - .max_by(|(mes, obj), (m, o)| { + .max_by(|(mes, obj, _ik), (m, o, _i)| { let x = mes.chars().count() + obj.chars().count(); let y = m.chars().count() + o.chars().count(); x.cmp(&y) @@ -344,15 +374,29 @@ pub fn render_popup(app: &mut App, cfg: &BibiConfig, frame: &mut Frame) { // Now take the max number for the width of the popup // let max_item = list_widths.iter().max().unwrap().to_owned(); - let max_item = - list_widths.0.chars().count() as u16 + list_widths.1.chars().count() as u16; - + let max_item = list_widths.0.clone() + &list_widths.1; + // list_widths.0.chars().count() as u16 + list_widths.1.chars().count() as u16; + + let fitting_width: u16 = { + let lines = vec![title, bottom_info, &max_item]; + let lline = lines + .iter() + .max_by(|a, b| a.chars().count().cmp(&b.chars().count())) + .unwrap(); + // lines.first().unwrap().chars().count() as u16 + lline.chars().count() as u16 + }; // Check if the popup would exceed the terminal frame width - let popup_width = if max_item + 2 > frame.area().width - 2 { + let popup_width = if fitting_width + 2 > frame.area().width - 2 { frame.area().width - 2 } else { - max_item + 2 + fitting_width + 2 }; + // } else if title.chars().count() as u16 > max_item { + // (title.chars().count() + 2) as u16 + // } else { + // max_item + 2 + // }; let popup_heigth = list.len() + 2; let popup_area = popup_area(frame.area(), popup_width, popup_heigth as u16); @@ -580,6 +624,7 @@ pub fn render_entrytable(app: &mut App, cfg: &BibiConfig, frame: &mut Frame, rec .bg(cfg.colors.bar_bg_color); let header = Row::new(vec![ + Cell::from(Line::from("Res.")).bg(cfg.colors.bar_bg_color), Cell::from( Line::from(vec![{ Span::raw("Author") }, { if let Some(EntryTableColumn::Authors) = @@ -698,33 +743,74 @@ pub fn render_entrytable(app: &mut App, cfg: &BibiConfig, frame: &mut Frame, rec .iter_mut() .enumerate() .map(|(_i, data)| { - let item = data.ref_vec(); - item.into_iter() - .map(|content| Cell::from(Text::from(content.to_string()))) - .collect::<Row>() - .style( - // Style::new().fg(color_list( - // args, - // i as i32, - // app.bibiman - // .entry_table - // .entry_table_state - // .selected() - // .unwrap_or(0) as i32, - // args.colors.highlight_text_color, - // 20, - // )), - Style::new().fg(if let CurrentArea::EntryArea = app.bibiman.current_area { - cfg.colors.highlight_text_color - } else { - cfg.colors.main_text_color - }), - ) - .height(1) + let item = data.ref_vec(cfg); + + let mut symbol_vec = vec![]; + + // use default or custom symbols for resources + // if an entry has no, replace it with the correct number + // of whitespace to align the symbols correct + if let Some(f) = &item.symbols[0] { + symbol_vec.push(Span::styled( + f, + Style::new().fg(cfg.colors.file_color).bold(), + )); + } else { + symbol_vec.push(Span::raw( + " ".repeat(cfg.general.file_symbol.chars().count()), + )); + } + if let Some(l) = &item.symbols[1] { + symbol_vec.push(Span::styled( + l, + Style::new().fg(cfg.colors.link_color).bold(), + )); + } else { + symbol_vec.push(Span::raw( + " ".repeat(cfg.general.link_symbol.chars().count()), + )); + } + if let Some(n) = &item.symbols[2] { + symbol_vec.push(Span::styled( + n, + Style::new().fg(cfg.colors.note_color).bold(), + )) + } else { + symbol_vec.push(Span::raw( + " ".repeat(cfg.general.note_symbol.chars().count()), + )); + } + + let row = Row::new(vec![ + Cell::from(Line::from(symbol_vec)), + Cell::from(Line::from(item.authors)), + Cell::from(Line::from(item.title)), + Cell::from(Line::from(item.year)), + Cell::from(Line::from(item.pubtype)), + ]); + + // let row = item + // .into_iter() + // .map(|content| Cell::from(Text::from(content.to_string()))) + // .collect::<Row>(); + + row.style( + Style::new().fg(if let CurrentArea::EntryArea = app.bibiman.current_area { + cfg.colors.highlight_text_color + } else { + cfg.colors.main_text_color + }), + ) + .height(1) }); let entry_table = Table::new( rows, [ + Constraint::Length( + (cfg.general.file_symbol.chars().count() + + cfg.general.link_symbol.chars().count() + + cfg.general.note_symbol.chars().count()) as u16, + ), Constraint::Percentage(20), Constraint::Fill(1), Constraint::Length( @@ -793,7 +879,10 @@ pub fn render_selected_item(app: &mut App, cfg: &BibiConfig, frame: &mut Frame, lines.push(Line::from(vec![ Span::styled("Authors: ", style_value), // Span::styled(cur_entry.authors.clone(), Style::new().green()), - Span::styled(cur_entry.authors(), Style::new().fg(cfg.colors.info_color)), + Span::styled( + cur_entry.authors(), + Style::new().fg(cfg.colors.author_color), + ), ])); if cur_entry.subtitle.is_some() { lines.push(Line::from(vec![ @@ -801,19 +890,19 @@ pub fn render_selected_item(app: &mut App, cfg: &BibiConfig, frame: &mut Frame, Span::styled( cur_entry.title(), Style::new() - .fg(cfg.colors.entry_color) + .fg(cfg.colors.title_color) .add_modifier(Modifier::ITALIC), ), Span::styled( ": ", Style::new() - .fg(cfg.colors.entry_color) + .fg(cfg.colors.title_color) .add_modifier(Modifier::ITALIC), ), Span::styled( cur_entry.subtitle(), Style::new() - .fg(cfg.colors.entry_color) + .fg(cfg.colors.title_color) .add_modifier(Modifier::ITALIC), ), ])); @@ -823,14 +912,14 @@ pub fn render_selected_item(app: &mut App, cfg: &BibiConfig, frame: &mut Frame, Span::styled( cur_entry.title(), Style::new() - .fg(cfg.colors.entry_color) + .fg(cfg.colors.title_color) .add_modifier(Modifier::ITALIC), ), ])); } lines.push(Line::from(vec![ Span::styled("Year: ", style_value), - Span::styled(cur_entry.year(), Style::new().fg(cfg.colors.keyword_color)), + Span::styled(cur_entry.year(), Style::new().fg(cfg.colors.year_color)), ])); // Render keywords in info box in Markdown code style if !cur_entry.keywords.is_empty() { @@ -873,7 +962,7 @@ pub fn render_selected_item(app: &mut App, cfg: &BibiConfig, frame: &mut Frame, Span::styled("DOI/URL: ", style_value), Span::styled( cur_entry.doi_url(), - Style::new().fg(cfg.colors.main_text_color).underlined(), + Style::new().fg(cfg.colors.link_color).underlined(), ), ])); } @@ -882,19 +971,19 @@ pub fn render_selected_item(app: &mut App, cfg: &BibiConfig, frame: &mut Frame, Span::styled("File: ", style_value), Span::styled( p.iter().map(|f| f.to_str().unwrap()).join("; "), - Style::new().fg(cfg.colors.main_text_color), + Style::new().fg(cfg.colors.file_color), + ), + ])); + } + if let Some(n) = &cur_entry.notes { + lines.push(Line::from(vec![ + Span::styled("Note: ", style_value), + Span::styled( + n.iter().map(|n| n.to_str().unwrap()).join("; "), + Style::new().fg(cfg.colors.note_color), ), ])); } - // if cur_entry.filepath.is_some() { - // lines.push(Line::from(vec![ - // Span::styled("File: ", style_value), - // Span::styled( - // cur_entry.filepath().to_string_lossy(), - // Style::new().fg(cfg.colors.main_text_color), - // ), - // ])); - // } lines.push(Line::from("")); lines.push(Line::from(vec![Span::styled( cur_entry.abstract_text.clone(), @@ -913,7 +1002,12 @@ pub fn render_selected_item(app: &mut App, cfg: &BibiConfig, frame: &mut Frame, // We show the list item's info under the list in this paragraph let block = Block::bordered() - .title(Line::raw(" Entry Information ").centered().bold()) + .title( + Line::raw(" Entry Information ") + .centered() + .bold() + .fg(cfg.colors.info_color), + ) .border_set(symbols::border::PLAIN) .border_style(Style::new().fg(cfg.colors.main_text_color)) .padding(Padding::horizontal(1)); diff --git a/tests/biblatex-test.bib b/tests/biblatex-test.bib index 4071dcb..692375e 100644 --- a/tests/biblatex-test.bib +++ b/tests/biblatex-test.bib @@ -9,7 +9,7 @@ model of particle physics.}, } -@collection{matuz:doody, +@collection{matuz_doody, title = {Contemporary Literary Criticism}, year = {1990}, location = {Detroit}, @@ -54,7 +54,7 @@ field}, } -@book{aristotle:anima, +@book{aristotle_anima, title = {De Anima}, author = {Aristotle}, location = {Cambridge}, @@ -68,7 +68,7 @@ editor}}, } -@book{aristotle:physics, +@book{aristotle_physics, title = {Physics}, shorttitle = {Physics}, author = {Aristotle}, @@ -84,7 +84,7 @@ annotation = {A \texttt{book} entry with a \texttt{translator} field}, } -@book{aristotle:poetics, +@book{aristotle_poetics, title = {Poetics}, shorttitle = {Poetics}, author = {Aristotle}, @@ -100,7 +100,7 @@ editor} as well as a \texttt{series} field}, } -@mvbook{aristotle:rhetoric, +@mvbook{aristotle_rhetoric, title = {The Rhetoric of {Aristotle} with a commentary by the late {Edward Meredith Cope}}, shorttitle = {Rhetoric}, @@ -441,7 +441,7 @@ @string{pup = {Princeton University Press}} -@incollection{westfahl:space, +@incollection{westfahl_space, title = {The True Frontier}, author = {Westfahl, Gary}, pages = {55--65}, diff --git a/tests/note-files/aristotle_physics.md b/tests/note-files/aristotle_physics.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/note-files/aristotle_physics.md diff --git a/tests/note-files/aristotle_poetics.txt b/tests/note-files/aristotle_poetics.txt new file mode 100644 index 0000000..a156c76 --- /dev/null +++ b/tests/note-files/aristotle_poetics.txt @@ -0,0 +1 @@ +Here some very boring information regarding Aristotle diff --git a/tests/note-files/aristotle_rhetoric.md b/tests/note-files/aristotle_rhetoric.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/note-files/aristotle_rhetoric.md diff --git a/tests/note-files/augustine.md b/tests/note-files/augustine.md new file mode 100644 index 0000000..7af8fa6 --- /dev/null +++ b/tests/note-files/augustine.md @@ -0,0 +1,8 @@ +--- +title: Notes about Augustine +author: Great scholar +--- + +# Augustine + +A crazy dude writing some interesting stuff diff --git a/tests/note-files/bertram.txt b/tests/note-files/bertram.txt new file mode 100644 index 0000000..54d31e5 --- /dev/null +++ b/tests/note-files/bertram.txt @@ -0,0 +1 @@ +A simple text file with notes about this Betram dude diff --git a/tests/note-files/doody.md b/tests/note-files/doody.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/note-files/doody.md diff --git a/tests/pdf-files/annotated-pdfs/ARIStotle:rheTORIC.PDF b/tests/pdf-files/annotated-pdfs/ARIStotle_rheTORIC.PDF Binary files differindex 6aaba88..6aaba88 100644 --- a/tests/pdf-files/annotated-pdfs/ARIStotle:rheTORIC.PDF +++ b/tests/pdf-files/annotated-pdfs/ARIStotle_rheTORIC.PDF diff --git a/tests/pdf-files/aristotle:physics.pdf b/tests/pdf-files/aristotle_physics.pdf Binary files differindex 6aaba88..6aaba88 100644 --- a/tests/pdf-files/aristotle:physics.pdf +++ b/tests/pdf-files/aristotle_physics.pdf diff --git a/tests/pdf-files/aristotle:rhetoric.pdf b/tests/pdf-files/aristotle_rhetoric.pdf Binary files differindex 6aaba88..6aaba88 100644 --- a/tests/pdf-files/aristotle:rhetoric.pdf +++ b/tests/pdf-files/aristotle_rhetoric.pdf diff --git a/tests/pdf-files/aristotle_rhetoric.txt b/tests/pdf-files/aristotle_rhetoric.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/pdf-files/aristotle_rhetoric.txt diff --git a/tests/test-config.toml b/tests/test-config.toml index 6d05b64..1d29043 100644 --- a/tests/test-config.toml +++ b/tests/test-config.toml @@ -4,7 +4,7 @@ bibfiles = [ "tests/biblatex-test.bib" ] ## Default editor to use when editing files. Arguments are possible -# editor = "vim" # with args: "vim -y" +editor = "vim" # with args: "vim -y" ## Default app to open PDFs/Epubs # pdf_opener = "xdg-open" @@ -19,7 +19,18 @@ file_prefix = "/some/path/prefix" ## Path to folder (with subfolders) containing PDF files with the basename ## of the format "citekey.pdf". Other PDF basenames are not accepted. ## Use absolute paths (~ for HOME works). Otherwise, loading might not work. -pdf_path = "~/Documents/coding/projects/bibiman/tests/pdf-files" +pdf_path = "tests/pdf-files" + +## Path to folder (with subfolders) containing note files with the basename of +## the format "citekey.extension". Other basenames are not accepted. The possible +## extensions can be set through the "note_extensions" array. +note_path = "tests/note-files" +note_extensions = [ "md", "txt" ] + +## Symbols/chars to show if not has specific attachement +file_symbol = " " +link_symbol = " " +note_symbol = "" # [colors] ## Default values for dark-themed terminal @@ -37,3 +48,9 @@ pdf_path = "~/Documents/coding/projects/bibiman/tests/pdf-files" # bar_bg_color = "234" # popup_bg_color = "234" # selected_row_bg_color = "237" +# note_color = "123" +# file_color = "209" +# link_color = "27" +# author_color = "38" +# title_color = "37" +# year_color = "135" |
