#![warn(clippy::pedantic)]
#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::cast_sign_loss
)]
mod app;
mod cli;
mod config;
mod consts;
mod core;
mod error;
mod output;
mod pricing;
mod source;
mod utils;
use chrono::Utc;
use clap::Parser;
use app::{CommandContext, handle_source_command};
use cli::{Cli, parse_command};
use config::Config;
use core::DateFilter;
use output::NumberFormat;
use pricing::{CurrencyConverter, PricingDb};
use source::get_source;
use utils::{Timezone, parse_date};
enum TimezoneSource {
Cli,
Config,
}
#[allow(clippy::too_many_lines)] fn main() {
let raw_cli = Cli::parse();
let raw_timezone = raw_cli.timezone.clone();
let parsed_command = parse_command(raw_cli.command.as_ref());
let source_cmd = parsed_command.command;
let is_statusline = source_cmd.is_statusline();
let config = if is_statusline {
Config::load_quiet()
} else {
Config::load()
};
let cli = raw_cli.with_config(&config);
let timezone_source = if raw_timezone.is_some() {
Some(TimezoneSource::Cli)
} else if cli.timezone.is_some() {
Some(TimezoneSource::Config)
} else {
None
};
let timezone = match Timezone::parse(cli.timezone.as_deref()) {
Ok(tz) => tz,
Err(err) => {
if let Some(TimezoneSource::Config) = timezone_source {
eprintln!("Warning: {err}. Falling back to local timezone.");
Timezone::Local
} else {
eprintln!("{err}");
std::process::exit(1);
}
}
};
let number_format = match NumberFormat::from_locale(cli.locale.as_deref()) {
Ok(format) => format,
Err(err) => {
eprintln!("Warning: {err}. Using default locale.");
NumberFormat::default()
}
};
let jq_filter = cli.jq.as_deref();
let parse_date_flag = |value: &Option<String>, flag: &str| {
value.as_ref().map(|s| match parse_date(s) {
Ok(date) => date,
Err(err) => {
eprintln!("{flag}: {err}");
std::process::exit(1);
}
})
};
let since = parse_date_flag(&cli.since, "--since");
let until = parse_date_flag(&cli.until, "--until");
if let (Some(s), Some(u)) = (since, until)
&& s > u
{
eprintln!("Error: --since ({s}) is after --until ({u})");
std::process::exit(1);
}
let filter = if source_cmd.needs_today_filter() {
let today = timezone.to_fixed_offset(Utc::now()).date_naive();
DateFilter::new(Some(today), Some(today))
} else {
DateFilter::new(since, until)
};
let pricing_db = if is_statusline {
PricingDb::load_quiet(cli.offline, cli.strict_pricing)
} else {
PricingDb::load(cli.offline, cli.strict_pricing)
};
let source_name = match (parsed_command.source_hint, cli.source.as_deref()) {
(Some(hint), Some(override_name)) => {
let Some(override_source) = get_source(override_name) else {
eprintln!("Error: unknown source '{override_name}'");
std::process::exit(1);
};
if override_source.name() != hint {
eprintln!(
"Error: command source '{hint}' conflicts with --source '{}'",
override_source.name()
);
std::process::exit(1);
}
override_source.name()
}
(Some(hint), None) => hint,
(None, Some(name)) => name,
(None, None) => "claude",
};
let Some(source) = get_source(source_name) else {
eprintln!("Error: {source_name} source not found");
std::process::exit(1);
};
let currency_converter =
cli.currency
.as_ref()
.map(|code| match CurrencyConverter::load(code, cli.offline) {
Some(conv) => {
if !is_statusline {
eprintln!(
"Converting costs to {} (rate: displayed as {})",
conv.currency_code(),
conv.format(1.0)
);
}
conv
}
None => {
eprintln!("Error: failed to load exchange rate for '{code}'");
std::process::exit(1);
}
});
handle_source_command(
source,
source_cmd,
&CommandContext {
filter: &filter,
cli: &cli,
pricing_db: &pricing_db,
timezone,
number_format,
jq_filter,
currency: currency_converter.as_ref(),
},
);
}