grav-bar 26.7.1

Fast, zero-dependency, and highly customizable custom status line for the Google Antigravity CLI
use std::io::{self, Read, Write};
use std::process::Command;

const YELLOW: &str = "\x1b[33m";
const MAGENTA: &str = "\x1b[35m";
const BLUE: &str = "\x1b[34m";
const GREEN: &str = "\x1b[32m";
const WHITE: &str = "\x1b[97m";
const RESET: &str = "\x1b[0m";
const RED: &str = "\x1b[31m";

fn get_quota_color(pct: u32) -> &'static str {
    if pct <= 10 {
        RED
    } else if pct <= 30 {
        YELLOW
    } else {
        GREEN
    }
}

fn get_context_color(pct: u32) -> &'static str {
    if pct >= 90 { RED } else { YELLOW }
}

fn get_git_branch(cwd: &str) -> String {
    let mut cmd = Command::new("git");
    cmd.args(["branch", "--show-current"]);
    if !cwd.is_empty() {
        cmd.current_dir(cwd);
    }
    match cmd.output() {
        Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_string(),
        _ => "none".to_string(),
    }
}

fn extract_string_field(json: &str, field: &str) -> Option<String> {
    let search = format!("\"{}\":", field);
    if let Some(idx) = json.find(&search) {
        let remainder = json[idx + search.len()..].trim_start();
        if let Some(stripped) = remainder.strip_prefix('"')
            && let Some(end) = stripped.find('"')
        {
            return Some(stripped[..end].to_string());
        }
    }
    None
}

fn extract_f64_field(json: &str, field: &str) -> Option<f64> {
    let search = format!("\"{}\":", field);
    if let Some(idx) = json.find(&search) {
        let remainder = &json[idx + search.len()..];
        let remainder = remainder.trim_start();
        let end = remainder
            .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
            .unwrap_or(remainder.len());
        if let Ok(val) = remainder[..end].parse::<f64>() {
            return Some(val);
        }
    }
    None
}

fn build_progress_bar(pct: u32, total_blocks: usize) -> String {
    let safe_pct = if pct > 100 { 100 } else { pct };
    let filled = ((safe_pct as f32 / 100.0) * (total_blocks as f32)).round() as usize;
    let empty = total_blocks.saturating_sub(filled);

    format!("{}{}", "".repeat(filled), "".repeat(empty))
}

fn extract_quota_frac(json: &str, key: &str) -> Option<f64> {
    if let Some(quota_idx) = json.find("\"quota\":") {
        let q_block = &json[quota_idx..];
        let search = format!("\"{}\":", key);
        if let Some(k_idx) = q_block.find(&search)
            && let Some(frac_idx) = q_block[k_idx..].find("\"remaining_fraction\":")
        {
            let frac_str = q_block[k_idx + frac_idx + 21..].trim_start();
            let end_frac = frac_str
                .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
                .unwrap_or(frac_str.len());
            if let Ok(val) = frac_str[..end_frac].parse::<f64>() {
                return Some(val);
            }
        }
    }
    None
}

fn extract_quota_reset_secs(json: &str, key: &str) -> Option<u32> {
    if let Some(quota_idx) = json.find("\"quota\":") {
        let q_block = &json[quota_idx..];
        let search = format!("\"{}\":", key);
        if let Some(k_idx) = q_block.find(&search)
            && let Some(reset_idx) = q_block[k_idx..].find("\"reset_in_seconds\":")
        {
            let reset_str = q_block[k_idx + reset_idx + 19..].trim_start();
            let end = reset_str
                .find(|c: char| !c.is_ascii_digit())
                .unwrap_or(reset_str.len());
            if let Ok(val) = reset_str[..end].parse::<u32>() {
                return Some(val);
            }
        }
    }
    None
}

fn format_time_left(secs: u32) -> String {
    let hours = secs / 3600;
    let mins = (secs % 3600) / 60;
    if hours > 48 {
        let days = hours / 24;
        format!("{}d", days)
    } else if hours > 24 {
        let days = hours / 24;
        let rem_hours = hours % 24;
        format!("{}d{}h", days, rem_hours)
    } else if hours > 0 {
        format!("{}h{}m", hours, mins)
    } else {
        format!("{}m", mins)
    }
}

fn visible_len(s: &str) -> usize {
    let mut len = 0;
    let mut in_ansi = false;
    for c in s.chars() {
        if c == '\x1b' {
            in_ansi = true;
        } else if in_ansi {
            if c.is_ascii_alphabetic() {
                in_ansi = false;
            }
        } else {
            len += 1;
        }
    }
    len
}

