grav-bar 26.9.1

Fast, zero-dependency, and themed status line for the Google Antigravity CLI. Compatible also with Claude code.
//! grav-bar: a fast, zero-dependency status line for the Claude Code and
//! Google Antigravity CLIs. Reads one JSON payload from stdin, prints one line.

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();
}