void-focus 0.3.0-alpha.4

A feature-rich terminal focus timer with task tracking
Documentation
use std::collections::HashMap;
use std::path::Path;

use anyhow::{bail, Context, Result};
use serde::Deserialize;

use super::color::resolve_color;
use super::tokens::{ThemeTokens, NAMES};

#[derive(Debug, Deserialize)]
pub struct ThemeFile {
    pub name: String,
    #[serde(default)]
    pub palette: HashMap<String, String>,
    pub tokens: HashMap<String, String>,
}

impl ThemeFile {
    pub fn from_str(source: &str) -> Result<Self> {
        toml::from_str(source).context("parse theme toml")
    }

    pub fn from_path(path: &Path) -> Result<Self> {
        let source = std::fs::read_to_string(path)
            .with_context(|| format!("read theme file {}", path.display()))?;
        Self::from_str(&source)
    }

    pub fn into_tokens(self) -> Result<ThemeTokens> {
        for required in NAMES {
            if !self.tokens.contains_key(*required) {
                bail!("missing required token `{required}`");
            }
        }

        let get = |name: &str| -> Result<ratatui::style::Color> {
            let raw = self
                .tokens
                .get(name)
                .map(String::as_str)
                .expect("checked above");
            resolve_color(raw, &self.palette).with_context(|| format!("token `{name}`"))
        };

        Ok(ThemeTokens {
            bg: get("bg")?,
            text: get("text")?,
            dim: get("dim")?,
            accent: get("accent")?,
            on_accent: get("on_accent")?,
            success: get("success")?,
            warning: get("warning")?,
            error: get("error")?,
            info: get("info")?,
            progress_dim: get("progress_dim")?,
            task_track: get("task_track")?,
            panel: get("panel")?,
            panel_border: get("panel_border")?,
            select_bg: get("select_bg")?,
            select_fg: get("select_fg")?,
            active_bg: get("active_bg")?,
            active_fg: get("active_fg")?,
        })
    }
}

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

    const MOCHA: &str = include_str!("../../themes/catppuccin-mocha.toml");

    #[test]
    fn loads_catppuccin_mocha_tokens() {
        let file = ThemeFile::from_str(MOCHA).unwrap();
        assert_eq!(file.name, "Catppuccin Mocha");
        let tokens = file.into_tokens().unwrap();
        assert_eq!(tokens.bg, ratatui::style::Color::Rgb(30, 30, 46));
        assert_eq!(tokens.accent, ratatui::style::Color::Rgb(137, 182, 250));
    }
}