use std::collections::HashMap;
use std::path::PathBuf;
use serde::Deserialize;
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct Config {
pub default_namespace: Option<String>,
pub default_resource: Option<String>,
pub aliases: HashMap<String, String>,
pub plugins: Vec<Plugin>,
pub skin: Skin,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct Skin {
pub name: Option<String>,
pub colors: HashMap<String, String>,
pub contexts: HashMap<String, String>,
pub background: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Plugin {
pub key: char,
pub name: String,
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub scopes: Vec<String>,
}
impl Config {
pub fn load() -> Self {
let Some(path) = config_path() else {
return Self::default();
};
let Ok(text) = std::fs::read_to_string(&path) else {
return Self::default();
};
match toml::from_str(&text) {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("warning: ignoring invalid {}: {e}", path.display());
Self::default()
}
}
}
}
fn config_path() -> Option<PathBuf> {
let base = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
Some(base.join("sofka").join("config.toml"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_full_config() {
let toml = r#"
default_namespace = "kube-system"
default_resource = "deployments"
[aliases]
dep = "deployments"
[[plugins]]
key = "g"
name = "argocd-sync"
command = "argocd"
args = ["app", "sync", "$NAME"]
scopes = ["deployments"]
"#;
let cfg: Config = toml::from_str(toml).unwrap();
assert_eq!(cfg.default_namespace.as_deref(), Some("kube-system"));
assert_eq!(
cfg.aliases.get("dep").map(String::as_str),
Some("deployments")
);
assert_eq!(cfg.plugins.len(), 1);
let p = &cfg.plugins[0];
assert_eq!(p.key, 'g');
assert_eq!(p.args, vec!["app", "sync", "$NAME"]);
assert_eq!(p.scopes, vec!["deployments"]);
}
#[test]
fn parses_per_context_skins() {
let toml = r#"
[skin]
name = "gruvbox-dark"
[skin.contexts]
prod = "catppuccin-latte"
"#;
let cfg: Config = toml::from_str(toml).unwrap();
assert_eq!(cfg.skin.name.as_deref(), Some("gruvbox-dark"));
assert_eq!(
cfg.skin.contexts.get("prod").map(String::as_str),
Some("catppuccin-latte")
);
}
#[test]
fn empty_config_is_default() {
let cfg: Config = toml::from_str("").unwrap();
assert!(cfg.aliases.is_empty());
assert!(cfg.plugins.is_empty());
assert!(cfg.default_resource.is_none());
}
}