typetui 0.2.0

A terminal-based typing test.
Documentation
use crate::content::Mode;
use crate::ui::theme::{Theme, ThemeName};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    pub theme: String,
    pub default_mode: String,
    pub default_language: String,
    pub default_duration: u64,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            theme: "pastel".to_string(),
            default_mode: "text".to_string(),
            default_language: "english".to_string(),
            default_duration: 60,
        }
    }
}

impl Config {
    #[must_use]
    pub fn config_dir() -> PathBuf {
        dirs::config_dir()
            .unwrap_or_else(|| PathBuf::from("."))
            .join("typetui")
    }

    #[must_use]
    pub fn config_path() -> PathBuf {
        Self::config_dir().join("config.json")
    }

    #[must_use]
    pub fn results_path() -> PathBuf {
        Self::config_dir().join("results.jsonl")
    }

    #[must_use]
    pub fn load() -> Self {
        let path = Self::config_path();
        if path.exists() {
            match std::fs::read_to_string(&path) {
                Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
                Err(_) => Self::default(),
            }
        } else {
            Self::default()
        }
    }

    pub fn save(&self) -> Result<(), std::io::Error> {
        let dir = Self::config_dir();
        if !dir.exists() {
            std::fs::create_dir_all(&dir)?;
        }
        let content = serde_json::to_string_pretty(self)?;
        std::fs::write(Self::config_path(), content)
    }

    pub fn theme(&self) -> Theme {
        ThemeName::from_str(&self.theme)
            .map(Theme::from_name)
            .unwrap_or_default()
    }

    pub fn set_theme(&mut self, theme: ThemeName) {
        self.theme = theme.as_str().to_string();
    }

    #[must_use]
    pub fn mode(&self) -> Mode {
        Mode::from_str(&self.default_mode).unwrap_or(Mode::Text)
    }

    pub fn set_mode(&mut self, mode: Mode) {
        self.default_mode = mode.as_str().to_string();
    }

    #[must_use]
    pub fn duration(&self) -> u64 {
        match self.default_duration {
            15 | 30 | 60 | 120 => self.default_duration,
            _ => 60,
        }
    }

    pub fn set_duration(&mut self, duration: u64) {
        if [15, 30, 60, 120].contains(&duration) {
            self.default_duration = duration;
        }
    }
}

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

    #[test]
    fn test_default_config() {
        let config = Config::default();
        assert_eq!(config.theme, "pastel");
        assert_eq!(config.default_mode, "text");
        assert_eq!(config.default_duration, 60);
    }

    #[test]
    fn test_duration_validation() {
        let mut config = Config::default();
        config.set_duration(45);
        assert_eq!(config.duration(), 60); // unchanged
        config.set_duration(30);
        assert_eq!(config.duration(), 30);
    }
}