use crate::error::{Error, Result};
use crate::rules::RuleDef;
use crate::theme::Theme;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RulesMode {
#[default]
Prepend,
Extend,
Replace,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Config {
pub theme: Option<String>,
pub rules_mode: RulesMode,
pub themes: Vec<Theme>,
pub rules: Vec<RuleDef>,
pub modules: Vec<String>,
}
impl Config {
pub fn default_path() -> Option<PathBuf> {
dirs::config_dir().map(|d| d.join("zcolorizer").join("config.toml"))
}
pub fn load(path: &Path) -> Result<Config> {
let text = std::fs::read_to_string(path).map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})?;
Config::parse(&text, path)
}
pub fn parse(text: &str, path: &Path) -> Result<Config> {
toml::from_str(text).map_err(|source| Error::ConfigParse {
path: path.to_path_buf(),
source,
})
}
pub fn load_default() -> Result<Config> {
match Config::default_path() {
Some(p) if p.exists() => Config::load(&p),
_ => Ok(Config::default()),
}
}
pub fn resolve_theme(&self, name: Option<&str>) -> Result<Theme> {
let want = name
.or(self.theme.as_deref())
.unwrap_or(crate::theme::DEFAULT_THEME);
let user = self.themes.iter().find(|t| t.name.eq_ignore_ascii_case(want));
match crate::theme::builtin(want) {
Some(base) => Ok(match user {
Some(u) => base.merged_with(u),
None => base,
}),
None => user
.cloned()
.ok_or_else(|| Error::UnknownTheme(want.to_string())),
}
}
pub fn resolve_rule_defs(&self) -> Vec<RuleDef> {
let generic = crate::rules::builtin_generic();
let module_rules = crate::modules::resolve(&self.modules).unwrap_or_else(|_| {
let known: Vec<String> = self
.modules
.iter()
.filter(|n| {
n.eq_ignore_ascii_case("all") || crate::modules::get(n).is_some()
})
.cloned()
.collect();
crate::modules::resolve(&known).unwrap_or_default()
});
match self.rules_mode {
RulesMode::Replace => {
let mut v = self.rules.clone();
v.extend(module_rules);
v
}
RulesMode::Extend => {
let mut v = module_rules;
v.extend(generic);
v.extend(self.rules.iter().cloned());
v
}
RulesMode::Prepend => {
let mut v = self.rules.clone();
v.extend(module_rules);
v.extend(generic);
v
}
}
}
pub fn unknown_modules(&self) -> Vec<String> {
match crate::modules::resolve(&self.modules) {
Ok(_) => Vec::new(),
Err(unknown) => unknown,
}
}
pub fn available_theme_names(&self) -> Vec<String> {
let mut names: Vec<String> =
crate::theme::builtins().into_iter().map(|t| t.name).collect();
for t in &self.themes {
if !names.iter().any(|n| n.eq_ignore_ascii_case(&t.name)) {
names.push(t.name.clone());
}
}
names
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_config_resolves_default_theme() {
let c = Config::default();
let t = c.resolve_theme(None).unwrap();
assert_eq!(t.name, crate::theme::DEFAULT_THEME);
}
#[test]
fn cyberpunk_alias_still_works() {
let c = Config::default();
assert_eq!(c.resolve_theme(Some("cyberpunk")).unwrap().name, "neon-sprawl");
}
#[test]
fn module_rules_sit_before_generic() {
let mut c = Config::default();
c.modules.push("syslog".into());
let defs = c.resolve_rule_defs();
let syslog_idx = defs.iter().position(|d| d.name == "syslog-line").unwrap();
let number_idx = defs.iter().position(|d| d.name == "number").unwrap();
assert!(syslog_idx < number_idx, "module rule must precede generic number rule");
}
#[test]
fn name_override_beats_config_theme() {
let c = Config { theme: Some("cyberpunk".into()), ..Config::default() };
let t = c.resolve_theme(Some("ccze-classic")).unwrap();
assert_eq!(t.name, "ccze-classic");
}
#[test]
fn extend_mode_appends_user_rules() {
let mut c = Config { rules_mode: RulesMode::Extend, ..Config::default() };
c.rules.push(RuleDef::with_token("x", "FOO", "error"));
let defs = c.resolve_rule_defs();
assert!(defs.len() > 1);
assert_eq!(defs.last().unwrap().name, "x");
}
#[test]
fn prepend_is_default_and_user_rules_lead() {
let mut c = Config::default();
c.rules.push(RuleDef::with_token("x", "FOO", "error"));
let defs = c.resolve_rule_defs();
assert_eq!(c.rules_mode, RulesMode::Prepend);
assert_eq!(defs.first().unwrap().name, "x");
}
#[test]
fn replace_mode_drops_builtins() {
let mut c = Config { rules_mode: RulesMode::Replace, ..Config::default() };
c.rules.push(RuleDef::with_token("x", "FOO", "error"));
let defs = c.resolve_rule_defs();
assert_eq!(defs.len(), 1);
}
#[test]
fn parse_minimal_toml() {
let c = Config::parse("theme = \"ccze-classic\"\n", Path::new("test")).unwrap();
assert_eq!(c.theme.as_deref(), Some("ccze-classic"));
}
#[test]
fn user_theme_overrides_builtin_token() {
let toml = r##"
theme = "cyberpunk"
[[themes]]
name = "cyberpunk"
[themes.styles.error]
fg = "#123456"
"##;
let c = Config::parse(toml, Path::new("test")).unwrap();
let t = c.resolve_theme(None).unwrap();
assert_eq!(t.style("error").fg, Some(crate::color::Color::hex("#123456")));
assert!(t.style("good").fg.is_some());
}
}