claude-dashboard 0.2.2

A terminal TUI dashboard that renders Claude Code usage reports with token stats, project breakdowns, and language distribution.
Documentation
use std::path::PathBuf;

/// Which tab to start on (from --month / --year CLI flags).
#[derive(Debug, Clone)]
pub enum StartTab {
    Monthly(i32, u32),
    Yearly(i32),
}

/// Parsed CLI arguments.
#[derive(Debug, Default)]
pub struct CliArgs {
    pub data_dir: Option<PathBuf>,
    pub start_tab: Option<StartTab>,
}

/// Parse CLI arguments.
///
///   --data-dir <PATH>     Override .claude directory
///   --month YYYY-MM       Start in monthly view (e.g. --month 2026-05)
///   --year YYYY           Start in yearly view (e.g. --year 2026)
pub fn parse_cli_args() -> CliArgs {
    let args: Vec<String> = std::env::args().collect();
    let mut result = CliArgs::default();
    let mut i = 1;
    while i < args.len() {
        match args[i].as_str() {
            "--data-dir" if i + 1 < args.len() => {
                result.data_dir = Some(PathBuf::from(&args[i + 1]));
                i += 1;
            }
            "--month" if i + 1 < args.len() => {
                if let Some((y, m)) = parse_year_month(&args[i + 1]) {
                    result.start_tab = Some(StartTab::Monthly(y, m));
                }
                i += 1;
            }
            "--year" if i + 1 < args.len() => {
                if let Ok(y) = args[i + 1].parse::<i32>() {
                    result.start_tab = Some(StartTab::Yearly(y));
                }
                i += 1;
            }
            _ => {}
        }
        i += 1;
    }
    result
}

fn parse_year_month(s: &str) -> Option<(i32, u32)> {
    let parts: Vec<&str> = s.split('-').collect();
    if parts.len() == 2 {
        let y = parts[0].parse::<i32>().ok()?;
        let m = parts[1].parse::<u32>().ok()?;
        if m >= 1 && m <= 12 {
            return Some((y, m));
        }
    }
    None
}

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

    #[test]
    fn test_parse_year_month_valid() {
        assert_eq!(parse_year_month("2026-05"), Some((2026, 5)));
        assert_eq!(parse_year_month("2024-01"), Some((2024, 1)));
        assert_eq!(parse_year_month("2024-12"), Some((2024, 12)));
        assert_eq!(parse_year_month("2000-06"), Some((2000, 6)));
    }

    #[test]
    fn test_parse_year_month_invalid() {
        assert_eq!(parse_year_month(""), None);
        assert_eq!(parse_year_month("2026"), None);
        assert_eq!(parse_year_month("2026-00"), None); // month 0
        assert_eq!(parse_year_month("2026-13"), None); // month 13
        assert_eq!(parse_year_month("abc-def"), None);
        assert_eq!(parse_year_month("2026-5-extra"), None); // too many parts
    }

    #[test]
    fn test_parse_year_month_edge_cases() {
        assert_eq!(parse_year_month("0-1"), Some((0, 1)));
        assert_eq!(parse_year_month("9999-12"), Some((9999, 12)));
        // Negative year has two dashes → splits into 3 parts → None
        assert_eq!(parse_year_month("-1-6"), None);
    }
}