// bibiman - a TUI for managing BibLaTeX databases // Copyright (C) 2024 lukeflo // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . ///// use std::{ fs::{File, create_dir_all}, io::{Write, stdin}, path::PathBuf, str::FromStr, }; use color_eyre::{eyre::Result, owo_colors::OwoColorize}; use figment::{ Figment, providers::{Format, Serialized, Toml}, }; use ratatui::style::Color; use serde::{Deserialize, Serialize}; use crate::{ bibiman::{bibisetup::CustomField, citekeys::CitekeyCase}, cliargs::CLIArgs, }; pub const IGNORED_SPECIAL_CHARS: [char; 33] = [ '?', '!', '\\', '\'', '.', '-', '–', ':', ',', '[', ']', '(', ')', '{', '}', '§', '$', '%', '&', '/', '`', '´', '#', '+', '*', '=', '|', '<', '>', '^', '°', '_', '"', ]; const DEFAULT_CONFIG: &str = r##" # [general] ## Default files/dirs which are loaded on startup ## Use absolute paths (~ for HOME works). Otherwise, loading might not work. # bibfiles = [ "/path/to/bibfile", "path/to/dir/with/bibfiles" ] ## Default editor to use when editing files. Arguments are possible # editor = "vim" # with args: "vim -y" ## Default app to open PDFs/Epubs # pdf_opener = "xdg-open" ## Default app to open URLs/DOIs # url_opener = "xdg-open" ## Prefix which is prepended to the filepath from the `file` field ## Use absolute paths (~ for HOME works). Otherwise, loading might not work. # file_prefix = "/some/path/prefix" ## Path to folder (with subfolders) containing PDF files with the basename ## of the format "citekey.pdf". Other PDF basenames are not accepted. ## Use absolute paths (~ for HOME works). Otherwise, loading might not work. # pdf_path = "/path/to/pdf/folder" ## Path to folder (with subfolders) containing note files with the basename of ## the format "citekey.extension". Other basenames are not accepted. The possible ## extensions can be set through the "note_extensions" array. # note_path = "/path/to/notes/folder" # note_extensions = [ "md", "txt", "org" ] ## Symbols/chars to show if not has specific attachement # note_symbol = "N" # file_symbol = "F" # link_symbol = "L" ## Select a custom column beside standard "author", "title" and "year" ## Possible values are "journaltitle", "organization", "instituion", "publisher" ## and "pubtype" (which is the default) # custom_column = "pubtype" # [colors] ## Default values for dark-themed terminal ## Possible values are: ## ANSI color names. E.g. "bright-black" ## 256-colors indices. E.g. "250" ## Hex color codes. E.g. "#3a3a3a" # main_text_color = "250" # highlight_text_color = "254" # entry_color = "36" # keyword_color = "101" # info_color = "99" # confirm_color = "47" # warn_color = "124" # bar_bg_color = "234" # popup_fg_color = "43" # popup_bg_color = "234" # selected_row_bg_color = "237" # note_color = "123" # file_color = "209" # link_color = "39" # author_color = "38" # title_color = "37" # year_color = "135" # [citekey_formatter] ## Define the patterns for creating citekeys. Every item of the array consists of ## five components separated by semicolons. Despite the field name every component ## can be left blank: ## - name of the biblatex field ("author", "title"...) ## - number of max words from the given field ## - number of chars used from each word ## - delimiter to separate words of the same field ## - trailing delimiter separating the current field from the following # fields = [ "author;2;;-;_", "title;3;6;_;_", "year" ] ## Convert chars to specified case. Possible values: ## "upper", "uppercase", "lower", "lowercase" # case = "lowercase" "##; /// Main struct of the config file. Contains substructs/headings in toml #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct BibiConfig { pub general: General, pub colors: Colors, pub citekey_formatter: CitekeyFormatter, } /// Substruct [general] in config.toml #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct General { pub bibfiles: Option>, pub editor: Option, pub pdf_opener: String, pub url_opener: String, pub file_prefix: Option, pub pdf_path: Option, pub note_path: Option, pub note_extensions: Option>, pub note_symbol: String, pub file_symbol: String, pub link_symbol: String, pub custom_column: CustomField, } /// Substruct [colors] in config.toml #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct Colors { pub main_text_color: Color, pub highlight_text_color: Color, pub entry_color: Color, pub keyword_color: Color, pub info_color: Color, pub confirm_color: Color, pub warn_color: Color, pub bar_bg_color: Color, pub popup_fg_color: Color, pub popup_bg_color: Color, pub selected_row_bg_color: Color, pub note_color: Color, pub file_color: Color, pub link_color: Color, pub author_color: Color, pub title_color: Color, pub year_color: Color, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct CitekeyFormatter { pub fields: Option>, pub case: Option, } impl Default for BibiConfig { fn default() -> Self { Self { general: General { bibfiles: None, editor: None, pdf_opener: select_opener(), url_opener: select_opener(), file_prefix: None, pdf_path: None, note_path: None, note_extensions: None, note_symbol: String::from("N"), file_symbol: String::from("F"), link_symbol: String::from("L"), custom_column: CustomField::Pubtype, }, colors: Self::dark_colors(), citekey_formatter: CitekeyFormatter { fields: None, case: None, }, } } } impl BibiConfig { pub fn new(args: &CLIArgs) -> Self { Self { general: General { bibfiles: None, editor: None, pdf_opener: select_opener(), url_opener: select_opener(), file_prefix: None, pdf_path: None, note_path: None, note_extensions: None, note_symbol: String::from("N"), file_symbol: String::from("F"), link_symbol: String::from("L"), custom_column: CustomField::Pubtype, }, colors: if args.light_theme { Self::light_colors() } else { Self::dark_colors() }, citekey_formatter: CitekeyFormatter { fields: None, case: None, }, } } pub fn parse_config(args: &CLIArgs) -> Result { let cfg_file: BibiConfig = if args.cfg_path.as_ref().unwrap().is_file() { Figment::from(Serialized::defaults(BibiConfig::new(args))) .merge(Toml::file(&args.cfg_path.as_ref().unwrap())) .extract()? } else { BibiConfig::new(args) }; 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(); } } /// Standard color scheme for terminals with dark background (default) pub fn dark_colors() -> Colors { Colors { main_text_color: Color::Indexed(250), highlight_text_color: Color::Indexed(254), entry_color: Color::Indexed(36), keyword_color: Color::Indexed(101), info_color: Color::Indexed(99), confirm_color: Color::Indexed(47), warn_color: Color::Indexed(124), bar_bg_color: Color::Indexed(235), popup_fg_color: Color::Indexed(43), popup_bg_color: Color::Indexed(234), selected_row_bg_color: Color::Indexed(237), note_color: Color::Indexed(123), file_color: Color::Indexed(209), link_color: Color::Indexed(39), author_color: Color::Indexed(38), title_color: Color::Indexed(37), year_color: Color::Indexed(135), } } /// Activates the default color scheme for light background terminals pub fn light_colors() -> Colors { Colors { main_text_color: Color::Indexed(235), highlight_text_color: Color::Indexed(232), entry_color: Color::Indexed(23), keyword_color: Color::Indexed(58), info_color: Color::Indexed(57), bar_bg_color: Color::Indexed(144), popup_fg_color: Color::Indexed(43), popup_bg_color: Color::Indexed(187), confirm_color: Color::Indexed(22), warn_color: Color::Indexed(124), selected_row_bg_color: Color::Indexed(107), note_color: Color::Indexed(123), file_color: Color::Indexed(209), link_color: Color::Indexed(27), author_color: Color::Indexed(38), title_color: Color::Indexed(37), year_color: Color::Indexed(135), } } /// Function which offers the user to create a default config /// if no exists at the standard config path. pub fn create_default_config(args: &CLIArgs) { let path = args.cfg_path.as_ref().unwrap().to_str(); let mut input_str = String::new(); match path { Some(p) => { println!("It seems no config file {} exists.", p.bold()); } None => { println!( "Can't parse config file path. Running {} without any config file.", "bibiman".bold() ); return; } } loop { println!( "\nDo you want to create a default config? {}", "[Y|N]".bold() ); stdin() .read_line(&mut input_str) .expect("Couldn't read input"); match input_str.trim().to_lowercase().as_str() { "y" | "yes" => { break; } "n" | "no" => { println!("\nNo config file will be created."); return; } v => { println!("\nInvalid value {}.", v.red()); println!("Please type {} or {}.", "[Y]es".bold(), "[N]o".bold()); input_str.clear(); continue; } } } { // Ignore any errors of this function, if something goes wrong creating a file will fail too. let mut dirpath = PathBuf::from_str(path.unwrap()).unwrap_or_else(|_| PathBuf::new()); dirpath.pop(); let _ = create_dir_all(dirpath); } let cfg_file = File::create_new(path.unwrap()); match cfg_file { Ok(mut file) => { file.write_all(DEFAULT_CONFIG.as_bytes()).unwrap(); println!("\nCreated default config file {}", path.unwrap().bold()); println!( "Check {} for explanations how to configure it.", "https://codeberg.org/lukeflo/bibiman#configuration".bright_yellow() ) } Err(e) => { println!( "\nCouldn't create default config due to the following error:\n{}", e.red() ) } } } } fn select_opener() -> String { match std::env::consts::OS { "linux" => String::from("xdg-open"), "macos" | "apple" => String::from("open"), "windows" => String::from("start"), "freebsd" => String::from("xdg-open"), "openbsd" | "netbsd" | "solaris" | "redox" => String::from("open"), _ => panic!("Couldn't detect OS for setting correct opener"), } } #[cfg(test)] mod tests { use figment::{ Figment, providers::{Format, Toml}, }; use super::BibiConfig; #[test] fn parse_default_config() { figment::Jail::expect_with(|jail| { jail.create_file( "bibiman.toml", r#" [general] pdf_opener = "xdg-open" url_opener = "xdg-open" note_symbol = "N" file_symbol = "F" link_symbol = "L" custom_column = "pubtype" [colors] main_text_color = "250" highlight_text_color = "254" entry_color = "36" keyword_color = "101" info_color = "99" confirm_color = "47" warn_color = "124" bar_bg_color = "235" popup_fg_color = "43" popup_bg_color = "234" selected_row_bg_color = "237" note_color = "123" file_color = "209" link_color = "39" author_color = "38" title_color = "37" year_color = "135" [citekey_formatter] "#, )?; let config: BibiConfig = Figment::new().merge(Toml::file("bibiman.toml")).extract()?; let default_config: BibiConfig = BibiConfig::default(); assert_eq!(config, default_config); assert_eq!(config.general.bibfiles, None); assert_eq!(config.general.editor, None); Ok(()) }); } }