use std::path::PathBuf;
use serde::Deserialize;
use crate::snapshot::OnCollision;
#[derive(Debug, Clone, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub output_dir: Option<String>,
pub prefix: String,
pub on_collision: OnCollision,
}
impl Default for Config {
fn default() -> Self {
Self {
output_dir: None,
prefix: "snapshooter".into(),
on_collision: OnCollision::Suffix,
}
}
}
impl Config {
pub fn output_dir_path(&self) -> Option<PathBuf> {
self.output_dir.as_deref().map(expand_tilde)
}
}
fn config_path() -> Option<PathBuf> {
let base = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
Some(base.join("camshooter").join("config.toml"))
}
fn expand_tilde(p: &str) -> PathBuf {
if let Some(home) = std::env::var_os("HOME") {
if p == "~" {
return PathBuf::from(home);
}
if let Some(rest) = p.strip_prefix("~/") {
return PathBuf::from(home).join(rest);
}
}
PathBuf::from(p)
}
pub fn load() -> Result<Config, String> {
let cfg = read_config()?;
if cfg.prefix.contains('/') || cfg.prefix.contains('\\') {
return Err(format!(
"config `prefix` {:?} must not contain path separators",
cfg.prefix
));
}
Ok(cfg)
}
fn read_config() -> Result<Config, String> {
let Some(path) = config_path() else {
return Ok(Config::default());
};
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Config::default()),
Err(e) => return Err(format!("failed to read {}: {e}", path.display())),
};
toml::from_str::<Config>(&text)
.map_err(|e| format!("invalid config at {}:\n{e}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_file_is_all_defaults() {
let cfg: Config = toml::from_str("").unwrap();
assert_eq!(cfg.prefix, "snapshooter");
assert_eq!(cfg.on_collision, OnCollision::Suffix);
assert!(cfg.output_dir.is_none());
}
#[test]
fn parses_a_full_config() {
let cfg: Config = toml::from_str(
r#"
output_dir = "/tmp/shots"
prefix = "cam"
on_collision = "overwrite"
"#,
)
.unwrap();
assert_eq!(cfg.output_dir.as_deref(), Some("/tmp/shots"));
assert_eq!(cfg.prefix, "cam");
assert_eq!(cfg.on_collision, OnCollision::Overwrite);
}
#[test]
fn unknown_key_is_rejected() {
let err = toml::from_str::<Config>("nope = 1");
assert!(err.is_err(), "unknown keys should be rejected");
}
}