Skip to main content

context_bar_core/
hud.rs

1//! HUD rendering.
2//!
3//! Produces `.context-bar/hud.md`, the always-visible surface that mirrors
4//! claude-hud's session / week / context-percent panel for both Claude Code
5//! and Codex CLI. The file is plain markdown so it renders in any Zed buffer
6//! today; once a real status-bar extension API ships, the same fields will
7//! drive it directly from `state.json`.
8
9use crate::context_engine::ContextSnapshot;
10use crate::usage_signal::{AccountInfo, AgentUsage, UsageSnapshot};
11
12pub fn render(snapshot: &ContextSnapshot, usage: &UsageSnapshot) -> String {
13    let mut out = String::new();
14    out.push_str("# Agent HUD\n\n");
15    out.push_str(&format!(
16        "_Updated: `{}` · Source: `{}`_\n\n",
17        snapshot.updated_at, usage.source
18    ));
19
20    out.push_str("| Agent | Session (5h) | Week (7d) | Context | Model | Last turn |\n");
21    out.push_str("|---|---:|---:|---:|---|---|\n");
22    out.push_str(&format_row("Claude", &usage.claude));
23    out.push_str(&format_row("Codex", &usage.codex));
24    out.push('\n');
25
26    let active = usage.accounts.iter().find(|a| a.is_active)
27        .or_else(|| usage.accounts.first());
28    if let Some(a) = active {
29        out.push_str("## Limits\n\n");
30        out.push_str("| | 5h session | 7d week |\n");
31        out.push_str("|---|---|---|\n");
32        out.push_str(&format_limit_row(a, &usage.claude));
33        out.push('\n');
34    }
35
36    if usage.source != "python3" {
37        out.push_str(&format!(
38            "> Usage data unavailable: {}\n> Falling back to git-only signals.\n\n",
39            usage.source
40        ));
41    }
42
43    out.push_str(&format!("## Worktree\n- `{}`\n- branch `{}`\n",
44        snapshot.worktree_root, snapshot.branch));
45
46    out
47}
48
49fn format_limit_row(a: &AccountInfo, claude: &AgentUsage) -> String {
50    let plan = match a.subscription_type.as_str() {
51        "pro" => "Pro",
52        "max" => {
53            if a.rate_limit_tier.contains("20x") { "Max 20×" }
54            else if a.rate_limit_tier.contains("5x") { "Max 5×" }
55            else { "Max" }
56        }
57        _ => &a.subscription_type,
58    };
59    let cell5h = format_pct_cell(claude.session_5h_percent, a.limit_5h_messages);
60    let cell7d = format_pct_cell(claude.week_7d_percent, a.limit_7d_messages);
61    format!("| {} {} | {} | {} |\n", a.name, plan, cell5h, cell7d)
62}
63
64fn format_pct_cell(pct: Option<f64>, total: u32) -> String {
65    match pct {
66        Some(p) => {
67            let bar = ascii_bar(p, 10);
68            let used = ((p / 100.0) * total as f64).round() as u32;
69            format!("{bar} **{p:.0}%** ({used}/{total})")
70        }
71        None => {
72            if total > 0 { format!("— / {total} msgs") } else { "—".to_string() }
73        }
74    }
75}
76
77fn ascii_bar(pct: f64, width: usize) -> String {
78    let filled = ((pct / 100.0) * width as f64).round() as usize;
79    let filled = filled.min(width);
80    format!("{}{}", "█".repeat(filled), "░".repeat(width - filled))
81}
82
83fn format_row(label: &str, usage: &AgentUsage) -> String {
84    let ctx = match (usage.last_context_pct, usage.last_context_window) {
85        (Some(pct), Some(window)) => format!("{pct:.1}% of {}", format_tokens(window)),
86        (Some(pct), None) => format!("{pct:.1}%"),
87        _ => "—".to_string(),
88    };
89    let model = usage.last_model.as_deref().unwrap_or("—");
90    let last = usage.last_turn_at.as_deref().unwrap_or("—");
91    format!(
92        "| {label} | {} | {} | {ctx} | `{model}` | {last} |\n",
93        format_tokens(usage.session_5h_tokens),
94        format_tokens(usage.week_7d_tokens),
95    )
96}
97
98fn format_tokens(value: u64) -> String {
99    if value >= 1_000_000 {
100        format!("{:.2}M", value as f64 / 1_000_000.0)
101    } else if value >= 1_000 {
102        format!("{:.1}k", value as f64 / 1_000.0)
103    } else {
104        value.to_string()
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::format_tokens;
111
112    #[test]
113    fn token_formatting_uses_compact_units() {
114        assert_eq!(format_tokens(42), "42");
115        assert_eq!(format_tokens(1500), "1.5k");
116        assert_eq!(format_tokens(2_500_000), "2.50M");
117    }
118}