use crate::color; use crate::section::{RenderContext, SectionOutput}; use crate::shell; use std::time::Duration; /// Cached format: "open:N,wip:N,ready:N,closed:N" pub fn render(ctx: &RenderContext) -> Option { if !ctx.config.sections.beads.base.enabled { return None; } // Check if .beads/ exists in project dir if !ctx.project_dir.join(".beads").is_dir() { return None; } // --no-shell: serve stale cache only if ctx.no_shell { return render_from_summary(ctx, &ctx.cache.get_stale("beads_stats")?); } let ttl = Duration::from_secs(ctx.config.sections.beads.ttl); let cached = ctx.cache.get("beads_stats", ttl); let summary = cached.or_else(|| { let out = ctx.shell_results.get("beads").cloned().unwrap_or_else(|| { shell::exec_gated( ctx.shell_config, "br", &["stats", "--json"], Some(ctx.project_dir.to_str()?), ) })?; let summary = parse_stats(&out)?; ctx.cache.set("beads_stats", &summary); Some(summary) })?; render_from_summary(ctx, &summary) } /// Parse `br stats --json` output into "open:N,wip:N,ready:N,closed:N" fn parse_stats(json: &str) -> Option { let v: serde_json::Value = serde_json::from_str(json).ok()?; let s = v.get("summary")?; let open = s.get("open_issues")?.as_u64().unwrap_or(0); let wip = s.get("in_progress_issues")?.as_u64().unwrap_or(0); let ready = s.get("ready_issues")?.as_u64().unwrap_or(0); let closed = s.get("closed_issues")?.as_u64().unwrap_or(0); Some(format!( "open:{open},wip:{wip},ready:{ready},closed:{closed}" )) } fn render_from_summary(ctx: &RenderContext, summary: &str) -> Option { let mut open = 0u64; let mut wip = 0u64; let mut ready = 0u64; let mut closed = 0u64; for part in summary.split(',') { if let Some((key, val)) = part.split_once(':') { let n: u64 = val.parse().unwrap_or(0); match key { "open" => open = n, "wip" => wip = n, "ready" => ready = n, "closed" => closed = n, _ => {} } } } let cfg = &ctx.config.sections.beads; let mut parts: Vec = Vec::new(); if cfg.show_ready_count && ready > 0 { parts.push(format!("{ready} ready")); } if cfg.show_wip_count && wip > 0 { parts.push(format!("{wip} wip")); } if cfg.show_open_count && open > 0 { parts.push(format!("{open} open")); } if cfg.show_closed_count && closed > 0 { parts.push(format!("{closed} done")); } if parts.is_empty() { return None; } let raw = parts.join(" "); let ansi = if ctx.color_enabled { format!("{}{raw}{}", color::DIM, color::RESET) } else { raw.clone() }; Some(SectionOutput { raw, ansi }) }