use chrono::NaiveDate;
use clap::{Parser, ValueEnum};
use std::collections::BTreeSet;
use crate::{
apps,
error::AppError,
io::cli_print,
model::{AppKind, TokenTotals, UsageEntry},
tokens,
};
#[derive(Clone, Copy, Debug, ValueEnum)]
enum GroupBy {
App,
Model,
Day,
}
#[derive(Parser)]
#[command(name = "tokrs", about = "Local Token Usage Statistics CLI")]
pub struct Cli {
#[arg(
long,
value_delimiter = ',',
ignore_case = true,
help = "Applications of Statistics (Claude, Codex, OpenCode, Gemini, Grok, Pi, Kimi)—all by default"
)]
app: Vec<AppKind>,
#[arg(
long,
value_enum,
default_value_t = GroupBy::App,
ignore_case = true,
help = "Grouping"
)]
by: GroupBy,
#[arg(long, short, help = "Start Date YYYY-MM-DD (inclusive)")]
since: Option<NaiveDate>,
#[arg(long, short, help = "End Date YYYY-MM-DD (inclusive)")]
until: Option<NaiveDate>,
#[arg(long, help = "Output in JSON format")]
json: bool,
}
pub fn run(cli: Cli) -> Result<(), AppError> {
let app_kinds: Vec<AppKind> = if cli.app.is_empty() {
AppKind::ALL.to_vec()
} else {
cli.app
.iter()
.copied()
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
};
let mut entries = apps::collect(&app_kinds)?;
let pricing_path = apps::prince::pricing_path()?;
let mut table = apps::prince::load_pricing(&pricing_path)?;
let added = apps::prince::sync_models(&mut table, &pricing_path, &entries)?;
if added > 0 {
eprintln!(
"> added {added} model(s) to {} (prices null until filled in)",
pricing_path.display()
);
}
apps::prince::resolve(&mut entries, &table);
let entries = tokens::filter_by_range(entries, cli.since, cli.until);
let total = tokens::grand_total(&entries);
let today = tokens::today_total(&entries);
let rows = build_rows(&entries, cli.by);
if cli.json {
cli_print::print_json(&rows, &total, &today);
} else {
cli_print::print_report(&rows, &total, &today);
}
Ok(())
}
fn build_rows(entries: &[UsageEntry], by: GroupBy) -> Vec<(String, TokenTotals)> {
match by {
GroupBy::App => tokens::aggregate_by_app(entries)
.into_iter()
.map(|(app, totals)| (app.to_string(), totals))
.collect(),
GroupBy::Model => tokens::aggregate_by_model(entries)
.into_iter()
.map(|((app, model), totals)| (format!("{app}/{model}"), totals))
.collect(),
GroupBy::Day => tokens::aggregate_by_day(entries)
.into_iter()
.map(|(day, totals)| (day.format("%Y-%m-%d").to_string(), totals))
.collect(),
}
}
#[cfg(test)]
#[path = "tests/commands_test.rs"]
mod tests;