use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;
use serde::Deserialize;
use super::settings::settings_to_args;
pub const CONFIG_VERSION: &str = "0.6";
pub(super) const ACCEPTED_VERSIONS: &[&str] = &["0.3", "0.4", "0.5", "0.6"];
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct Config {
pub version: Option<String>,
#[serde(default)]
pub format: HashMap<String, Vec<String>>,
#[serde(default)]
pub personality: HashMap<String, PersonalityDef>,
#[serde(default)]
pub theme: HashMap<String, ThemeDef>,
#[serde(default)]
pub style: HashMap<String, StyleDef>,
#[serde(default)]
pub class: HashMap<String, Vec<String>>,
#[serde(skip)]
pub drop_in_paths: Vec<PathBuf>,
}
impl Config {
pub(super) fn merge(&mut self, other: Config) {
for (k, v) in other.format { self.format.insert(k, v); }
for (k, v) in other.personality { self.personality.insert(k, v); }
for (k, v) in other.theme { self.theme.insert(k, v); }
for (k, v) in other.style { self.style.insert(k, v); }
for (k, v) in other.class { self.class.insert(k, v); }
}
}
#[derive(Debug, Default, Deserialize, Clone)]
#[serde(default, rename_all = "kebab-case")]
pub struct ThemeDef {
pub inherits: Option<String>,
pub use_style: Option<String>,
#[serde(flatten)]
pub ui: HashMap<String, String>,
}
#[derive(Debug, Default, Deserialize, Clone)]
#[serde(default)]
pub struct StyleDef {
#[serde(default, rename = "class")]
pub classes: HashMap<String, String>,
#[serde(flatten)]
pub patterns: HashMap<String, String>,
}
#[derive(Debug, Default, Deserialize, Clone)]
#[serde(default)]
pub struct ConditionalOverride {
#[serde(default)]
pub env: HashMap<String, toml::Value>,
#[serde(flatten)]
pub settings: HashMap<String, toml::Value>,
}
impl ConditionalOverride {
pub(super) fn matches(&self) -> bool {
self.env.iter().all(|(key, condition)| {
let actual = env::var(key).unwrap_or_default();
match condition {
toml::Value::String(expected) => super::load::match_string(&actual, expected),
toml::Value::Array(items) => items.iter().any(|item| match item {
toml::Value::String(s) => super::load::match_string(&actual, s),
_ => false,
}),
toml::Value::Boolean(true) => env::var(key).is_ok(),
toml::Value::Boolean(false) => env::var(key).is_err(),
_ => true,
}
})
}
}
#[derive(Debug, Default, Deserialize, Clone)]
#[serde(default)]
pub struct PersonalityDef {
pub inherits: Option<String>,
pub format: Option<String>,
pub columns: Option<StringOrList>,
#[serde(default)]
pub when: Vec<ConditionalOverride>,
#[serde(flatten)]
pub settings: HashMap<String, toml::Value>,
}
impl PersonalityDef {
pub fn to_args(&self) -> Vec<OsString> {
let mut args = Vec::new();
if let Some(ref cols) = self.columns {
args.push(format!("--columns={}", cols.to_csv()).into());
} else if let Some(ref fmt) = self.format {
args.push(format!("--format={fmt}").into());
}
args.extend(settings_to_args(&self.settings, "[personality]"));
args
}
}
#[derive(Debug, Clone)]
pub enum StringOrList {
Str(String),
List(Vec<String>),
}
impl StringOrList {
pub fn to_csv(&self) -> String {
match self {
Self::Str(s) => s.clone(),
Self::List(v) => v.join(","),
}
}
}
impl<'de> Deserialize<'de> for StringOrList {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: serde::Deserializer<'de>,
{
use serde::de;
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = StringOrList;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a string or array of strings")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<StringOrList, E> {
Ok(StringOrList::Str(v.to_string()))
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<StringOrList, A::Error> {
let mut v = Vec::new();
while let Some(s) = seq.next_element::<String>()? {
v.push(s);
}
Ok(StringOrList::List(v))
}
}
deserializer.deserialize_any(Visitor)
}
}