opencode-stats 1.3.7

A terminal dashboard for OpenCode usage statistics inspired by the /stats command in Claude Code
use chrono::{Datelike, Duration, NaiveDate, Weekday};

use crate::db::models::UsageEvent;

#[derive(Clone, Debug)]
pub struct HeatmapCell {
    pub intensity: u8,
    pub is_future: bool,
}

#[derive(Clone, Debug)]
pub struct HeatmapData {
    pub weeks: Vec<Vec<HeatmapCell>>,
    pub month_labels: Vec<(usize, String)>,
}

pub fn build_heatmap_data(events: &[UsageEvent], today: NaiveDate) -> HeatmapData {
    let end = today;
    let start = today - Duration::days(364);
    let grid_start = start - Duration::days(start.weekday().num_days_from_monday() as i64);
    let grid_end = end + Duration::days((6 - end.weekday().num_days_from_monday()) as i64);

    let mut totals = std::collections::BTreeMap::<NaiveDate, u64>::new();
    for event in events {
        let Some(date) = event.activity_date() else {
            continue;
        };
        if date < start || date > end {
            continue;
        }
        let total = totals.entry(date).or_default();
        *total = total.saturating_add(event.tokens.total());
    }

    let max = totals.values().copied().max().unwrap_or_default();
    let mut date = grid_start;
    let mut weeks = Vec::new();
    let mut month_labels = Vec::new();
    let mut week_index = 0usize;
    while date <= grid_end {
        let mut week = Vec::new();
        for _ in 0..7 {
            let tokens = totals.get(&date).copied().unwrap_or_default();
            let intensity = map_intensity(tokens, max);
            week.push(HeatmapCell {
                intensity,
                is_future: date > end,
            });
            if date.day() == 1 {
                let label = date.format("%b").to_string();
                if month_labels
                    .last()
                    .map(|(_, current)| current != &label)
                    .unwrap_or(true)
                {
                    month_labels.push((week_index, label));
                }
            }
            date += Duration::days(1);
        }
        weeks.push(week);
        week_index += 1;
    }

    HeatmapData {
        weeks,
        month_labels,
    }
}

fn map_intensity(tokens: u64, max: u64) -> u8 {
    if tokens == 0 || max == 0 {
        0
    } else {
        let ratio = tokens as f64 / max as f64;
        if ratio < 0.15 {
            1
        } else if ratio < 0.4 {
            2
        } else if ratio < 0.75 {
            3
        } else {
            4
        }
    }
}

pub fn weekday_labels() -> [Weekday; 7] {
    [
        Weekday::Mon,
        Weekday::Tue,
        Weekday::Wed,
        Weekday::Thu,
        Weekday::Fri,
        Weekday::Sat,
        Weekday::Sun,
    ]
}

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

    #[test]
    fn maps_heat_levels() {
        assert_eq!(map_intensity(0, 100), 0);
        assert_eq!(map_intensity(10, 100), 1);
        assert_eq!(map_intensity(30, 100), 2);
        assert_eq!(map_intensity(60, 100), 3);
        assert_eq!(map_intensity(90, 100), 4);
    }
}