From 7fe5acb85b9e665ec83f590942ee78b8c90d6aae Mon Sep 17 00:00:00 2001 From: lukeflo Date: Sun, 18 May 2025 22:44:36 +0200 Subject: first tests for pdf folder --- src/bibiman/bibisetup.rs | 22 ++++++++++++++++++++-- src/cliargs.rs | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/bibiman/bibisetup.rs b/src/bibiman/bibisetup.rs index cba1536..92731b5 100644 --- a/src/bibiman/bibisetup.rs +++ b/src/bibiman/bibisetup.rs @@ -22,7 +22,7 @@ use itertools::Itertools; use std::ffi::{OsStr, OsString}; use std::{fs, path::PathBuf}; -use crate::cliargs; +use crate::cliargs::{self, CLIArgs}; // Set necessary fields // TODO: can surely be made more efficient/simpler @@ -328,9 +328,11 @@ impl BibiSetup { } } - pub fn get_filepath(citekey: &str, biblio: &Bibliography) -> Option { + pub fn get_filepath(citekey: &str, biblio: &Bibliography, args: &CLIArgs) -> Option { if biblio.get(citekey).unwrap().file().is_ok() { Some(biblio.get(citekey).unwrap().file().unwrap().trim().into()) + } else if args.pdf_files.is_some() { + Self::merge_filepath_or_none(&citekey, &biblio, &args) } else { None } @@ -350,4 +352,20 @@ impl BibiSetup { None } } + + fn merge_filepath_or_none( + citekey: &str, + biblio: &Bibliography, + args: &CLIArgs, + ) -> Option { + // Oder n Loop??? + let pdf_file = { + 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; + } + } + }; + } } diff --git a/src/cliargs.rs b/src/cliargs.rs index d4fac46..6fd30e1 100644 --- a/src/cliargs.rs +++ b/src/cliargs.rs @@ -33,6 +33,7 @@ pub struct CLIArgs { pub pos_args: Vec, pub cfg_path: Option, pub light_theme: bool, + pub pdf_files: Option>, } impl CLIArgs { @@ -55,6 +56,10 @@ 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("merge-pdf-paths") => { + let pdf_dir: PathBuf = parser.value()?.parse()?; + args.pdf_files = collect_pdf_files(pdf_dir); + } // Value(pos_arg) => parse_files(&mut args, pos_arg), Value(pos_arg) => args.pos_args.push(pos_arg.into()), _ => return Err(arg.unexpected()), @@ -65,6 +70,37 @@ impl CLIArgs { } } +/// This function walks the given dir and collects all pdf files into a `Vec` +pub fn collect_pdf_files(pdf_dir: PathBuf) -> Option> { + let mut files: Vec = Vec::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 vec + 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" + { + files.push(f); + } + } + } + + if files.is_empty() { + None + } else { + Some(files) + } +} + /// This function maps a vector containing paths to another vector containing paths. /// But it will walk all entries of the first vec which are directories /// and put only valid file paths with `.bib` ending to the resulting vec. @@ -126,7 +162,10 @@ FLAGS: -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", + --light-terminal Enable color mode for light terminal background + --merge-pdf-paths Merge PDF files named by citekey at the given path into + the `file` field of the entry matching the citekey + (might not work with citekeys containing special chars)", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"), ); -- cgit v1.2.3 From 28030fd0830478872be2b9e86b74e0c054a96111 Mon Sep 17 00:00:00 2001 From: lukeflo Date: Sat, 24 May 2025 17:31:33 +0200 Subject: implemented a workflow, but citekey matching still buggy --- src/app.rs | 2 +- src/bibiman.rs | 12 ++--- src/bibiman/bibisetup.rs | 95 +++++++++++++++++++++++++++------ src/cliargs.rs | 36 +------------ src/config.rs | 15 ++++++ src/main.rs | 2 + tests/pdf-files/aristotle:rhetoric.pdf | Bin 0 -> 674743 bytes tests/test-config.toml | 5 ++ 8 files changed, 110 insertions(+), 57 deletions(-) create mode 100644 tests/pdf-files/aristotle:rhetoric.pdf diff --git a/src/app.rs b/src/app.rs index 88f37b0..e0c149c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -277,7 +277,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()? } diff --git a/src/bibiman.rs b/src/bibiman.rs index c90905f..ef5dfe3 100644 --- a/src/bibiman.rs +++ b/src/bibiman.rs @@ -89,7 +89,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()); @@ -133,8 +133,8 @@ impl Bibiman { self.former_area = None; } - pub fn update_lists(&mut self) { - self.main_biblio = BibiSetup::new(&self.main_bibfiles); + 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 +397,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); @@ -466,7 +466,7 @@ impl Bibiman { 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(); @@ -535,7 +535,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 diff --git a/src/bibiman/bibisetup.rs b/src/bibiman/bibisetup.rs index 92731b5..b09747a 100644 --- a/src/bibiman/bibisetup.rs +++ b/src/bibiman/bibisetup.rs @@ -21,8 +21,11 @@ use color_eyre::owo_colors::OwoColorize; use itertools::Itertools; use std::ffi::{OsStr, OsString}; use std::{fs, path::PathBuf}; +use walkdir::WalkDir; +use crate::app; use crate::cliargs::{self, CLIArgs}; +use crate::config::BibiConfig; // Set necessary fields // TODO: can surely be made more efficient/simpler @@ -121,14 +124,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,7 +184,16 @@ impl BibiSetup { file_strings.join("\n") } - fn create_entry_list(citekeys: &[String], bibliography: &Bibliography) -> Vec { + fn create_entry_list( + citekeys: &[String], + bibliography: &Bibliography, + cfg: &BibiConfig, + ) -> Vec { + let pdf_files = if cfg.general.pdf_path.is_some() { + collect_pdf_files(cfg.general.pdf_path.as_ref().unwrap()) + } else { + None + }; citekeys .iter() .enumerate() @@ -196,7 +208,7 @@ impl BibiSetup { citekey: k.to_owned(), abstract_text: Self::get_abstract(k, bibliography), doi_url: Self::get_weblink(k, bibliography), - filepath: Self::get_filepath(k, bibliography), + filepath: Self::get_filepath(k, bibliography, &pdf_files), subtitle: Self::get_subtitle(k, bibliography), }) .collect() @@ -328,11 +340,15 @@ impl BibiSetup { } } - pub fn get_filepath(citekey: &str, biblio: &Bibliography, args: &CLIArgs) -> Option { + pub fn get_filepath( + citekey: &str, + biblio: &Bibliography, + pdf_files: &Option>, + ) -> Option { if biblio.get(citekey).unwrap().file().is_ok() { Some(biblio.get(citekey).unwrap().file().unwrap().trim().into()) - } else if args.pdf_files.is_some() { - Self::merge_filepath_or_none(&citekey, &biblio, &args) + } else if pdf_files.is_some() { + Self::merge_filepath_or_none(&citekey, pdf_files) } else { None } @@ -353,19 +369,66 @@ impl BibiSetup { } } - fn merge_filepath_or_none( - citekey: &str, - biblio: &Bibliography, - args: &CLIArgs, - ) -> Option { + fn merge_filepath_or_none(citekey: &str, pdf_files: &Option>) -> Option { // Oder n Loop??? let pdf_file = { - 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; + 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; + // } + // } + 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 + } }; + + pdf_file + } +} + +/// This function walks the given dir and collects all pdf files into a `Vec` +pub fn collect_pdf_files(pdf_dir: &PathBuf) -> Option> { + let mut files: Vec = Vec::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 vec + 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" + { + files.push(f); + } + } + } + + if files.is_empty() { + None + } else { + Some(files) } } diff --git a/src/cliargs.rs b/src/cliargs.rs index 6fd30e1..a9b12fd 100644 --- a/src/cliargs.rs +++ b/src/cliargs.rs @@ -33,7 +33,7 @@ pub struct CLIArgs { pub pos_args: Vec, pub cfg_path: Option, pub light_theme: bool, - pub pdf_files: Option>, + pub pdf_path: Option, } impl CLIArgs { @@ -57,8 +57,7 @@ impl CLIArgs { Short('c') | Long("config-file") => args.cfg_path = Some(parser.value()?.parse()?), Long("light-terminal") => args.light_theme = true, Long("merge-pdf-paths") => { - let pdf_dir: PathBuf = parser.value()?.parse()?; - args.pdf_files = collect_pdf_files(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()), @@ -70,37 +69,6 @@ impl CLIArgs { } } -/// This function walks the given dir and collects all pdf files into a `Vec` -pub fn collect_pdf_files(pdf_dir: PathBuf) -> Option> { - let mut files: Vec = Vec::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 vec - 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" - { - files.push(f); - } - } - } - - if files.is_empty() { - None - } else { - Some(files) - } -} - /// This function maps a vector containing paths to another vector containing paths. /// But it will walk all entries of the first vec which are directories /// and put only valid file paths with `.bib` ending to the resulting vec. diff --git a/src/config.rs b/src/config.rs index d554c58..1532d31 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, + pub pdf_path: Option, } /// 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_overwright(&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..dd82c0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,8 @@ async fn main() -> Result<()> { BibiConfig::default() }; + cfg.cli_overwright(&parsed_args); + init_error_hooks()?; // Create an application. diff --git a/tests/pdf-files/aristotle:rhetoric.pdf b/tests/pdf-files/aristotle:rhetoric.pdf new file mode 100644 index 0000000..a6d6eff Binary files /dev/null and b/tests/pdf-files/aristotle:rhetoric.pdf differ diff --git a/tests/test-config.toml b/tests/test-config.toml index 3913f16..1b60acd 100644 --- a/tests/test-config.toml +++ b/tests/test-config.toml @@ -16,6 +16,11 @@ bibfiles = [ "tests/biblatex-test.bib" ] ## 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 = "tests/pdf-files" + # [colors] ## Default values for dark-themed terminal ## Possible values are: -- cgit v1.2.3 From ee2d8c04c5fdce6e3384ecceee9a5250d8d586fe Mon Sep 17 00:00:00 2001 From: lukeflo Date: Sat, 24 May 2025 17:48:09 +0200 Subject: impl loop over pdf files in dir, break if match --- src/bibiman/bibisetup.rs | 45 +++++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/bibiman/bibisetup.rs b/src/bibiman/bibisetup.rs index b09747a..b968658 100644 --- a/src/bibiman/bibisetup.rs +++ b/src/bibiman/bibisetup.rs @@ -380,22 +380,43 @@ impl BibiSetup { // break f; // } // } - 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; + + 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 + { + break Some(cur_entry.to_owned().into_os_string()); } else { - idx += 1; + 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 - } + // 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 + // } }; pdf_file -- cgit v1.2.3 From d112312c3fcf79c679e2aa4a47a60b69f83cbce9 Mon Sep 17 00:00:00 2001 From: lukeflo Date: Sat, 24 May 2025 18:12:43 +0200 Subject: remove already assigned filepaths from pdf_files vec --- src/bibiman/bibisetup.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/bibiman/bibisetup.rs b/src/bibiman/bibisetup.rs index b968658..aa0640c 100644 --- a/src/bibiman/bibisetup.rs +++ b/src/bibiman/bibisetup.rs @@ -189,7 +189,7 @@ impl BibiSetup { bibliography: &Bibliography, cfg: &BibiConfig, ) -> Vec { - let pdf_files = if cfg.general.pdf_path.is_some() { + let mut pdf_files = if cfg.general.pdf_path.is_some() { collect_pdf_files(cfg.general.pdf_path.as_ref().unwrap()) } else { None @@ -208,7 +208,7 @@ impl BibiSetup { citekey: k.to_owned(), abstract_text: Self::get_abstract(k, bibliography), doi_url: Self::get_weblink(k, bibliography), - filepath: Self::get_filepath(k, bibliography, &pdf_files), + filepath: Self::get_filepath(k, bibliography, &mut pdf_files), subtitle: Self::get_subtitle(k, bibliography), }) .collect() @@ -343,7 +343,7 @@ impl BibiSetup { pub fn get_filepath( citekey: &str, biblio: &Bibliography, - pdf_files: &Option>, + pdf_files: &mut Option>, ) -> Option { if biblio.get(citekey).unwrap().file().is_ok() { Some(biblio.get(citekey).unwrap().file().unwrap().trim().into()) @@ -369,7 +369,10 @@ impl BibiSetup { } } - fn merge_filepath_or_none(citekey: &str, pdf_files: &Option>) -> Option { + fn merge_filepath_or_none( + citekey: &str, + pdf_files: &mut Option>, + ) -> Option { // Oder n Loop??? let pdf_file = { let mut idx = 0; @@ -395,7 +398,9 @@ impl BibiSetup { .unwrap() == citekey { - break Some(cur_entry.to_owned().into_os_string()); + let path = cur_entry.to_owned().into_os_string(); + pdf_files.as_mut().unwrap().swap_remove(idx); + break Some(path); } else { idx += 1 } -- cgit v1.2.3 From 06782b5ed1527b1d4eb6e26feb5a260415efe1af Mon Sep 17 00:00:00 2001 From: lukeflo Date: Sun, 25 May 2025 14:21:17 +0200 Subject: enhancement from PR \#28 discussion + Collect basenames and filepaths from pdf dir into HashMap + Identical basenames in different subdirs are possible + Collected into Vector + TODO: implement selection for multiple files (yank and open) --- src/bibiman.rs | 5 +- src/bibiman/bibisetup.rs | 140 ++++++++++++++++----- src/tui/ui.rs | 14 ++- .../annotated-pdfs/ARIStotle:rheTORIC.PDF | Bin 0 -> 25294 bytes tests/pdf-files/aristotle:physics.pdf | Bin 0 -> 25294 bytes tests/pdf-files/aristotle:rhetoric.pdf | Bin 674743 -> 25294 bytes 6 files changed, 122 insertions(+), 37 deletions(-) create mode 100644 tests/pdf-files/annotated-pdfs/ARIStotle:rheTORIC.PDF create mode 100644 tests/pdf-files/aristotle:physics.pdf diff --git a/src/bibiman.rs b/src/bibiman.rs index ef5dfe3..646a078 100644 --- a/src/bibiman.rs +++ b/src/bibiman.rs @@ -557,7 +557,8 @@ impl Bibiman { 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(); + // TODO: Selection for multiple files + let object = self.entry_table.entry_table_items[entry_idx].filepath()[0]; app::open_connected_file(cfg, object)?; } else { eprintln!("Unable to find ressource to open"); @@ -604,7 +605,7 @@ impl Bibiman { .filepath .clone(); if let Some(p) = path { - let p = p.as_os_str().to_str(); + let p = p[0].as_os_str().to_str(); if let Some(p) = p { Bibiman::yank_text(p); self.popup_area.popup_message( diff --git a/src/bibiman/bibisetup.rs b/src/bibiman/bibisetup.rs index aa0640c..0914f7c 100644 --- a/src/bibiman/bibisetup.rs +++ b/src/bibiman/bibisetup.rs @@ -19,12 +19,13 @@ 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::app; -use crate::cliargs::{self, CLIArgs}; +use crate::cliargs::{self}; use crate::config::BibiConfig; // Set necessary fields @@ -51,7 +52,7 @@ pub struct BibiData { pub citekey: String, pub abstract_text: String, pub doi_url: Option, - pub filepath: Option, + pub filepath: Option>, pub subtitle: Option, } @@ -114,8 +115,17 @@ impl BibiData { self.doi_url.as_ref().unwrap() } - pub fn filepath(&self) -> &OsStr { - self.filepath.as_ref().unwrap() + // pub fn filepath(&self) -> &OsStr { + // self.filepath.as_ref().unwrap() + // } + + 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 { @@ -190,7 +200,7 @@ impl BibiSetup { cfg: &BibiConfig, ) -> Vec { let mut pdf_files = if cfg.general.pdf_path.is_some() { - collect_pdf_files(cfg.general.pdf_path.as_ref().unwrap()) + collect_pdf_file_paths(cfg.general.pdf_path.as_ref().unwrap()) } else { None }; @@ -343,10 +353,16 @@ impl BibiSetup { pub fn get_filepath( citekey: &str, biblio: &Bibliography, - pdf_files: &mut Option>, - ) -> Option { + pdf_files: &mut Option>>, + ) -> Option> { 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()]) } else if pdf_files.is_some() { Self::merge_filepath_or_none(&citekey, pdf_files) } else { @@ -371,11 +387,10 @@ impl BibiSetup { fn merge_filepath_or_none( citekey: &str, - pdf_files: &mut Option>, - ) -> Option { - // Oder n Loop??? + pdf_files: &mut Option>>, + ) -> Option> { let pdf_file = { - let mut idx = 0; + // 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() { @@ -384,27 +399,27 @@ impl BibiSetup { // } // } - 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 - } - } + // 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(); @@ -422,6 +437,18 @@ impl BibiSetup { // } 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 @@ -458,3 +485,50 @@ pub fn collect_pdf_files(pdf_dir: &PathBuf) -> Option> { Some(files) } } + +/// This function walks the given dir and collects all pdf files into a `HashMap` +/// of the format `[String, Vec]`, where `String` represents the basename +/// of the file and the `Vec` 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>> { + let mut files: HashMap> = 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/tui/ui.rs b/src/tui/ui.rs index 921cbb1..883e4df 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; @@ -833,15 +834,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 Binary files /dev/null and b/tests/pdf-files/annotated-pdfs/ARIStotle:rheTORIC.PDF differ diff --git a/tests/pdf-files/aristotle:physics.pdf b/tests/pdf-files/aristotle:physics.pdf new file mode 100644 index 0000000..6aaba88 Binary files /dev/null and b/tests/pdf-files/aristotle:physics.pdf differ diff --git a/tests/pdf-files/aristotle:rhetoric.pdf b/tests/pdf-files/aristotle:rhetoric.pdf index a6d6eff..6aaba88 100644 Binary files a/tests/pdf-files/aristotle:rhetoric.pdf and b/tests/pdf-files/aristotle:rhetoric.pdf differ -- cgit v1.2.3 From eac547bce389169b867ccdeedc47ba2b8b90511d Mon Sep 17 00:00:00 2001 From: lukeflo Date: Sun, 25 May 2025 21:51:06 +0200 Subject: remove unneeded function --- src/bibiman/bibisetup.rs | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/src/bibiman/bibisetup.rs b/src/bibiman/bibisetup.rs index 0914f7c..92e02fc 100644 --- a/src/bibiman/bibisetup.rs +++ b/src/bibiman/bibisetup.rs @@ -455,37 +455,6 @@ impl BibiSetup { } } -/// This function walks the given dir and collects all pdf files into a `Vec` -pub fn collect_pdf_files(pdf_dir: &PathBuf) -> Option> { - let mut files: Vec = Vec::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 vec - 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" - { - files.push(f); - } - } - } - - if files.is_empty() { - None - } else { - Some(files) - } -} - /// This function walks the given dir and collects all pdf files into a `HashMap` /// of the format `[String, Vec]`, where `String` represents the basename /// of the file and the `Vec` holds all filepaths ending with this basename. -- cgit v1.2.3 From c0dcbcd18a3fba111885fd0eaf8ef18f71cf693a Mon Sep 17 00:00:00 2001 From: lukeflo Date: Mon, 26 May 2025 14:55:21 +0200 Subject: some more doc strings --- src/cliargs.rs | 20 +++++++++++--------- src/config.rs | 2 +- src/main.rs | 2 +- src/tui/popup.rs | 17 +++++++++++++++++ 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/cliargs.rs b/src/cliargs.rs index a9b12fd..c7a0eb1 100644 --- a/src/cliargs.rs +++ b/src/cliargs.rs @@ -56,7 +56,7 @@ 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("merge-pdf-paths") => { + Long("pdf-dir") => { args.pdf_path = Some(parser.value()?.parse()?); } // Value(pos_arg) => parse_files(&mut args, pos_arg), @@ -126,14 +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 - --merge-pdf-paths Merge PDF files named by citekey at the given path into - the `file` field of the entry matching the citekey - (might not work with citekeys containing special chars)", + -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 + --pdf-dir= 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 1532d31..8d50d93 100644 --- a/src/config.rs +++ b/src/config.rs @@ -149,7 +149,7 @@ impl BibiConfig { /// overwright config values with values set explicitly through the /// command line interface - pub fn cli_overwright(&mut self, args: &CLIArgs) { + pub fn cli_overwrite(&mut self, args: &CLIArgs) { if args.pdf_path.is_some() { self.general.pdf_path = args.pdf_path.clone(); } diff --git a/src/main.rs b/src/main.rs index dd82c0f..add1f2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,7 +59,7 @@ async fn main() -> Result<()> { BibiConfig::default() }; - cfg.cli_overwright(&parsed_args); + cfg.cli_overwrite(&parsed_args); init_error_hooks()?; diff --git a/src/tui/popup.rs b/src/tui/popup.rs index 2a6f18a..a0a61a4 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, } @@ -108,6 +114,14 @@ impl PopupArea { Text::from(helptext) } + /// Creates a popup message. The needed arguments are: + /// + /// - `message` as `str`: The message displayed in the popup. + /// - `object` as `str`: A possible object added to the message. E.g. the content + /// which gets copied to the clipboard. + /// - `msg_confirm` as `bool`: if `true` its a confirmation message displayed in + /// in the set `confirm_color` (default: green), if `false` its a warning + /// message displayed in the set `warn_color` (default: red). pub fn popup_message(&mut self, message: &str, object: &str, msg_confirm: bool) { if object.is_empty() { self.popup_message = message.to_owned(); @@ -122,6 +136,9 @@ impl PopupArea { self.is_popup = true; } + /// Opens a popup with a selectable list + /// + /// The list items are passed as argument of the kind `Vec`. pub fn popup_selection(&mut self, items: Vec) { self.popup_list = items; // self.popup_kind = Some(PopupKind::SelectRes); -- cgit v1.2.3 From 5ab55a263a5ae9cc5cbadf52f6000621e2552f85 Mon Sep 17 00:00:00 2001 From: lukeflo Date: Mon, 26 May 2025 17:11:55 +0200 Subject: first steps in rewriting popups --- src/app.rs | 73 ++++++++++++++++++-------- src/bibiman.rs | 155 ++++++++++++++++++++++++++++++++++++++++++++++--------- src/tui/popup.rs | 6 +-- src/tui/ui.rs | 2 +- 4 files changed, 186 insertions(+), 50 deletions(-) diff --git a/src/app.rs b/src/app.rs index e0c149c..23230ba 100644 --- a/src/app.rs +++ b/src/app.rs @@ -300,18 +300,24 @@ 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()) + 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 +334,40 @@ 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()) + items.push(( + "File (PDF/EPUB): ".into(), + entry.filepath.unwrap()[0].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.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 ressources: "), + Some(&entry.citekey), + None, + ); + // self.bibiman.popup_area.popup_message( + // "Selected entry has no connected ressources: ", + // &entry.citekey, + // false, + // ) } } } @@ -357,7 +378,7 @@ impl App { } } CmdAction::ShowHelp => { - self.bibiman.show_help(); + self.bibiman.open_popup(PopupKind::Help, None, None, None); } CmdAction::Exit => { self.quit(); @@ -430,6 +451,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 646a078..9606c19 100644 --- a/src/bibiman.rs +++ b/src/bibiman.rs @@ -118,6 +118,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,6 +134,80 @@ impl Bibiman { self.former_area = None; } + /// 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 `&str`. 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`. + pub fn open_popup( + &mut self, + popup_kind: PopupKind, + message: Option<&str>, + object: Option<&str>, + items: Option>, + ) { + 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); + } + 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() + } else if object.is_none() && message.is_some() { + self.popup_area.popup_message = message.unwrap().to_owned() + } else { + return; + } + } + 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() + } else if object.is_none() && message.is_some() { + self.popup_area.popup_message = message.unwrap().to_owned() + } else { + return; + } + } + PopupKind::OpenRes => { + self.popup_area.popup_kind = Some(PopupKind::OpenRes); + self.popup_area.popup_selection(items.unwrap()); + self.popup_area.popup_state.select(Some(0)); + } + PopupKind::AppendToFile => { + self.popup_area.popup_kind = Some(PopupKind::AppendToFile); + } + PopupKind::AddEntry => { + self.popup_area.popup_kind = Some(PopupKind::AddEntry); + } + PopupKind::YankItem => { + self.popup_area.popup_kind = Some(PopupKind::YankItem); + self.popup_area.popup_selection(items.unwrap()); + self.popup_area.popup_state.select(Some(0)); + } + } + } + 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()); @@ -442,26 +517,33 @@ 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); } } 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); } @@ -482,7 +564,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() { @@ -552,11 +637,11 @@ impl Bibiman { let popup_idx = self.popup_area.popup_state.selected().unwrap(); // 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") { + } 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]; app::open_connected_file(cfg, object)?; @@ -570,6 +655,7 @@ impl Bibiman { } pub fn yank_entry_field(&mut self) -> Result<()> { + // self.close_popup(); // Index of selected entry let entry_idx = self.entry_table.entry_table_state.selected().unwrap(); @@ -577,30 +663,45 @@ impl Bibiman { let popup_idx = self.popup_area.popup_state.selected().unwrap(); match self.popup_area.popup_list[popup_idx] + .0 .to_lowercase() .as_str() + .split_whitespace() + .next() { - "citekey" => { + Some("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, + self.open_popup( + PopupKind::MessageConfirm, + Some("Yanked citekey to clipboard: "), + Some(citekey.clone().as_str()), + None, ); + // self.popup_area.popup_message( + // "Yanked citekey to clipboard: ", + // citekey, // self.bibiman.get_selected_citekey(), + // true, + // ); } - "weblink" => { + Some("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, + self.open_popup( + PopupKind::MessageConfirm, + Some("Yanked weblink to clipboard: "), + Some(l.clone().as_str()), + None, ); + // self.popup_area.popup_message( + // "Yanked weblink to clipboard: ", + // l, // self.bibiman.get_selected_link(), + // true, + // ); } } - "filepath" => { + Some("filepath:") => { let path = self.entry_table.entry_table_items[entry_idx] .filepath .clone(); @@ -608,11 +709,17 @@ impl Bibiman { let p = p[0].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, + self.open_popup( + PopupKind::MessageConfirm, + Some("Yanked filepath to clipboard: "), + Some(p), + None, ); + // self.popup_area.popup_message( + // "Yanked filepath to clipboard: ", + // p, // self.bibiman.get_selected_link(), + // true, + // ); } } } diff --git a/src/tui/popup.rs b/src/tui/popup.rs index a0a61a4..f3f6d58 100644 --- a/src/tui/popup.rs +++ b/src/tui/popup.rs @@ -46,7 +46,7 @@ pub struct PopupArea { pub popup_kind: Option, pub popup_message: String, pub popup_scroll_pos: u16, - pub popup_list: Vec, + pub popup_list: Vec<(String, String)>, pub popup_state: ListState, pub popup_sel_item: String, // pub add_entry_input: String, @@ -138,8 +138,8 @@ impl PopupArea { /// Opens a popup with a selectable list /// - /// The list items are passed as argument of the kind `Vec`. - pub fn popup_selection(&mut self, items: Vec) { + /// 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 883e4df..9cbd075 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -279,7 +279,7 @@ 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(mes.to_owned() + obj)) .collect(); let title = if let Some(PopupKind::OpenRes) = app.bibiman.popup_area.popup_kind { -- cgit v1.2.3 From 0eae6de6df392fb3b8fa9d39dde42cecff97d240 Mon Sep 17 00:00:00 2001 From: lukeflo Date: Fri, 30 May 2025 13:39:24 +0200 Subject: impl open_popup function for all popup cases --- src/app.rs | 23 +++++++++--------- src/bibiman.rs | 73 +++++++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/src/app.rs b/src/app.rs index 23230ba..92d89ba 100644 --- a/src/app.rs +++ b/src/app.rs @@ -15,7 +15,7 @@ // along with this program. If not, see . ///// -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, + )?; } } _ => {} @@ -317,7 +318,7 @@ impl App { // 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)); + .open_popup(PopupKind::YankItem, None, None, Some(items))?; } } CmdAction::EditFile => { @@ -355,14 +356,14 @@ impl App { // self.bibiman.popup_area.popup_state.select(Some(0)) self.bibiman - .open_popup(PopupKind::OpenRes, None, None, Some(items)); + .open_popup(PopupKind::OpenRes, None, None, Some(items))?; } else { self.bibiman.open_popup( PopupKind::MessageError, Some("Selected entry has no connected ressources: "), Some(&entry.citekey), None, - ); + )?; // self.bibiman.popup_area.popup_message( // "Selected entry has no connected ressources: ", // &entry.citekey, @@ -378,7 +379,7 @@ impl App { } } CmdAction::ShowHelp => { - self.bibiman.open_popup(PopupKind::Help, None, None, None); + self.bibiman.open_popup(PopupKind::Help, None, None, None)?; } CmdAction::Exit => { self.quit(); diff --git a/src/bibiman.rs b/src/bibiman.rs index 9606c19..e3509a9 100644 --- a/src/bibiman.rs +++ b/src/bibiman.rs @@ -24,7 +24,7 @@ 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; @@ -147,16 +147,19 @@ impl Bibiman { /// 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 `&str`. The second item of + /// 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>, - ) { + ) -> Result<()> { if let CurrentArea::EntryArea = self.current_area { self.former_area = Some(FormerArea::EntryArea); } else if let CurrentArea::TagArea = self.current_area { @@ -168,42 +171,69 @@ impl Bibiman { 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() + 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() + self.popup_area.popup_message = message.unwrap().to_owned(); + Ok(()) } else { - return; + 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() + 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() + self.popup_area.popup_message = message.unwrap().to_owned(); + Ok(()) } else { - return; + Err(Error::msg("You need to past at least a message via Some(&str) to create a message popup")) } } PopupKind::OpenRes => { - self.popup_area.popup_kind = Some(PopupKind::OpenRes); - self.popup_area.popup_selection(items.unwrap()); - self.popup_area.popup_state.select(Some(0)); + 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 => { - self.popup_area.popup_kind = Some(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 => { - self.popup_area.popup_kind = Some(PopupKind::YankItem); - self.popup_area.popup_selection(items.unwrap()); - self.popup_area.popup_state.select(Some(0)); + 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", + )) + } } } } @@ -493,7 +523,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 { @@ -522,10 +552,11 @@ impl Bibiman { 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) { @@ -677,7 +708,7 @@ impl Bibiman { Some("Yanked citekey to clipboard: "), Some(citekey.clone().as_str()), None, - ); + )?; // self.popup_area.popup_message( // "Yanked citekey to clipboard: ", // citekey, // self.bibiman.get_selected_citekey(), @@ -693,7 +724,7 @@ impl Bibiman { Some("Yanked weblink to clipboard: "), Some(l.clone().as_str()), None, - ); + )?; // self.popup_area.popup_message( // "Yanked weblink to clipboard: ", // l, // self.bibiman.get_selected_link(), @@ -714,7 +745,7 @@ impl Bibiman { Some("Yanked filepath to clipboard: "), Some(p), None, - ); + )?; // self.popup_area.popup_message( // "Yanked filepath to clipboard: ", // p, // self.bibiman.get_selected_link(), -- cgit v1.2.3 From fbcd12fb9e215852a12e0c3f5963aa52996b26aa Mon Sep 17 00:00:00 2001 From: lukeflo Date: Fri, 30 May 2025 17:04:42 +0200 Subject: better UI for popups, some testings --- src/app.rs | 31 +++++++++------------ src/bibiman.rs | 86 ++++++++++++++-------------------------------------------- src/tui/ui.rs | 29 ++++++++++++++++++-- 3 files changed, 60 insertions(+), 86 deletions(-) diff --git a/src/app.rs b/src/app.rs index 92d89ba..f751716 100644 --- a/src/app.rs +++ b/src/app.rs @@ -306,10 +306,13 @@ impl App { items.push(("Weblink: ".into(), entry.doi_url.unwrap().clone())) } if entry.filepath.is_some() { - items.push(( - "Filepath: ".into(), - entry.filepath.unwrap()[0].clone().into_string().unwrap(), - )) + 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); @@ -344,16 +347,13 @@ impl App { )) } if entry.filepath.is_some() { - items.push(( - "File (PDF/EPUB): ".into(), - entry.filepath.unwrap()[0].clone().into_string().unwrap(), - )) + entry.filepath.unwrap().iter().for_each(|p| { + items.push(( + "File (PDF/EPUB): ".into(), + 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))?; @@ -364,11 +364,6 @@ impl App { Some(&entry.citekey), None, )?; - // self.bibiman.popup_area.popup_message( - // "Selected entry has no connected ressources: ", - // &entry.citekey, - // false, - // ) } } } diff --git a/src/bibiman.rs b/src/bibiman.rs index e3509a9..583d849 100644 --- a/src/bibiman.rs +++ b/src/bibiman.rs @@ -28,6 +28,7 @@ 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; @@ -666,6 +667,7 @@ 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].0.contains("Weblink") { @@ -674,8 +676,9 @@ impl Bibiman { app::open_connected_link(cfg, &url)?; } 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]; - app::open_connected_file(cfg, object)?; + // let object = self.entry_table.entry_table_items[entry_idx].filepath()[0]; + let object: OsString = popup_entry.into(); + app::open_connected_file(cfg, &object)?; } else { eprintln!("Unable to find ressource to open"); }; @@ -686,76 +689,27 @@ impl Bibiman { } pub fn yank_entry_field(&mut self) -> Result<()> { - // self.close_popup(); - // 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() - .split_whitespace() + .split(":") .next() - { - Some("citekey:") => { - let citekey = &self.entry_table.entry_table_items[entry_idx].citekey; - Bibiman::yank_text(citekey); - self.open_popup( - PopupKind::MessageConfirm, - Some("Yanked citekey to clipboard: "), - Some(citekey.clone().as_str()), - None, - )?; - // self.popup_area.popup_message( - // "Yanked citekey to clipboard: ", - // citekey, // self.bibiman.get_selected_citekey(), - // true, - // ); - } - Some("weblink:") => { - let link = &self.entry_table.entry_table_items[entry_idx].doi_url; - if let Some(l) = link { - Bibiman::yank_text(l); - self.open_popup( - PopupKind::MessageConfirm, - Some("Yanked weblink to clipboard: "), - Some(l.clone().as_str()), - None, - )?; - // self.popup_area.popup_message( - // "Yanked weblink to clipboard: ", - // l, // self.bibiman.get_selected_link(), - // true, - // ); - } - } - Some("filepath:") => { - let path = self.entry_table.entry_table_items[entry_idx] - .filepath - .clone(); - if let Some(p) = path { - let p = p[0].as_os_str().to_str(); - if let Some(p) = p { - Bibiman::yank_text(p); - self.open_popup( - PopupKind::MessageConfirm, - Some("Yanked filepath to clipboard: "), - Some(p), - None, - )?; - // self.popup_area.popup_message( - // "Yanked filepath to clipboard: ", - // p, // self.bibiman.get_selected_link(), - // true, - // ); - } - } - } - _ => {} - }; + .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/tui/ui.rs b/src/tui/ui.rs index 9cbd075..a998bc7 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -279,7 +279,14 @@ pub fn render_popup(app: &mut App, cfg: &BibiConfig, frame: &mut Frame) { .popup_area .popup_list .iter() - .map(|(mes, obj)| ListItem::from(mes.to_owned() + obj)) + .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 { @@ -311,7 +318,25 @@ 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: Vec = app + .bibiman + .popup_area + .popup_list + .iter() + .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(); + + // 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); -- cgit v1.2.3 From 202dc44a6dba403c5d13986fe4c3701a45a223c9 Mon Sep 17 00:00:00 2001 From: lukeflo Date: Fri, 30 May 2025 17:18:13 +0200 Subject: update README: pdf_path variable --- README.md | 56 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index e39d8dd..01268a7 100644 --- a/README.md +++ b/README.md @@ -122,12 +122,16 @@ POSITIONAL ARGS: 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= 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= 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,6 +204,10 @@ 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 @@ -207,22 +215,32 @@ 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. -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: +`file_prefix` -```toml -# bibiman.toml -file_prefix = "~/Documents/literature" -``` +: 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: -```bibtex -# bibfile.bib -file = {aristotle.pdf} -``` + ```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! + +`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. 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 -- cgit v1.2.3 From 62580d8cc537808c34b0d9a0fe5554b4806a7aa6 Mon Sep 17 00:00:00 2001 From: lukeflo Date: Fri, 30 May 2025 17:36:50 +0200 Subject: typo in --help function --- README.md | 2 +- src/cliargs.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 01268a7..d2827b4 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ FLAGS: -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 + --light-terminal Enable color mode for light terminal background --pdf-dir= 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. diff --git a/src/cliargs.rs b/src/cliargs.rs index c7a0eb1..04886d1 100644 --- a/src/cliargs.rs +++ b/src/cliargs.rs @@ -130,7 +130,7 @@ FLAGS: -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 + --light-terminal Enable color mode for light terminal background --pdf-dir= 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. -- cgit v1.2.3 From d64596242ab185fffebc773ad2dcb5f1be2fccc2 Mon Sep 17 00:00:00 2001 From: lukeflo Date: Sun, 1 Jun 2025 14:54:38 +0200 Subject: some fixes and test for `file_prefix` with new bool --- src/app.rs | 27 +++++++++++++++------ src/bibiman/bibisetup.rs | 63 +++++++++++++++++++++++++++--------------------- src/bibiman/entries.rs | 2 ++ src/bibiman/search.rs | 5 +++- src/tui/popup.rs | 22 ----------------- src/tui/ui.rs | 16 +++++++++--- tests/test-config.toml | 2 +- 7 files changed, 75 insertions(+), 62 deletions(-) diff --git a/src/app.rs b/src/app.rs index f751716..15cdfe3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -350,7 +350,19 @@ impl App { entry.filepath.unwrap().iter().for_each(|p| { items.push(( "File (PDF/EPUB): ".into(), - p.clone().into_string().unwrap(), + // 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() + }, )) }); } @@ -360,7 +372,7 @@ impl App { } else { self.bibiman.open_popup( PopupKind::MessageError, - Some("Selected entry has no connected ressources: "), + Some("Selected entry has no connected resources: "), Some(&entry.citekey), None, )?; @@ -389,11 +401,12 @@ 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(); diff --git a/src/bibiman/bibisetup.rs b/src/bibiman/bibisetup.rs index 92e02fc..bf5baf5 100644 --- a/src/bibiman/bibisetup.rs +++ b/src/bibiman/bibisetup.rs @@ -53,6 +53,7 @@ pub struct BibiData { pub abstract_text: String, pub doi_url: Option, pub filepath: Option>, + pub file_field: bool, pub subtitle: Option, } @@ -115,10 +116,9 @@ 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() @@ -207,19 +207,25 @@ impl BibiSetup { 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, &mut pdf_files), - subtitle: Self::get_subtitle(k, bibliography), + .map(|(i, k)| { + let filepaths: (Option>, 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() } @@ -354,19 +360,22 @@ impl BibiSetup { citekey: &str, biblio: &Bibliography, pdf_files: &mut Option>>, - ) -> Option> { + ) -> (Option>, bool) { if biblio.get(citekey).unwrap().file().is_ok() { - Some(vec![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) + (Self::merge_filepath_or_none(&citekey, pdf_files), false) } else { - None + (None, false) } } 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/tui/popup.rs b/src/tui/popup.rs index f3f6d58..93b01c3 100644 --- a/src/tui/popup.rs +++ b/src/tui/popup.rs @@ -114,28 +114,6 @@ impl PopupArea { Text::from(helptext) } - /// Creates a popup message. The needed arguments are: - /// - /// - `message` as `str`: The message displayed in the popup. - /// - `object` as `str`: A possible object added to the message. E.g. the content - /// which gets copied to the clipboard. - /// - `msg_confirm` as `bool`: if `true` its a confirmation message displayed in - /// in the set `confirm_color` (default: green), if `false` its a warning - /// message displayed in the set `warn_color` (default: red). - 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; - } - /// Opens a popup with a selectable list /// /// The list items are passed as argument of the kind `Vec<(String, String)>`. diff --git a/src/tui/ui.rs b/src/tui/ui.rs index a998bc7..2126135 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -320,16 +320,24 @@ pub fn render_popup(app: &mut App, cfg: &BibiConfig, frame: &mut Frame) { // To find the longest line, we need to collect the chars of every item // and add. - let list_widths: Vec = app + let list_widths = app .bibiman .popup_area .popup_list .iter() - .map(|(m, o)| m.chars().count() as u16 + o.chars().count() as u16) - .collect(); + .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.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 { diff --git a/tests/test-config.toml b/tests/test-config.toml index 1b60acd..4e4f8c5 100644 --- a/tests/test-config.toml +++ b/tests/test-config.toml @@ -14,7 +14,7 @@ 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. -- cgit v1.2.3 From 1a34950a45a8fba97dca432fe36569eec1a05c8f Mon Sep 17 00:00:00 2001 From: lukeflo Date: Mon, 2 Jun 2025 10:01:15 +0200 Subject: update the README --- README.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d2827b4..a9de6f3 100644 --- a/README.md +++ b/README.md @@ -210,10 +210,12 @@ file_prefix = "/some/path/prefix" 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` + +: 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. `file_prefix` @@ -226,7 +228,7 @@ the started `bibiman` session. ``` ```latex - # bibfile.bib + % bibfile.bib file = {aristotle.pdf} ``` @@ -234,13 +236,18 @@ the started `bibiman` session. 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` : 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. 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. + 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 -- cgit v1.2.3 From 201aecebcd7c85127df9c43da01fdafc3465e53e Mon Sep 17 00:00:00 2001 From: lukeflo Date: Mon, 2 Jun 2025 17:40:50 +0200 Subject: impl check for valid filepath, update README --- README.md | 13 +++++++++---- src/app.rs | 4 ++-- src/bibiman.rs | 16 ++++++++++++++-- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a9de6f3..4c176af 100644 --- a/README.md +++ b/README.md @@ -104,10 +104,15 @@ Or through `home-manager` config file: ### Void Linux -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 diff --git a/src/app.rs b/src/app.rs index 15cdfe3..f7e7891 100644 --- a/src/app.rs +++ b/src/app.rs @@ -406,9 +406,9 @@ pub fn open_connected_file(cfg: &BibiConfig, file: &OsStr) -> Result<()> { // } else { // PathBuf::from(file) // }; - let file = 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???) diff --git a/src/bibiman.rs b/src/bibiman.rs index 583d849..ea9dbf5 100644 --- a/src/bibiman.rs +++ b/src/bibiman.rs @@ -15,6 +15,7 @@ // along with this program. If not, see . ///// +use crate::app::expand_home; use crate::bibiman::entries::EntryTableColumn; use crate::bibiman::{bibisetup::*, search::BibiSearch}; use crate::cliargs::CLIArgs; @@ -674,16 +675,27 @@ impl Bibiman { 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") { // 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(); - app::open_connected_file(cfg, &object)?; + 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(()) } -- cgit v1.2.3