fn main() {
    let mut input = String::new();
    let _ = io::stdin().read_to_string(&mut input);

    let term_width = extract_f64_field(&input, "terminal_width").unwrap_or(0.0) as usize;

    let cwd = extract_string_field(&input, "cwd").unwrap_or_default();
    let cwd_basename = cwd.rsplit('/').next().unwrap_or(&cwd);
    let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/ash".to_string());

    let branch_json = extract_string_field(&input, "branch").unwrap_or_default();
    let branch_raw = if branch_json.is_empty() || branch_json == "none" {
        get_git_branch(&cwd)
    } else {
        branch_json
    };

    let model = extract_string_field(&input, "display_name")
        .or_else(|| extract_string_field(&input, "id"))
        .unwrap_or_else(|| "Unknown Model".to_string());

    let context_frac = extract_f64_field(&input, "used_percentage").unwrap_or(0.0);
    let context_pct = context_frac.round() as u32;

    let active_agents =
        extract_string_field(&input, "agent_state").unwrap_or_else(|| "working".to_string());

    let is_3p = model.to_lowercase().contains("claude")
        || model.to_lowercase().contains("gpt")
        || model.to_lowercase().contains("oss");

    let prefix = if is_3p { "3p" } else { "gemini" };
    let key_5h = format!("{}-5h", prefix);
    let key_w = format!("{}-weekly", prefix);

    let frac_5h = extract_quota_frac(&input, &key_5h).unwrap_or(1.0);
    let frac_w = extract_quota_frac(&input, &key_w).unwrap_or(1.0);

    let reset_5h = extract_quota_reset_secs(&input, &key_5h).unwrap_or(0);
    let reset_w = extract_quota_reset_secs(&input, &key_w).unwrap_or(0);

    let pct_5h = (frac_5h * 100.0).round() as u32;
    let pct_w = (frac_w * 100.0).round() as u32;

    let context_color = get_context_color(context_pct);
    let context_bar = build_progress_bar(context_pct, 5);
    let context_bar_colored = format!("{}{}{}", context_color, context_bar, WHITE);

    let bar_5h = build_progress_bar(pct_5h, 5);
    let bar_w = build_progress_bar(pct_w, 5);

    let r_5h_str = if reset_5h > 0 {
        format!(" (↻ {})", format_time_left(reset_5h))
    } else {
        "".to_string()
    };
    let r_w_str = if reset_w > 0 {
        format!(" (↻ {})", format_time_left(reset_w))
    } else {
        "".to_string()
    };

    let color_5h = get_quota_color(pct_5h);
    let color_w = get_quota_color(pct_w);

    let mut flags = [false, false, false, false, false]; // dir, branch, model, quotas, context
    let min_padding = 3;

    let get_parts = |flags: &[bool; 5]| {
        let cwd_display = if flags[0] {
            cwd_basename.to_string()
        } else if cwd.starts_with(&home) {
            cwd.replacen(&home, "~", 1)
        } else {
            cwd.clone()
        };

        let branch_display = if flags[1] {
            let mut b = branch_raw.clone();
            if b.len() > 15 {
                let parts: Vec<&str> = b.split('-').collect();
                if parts.len() >= 2 {
                    b = format!("{}-{}", parts[0], parts[1]);
                }
            }
            b
        } else {
            branch_raw.clone()
        };

        let model_display = if flags[2] {
            let mut m = model.clone();
            if let Some(idx) = m.rfind(" (") {
                m.truncate(idx);
            }
            m
        } else {
            model.clone()
        };

        let mut location = String::new();
        if !cwd_display.is_empty() {
            location.push_str(&format!(" {YELLOW}{}", cwd_display));
        }
        if branch_display != "none" && !branch_display.is_empty() {
            if !location.is_empty() {
                location.push(' ');
            }
            location.push_str(&format!("{WHITE}({}{}{WHITE})", MAGENTA, branch_display));
        }
        let location_display = if location.is_empty() {
            "".to_string()
        } else {
            format!("{} {RESET}", location)
        };

        let quotas_display = if flags[3] {
            format!(
                "5h {}{}{WHITE} {}% - W {}{}{WHITE} {}%",
                color_5h, bar_5h, pct_5h, color_w, bar_w, pct_w
            )
        } else {
            format!(
                "5h {}{}{WHITE} {}%{} - W {}{}{WHITE} {}%{}",
                color_5h, bar_5h, pct_5h, r_5h_str, color_w, bar_w, pct_w, r_w_str
            )
        };

        let context_label = if flags[4] { "Ctx" } else { "Context" };

        let left_side = format!(
            "{}{WHITE} {} {} {}% {RESET}{WHITE} Quotas {} {RESET}",
            location_display, context_label, context_bar_colored, context_pct, quotas_display
        );

        let right_side = format!("{BLUE}{} - {} {RESET}", active_agents, model_display);

        (left_side, right_side)
    };

    let (mut left_side, mut right_side) = get_parts(&flags);

    if term_width > 0 {
        let mut i = 0;
        while i < 5 && visible_len(&left_side) + visible_len(&right_side) + min_padding > term_width
        {
            flags[i] = true;
            let (l, r) = get_parts(&flags);
            left_side = l;
            right_side = r;
            i += 1;
        }
    }

    let left_len = visible_len(&left_side);
    let right_len = visible_len(&right_side);

    let padding = if term_width > left_len + right_len {
        " ".repeat(term_width - left_len - right_len)
    } else {
        "   ".to_string()
    };

    let status_line = format!("{}{}{}", left_side, padding, right_side);

    print!("{}", status_line);
    let _ = io::stdout().flush();
}