mod json;
mod render;
mod status;
mod theme;
use std::io::{self, Read, Write};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use status::{Caller, Env, Status};
use theme::Theme;
const USAGE: &str = "\
Usage: grav-bar [OPTIONS] < payload.json
Reads a Claude Code or Google Antigravity status-line payload from stdin and
prints a single colored line.
Options:
--theme <name> Color theme (or set GRAV_BAR_THEME). Default: default
--caller <name> Force the payload format: claude | agy (default: auto-detect)
--list-themes Print available theme names and exit
-h, --help Show this help
-V, --version Show version";
struct Args {
theme: Option<String>,
caller: Option<Caller>,
}
fn parse_args() -> Option<Args> {
let mut theme = None;
let mut caller = None;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--theme" => theme = args.next(),
"--caller" => caller = args.next().and_then(|v| parse_caller(&v)),
"--list-themes" => {
for t in Theme::all() {
println!("{}", t.name);
}
return None;
}
"-h" | "--help" => {
println!("{USAGE}");
return None;
}
"-V" | "--version" => {
println!("grav-bar {}", env!("CARGO_PKG_VERSION"));
return None;
}
other => {
if let Some(v) = other.strip_prefix("--theme=") {
theme = Some(v.to_string());
} else if let Some(v) = other.strip_prefix("--caller=") {
caller = parse_caller(v);
} else {
eprintln!("grav-bar: ignoring unknown argument `{other}`");
}
}
}
}
Some(Args { theme, caller })
}
fn parse_caller(v: &str) -> Option<Caller> {
let parsed = Caller::parse(v);
if parsed.is_none() {
eprintln!("grav-bar: unknown --caller `{v}` (expected claude or agy); auto-detecting");
}
parsed
}
fn resolve_theme(requested: Option<String>) -> &'static Theme {
let name = requested.or_else(|| std::env::var("GRAV_BAR_THEME").ok());
match name {
None => Theme::default_theme(),
Some(n) => Theme::by_name(&n).unwrap_or_else(|| {
eprintln!("grav-bar: unknown theme `{n}`; using default (see --list-themes)");
Theme::default_theme()
}),
}
}
fn username() -> String {
if let Ok(u) = std::env::var("USER")
&& !u.is_empty()
{
return u;
}
Command::new("whoami")
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default()
}
fn main() {
let Some(args) = parse_args() else {
return;
};
let theme = resolve_theme(args.theme);
let mut input = String::new();
let _ = io::stdin().read_to_string(&mut input);
let caller = args.caller.unwrap_or_else(|| Caller::detect(&input));
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let parsed = status::parse(&input, caller, now);
let branch = parsed
.branch
.clone()
.or_else(|| status::get_git_branch(&parsed.cwd));
let env = Env {
username: username(),
home: std::env::var("HOME").unwrap_or_default(),
columns: std::env::var("COLUMNS")
.ok()
.and_then(|c| c.trim().parse().ok())
.filter(|c| *c > 0),
};
let st = Status::assemble(parsed, caller, branch, env);
print!("{}", render::render(&st, theme));
let _ = io::stdout().flush();
}