todo-txt 4.0.2

Parser for the todo.txt format
Documentation
use std::collections::HashMap;

#[derive(Clone, envir::Deserialize, envir::Serialize)]
#[envir(prefix = "COLOR_")]
pub struct Colors {
    #[envir(default)]
    pub context: crate::Color,
    #[envir(default)]
    pub done: crate::Color,
    #[envir(default)]
    pub date: crate::Color,
    #[envir(skip)]
    pub none: crate::Color,
    #[envir(default)]
    pub meta: crate::Color,
    #[envir(default)]
    pub number: crate::Color,
    #[envir(default)]
    pub project: crate::Color,
    #[envir(load_with = "priorities_load", export_with = "priorities_export")]
    priorities: HashMap<String, crate::Color>,
}

fn priorities_load(env: &HashMap<String, String>) -> envir::Result<HashMap<String, crate::Color>> {
    let mut priorities = HashMap::new();
    priorities.insert("PRI_A".to_string(), "yellow".into());
    priorities.insert("PRI_B".to_string(), "green".into());
    priorities.insert("PRI_C".to_string(), "bright blue".into());
    priorities.insert("PRI_X".to_string(), "white".into());

    for (k, v) in env {
        if k.starts_with("PRI_") {
            priorities.insert(k.clone(), v.parse().unwrap_or_default());
        }
    }

    Ok(priorities)
}

fn priorities_export(priorities: &HashMap<String, crate::Color>) -> HashMap<String, String> {
    priorities
        .iter()
        .map(|(k, v)| (k.clone(), v.to_string()))
        .collect()
}

impl Colors {
    pub fn for_pri(&self, pri: &crate::Priority) -> crate::Color {
        self.priorities
            .get(&format!("PRI_{pri}"))
            .unwrap_or_else(|| self.priorities.get("PRI_X").unwrap())
            .clone()
    }
}

#[derive(envir::Deserialize, envir::Serialize)]
#[envir(prefix = "TODOTXT_")]
pub struct Config {
    #[envir(noprefix, name = "TODO_ACTIONS_DIR", default = "${HOME}/.todo/actions")]
    pub action_dir: String,
    #[envir(default = "true")]
    pub auto_archive: bool,
    #[envir(nested)]
    pub colors: Colors,
    #[envir(default)]
    pub date_on_add: bool,
    #[envir(default = "help")]
    pub default_action: String,
    #[envir(default)]
    pub disable_filter: bool,
    #[envir(default = "cat")]
    pub final_filter: String,
    #[envir(default)]
    pub force: bool,
    #[envir(
        noprefix,
        name = "TODO_NOTE_ARCHIVE",
        default = "${TODO_DIR}/notes/archive.txt"
    )]
    pub note_archive: String,
    #[envir(noprefix, name = "TODO_NOTES_DIR", default = "${TODO_DIR}/notes")]
    pub notes_dir: String,
    #[envir(noprefix, name = "TODO_NOTE_FILTER", default = "cat")]
    pub note_filter: String,
    #[envir(default)]
    pub plain: bool,
    #[envir(default = "true")]
    pub preserve_line_numbers: bool,
    pub priority_on_add: Option<char>,
    #[envir(default = "env LC_COLLATE=C sort -f -k2")]
    pub sort_command: String,
    #[envir(noprefix)]
    pub todo_dir: String,
    #[envir(noprefix, default = "${TODO_DIR}/todo.txt")]
    pub todo_file: String,
    #[envir(noprefix, default = "${TODO_DIR}/done.txt")]
    pub done_file: String,
    #[envir(noprefix, default = "${TODO_DIR}/report.txt")]
    pub report_file: String,
    #[envir(default = "1")]
    pub verbose: u8,
}

impl Config {
    #[must_use]
    pub fn from_env() -> Self {
        Self::load_env();

        envir::from_env().unwrap()
    }

    pub fn load_env() {
        envir::set("NONE", "");
        envir::set("BLACK", "\x1B[0;30m");
        envir::set("RED", "\x1B[0;31m");
        envir::set("GREEN", "\x1B[0;32m");
        envir::set("BROWN", "\x1B[0;33m");
        envir::set("BLUE", "\x1B[0;34m");
        envir::set("PURPLE", "\x1B[0;35m");
        envir::set("CYAN", "\x1B[0;36m");
        envir::set("LIGHT_GREY", "\x1B[0;37m");
        envir::set("DARK_GREY", "\x1B[1;30m");
        envir::set("LIGHT_RED", "\x1B[1;31m");
        envir::set("LIGHT_GREEN", "\x1B[1;32m");
        envir::set("YELLOW", "\x1B[1;33m");
        envir::set("LIGHT_BLUE", "\x1B[1;3");
        envir::set("LIGHT_PURPLE", "\x1B[1;35m");
        envir::set("LIGHT_CYAN", "\x1B[1;36m");
        envir::set("WHITE", "\x1B[1;37m");
        envir::set("DEFAULT", "\x1B[0m");

        Self::load_config_file();
    }

    #[cfg(not(debug_assertions))]
    fn load_config_file() {
        let home = envir::get("HOME").unwrap();

        let configs: Vec<Box<dyn Fn() -> String>> = vec![
            Box::new(|| format!("{home}/.todo/config")),
            Box::new(|| format!("{home}/todo.cfg")),
            Box::new(|| format!("{home}/.todo.cfg")),
            Box::new(|| format!("{home}/.config/todo/config")),
            Box::new(|| {
                let filename = std::env::args().next().unwrap();
                let mut path = std::path::PathBuf::from(filename);
                path.pop();

                format!("{}/todo.cfg", path.display())
            }),
            Box::new(|| {
                envir::get("TODOTXT_GLOBAL_CFG_FILE")
                    .unwrap_or_else(|_| "/etc/todo/config".to_string())
            }),
        ];

        for config in configs {
            if envir::from_path(config()).is_ok() {
                break;
            }
        }
    }

    #[cfg(debug_assertions)]
    fn load_config_file() {
        envir::dotenv();
    }
}