use clap::{Parser, ValueEnum};
#[derive(Parser, Debug, Clone)]
#[command(
name = "ai-usagebar",
about = "Waybar widget for AI plan usage (Anthropic / OpenAI / Z.AI / OpenRouter)",
long_about = "\
Drop-in replacement for `claudebar` with multi-vendor support.
Output modes:
- Default: Waybar JSON ({text, tooltip, class}). Used when stdout is piped.
- --pretty: human-readable terminal output for local testing. Auto-enabled
when stdout is a TTY, so just running `ai-usagebar --vendor anthropic`
in a terminal Does The Right Thing.
- --watch N: like --pretty but refreshes every N seconds, clearing the screen
between ticks. Useful while iterating on `--format` or `--tooltip-format`.
- --json: force JSON output even when stdout is a TTY (for scripting)."
)]
pub struct Cli {
#[arg(long, value_enum)]
pub vendor: Option<Vendor>,
#[arg(long)]
pub icon: Option<String>,
#[arg(long)]
pub format: Option<String>,
#[arg(long)]
pub tooltip_format: Option<String>,
#[arg(long, default_value_t = 5)]
pub pace_tolerance: u32,
#[arg(long)]
pub format_pace_color: bool,
#[arg(long)]
pub tooltip_pace_pts: bool,
#[arg(long)]
pub color_low: Option<String>,
#[arg(long)]
pub color_mid: Option<String>,
#[arg(long)]
pub color_high: Option<String>,
#[arg(long)]
pub color_critical: Option<String>,
#[arg(long)]
pub pretty: bool,
#[arg(long, conflicts_with = "pretty")]
pub json: bool,
#[arg(long, value_name = "SECS")]
pub watch: Option<u64>,
#[arg(long, conflicts_with_all = ["cycle_prev", "watch", "pretty", "json"])]
pub cycle_next: bool,
#[arg(long, conflicts_with_all = ["cycle_next", "watch", "pretty", "json"])]
pub cycle_prev: bool,
#[arg(long, hide = true)]
pub cache_dir: Option<std::path::PathBuf>,
#[arg(long, hide = true)]
pub creds_path: Option<std::path::PathBuf>,
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum Vendor {
Anthropic,
Openai,
Zai,
Openrouter,
}
impl Vendor {
pub fn to_id(self) -> crate::vendor::VendorId {
match self {
Vendor::Anthropic => crate::vendor::VendorId::Anthropic,
Vendor::Openai => crate::vendor::VendorId::Openai,
Vendor::Zai => crate::vendor::VendorId::Zai,
Vendor::Openrouter => crate::vendor::VendorId::Openrouter,
}
}
}
impl Cli {
pub fn resolved_vendor(&self, config: &crate::config::Config) -> Vendor {
if let Some(v) = self.vendor {
return v;
}
if let Some(id) = crate::active::read() {
if config.is_enabled(id) {
return id_to_vendor(id);
}
}
match config.ui.primary {
Some(id) => id_to_vendor(id),
None => Vendor::Anthropic,
}
}
}
fn id_to_vendor(id: crate::vendor::VendorId) -> Vendor {
match id {
crate::vendor::VendorId::Anthropic => Vendor::Anthropic,
crate::vendor::VendorId::Openai => Vendor::Openai,
crate::vendor::VendorId::Zai => Vendor::Zai,
crate::vendor::VendorId::Openrouter => Vendor::Openrouter,
}
}
impl Cli {
pub fn output_json(&self) -> bool {
if self.json {
return true;
}
if self.pretty || self.watch.is_some() {
return false;
}
!is_stdout_tty()
}
}
fn is_stdout_tty() -> bool {
use std::io::IsTerminal;
std::io::stdout().is_terminal()
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn defaults_match_claudebar() {
let cli = Cli::parse_from(["ai-usagebar"]);
assert_eq!(cli.vendor, None);
let cfg = crate::config::Config::default();
assert_eq!(cli.resolved_vendor(&cfg), Vendor::Anthropic);
assert_eq!(cli.pace_tolerance, 5);
assert!(cli.format.is_none());
assert!(cli.tooltip_format.is_none());
assert!(cli.icon.is_none());
assert!(!cli.format_pace_color);
assert!(!cli.tooltip_pace_pts);
assert!(!cli.pretty);
assert!(!cli.json);
assert!(cli.watch.is_none());
}
#[test]
fn primary_from_config_wins_when_vendor_unset() {
let cli = Cli::parse_from(["ai-usagebar"]);
let mut cfg = crate::config::Config::default();
cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
assert_eq!(cli.resolved_vendor(&cfg), Vendor::Openrouter);
}
#[test]
fn explicit_vendor_overrides_config_primary() {
let cli = Cli::parse_from(["ai-usagebar", "--vendor", "zai"]);
let mut cfg = crate::config::Config::default();
cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
assert_eq!(cli.resolved_vendor(&cfg), Vendor::Zai);
}
#[test]
fn claudebar_compatible_flag_surface() {
let cli = Cli::parse_from([
"ai-usagebar",
"--icon",
"",
"--format",
"{session_pct}% · {session_reset}",
"--tooltip-format",
"S:{session_pct}",
"--pace-tolerance",
"10",
"--format-pace-color",
"--tooltip-pace-pts",
"--color-low",
"#50fa7b",
"--color-mid",
"#f1fa8c",
"--color-high",
"#ffb86c",
"--color-critical",
"#ff5555",
]);
assert_eq!(cli.icon.as_deref(), Some(""));
assert_eq!(
cli.format.as_deref(),
Some("{session_pct}% · {session_reset}")
);
assert_eq!(cli.tooltip_format.as_deref(), Some("S:{session_pct}"));
assert_eq!(cli.pace_tolerance, 10);
assert!(cli.format_pace_color);
assert!(cli.tooltip_pace_pts);
assert_eq!(cli.color_low.as_deref(), Some("#50fa7b"));
assert_eq!(cli.color_critical.as_deref(), Some("#ff5555"));
}
#[test]
fn pretty_and_json_conflict() {
let res = Cli::try_parse_from(["ai-usagebar", "--pretty", "--json"]);
assert!(res.is_err());
}
#[test]
fn watch_disables_json_output() {
let cli = Cli::parse_from(["ai-usagebar", "--watch", "5"]);
assert_eq!(cli.watch, Some(5));
assert!(!cli.output_json());
}
}