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 description: Option<String>,
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(default)]
pub platform: Option<toml::Value>,
#[serde(flatten)]
pub settings: HashMap<String, toml::Value>,
}
impl ConditionalOverride {
pub(super) fn matches(&self) -> bool {
let env_ok = 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,
}
});
let platform_ok = match &self.platform {
None => true,
Some(toml::Value::String(want)) => want == std::env::consts::OS,
Some(toml::Value::Array(items)) => items.iter().any(|item| match item {
toml::Value::String(s) => s == std::env::consts::OS,
_ => false,
}),
Some(_) => true,
};
env_ok && platform_ok
}
pub fn explain(&self) -> Vec<ConditionOutcome> {
let mut out = Vec::new();
let mut env_keys: Vec<_> = self.env.keys().collect();
env_keys.sort();
for key in env_keys {
let condition = &self.env[key];
let actual = env::var(key).unwrap_or_default();
let matched = 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,
};
out.push(ConditionOutcome {
description: format!("env.{key} = {}", condition_repr(condition)),
matched,
});
}
if let Some(p) = &self.platform {
let current = std::env::consts::OS;
let matched = match p {
toml::Value::String(want) => want == current,
toml::Value::Array(items) => items.iter().any(|item| match item {
toml::Value::String(s) => s == current,
_ => false,
}),
_ => true,
};
out.push(ConditionOutcome {
description: format!("platform = {}", condition_repr(p)),
matched,
});
}
out
}
}
#[derive(Debug, Clone)]
pub struct ConditionOutcome {
pub description: String,
pub matched: bool,
}
fn condition_repr(v: &toml::Value) -> String {
match v {
toml::Value::String(s) => format!("\"{s}\""),
toml::Value::Boolean(b) => b.to_string(),
toml::Value::Array(items) => {
let parts: Vec<String> = items.iter().map(condition_repr).collect();
format!("[{}]", parts.join(", "))
}
other => other.to_string(),
}
}
#[derive(Debug, Default, Deserialize, Clone)]
#[serde(default)]
pub struct PersonalityDef {
pub description: Option<String>,
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)
}
}