// 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"
"##;
/// 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(())
});
}
}