use std::path::PathBuf;
#[cfg(test)]
mod tests {
use super::*;
fn tmp_config(name: &str, content: &str) -> PathBuf {
let path = std::env::temp_dir().join(format!("mdr_test_config_{}.kdl", name));
let _ = std::fs::remove_file(&path);
std::fs::write(&path, content).unwrap();
path
}
#[test]
fn default_config_is_valid_kdl_v2() {
kdl::KdlDocument::parse_v2(DEFAULT_CONFIG).expect("DEFAULT_CONFIG must be valid KDL v2");
}
#[test]
fn load_parses_backend_unquoted() {
let path = tmp_config("backend", "backend webview\n");
let cfg = load(&path).unwrap();
assert_eq!(cfg.backend.as_deref(), Some("webview"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn load_parses_backend_quoted() {
let path = tmp_config("backend_quoted", "backend \"egui\"\n");
let cfg = load(&path).unwrap();
assert_eq!(cfg.backend.as_deref(), Some("egui"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn load_parses_verbose_bool() {
let path = tmp_config("verbose_true", "verbose #true\n");
let cfg = load(&path).unwrap();
assert_eq!(cfg.verbose, Some(true));
let path2 = tmp_config("verbose_false", "verbose #false\n");
let cfg2 = load(&path2).unwrap();
assert_eq!(cfg2.verbose, Some(false));
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(&path2);
}
#[test]
fn load_bare_verbose_node_means_true() {
let path = tmp_config("verbose_bare", "verbose\n");
let cfg = load(&path).unwrap();
assert_eq!(cfg.verbose, Some(true));
let _ = std::fs::remove_file(&path);
}
#[test]
fn load_parses_offline_bool() {
let path = tmp_config("offline_true", "offline #true\n");
let cfg = load(&path).unwrap();
assert_eq!(cfg.offline, Some(true));
let path2 = tmp_config("offline_false", "offline #false\n");
let cfg2 = load(&path2).unwrap();
assert_eq!(cfg2.offline, Some(false));
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(&path2);
}
#[test]
fn load_bare_offline_node_means_true() {
let path = tmp_config("offline_bare", "offline\n");
let cfg = load(&path).unwrap();
assert_eq!(cfg.offline, Some(true));
let _ = std::fs::remove_file(&path);
}
#[test]
fn load_parses_theme() {
let path = tmp_config("theme_light", "theme \"light\"\n");
assert_eq!(load(&path).unwrap().theme.as_deref(), Some("light"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn an_unknown_theme_is_refused_rather_than_stored() {
let path = tmp_config("theme_bogus", "theme \"neon\"\n");
assert_eq!(load(&path).unwrap().theme, None);
let _ = std::fs::remove_file(&path);
}
#[test]
fn load_returns_defaults_for_missing_file() {
let path = PathBuf::from("/nonexistent/mdr_no_such_config.kdl");
let cfg = load(&path).unwrap();
assert!(cfg.backend.is_none());
assert!(cfg.verbose.is_none());
assert!(cfg.offline.is_none());
assert!(cfg.theme.is_none());
}
#[test]
fn write_default_creates_valid_kdl_v2_file() {
let path = std::env::temp_dir().join("mdr_test_config_write_default.kdl");
let _ = std::fs::remove_file(&path);
write_default(&path).unwrap();
assert!(path.exists());
let content = std::fs::read_to_string(&path).unwrap();
kdl::KdlDocument::parse_v2(&content).expect("written config must be valid KDL v2");
assert!(content.contains("backend webview"));
assert!(content.contains("offline"));
assert!(content.contains("theme"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn write_default_errors_if_file_exists() {
let path = tmp_config("write_exists", "// existing\n");
assert!(write_default(&path).is_err());
let _ = std::fs::remove_file(&path);
}
}
#[derive(Default, Debug)]
pub struct Config {
pub backend: Option<String>,
pub verbose: Option<bool>,
pub offline: Option<bool>,
pub theme: Option<String>,
}
const DEFAULT_CONFIG: &str = "\
// mdr configuration — https://github.com/CleverCloud/mdr
// Rendering backend: auto, egui, webview, tui
backend webview
// Uncomment to enable verbose logging by default
// verbose #true
// Uncomment to never access the network (remote images are not downloaded)
// offline #true
// Colour scheme the terminal backend assumes for syntax highlighting:
// auto (read COLORFGBG, fall back to dark), dark, or light
// theme \"auto\"
";
pub fn default_path() -> PathBuf {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."))
.join(".config/mdr/config.kdl")
}
pub fn write_default(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
if path.exists() {
return Err(format!(
"config file already exists at '{}' — delete it first to reinitialise",
path.display()
)
.into());
}
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
std::fs::write(path, DEFAULT_CONFIG)?;
Ok(())
}
pub fn load(path: &PathBuf) -> Result<Config, Box<dyn std::error::Error>> {
if !path.exists() {
return Ok(Config::default());
}
let text = std::fs::read_to_string(path)?;
let doc = kdl::KdlDocument::parse_v2(&text)?;
let mut cfg = Config::default();
for node in doc.nodes() {
match node.name().value() {
"backend" => {
if let Some(kdl::KdlValue::String(s)) = node.get(0) {
cfg.backend = Some(s.clone());
}
}
"verbose" => {
cfg.verbose = Some(match node.get(0) {
None => true, Some(kdl::KdlValue::Bool(b)) => *b,
_ => true,
});
}
"theme" => {
if let Some(kdl::KdlValue::String(s)) = node.get(0) {
if crate::core::Theme::parse(s).is_some() {
cfg.theme = Some(s.clone());
} else {
eprintln!(
"mdr: unknown theme '{}', expected 'auto', 'dark' or 'light'",
s
);
}
}
}
"offline" => {
cfg.offline = Some(match node.get(0) {
None => true, Some(kdl::KdlValue::Bool(b)) => *b,
_ => true,
});
}
other => eprintln!("mdr: unknown config key '{}'", other),
}
}
Ok(cfg)
}