aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md84
-rw-r--r--src/app.rs110
-rw-r--r--src/bibiman.rs233
-rw-r--r--src/bibiman/bibisetup.rs205
-rw-r--r--src/bibiman/entries.rs2
-rw-r--r--src/bibiman/search.rs5
-rw-r--r--src/cliargs.rs19
-rw-r--r--src/config.rs15
-rw-r--r--src/main.rs2
-rw-r--r--src/tui/popup.rs27
-rw-r--r--src/tui/ui.rs51
-rw-r--r--tests/pdf-files/annotated-pdfs/ARIStotle:rheTORIC.PDFbin0 -> 25294 bytes
-rw-r--r--tests/pdf-files/aristotle:physics.pdfbin0 -> 25294 bytes
-rw-r--r--tests/pdf-files/aristotle:rhetoric.pdfbin0 -> 25294 bytes
-rw-r--r--tests/test-config.toml7
15 files changed, 583 insertions, 177 deletions
diff --git a/README.md b/README.md
index e39d8dd..4c176af 100644
--- a/README.md
+++ b/README.md
@@ -104,10 +104,15 @@ Or through `home-manager` config file:
### Void Linux<a name="void-linux"></a>
-I maintain a [PR](https://github.com/void-linux/void-packages/pull/53803) in the
-official Void package repos and hope it gets merged soon. Until that happens,
-you can easily pull that PR into your local clone of Void packages and build it
-with `xbps-src`.
+You can install `bibiman` through `xbps`-package manager:
+
+```bash
+# Through xbps directly
+sudo xpbs-install bibiman
+
+# Or using xi from xtools package
+xi bibiman
+```
## Usage<a name="usage"></a>
@@ -122,12 +127,16 @@ POSITIONAL ARGS:
<dir> Path to directory containing .bib files
FLAGS:
- -h, --help Show this help and exit
- -v, --version Show the version and exit
- -c, --config-file= Path to config file for current session needed as argument.
- Takes precedence over standard config file
- --light-terminal Enable color mode for light terminal background
-
+ -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-dir=<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)"
```
As seen, you can pass a single file, multiple files, the path of a directory
@@ -200,29 +209,50 @@ 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"
```
-If no file or dir is set as `bibfiles` value, you *have to* add a path via CLI
-interface. If the `bibfiles` value is set *and* a further path (or multiple) is
-provided through the CLI call, the entries of all those files will be opened in
-the started `bibiman` session.
+`bibfiles`
-The file prefix offers the possibility to keep file paths in your `.bib` file
-short: E.g. a combination of those values in config and bibfile:
+: If no file or dir is set as `bibfiles` value, you *have to* add a path via CLI
+ interface. If the `bibfiles` value is set *and* a further path (or multiple)
+ is provided through the CLI call, the entries of all those files will be
+ opened in the started `bibiman` session.
-```toml
-# bibiman.toml
-file_prefix = "~/Documents/literature"
-```
+`file_prefix`
-```bibtex
-# bibfile.bib
-file = {aristotle.pdf}
-```
+: The `file_prefix` offers the possibility to keep file paths in your `.bib`
+ file short: E.g. a combination of those values in config and bibfile:
+
+ ```toml
+ # bibiman.toml
+ file_prefix = "~/Documents/literature"
+ ```
+
+ ```latex
+ % bibfile.bib
+ file = {aristotle.pdf}
+ ```
+
+ Will result in opening the file `~/Documents/literature/aristotle.pdf` when
+ trying to open the PDF from inside `bibiman`. The `.bib` file itself will not
+ be edited!
+
+ The prefix will only be added to filepaths which are explicitly set in the
+ `.bib` file itself using the `file` field. It will *not* "destroy" file paths
+ created through the `pdf_path` variable. Thus, it is safe to mix both
+ approaches if wanted!
+
+`pdf_path`
-Will result in opening the file `~/Documents/literature/aristotle.pdf` when
-trying to open the PDF from inside `bibiman`. The `.bib` file itself will not be
-edited!
+: 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.
### Color Configuration<a name="color-configuration"></a>
diff --git a/src/app.rs b/src/app.rs
index 88f37b0..f7e7891 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -15,7 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/////
-use crate::bibiman::{CurrentArea, FormerArea};
+use crate::bibiman::CurrentArea;
use crate::config::BibiConfig;
use color_eyre::eyre::{Context, Ok, Result};
// use super::Event;
@@ -142,13 +142,14 @@ impl App {
|| doi.starts_with("http://doi.org")
|| doi.starts_with("http://dx.doi.org")
{
- self.bibiman.handle_new_entry_submission(doi);
+ self.bibiman.handle_new_entry_submission(doi)?;
} else {
- self.bibiman.popup_area.popup_message(
- "No valid DOI pattern: ",
- doi,
- false,
- );
+ self.bibiman.open_popup(
+ PopupKind::MessageError,
+ Some("No valid DOI pattern: "),
+ Some(doi),
+ None,
+ )?;
}
}
_ => {}
@@ -277,7 +278,7 @@ impl App {
self.bibiman.open_connected_res(cfg)?;
} else if let Some(PopupKind::AppendToFile) = self.bibiman.popup_area.popup_kind
{
- self.bibiman.append_entry_to_file()?
+ self.bibiman.append_entry_to_file(cfg)?
} else if let Some(PopupKind::YankItem) = self.bibiman.popup_area.popup_kind {
self.bibiman.yank_entry_field()?
}
@@ -300,18 +301,27 @@ impl App {
.selected()
.unwrap();
let entry = self.bibiman.entry_table.entry_table_items[idx].clone();
- let mut items = vec!["Citekey".to_owned()];
+ let mut items = vec![("Citekey: ".to_string(), entry.citekey.clone())];
if entry.doi_url.is_some() {
- items.push("Weblink".to_owned())
+ items.push(("Weblink: ".into(), entry.doi_url.unwrap().clone()))
}
if entry.filepath.is_some() {
- items.push("Filepath".to_owned())
+ entry.filepath.unwrap().iter().for_each(|p| {
+ items.push(("Filepath: ".into(), p.clone().into_string().unwrap()))
+ });
+ // items.push((
+ // "Filepath: ".into(),
+ // entry.filepath.unwrap()[0].clone().into_string().unwrap(),
+ // ))
}
- self.bibiman.popup_area.popup_kind = Some(PopupKind::YankItem);
- self.bibiman.popup_area.popup_selection(items);
- self.bibiman.former_area = Some(FormerArea::EntryArea);
- self.bibiman.current_area = CurrentArea::PopupArea;
- self.bibiman.popup_area.popup_state.select(Some(0));
+
+ // self.bibiman.popup_area.popup_kind = Some(PopupKind::YankItem);
+ // self.bibiman.popup_area.popup_selection(items);
+ // self.bibiman.former_area = Some(FormerArea::EntryArea);
+ // self.bibiman.current_area = CurrentArea::PopupArea;
+ // self.bibiman.popup_area.popup_state.select(Some(0));
+ self.bibiman
+ .open_popup(PopupKind::YankItem, None, None, Some(items))?;
}
}
CmdAction::EditFile => {
@@ -328,25 +338,44 @@ 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![];
if entry.doi_url.is_some() {
- items.push("Weblink (DOI/URL)".to_owned())
+ items.push((
+ "Weblink (DOI/URL): ".into(),
+ entry.doi_url.unwrap().clone(),
+ ))
}
if entry.filepath.is_some() {
- items.push("File (PDF/EPUB)".to_owned())
+ entry.filepath.unwrap().iter().for_each(|p| {
+ items.push((
+ "File (PDF/EPUB): ".into(),
+ // p.clone().into_string().unwrap(),
+ if entry.file_field && cfg.general.file_prefix.is_some() {
+ cfg.general
+ .file_prefix
+ .clone()
+ .unwrap()
+ .join(p)
+ .into_os_string()
+ .into_string()
+ .unwrap()
+ } else {
+ p.clone().into_string().unwrap()
+ },
+ ))
+ });
}
- self.bibiman.popup_area.popup_kind = Some(PopupKind::OpenRes);
- self.bibiman.popup_area.popup_selection(items);
- self.bibiman.former_area = Some(FormerArea::EntryArea);
- self.bibiman.current_area = CurrentArea::PopupArea;
- self.bibiman.popup_area.popup_state.select(Some(0))
+
+ self.bibiman
+ .open_popup(PopupKind::OpenRes, None, None, Some(items))?;
} else {
- self.bibiman.popup_area.popup_message(
- "Selected entry has no connected ressources: ",
- &entry.citekey,
- false,
- )
+ self.bibiman.open_popup(
+ PopupKind::MessageError,
+ Some("Selected entry has no connected resources: "),
+ Some(&entry.citekey),
+ None,
+ )?;
}
}
}
@@ -357,7 +386,7 @@ impl App {
}
}
CmdAction::ShowHelp => {
- self.bibiman.show_help();
+ self.bibiman.open_popup(PopupKind::Help, None, None, None)?;
}
CmdAction::Exit => {
self.quit();
@@ -372,13 +401,14 @@ 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 = 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();
+ // 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???)
@@ -430,6 +460,14 @@ pub fn expand_home(path: &PathBuf) -> PathBuf {
}
}
+/// Convert `Vec<(&str, &str)` to `Vec<(String, String)`
+pub fn convert_to_owned_vec(mut items: Vec<(&str, &str)>) -> Vec<(String, String)> {
+ items
+ .iter_mut()
+ .map(|(msg, obj)| (msg.to_string(), obj.to_string()))
+ .collect()
+}
+
#[cfg(test)]
mod test {
use super::*;
diff --git a/src/bibiman.rs b/src/bibiman.rs
index c90905f..ea9dbf5 100644
--- a/src/bibiman.rs
+++ b/src/bibiman.rs
@@ -15,6 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/////
+use crate::app::expand_home;
use crate::bibiman::entries::EntryTableColumn;
use crate::bibiman::{bibisetup::*, search::BibiSearch};
use crate::cliargs::CLIArgs;
@@ -24,10 +25,11 @@ use crate::tui::Tui;
use crate::{app, cliargs};
use crate::{bibiman::entries::EntryTable, bibiman::keywords::TagList};
use arboard::Clipboard;
-use color_eyre::eyre::Result;
+use color_eyre::eyre::{Error, Result};
use editor_command::EditorBuilder;
use ratatui::widgets::ScrollbarState;
use regex::Regex;
+use std::ffi::OsString;
use std::fs::{self, read_to_string};
use std::fs::{File, OpenOptions};
use std::io::Write;
@@ -89,7 +91,7 @@ impl Bibiman {
main_bibfiles.append(cfg.general.bibfiles.as_mut().unwrap())
};
let main_bibfiles = cliargs::parse_files(main_bibfiles);
- let main_biblio = BibiSetup::new(&main_bibfiles);
+ let main_biblio = BibiSetup::new(&main_bibfiles, cfg);
let tag_list = TagList::new(main_biblio.keyword_list.clone());
let search_struct = BibiSearch::default();
let entry_table = EntryTable::new(main_biblio.entry_list.clone());
@@ -118,6 +120,7 @@ impl Bibiman {
self.popup_area.popup_kind = Some(PopupKind::Help);
}
+ /// Close all current popups and select former tab of main app
pub fn close_popup(&mut self) {
// Reset all popup fields to default values
self.popup_area = PopupArea::default();
@@ -133,8 +136,112 @@ impl Bibiman {
self.former_area = None;
}
- pub fn update_lists(&mut self) {
- self.main_biblio = BibiSetup::new(&self.main_bibfiles);
+ /// Open a popup
+ ///
+ /// Necessary arguments are:
+ ///
+ /// - `popup_kind`: a valid value of the `PopupKind` `enum`. This determines the
+ /// further behaviour of the popup.
+ /// - `message`: A message shown in the popup. This is needed for the `PopupKind`
+ /// values `MessageConfirm` and `MessageError`. If not needed, set it to `None`.
+ /// - `object`: An object passed as `&str` which might explain the current popup
+ /// action. Its not needed, but very useful. Can be used with the `PopupKind`
+ /// values `MessageConfirm`, `MessageError` and `YankItem`. If not needed, set it
+ /// to `None`.
+ /// - `items`: A vector of items which are needed if a selectable list is rendered.
+ /// The vector consists of tuples including a pair of `String`. The second item of
+ /// the tuple is considered kind of an object which can be used e.g. to open
+ /// the given filepath etc. If not needed, set it to `None`.
+ ///
+ /// The function will panic if a needed argument for the particular `PopupKind`
+ /// is missing
+ pub fn open_popup(
+ &mut self,
+ popup_kind: PopupKind,
+ message: Option<&str>,
+ object: Option<&str>,
+ items: Option<Vec<(String, String)>>,
+ ) -> Result<()> {
+ if let CurrentArea::EntryArea = self.current_area {
+ self.former_area = Some(FormerArea::EntryArea);
+ } else if let CurrentArea::TagArea = self.current_area {
+ self.former_area = Some(FormerArea::TagArea);
+ }
+ self.popup_area.is_popup = true;
+ self.current_area = CurrentArea::PopupArea;
+
+ match popup_kind {
+ PopupKind::Help => {
+ self.popup_area.popup_kind = Some(PopupKind::Help);
+ Ok(())
+ }
+ PopupKind::MessageConfirm => {
+ self.popup_area.popup_kind = Some(PopupKind::MessageConfirm);
+ if object.is_some() && message.is_some() {
+ self.popup_area.popup_message = message.unwrap().to_owned() + object.unwrap();
+ Ok(())
+ } else if object.is_none() && message.is_some() {
+ self.popup_area.popup_message = message.unwrap().to_owned();
+ Ok(())
+ } else {
+ Err(Error::msg("You need to past at least a message via Some(&str) to create a message popup"))
+ }
+ }
+ PopupKind::MessageError => {
+ self.popup_area.popup_kind = Some(PopupKind::MessageError);
+ if object.is_some() && message.is_some() {
+ self.popup_area.popup_message = message.unwrap().to_owned() + object.unwrap();
+ Ok(())
+ } else if object.is_none() && message.is_some() {
+ self.popup_area.popup_message = message.unwrap().to_owned();
+ Ok(())
+ } else {
+ Err(Error::msg("You need to past at least a message via Some(&str) to create a message popup"))
+ }
+ }
+ PopupKind::OpenRes => {
+ if items.is_some() {
+ self.popup_area.popup_kind = Some(PopupKind::OpenRes);
+ 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",
+ ))
+ }
+ }
+ PopupKind::AppendToFile => {
+ if items.is_some() {
+ self.popup_area.popup_kind = Some(PopupKind::AppendToFile);
+ Ok(())
+ } else {
+ Err(Error::msg(
+ "No Vec<(String, String)> passed as argument to generate the items list",
+ ))
+ }
+ }
+ PopupKind::AddEntry => {
+ self.popup_area.popup_kind = Some(PopupKind::AddEntry);
+ Ok(())
+ }
+ PopupKind::YankItem => {
+ if items.is_some() {
+ self.popup_area.popup_kind = Some(PopupKind::YankItem);
+ 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",
+ ))
+ }
+ }
+ }
+ }
+
+ pub fn update_lists(&mut self, cfg: &BibiConfig) {
+ self.main_biblio = BibiSetup::new(&self.main_bibfiles, cfg);
self.tag_list = TagList::new(self.main_biblio.keyword_list.clone());
self.entry_table = EntryTable::new(self.main_biblio.entry_list.clone());
}
@@ -397,7 +504,7 @@ impl Bibiman {
tui.terminal.clear()?;
// Update the database and the lists to show changes
- Self::update_lists(self);
+ Self::update_lists(self, cfg);
// Select entry which was selected before entering editor
self.select_entry_by_citekey(citekey);
@@ -418,7 +525,7 @@ impl Bibiman {
///the new entry via `append_to_file()` function. If not, show error popup
///
///The method needs two arguments: the CLIArgs struct and the `str` containing the DOI
- pub fn handle_new_entry_submission(&mut self, doi_string: &str) {
+ pub fn handle_new_entry_submission(&mut self, doi_string: &str) -> Result<()> {
let doi_string = if doi_string.starts_with("10.") {
"https://doi.org/".to_string() + doi_string
} else {
@@ -442,31 +549,39 @@ impl Bibiman {
self.current_area = CurrentArea::PopupArea;
self.popup_area.popup_state.select(Some(0))
} else {
- self.popup_area
- .popup_message("Can't find DOI: ", &doi_string, false);
+ self.open_popup(
+ PopupKind::MessageError,
+ Some("Can't find DOI: "),
+ Some(&doi_string),
+ None,
+ )?;
+ // self.popup_area
+ // .popup_message("Can't find DOI: ", &doi_string, false);
}
+ Ok(())
}
pub fn append_to_file(&mut self) {
- let mut items = vec!["Create new file".to_owned()];
+ let mut items = vec![("Create new file".to_owned(), "".to_string())];
if self.main_bibfiles.len() > 1 {
for f in self.main_bibfiles.clone() {
- items.push(f.to_str().unwrap().to_owned());
+ items.push(("File: ".into(), f.to_str().unwrap().to_owned()));
}
} else {
- items.push(
+ items.push((
+ "File: ".into(),
self.main_bibfiles
.first()
.unwrap()
.to_str()
.unwrap()
.to_owned(),
- );
+ ));
}
self.popup_area.popup_selection(items);
}
- pub fn append_entry_to_file(&mut self) -> Result<()> {
+ pub fn append_entry_to_file(&mut self, cfg: &BibiConfig) -> Result<()> {
// Index of selected popup field
let popup_idx = self.popup_area.popup_state.selected().unwrap();
@@ -482,7 +597,10 @@ impl Bibiman {
.to_string();
// Check if new file or existing file was choosen
- let mut file = if self.popup_area.popup_list[popup_idx].contains("Create new file") {
+ let mut file = if self.popup_area.popup_list[popup_idx]
+ .0
+ .contains("Create new file")
+ {
let citekey = PathBuf::from(&citekey);
// Get path of current files
let path: PathBuf = if self.main_bibfiles[0].is_file() {
@@ -535,7 +653,7 @@ impl Bibiman {
// Write content to file
file.write_all(self.popup_area.popup_sel_item.as_bytes())?;
// Update the database and the lists to reflect the new content
- self.update_lists();
+ self.update_lists(cfg);
self.close_popup();
// Select newly created entry
@@ -550,73 +668,60 @@ impl Bibiman {
// Index of selected popup field
let popup_idx = self.popup_area.popup_state.selected().unwrap();
+ 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].contains("Weblink") {
+ if self.popup_area.popup_list[popup_idx].0.contains("Weblink") {
let object = self.entry_table.entry_table_items[entry_idx].doi_url();
let url = app::prepare_weblink(object);
app::open_connected_link(cfg, &url)?;
- } else if self.popup_area.popup_list[popup_idx].contains("File") {
- let object = self.entry_table.entry_table_items[entry_idx].filepath();
- app::open_connected_file(cfg, object)?;
+ self.close_popup();
+ } else if self.popup_area.popup_list[popup_idx].0.contains("File") {
+ // 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()));
+ let object: OsString = popup_entry.into();
+ if file.is_file() {
+ app::open_connected_file(cfg, &object)?;
+ self.close_popup();
+ } else {
+ self.open_popup(
+ PopupKind::MessageError,
+ Some("No valid file path: "),
+ Some(object.to_str().unwrap()),
+ None,
+ )?;
+ }
} else {
eprintln!("Unable to find ressource to open");
};
// run command to open file/Url
- self.close_popup();
Ok(())
}
pub fn yank_entry_field(&mut self) -> Result<()> {
- // Index of selected entry
- let entry_idx = self.entry_table.entry_table_state.selected().unwrap();
-
// Index of selected popup field
let popup_idx = self.popup_area.popup_state.selected().unwrap();
+ let popup_entry = self.popup_area.popup_list[popup_idx].1.clone();
- match self.popup_area.popup_list[popup_idx]
+ let kind = self.popup_area.popup_list[popup_idx]
+ .0
.to_lowercase()
- .as_str()
- {
- "citekey" => {
- let citekey = &self.entry_table.entry_table_items[entry_idx].citekey;
- Bibiman::yank_text(citekey);
- self.popup_area.popup_message(
- "Yanked citekey to clipboard: ",
- citekey, // self.bibiman.get_selected_citekey(),
- true,
- );
- }
- "weblink" => {
- let link = &self.entry_table.entry_table_items[entry_idx].doi_url;
- if let Some(l) = link {
- Bibiman::yank_text(l);
- self.popup_area.popup_message(
- "Yanked weblink to clipboard: ",
- l, // self.bibiman.get_selected_link(),
- true,
- );
- }
- }
- "filepath" => {
- let path = self.entry_table.entry_table_items[entry_idx]
- .filepath
- .clone();
- if let Some(p) = path {
- let p = p.as_os_str().to_str();
- if let Some(p) = p {
- Bibiman::yank_text(p);
- self.popup_area.popup_message(
- "Yanked filepath to clipboard: ",
- p, // self.bibiman.get_selected_link(),
- true,
- );
- }
- }
- }
- _ => {}
- };
+ .split(":")
+ .next()
+ .unwrap()
+ .to_owned();
+
+ let msg = format!("Yanked {} to clipboard: ", &kind);
+
+ Bibiman::yank_text(&popup_entry);
+ self.open_popup(
+ PopupKind::MessageConfirm,
+ Some(&msg),
+ Some(&popup_entry),
+ None,
+ )?;
Ok(())
}
diff --git a/src/bibiman/bibisetup.rs b/src/bibiman/bibisetup.rs
index cba1536..bf5baf5 100644
--- a/src/bibiman/bibisetup.rs
+++ b/src/bibiman/bibisetup.rs
@@ -19,10 +19,14 @@ use biblatex::{self, Bibliography};
use biblatex::{ChunksExt, Type};
use color_eyre::owo_colors::OwoColorize;
use itertools::Itertools;
+use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::{fs, path::PathBuf};
+use walkdir::WalkDir;
-use crate::cliargs;
+use crate::app;
+use crate::cliargs::{self};
+use crate::config::BibiConfig;
// Set necessary fields
// TODO: can surely be made more efficient/simpler
@@ -48,7 +52,8 @@ pub struct BibiData {
pub citekey: String,
pub abstract_text: String,
pub doi_url: Option<String>,
- pub filepath: Option<OsString>,
+ pub filepath: Option<Vec<OsString>>,
+ pub file_field: bool,
pub subtitle: Option<String>,
}
@@ -111,8 +116,16 @@ impl BibiData {
self.doi_url.as_ref().unwrap()
}
- pub fn filepath(&self) -> &OsStr {
- self.filepath.as_ref().unwrap()
+ // Gets the path of the associated file for a bib entry. If one is set explicitly
+ // as a field, use that. If not, try to match it to one of the files found in the
+ // pdf_files dir.
+ pub fn filepath(&mut self) -> Vec<&OsStr> {
+ self.filepath
+ .as_mut()
+ .unwrap()
+ .iter_mut()
+ .map(|p| p.as_os_str())
+ .collect_vec()
}
pub fn subtitle(&self) -> &str {
@@ -121,14 +134,14 @@ impl BibiData {
}
impl BibiSetup {
- pub fn new(main_bibfiles: &[PathBuf]) -> Self {
+ pub fn new(main_bibfiles: &[PathBuf], cfg: &BibiConfig) -> Self {
// TODO: Needs check for config file path as soon as config file is impl
Self::check_files(main_bibfiles);
let bibfilestring = Self::bibfiles_to_string(main_bibfiles);
let bibliography = biblatex::Bibliography::parse(&bibfilestring).unwrap();
let citekeys = Self::get_citekeys(&bibliography);
let keyword_list = Self::collect_tag_list(&citekeys, &bibliography);
- let entry_list = Self::create_entry_list(&citekeys, &bibliography);
+ let entry_list = Self::create_entry_list(&citekeys, &bibliography, cfg);
Self {
// bibfile,
bibfilestring,
@@ -181,23 +194,38 @@ impl BibiSetup {
file_strings.join("\n")
}
- fn create_entry_list(citekeys: &[String], bibliography: &Bibliography) -> Vec<BibiData> {
+ fn create_entry_list(
+ citekeys: &[String],
+ bibliography: &Bibliography,
+ 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())
+ } else {
+ None
+ };
citekeys
.iter()
.enumerate()
- .map(|(i, k)| BibiData {
- id: i as u32,
- authors: Self::get_authors(k, bibliography),
- short_author: String::new(),
- title: Self::get_title(k, bibliography),
- year: Self::get_year(k, bibliography),
- pubtype: Self::get_pubtype(k, bibliography),
- keywords: Self::get_keywords(k, bibliography),
- citekey: k.to_owned(),
- abstract_text: Self::get_abstract(k, bibliography),
- doi_url: Self::get_weblink(k, bibliography),
- filepath: Self::get_filepath(k, bibliography),
- subtitle: Self::get_subtitle(k, bibliography),
+ .map(|(i, k)| {
+ let filepaths: (Option<Vec<OsString>>, bool) =
+ { Self::get_filepath(k, bibliography, &mut pdf_files) };
+
+ BibiData {
+ id: i as u32,
+ authors: Self::get_authors(k, bibliography),
+ short_author: String::new(),
+ title: Self::get_title(k, bibliography),
+ year: Self::get_year(k, bibliography),
+ pubtype: Self::get_pubtype(k, bibliography),
+ keywords: Self::get_keywords(k, bibliography),
+ citekey: k.to_owned(),
+ abstract_text: Self::get_abstract(k, bibliography),
+ doi_url: Self::get_weblink(k, bibliography),
+ filepath: filepaths.0,
+ file_field: filepaths.1,
+ subtitle: Self::get_subtitle(k, bibliography),
+ }
})
.collect()
}
@@ -328,11 +356,26 @@ impl BibiSetup {
}
}
- pub fn get_filepath(citekey: &str, biblio: &Bibliography) -> Option<OsString> {
+ pub fn get_filepath(
+ citekey: &str,
+ biblio: &Bibliography,
+ pdf_files: &mut Option<HashMap<String, Vec<PathBuf>>>,
+ ) -> (Option<Vec<OsString>>, bool) {
if biblio.get(citekey).unwrap().file().is_ok() {
- Some(biblio.get(citekey).unwrap().file().unwrap().trim().into())
+ (
+ Some(vec![biblio
+ .get(citekey)
+ .unwrap()
+ .file()
+ .unwrap()
+ .trim()
+ .into()]),
+ true,
+ )
+ } else if pdf_files.is_some() {
+ (Self::merge_filepath_or_none(&citekey, pdf_files), false)
} else {
- None
+ (None, false)
}
}
@@ -350,4 +393,120 @@ impl BibiSetup {
None
}
}
+
+ fn merge_filepath_or_none(
+ citekey: &str,
+ pdf_files: &mut Option<HashMap<String, Vec<PathBuf>>>,
+ ) -> 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
+ .as_ref()
+ .unwrap()
+ .get(&citekey)
+ .unwrap()
+ .to_owned();
+ Some(path_vec.into_iter().map(|p| p.into_os_string()).collect())
+ } else {
+ None
+ }
+ };
+
+ pdf_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.
+///
+/// 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>>> {
+ 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)
+ } else {
+ pdf_dir
+ };
+
+ // Walk the passed dir and collect all pdf files into hashmap
+ if pdf_dir.is_dir() {
+ for file in WalkDir::new(pdf_dir) {
+ let f = file.unwrap().into_path();
+ if f.is_file()
+ && f.extension().is_some()
+ && f.extension().unwrap_or_default().to_ascii_lowercase() == "pdf"
+ {
+ 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]);
+ }
+ }
+ }
+ }
+
+ if files.is_empty() {
+ None
+ } else {
+ Some(files)
+ }
}
diff --git a/src/bibiman/entries.rs b/src/bibiman/entries.rs
index b7c7c9b..88a1583 100644
--- a/src/bibiman/entries.rs
+++ b/src/bibiman/entries.rs
@@ -157,6 +157,7 @@ mod tests {
abstract_text: "An abstract".to_string(),
doi_url: None,
filepath: None,
+ file_field: false,
subtitle: None,
};
@@ -174,6 +175,7 @@ mod tests {
abstract_text: "An abstract".to_string(),
doi_url: None,
filepath: None,
+ file_field: false,
subtitle: None,
};
diff --git a/src/bibiman/search.rs b/src/bibiman/search.rs
index 1855092..f391aed 100644
--- a/src/bibiman/search.rs
+++ b/src/bibiman/search.rs
@@ -117,6 +117,8 @@ pub fn search_pattern_in_file<'a>(pattern: &str, file: &'a PathBuf) -> Option<&'
#[cfg(test)]
mod tests {
+ use std::ffi::OsString;
+
use super::*;
#[test]
@@ -132,7 +134,8 @@ mod tests {
citekey: "author_1999".to_string(),
abstract_text: "An abstract with multiple sentences. Here is the second".to_string(),
doi_url: Some("https://www.bibiman.org".to_string()),
- filepath: Some("/home/file/path.pdf".to_string().into()),
+ filepath: Some(vec![OsString::from("/home/file/path.pdf")]),
+ file_field: true,
subtitle: None,
};
diff --git a/src/cliargs.rs b/src/cliargs.rs
index d4fac46..04886d1 100644
--- a/src/cliargs.rs
+++ b/src/cliargs.rs
@@ -33,6 +33,7 @@ pub struct CLIArgs {
pub pos_args: Vec<PathBuf>,
pub cfg_path: Option<PathBuf>,
pub light_theme: bool,
+ pub pdf_path: Option<PathBuf>,
}
impl CLIArgs {
@@ -55,6 +56,9 @@ impl CLIArgs {
Short('v') | Long("version") => args.versionarg = true,
Short('c') | Long("config-file") => args.cfg_path = Some(parser.value()?.parse()?),
Long("light-terminal") => args.light_theme = true,
+ Long("pdf-dir") => {
+ args.pdf_path = Some(parser.value()?.parse()?);
+ }
// Value(pos_arg) => parse_files(&mut args, pos_arg),
Value(pos_arg) => args.pos_args.push(pos_arg.into()),
_ => return Err(arg.unexpected()),
@@ -122,11 +126,16 @@ POSITIONAL ARGS:
Both can be passed multiple times
FLAGS:
- -h, --help Show this help and exit
- -v, --version Show the version and exit
- -c, --config-file Path to config file used for current session.
- Takes precedence over standard config file.
- --light-terminal Enable color mode for light terminal background",
+ -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-dir=<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"),
);
diff --git a/src/config.rs b/src/config.rs
index d554c58..8d50d93 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -51,6 +51,11 @@ const DEFAULT_CONFIG: &str = r##"
## 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"
+
# [colors]
## Default values for dark-themed terminal
## Possible values are:
@@ -84,6 +89,7 @@ pub struct General {
pub pdf_opener: String,
pub url_opener: String,
pub file_prefix: Option<PathBuf>,
+ pub pdf_path: Option<PathBuf>,
}
/// Substruct [colors] in config.toml
@@ -110,6 +116,7 @@ impl Default for BibiConfig {
pdf_opener: select_opener(),
url_opener: select_opener(),
file_prefix: None,
+ pdf_path: None,
},
colors: Colors {
main_text_color: Color::Indexed(250),
@@ -140,6 +147,14 @@ impl BibiConfig {
Ok(cfg_file)
}
+ /// overwright config values with values set explicitly through the
+ /// command line interface
+ pub fn cli_overwrite(&mut self, args: &CLIArgs) {
+ if args.pdf_path.is_some() {
+ self.general.pdf_path = args.pdf_path.clone();
+ }
+ }
+
/// Activates the default color scheme for light background terminals
pub fn light_colors(&mut self) {
self.colors.main_text_color = Color::Indexed(235);
diff --git a/src/main.rs b/src/main.rs
index b218f9b..add1f2e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -59,6 +59,8 @@ async fn main() -> Result<()> {
BibiConfig::default()
};
+ cfg.cli_overwrite(&parsed_args);
+
init_error_hooks()?;
// Create an application.
diff --git a/src/tui/popup.rs b/src/tui/popup.rs
index 2a6f18a..93b01c3 100644
--- a/src/tui/popup.rs
+++ b/src/tui/popup.rs
@@ -26,11 +26,17 @@ use crate::config::BibiConfig;
#[derive(Debug)]
pub enum PopupKind {
Help,
+ /// use for a confirmation message
MessageConfirm,
+ /// use for a warning message
MessageError,
+ /// open a resource connected to the entry
OpenRes,
+ /// select file to append entry to
AppendToFile,
+ /// append entry to a bibfile (selected in `AppendToFile` popup)
AddEntry,
+ /// select an item of the current entry to yank to clipboard
YankItem,
}
@@ -40,7 +46,7 @@ pub struct PopupArea {
pub popup_kind: Option<PopupKind>,
pub popup_message: String,
pub popup_scroll_pos: u16,
- pub popup_list: Vec<String>,
+ pub popup_list: Vec<(String, String)>,
pub popup_state: ListState,
pub popup_sel_item: String,
// pub add_entry_input: String,
@@ -108,21 +114,10 @@ impl PopupArea {
Text::from(helptext)
}
- pub fn popup_message(&mut self, message: &str, object: &str, msg_confirm: bool) {
- if object.is_empty() {
- self.popup_message = message.to_owned();
- } else {
- self.popup_message = message.to_owned() + object; //format!("{} \"{}\"", message, object);
- }
- if msg_confirm {
- self.popup_kind = Some(PopupKind::MessageConfirm);
- } else {
- self.popup_kind = Some(PopupKind::MessageError)
- }
- self.is_popup = true;
- }
-
- pub fn popup_selection(&mut self, items: Vec<String>) {
+ /// 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)>) {
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 921cbb1..2126135 100644
--- a/src/tui/ui.rs
+++ b/src/tui/ui.rs
@@ -24,6 +24,7 @@ use crate::cliargs::CLIArgs;
use crate::config::BibiConfig;
use crate::tui::popup::PopupKind;
use crate::App;
+use itertools::Itertools;
use ratatui::layout::{Direction, Position};
use ratatui::widgets::Clear;
use ratatui::Frame;
@@ -278,7 +279,14 @@ pub fn render_popup(app: &mut App, cfg: &BibiConfig, frame: &mut Frame) {
.popup_area
.popup_list
.iter()
- .map(|item| ListItem::from(item.to_owned()))
+ .map(
+ |(mes, obj)| {
+ ListItem::from(Line::from(vec![
+ Span::styled(mes, Style::new().bold()),
+ Span::raw(obj),
+ ]))
+ }, // ListItem::from(mes.to_owned() + obj)
+ )
.collect();
let title = if let Some(PopupKind::OpenRes) = app.bibiman.popup_area.popup_kind {
@@ -310,7 +318,33 @@ pub fn render_popup(app: &mut App, cfg: &BibiConfig, frame: &mut Frame) {
.add_modifier(Modifier::REVERSED),
);
- let popup_width = frame.area().width / 2;
+ // To find the longest line, we need to collect the chars of every item
+ // and add.
+ let list_widths = app
+ .bibiman
+ .popup_area
+ .popup_list
+ .iter()
+ .max_by(|(mes, obj), (m, o)| {
+ let x = mes.chars().count() + obj.chars().count();
+ let y = m.chars().count() + o.chars().count();
+ x.cmp(&y)
+ })
+ .unwrap();
+ // .map(|(m, o)| m.chars().count() as u16 + o.chars().count() as u16)
+ // .collect();
+
+ // 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;
+
+ // Check if the popup would exceed the terminal frame width
+ let popup_width = if max_item + 2 > frame.area().width - 2 {
+ frame.area().width - 2
+ } else {
+ max_item + 2
+ };
let popup_heigth = list.len() + 2;
let popup_area = popup_area(frame.area(), popup_width, popup_heigth as u16);
@@ -833,15 +867,24 @@ pub fn render_selected_item(app: &mut App, cfg: &BibiConfig, frame: &mut Frame,
),
]));
}
- if cur_entry.filepath.is_some() {
+ if let Some(p) = &cur_entry.filepath {
lines.push(Line::from(vec![
Span::styled("File: ", style_value),
Span::styled(
- cur_entry.filepath().to_string_lossy(),
+ p.iter().map(|f| f.to_str().unwrap()).join("; "),
Style::new().fg(cfg.colors.main_text_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(),
diff --git a/tests/pdf-files/annotated-pdfs/ARIStotle:rheTORIC.PDF b/tests/pdf-files/annotated-pdfs/ARIStotle:rheTORIC.PDF
new file mode 100644
index 0000000..6aaba88
--- /dev/null
+++ b/tests/pdf-files/annotated-pdfs/ARIStotle:rheTORIC.PDF
Binary files differ
diff --git a/tests/pdf-files/aristotle:physics.pdf b/tests/pdf-files/aristotle:physics.pdf
new file mode 100644
index 0000000..6aaba88
--- /dev/null
+++ b/tests/pdf-files/aristotle:physics.pdf
Binary files differ
diff --git a/tests/pdf-files/aristotle:rhetoric.pdf b/tests/pdf-files/aristotle:rhetoric.pdf
new file mode 100644
index 0000000..6aaba88
--- /dev/null
+++ b/tests/pdf-files/aristotle:rhetoric.pdf
Binary files differ
diff --git a/tests/test-config.toml b/tests/test-config.toml
index 3913f16..4e4f8c5 100644
--- a/tests/test-config.toml
+++ b/tests/test-config.toml
@@ -14,7 +14,12 @@ bibfiles = [ "tests/biblatex-test.bib" ]
## 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"
+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 = "tests/pdf-files"
# [colors]
## Default values for dark-themed terminal