typetui 0.2.1

A terminal-based typing test.
Documentation
use crate::config::Config;
use crate::stats::TestResult;
use std::fs::{self, File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};

pub struct Storage;

impl Storage {
    pub fn append_result(result: &TestResult) -> io::Result<()> {
        let path = Config::results_path();
        let dir = path.parent().unwrap();
        if !dir.exists() {
            fs::create_dir_all(dir)?;
        }

        let mut file = OpenOptions::new().create(true).append(true).open(&path)?;

        let line = serde_json::to_string(result)?;
        writeln!(file, "{line}")?;
        Ok(())
    }

    pub fn load_results() -> io::Result<Vec<TestResult>> {
        let path = Config::results_path();
        if !path.exists() {
            return Ok(Vec::new());
        }

        let file = File::open(&path)?;
        let reader = BufReader::new(file);
        let mut results = Vec::new();

        for line in reader.lines() {
            let line = line?;
            if line.trim().is_empty() {
                continue;
            }
            if let Ok(result) = serde_json::from_str(&line) {
                results.push(result);
            }
        }

        Ok(results)
    }

    pub fn personal_bests() -> io::Result<Vec<(String, f64)>> {
        let results = Self::load_results()?;
        let mut bests: std::collections::HashMap<String, f64> = std::collections::HashMap::new();

        for result in results {
            let key = format!("{}-{}", result.mode, result.duration);
            let entry = bests.entry(key).or_insert(0.0);
            if result.wpm > *entry {
                *entry = result.wpm;
            }
        }

        let mut bests_vec: Vec<(String, f64)> = bests.into_iter().collect();
        bests_vec.sort_by(|a, b| a.0.cmp(&b.0));
        Ok(bests_vec)
    }

    pub fn results_by_day() -> io::Result<std::collections::HashMap<String, Vec<TestResult>>> {
        let results = Self::load_results()?;
        let mut by_day: std::collections::HashMap<String, Vec<TestResult>> =
            std::collections::HashMap::new();

        for result in results {
            let day = result.timestamp.split('T').next().unwrap_or("").to_string();
            by_day.entry(day).or_default().push(result);
        }

        Ok(by_day)
    }
}

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

    #[test]
    fn test_load_empty_results() {
        let results = Storage::load_results().unwrap_or_default();
        assert!(results.is_empty() || !results.is_empty());
    }
}