tasks-cli-rs 0.9.0

Markdown-based TODO task management CLI: each task is a Markdown file with YAML front matter
use std::path::PathBuf;

use serde::Deserialize;

use crate::error::{Error, Result};

#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Config {
    pub editor: Option<String>,
    pub default_priority: Option<String>,
    pub filename_format: Option<String>,
    pub warn_invalid_files: Option<bool>,
}

pub fn base_dir() -> Result<PathBuf> {
    // no let-chains: keep compilable on Rust 1.85+ (see rust-version)
    match std::env::var("TASKS_CLI_HOME") {
        Ok(dir) if !dir.is_empty() => return Ok(PathBuf::from(dir)),
        _ => {}
    }
    Ok(dirs::home_dir().ok_or(Error::NoHomeDir)?.join(".tasks-cli"))
}

impl Config {
    pub fn load() -> Result<Self> {
        Self::load_from(&base_dir()?.join("config.toml"))
    }

    pub fn load_from(path: &std::path::Path) -> Result<Self> {
        if !path.exists() {
            return Ok(Self::default());
        }
        let content = std::fs::read_to_string(path)?;
        Self::parse(&content)
    }

    pub fn parse(content: &str) -> Result<Self> {
        Ok(toml::from_str(content)?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn defaults_when_empty() {
        let config = Config::parse("").unwrap();
        assert_eq!(config, Config::default());
        assert!(config.editor.is_none());
    }

    #[test]
    fn user_overrides() {
        let config = Config::parse("editor = \"vim\"\ndefault_priority = \"high\"").unwrap();
        assert_eq!(config.editor.as_deref(), Some("vim"));
        assert_eq!(config.default_priority.as_deref(), Some("high"));
    }

    #[test]
    fn rejects_unknown_fields() {
        assert!(Config::parse("nonsense = 1").is_err());
    }

    #[test]
    fn missing_file_yields_defaults() {
        let config = Config::load_from(std::path::Path::new("/nonexistent/config.toml")).unwrap();
        assert_eq!(config, Config::default());
    }
}