pub mod errors;
use crate::{
configs::errors::{ConfigError, TomlError},
create_recursive,
};
use serde::Deserialize;
use std::{io::Read, ops::Range, sync::OnceLock};
#[derive(Debug, PartialEq)]
pub enum SeriColor {
Black,
Blue,
Cyan,
DarkBlue,
DarkCyan,
DarkGreen,
DarkGrey,
DarkMagenta,
DarkRed,
DarkYellow,
Green,
Grey,
Magenta,
None,
Red,
White,
Yellow,
}
impl<'de> Deserialize<'de> for SeriColor {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let normalizer = |s: &str| -> String { s.to_lowercase().replace(['-', '_', ' '], "") };
let s = String::deserialize(deserializer)?;
let normalized = normalizer(&s);
match normalized.as_str() {
"black" => Ok(SeriColor::Black),
"blue" => Ok(SeriColor::Blue),
"cyan" => Ok(SeriColor::Cyan),
"darkblue" => Ok(SeriColor::DarkBlue),
"darkcyan" => Ok(SeriColor::DarkCyan),
"darkgreen" => Ok(SeriColor::DarkGreen),
"darkgrey" | "darkgray" => Ok(SeriColor::DarkGrey),
"darkmagenta" => Ok(SeriColor::DarkMagenta),
"darkred" => Ok(SeriColor::DarkRed),
"darkyellow" => Ok(SeriColor::DarkYellow),
"default" => Ok(SeriColor::None),
"green" => Ok(SeriColor::Green),
"grey" | "gray" => Ok(SeriColor::Grey),
"magenta" => Ok(SeriColor::Magenta),
"red" => Ok(SeriColor::Red),
"white" => Ok(SeriColor::White),
"yellow" => Ok(SeriColor::Yellow),
_ => Err(serde::de::Error::unknown_variant(
&s,
&[
"black",
"blue",
"cyan",
"dark-blue",
"dark-cyan",
"dark-green",
"dark-grey",
"dark-magenta",
"dark-red",
"dark-yellow",
"default",
"green",
"grey",
"magenta",
"red",
"white",
"yellow",
],
)),
}
}
}
impl From<&SeriColor> for crossterm::style::Color {
fn from(value: &SeriColor) -> crossterm::style::Color {
match value {
SeriColor::Black => crossterm::style::Color::Black,
SeriColor::Blue => crossterm::style::Color::Blue,
SeriColor::Cyan => crossterm::style::Color::Cyan,
SeriColor::DarkBlue => crossterm::style::Color::DarkBlue,
SeriColor::DarkCyan => crossterm::style::Color::DarkCyan,
SeriColor::DarkGreen => crossterm::style::Color::DarkGreen,
SeriColor::DarkGrey => crossterm::style::Color::DarkGrey,
SeriColor::DarkMagenta => crossterm::style::Color::DarkMagenta,
SeriColor::DarkRed => crossterm::style::Color::DarkRed,
SeriColor::DarkYellow => crossterm::style::Color::DarkYellow,
SeriColor::Green => crossterm::style::Color::Green,
SeriColor::Grey => crossterm::style::Color::Grey,
SeriColor::Magenta => crossterm::style::Color::Magenta,
SeriColor::None => crossterm::style::Color::Reset,
SeriColor::Red => crossterm::style::Color::Red,
SeriColor::White => crossterm::style::Color::White,
SeriColor::Yellow => crossterm::style::Color::Yellow,
}
}
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct Appearance {
#[serde(default = "default_fg")]
pub fg: SeriColor,
#[serde(default = "default_bg")]
pub bg: SeriColor,
#[serde(default = "default_hl_fg")]
pub hl_fg: SeriColor,
#[serde(default = "default_hl_bg")]
pub hl_bg: SeriColor,
}
fn default_hl_fg() -> SeriColor {
SeriColor::Black
}
fn default_hl_bg() -> SeriColor {
SeriColor::White
}
fn default_fg() -> SeriColor {
SeriColor::Green
}
fn default_bg() -> SeriColor {
SeriColor::None
}
impl Default for Appearance {
fn default() -> Self {
Self {
fg: SeriColor::Green,
bg: SeriColor::None,
hl_fg: SeriColor::Black,
hl_bg: SeriColor::White,
}
}
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct Defaults {
#[serde(default = "default_out_dir")]
pub out_dir: String,
}
impl Default for Defaults {
fn default() -> Self {
Self {
out_dir: "./".to_string(),
}
}
}
fn default_out_dir() -> String {
"./".to_string()
}
#[derive(Default, Debug, Deserialize, PartialEq)]
pub struct Config {
#[serde(default)]
pub appearance: Appearance,
#[serde(default)]
pub defaults: Defaults,
}
static CONFIG: OnceLock<Config> = OnceLock::new();
pub fn initialize_config() -> miette::Result<(), ConfigError> {
let config: Config = if let Ok(config_file) = get_config_file() {
let mut file = std::fs::File::open(config_file).expect("File should exist");
let mut contents = String::new();
file.read_to_string(&mut contents)?;
toml::from_str(&contents).map_err(|e| {
TomlError::new(
e.span().unwrap_or(Range { start: 0, end: 0 }),
contents,
e.message().to_string(),
)
})?
} else {
Config::default()
};
CONFIG
.set(config)
.map_err(|_| ConfigError::AlreadyInitialized)?;
Ok(())
}
pub fn get_config() -> &'static Config {
CONFIG.get().expect("Config not initialized")
}
fn get_conf_dir() -> std::path::PathBuf {
let mut user_home_dir = std::env::home_dir().expect("Failed to get home directory");
if cfg!(target_os = "windows") {
user_home_dir.push(".config\\sericom");
} else {
user_home_dir.push(".config/sericom");
}
let user_conf_dir = user_home_dir;
create_recursive!(user_conf_dir.as_path());
user_conf_dir
}
fn get_config_file() -> miette::Result<std::path::PathBuf, ConfigError> {
let mut conf_dir = get_conf_dir();
conf_dir.push("config.toml");
let conf_file = conf_dir;
if conf_file.exists() && conf_file.is_file() {
Ok(conf_file)
} else {
Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Could not find config file.").into())
}
}
#[test]
fn parse_test_config() {
let file: Config = toml::from_str(
r#"
[appearance]
fg = "dark-grey"
bg = "red"
hl_fg = "white"
hl_bg = "blue"
[defaults]
out_dir = "$HOME/.configs"
"#,
)
.unwrap();
let parsed_conf = Config {
appearance: Appearance {
fg: SeriColor::DarkGrey,
bg: SeriColor::Red,
hl_fg: SeriColor::White,
hl_bg: SeriColor::Blue,
},
defaults: Defaults {
out_dir: "$HOME/.configs".to_string(),
},
};
assert_eq!(file, parsed_conf)
}
#[test]
fn check_conf_dir_is_dir() {
let dir = get_conf_dir();
assert!(std::fs::metadata(dir).unwrap().is_dir())
}
#[test]
fn valid_conf_dir() {
let dir = get_conf_dir();
if cfg!(target_family = "windows") {
assert_eq!(dir.to_str().unwrap(), "C:\\Users\\Thomas\\.config\\sericom")
} else {
assert_eq!(dir.to_str().unwrap(), "/home/thomas/.config/sericom")
}
}