// 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::{create_dir_all, File},
io::{stdin, Write},
path::PathBuf,
str::FromStr,
};
use color_eyre::{eyre::Result, owo_colors::OwoColorize};
use figment::{
providers::{Format, Serialized, Toml},
Figment,
};
use ratatui::style::Color;
use serde::{Deserialize, Serialize};
use crate::cliargs::CLIArgs;
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"
# [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_bg_color = "234"
# selected_row_bg_color = "237"
"##;
/// 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,
}
/// 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,
}
/// 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_bg_color: Color,
pub selected_row_bg_color: Color,
}
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,
},
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_bg_color: Color::Indexed(234),
selected_row_bg_color: Color::Indexed(237),
},
}
}
}
impl BibiConfig {
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::default()))
.merge(Toml::file(&args.cfg_path.as_ref().unwrap()))
.extract()?
} else {
BibiConfig::default()
};
Ok(cfg_file)
}
/// overwright config values with values set explicitly through the
/// command line interface
pub fn cli_overwrite(&mut self, args: &CLIArgs) {
if args.pdf_path.is_some() {
self.general.pdf_path = args.pdf_path.clone();
}
}
/// Activates the default color scheme for light background terminals
pub fn light_colors(&mut self) {
self.colors.main_text_color = Color::Indexed(235);
self.colors.highlight_text_color = Color::Indexed(232);
self.colors.entry_color = Color::Indexed(23);
self.colors.keyword_color = Color::Indexed(58);
self.colors.info_color = Color::Indexed(57);
self.colors.bar_bg_color = Color::Indexed(144);
self.colors.popup_bg_color = Color::Indexed(187);
self.colors.confirm_color = Color::Indexed(22);
self.colors.selected_row_bg_color = Color::Indexed(107);
}
/// 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::{
providers::{Format, Toml},
Figment,
};
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"
[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_bg_color = "234"
selected_row_bg_color = "237"
"#,
)?;
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(())
});
}
}