feat: complete Rust port of claude-statusline

Port the entire 2236-line bash statusline script to Rust.
Implements all 25 sections, 3-phase layout engine (render, priority
drop, flex/justify), file-based caching with flock, 9-level terminal
width detection, trend sparklines, and deep-merge JSON config.

Release binary: 864K with LTO. Render time: <1ms warm.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-06 14:21:57 -05:00
commit b55d1aefd1
65 changed files with 12439 additions and 0 deletions

48
src/section/beads.rs Normal file
View File

@@ -0,0 +1,48 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
use crate::shell;
use std::time::Duration;
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
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;
}
let ttl = Duration::from_secs(ctx.config.sections.beads.ttl);
let timeout = Duration::from_millis(200);
let cached = ctx.cache.get("beads_summary", ttl);
let summary = cached.or_else(|| {
// Run br ready to get count of ready items
let out = shell::exec_with_timeout(
"br",
&["ready", "--json"],
Some(ctx.project_dir.to_str()?),
timeout,
)?;
// Count JSON array items (simple: count opening braces at indent level 1)
let count = out.matches("\"id\"").count();
let summary = format!("{count}");
ctx.cache.set("beads_summary", &summary);
Some(summary)
})?;
let count: usize = summary.trim().parse().unwrap_or(0);
if count == 0 {
return None;
}
let raw = format!("{count} ready");
let ansi = if ctx.color_enabled {
format!("{}{raw}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

View File

@@ -0,0 +1,29 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.cache_efficiency.enabled {
return None;
}
let pct = ctx.metrics.cache_efficiency_pct?;
let pct_int = pct.round() as u64;
let raw = format!("Cache: {pct_int}%");
let color_code = if pct >= 80.0 {
color::GREEN
} else if pct >= 50.0 {
color::YELLOW
} else {
color::RED
};
let ansi = if ctx.color_enabled {
format!("{color_code}{raw}{}", color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

View File

@@ -0,0 +1,45 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
/// Render context bar at a given bar_width. Called both at initial render
/// and during flex expansion (with wider bar_width).
pub fn render_at_width(ctx: &RenderContext, bar_width: u16) -> Option<SectionOutput> {
let pct = ctx.input.context_window.as_ref()?.used_percentage?;
let pct_int = pct.round() as u16;
let filled = (u32::from(pct_int) * u32::from(bar_width) / 100) as usize;
let empty = bar_width as usize - filled;
let bar = "=".repeat(filled) + &"-".repeat(empty);
let raw = format!("[{bar}] {pct_int}%");
let thresh = &ctx.config.sections.context_bar.thresholds;
let color_code = threshold_color(pct, thresh);
let ansi = if ctx.color_enabled {
format!("{color_code}[{bar}] {pct_int}%{}", color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.context_bar.base.enabled {
return None;
}
render_at_width(ctx, ctx.config.sections.context_bar.bar_width)
}
fn threshold_color(pct: f64, thresh: &crate::config::Thresholds) -> String {
if pct >= thresh.critical {
format!("{}{}", color::RED, color::BOLD)
} else if pct >= thresh.danger {
color::RED.to_string()
} else if pct >= thresh.warn {
color::YELLOW.to_string()
} else {
color::GREEN.to_string()
}
}

View File

@@ -0,0 +1,43 @@
use crate::color;
use crate::format;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.context_remaining.base.enabled {
return None;
}
let cw = ctx.input.context_window.as_ref()?;
let pct = cw.used_percentage.unwrap_or(0.0);
let capacity = cw.context_window_size.unwrap_or(200_000);
let used_tokens = (pct / 100.0 * capacity as f64) as u64;
let remaining = capacity.saturating_sub(used_tokens);
let remaining_str = format::human_tokens(remaining);
let raw = ctx
.config
.sections
.context_remaining
.format
.replace("{remaining}", &remaining_str);
let thresh = &ctx.config.sections.context_remaining.thresholds;
// Invert thresholds: high usage = low remaining = more danger
let color_code = if pct >= thresh.critical {
format!("{}{}", color::RED, color::BOLD)
} else if pct >= thresh.danger {
color::RED.to_string()
} else if pct >= thresh.warn {
color::YELLOW.to_string()
} else {
color::GREEN.to_string()
};
let ansi = if ctx.color_enabled {
format!("{color_code}{raw}{}", color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

View File

@@ -0,0 +1,47 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
use crate::trend;
use std::time::Duration;
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.context_trend.base.enabled {
return None;
}
let pct = ctx.input.context_window.as_ref()?.used_percentage?;
let pct_int = pct.round() as i64;
let width = ctx.config.sections.context_trend.width as usize;
let csv = trend::append(
ctx.cache,
"context",
pct_int,
width,
Duration::from_secs(30),
)?;
let spark = trend::sparkline(&csv, width);
if spark.is_empty() {
return None;
}
let thresh = &ctx.config.sections.context_trend.thresholds;
let color_code = if pct >= thresh.critical {
format!("{}{}", color::RED, color::BOLD)
} else if pct >= thresh.danger {
color::RED.to_string()
} else if pct >= thresh.warn {
color::YELLOW.to_string()
} else {
color::GREEN.to_string()
};
let raw = spark.clone();
let ansi = if ctx.color_enabled {
format!("{color_code}{spark}{}", color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

View File

@@ -0,0 +1,49 @@
use crate::color;
use crate::format;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.context_usage.base.enabled {
return None;
}
let cw = ctx.input.context_window.as_ref()?;
let pct = cw.used_percentage?;
let pct_int = pct.round() as u64;
let total_input = cw.total_input_tokens.unwrap_or(0);
let total_output = cw.total_output_tokens.unwrap_or(0);
let used = total_input + total_output;
let capacity = cw
.context_window_size
.unwrap_or(ctx.config.sections.context_usage.capacity);
let raw = format!(
"{}/{} ({pct_int}%)",
format::human_tokens(used),
format::human_tokens(capacity),
);
let thresh = &ctx.config.sections.context_usage.thresholds;
let color_code = threshold_color(pct, thresh);
let ansi = if ctx.color_enabled {
format!("{color_code}{raw}{}", color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}
fn threshold_color(pct: f64, thresh: &crate::config::Thresholds) -> String {
if pct >= thresh.critical {
format!("{}{}", color::RED, color::BOLD)
} else if pct >= thresh.danger {
color::RED.to_string()
} else if pct >= thresh.warn {
color::YELLOW.to_string()
} else {
color::GREEN.to_string()
}
}

39
src/section/cost.rs Normal file
View File

@@ -0,0 +1,39 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
use crate::width::WidthTier;
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.cost.base.enabled {
return None;
}
let cost_val = ctx.input.cost.as_ref()?.total_cost_usd?;
let decimals = match ctx.width_tier {
WidthTier::Narrow => 0,
WidthTier::Medium => 2,
WidthTier::Wide => 4,
};
let cost_str = format!("{cost_val:.decimals$}");
let raw = format!("${cost_str}");
let thresh = &ctx.config.sections.cost.thresholds;
let color_code = if cost_val >= thresh.critical {
format!("{}{}", color::RED, color::BOLD)
} else if cost_val >= thresh.danger {
color::RED.to_string()
} else if cost_val >= thresh.warn {
color::YELLOW.to_string()
} else {
color::GREEN.to_string()
};
let ansi = if ctx.color_enabled {
format!("{color_code}${cost_str}{}", color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

36
src/section/cost_trend.rs Normal file
View File

@@ -0,0 +1,36 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
use crate::trend;
use std::time::Duration;
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.cost_trend.base.enabled {
return None;
}
let cost_val = ctx.input.cost.as_ref()?.total_cost_usd?;
let cost_cents = (cost_val * 100.0) as i64;
let width = ctx.config.sections.cost_trend.width as usize;
let csv = trend::append(
ctx.cache,
"cost",
cost_cents,
width,
Duration::from_secs(30),
)?;
let spark = trend::sparkline(&csv, width);
if spark.is_empty() {
return None;
}
let raw = format!("${spark}");
let ansi = if ctx.color_enabled {
format!("{}{raw}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

View File

@@ -0,0 +1,19 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.cost_velocity.enabled {
return None;
}
let velocity = ctx.metrics.cost_velocity?;
let raw = format!("${velocity:.2}/min");
let ansi = if ctx.color_enabled {
format!("{}{raw}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

66
src/section/custom.rs Normal file
View File

@@ -0,0 +1,66 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
use crate::shell;
use std::time::Duration;
/// Render a custom command section by ID.
pub fn render(id: &str, ctx: &RenderContext) -> Option<SectionOutput> {
let cmd_cfg = ctx.config.custom.iter().find(|c| c.id == id)?;
let ttl = Duration::from_secs(cmd_cfg.ttl);
let timeout = Duration::from_millis(ctx.config.global.shell_timeout_ms);
let cache_key = format!("custom_{id}");
let cached = ctx.cache.get(&cache_key, ttl);
let output_str = cached.or_else(|| {
let result = if let Some(ref exec) = cmd_cfg.exec {
if exec.is_empty() {
return None;
}
let args: Vec<&str> = exec[1..].iter().map(|s| s.as_str()).collect();
shell::exec_with_timeout(&exec[0], &args, None, timeout)
} else if let Some(ref command) = cmd_cfg.command {
shell::exec_with_timeout("sh", &["-c", command], None, timeout)
} else {
None
};
if let Some(ref val) = result {
ctx.cache.set(&cache_key, val);
}
result
})?;
if output_str.is_empty() {
return None;
}
let label = cmd_cfg.label.as_deref().unwrap_or("");
let raw = if label.is_empty() {
output_str.clone()
} else {
format!("{label}: {output_str}")
};
let ansi = if ctx.color_enabled {
if let Some(ref color_cfg) = cmd_cfg.color {
if let Some(matched_color) = color_cfg.match_map.get(&output_str) {
let c = color::resolve_color(matched_color, ctx.theme, &ctx.config.colors);
format!("{c}{raw}{}", color::RESET)
} else if let Some(ref default_c) = cmd_cfg.default_color {
let c = color::resolve_color(default_c, ctx.theme, &ctx.config.colors);
format!("{c}{raw}{}", color::RESET)
} else {
raw.clone()
}
} else if let Some(ref default_c) = cmd_cfg.default_color {
let c = color::resolve_color(default_c, ctx.theme, &ctx.config.colors);
format!("{c}{raw}{}", color::RESET)
} else {
raw.clone()
}
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

23
src/section/duration.rs Normal file
View File

@@ -0,0 +1,23 @@
use crate::color;
use crate::format;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.duration.enabled {
return None;
}
let ms = ctx.input.cost.as_ref()?.total_duration_ms?;
if ms == 0 {
return None;
}
let raw = format::human_duration(ms);
let ansi = if ctx.color_enabled {
format!("{}{raw}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

34
src/section/hostname.rs Normal file
View File

@@ -0,0 +1,34 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.hostname.enabled {
return None;
}
let name = hostname()?;
let raw = name.clone();
let ansi = if ctx.color_enabled {
format!("{}{name}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}
fn hostname() -> Option<String> {
// Try gethostname via libc
let mut buf = [0u8; 256];
let ret = unsafe { libc::gethostname(buf.as_mut_ptr().cast(), buf.len()) };
if ret == 0 {
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
let name = String::from_utf8_lossy(&buf[..end]).to_string();
if !name.is_empty() {
return Some(name);
}
}
// Fallback: HOSTNAME env var
std::env::var("HOSTNAME").ok()
}

View File

@@ -0,0 +1,32 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.lines_changed.enabled {
return None;
}
let cost = ctx.input.cost.as_ref()?;
let added = cost.total_lines_added.unwrap_or(0);
let removed = cost.total_lines_removed.unwrap_or(0);
if added == 0 && removed == 0 {
return None;
}
let raw = format!("+{added}/-{removed}");
let ansi = if ctx.color_enabled {
format!(
"{}+{added}{}{}/-{removed}{}",
color::GREEN,
color::RESET,
color::RED,
color::RESET,
)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

53
src/section/load.rs Normal file
View File

@@ -0,0 +1,53 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
use std::time::Duration;
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.load.base.enabled {
return None;
}
let ttl = Duration::from_secs(ctx.config.sections.load.ttl);
let cached = ctx.cache.get("load_avg", ttl);
let load_str = cached.or_else(|| {
// Read load average from /proc/loadavg (Linux) or sysctl (macOS)
#[cfg(target_os = "linux")]
{
let content = std::fs::read_to_string("/proc/loadavg").ok()?;
let load1 = content.split_whitespace().next()?;
ctx.cache.set("load_avg", load1);
Some(load1.to_string())
}
#[cfg(target_os = "macos")]
{
let out = crate::shell::exec_with_timeout(
"sysctl",
&["-n", "vm.loadavg"],
None,
Duration::from_millis(100),
)?;
// sysctl output: "{ 1.23 4.56 7.89 }"
let load1 = out
.trim_start_matches(|c: char| !c.is_ascii_digit() && c != '.')
.split_whitespace()
.next()?
.to_string();
ctx.cache.set("load_avg", &load1);
Some(load1)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
None
}
})?;
let raw = format!("load {load_str}");
let ansi = if ctx.color_enabled {
format!("{}{raw}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

118
src/section/mod.rs Normal file
View File

@@ -0,0 +1,118 @@
use crate::cache::Cache;
use crate::config::Config;
use crate::input::InputData;
use crate::metrics::ComputedMetrics;
use crate::theme::Theme;
use crate::width::WidthTier;
use std::path::Path;
pub mod beads;
pub mod cache_efficiency;
pub mod context_bar;
pub mod context_remaining;
pub mod context_trend;
pub mod context_usage;
pub mod cost;
pub mod cost_trend;
pub mod cost_velocity;
pub mod custom;
pub mod duration;
pub mod hostname;
pub mod lines_changed;
pub mod load;
pub mod model;
pub mod output_style;
pub mod project;
pub mod provider;
pub mod time;
pub mod token_velocity;
pub mod tokens_raw;
pub mod tools;
pub mod turns;
pub mod vcs;
pub mod version;
/// What every section renderer returns.
#[derive(Debug, Clone)]
pub struct SectionOutput {
pub raw: String,
pub ansi: String,
}
/// Type alias for section render functions.
pub type RenderFn = fn(&RenderContext) -> Option<SectionOutput>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VcsType {
Git,
Jj,
None,
}
/// Context passed to every section renderer.
pub struct RenderContext<'a> {
pub input: &'a InputData,
pub config: &'a Config,
pub theme: Theme,
pub width_tier: WidthTier,
pub term_width: u16,
pub vcs_type: VcsType,
pub project_dir: &'a Path,
pub cache: &'a Cache,
pub glyphs_enabled: bool,
pub color_enabled: bool,
pub metrics: ComputedMetrics,
}
/// Build the registry of all built-in sections.
pub fn registry() -> Vec<(&'static str, RenderFn)> {
vec![
("model", model::render),
("provider", provider::render),
("project", project::render),
("vcs", vcs::render),
("beads", beads::render),
("context_bar", context_bar::render),
("context_usage", context_usage::render),
("context_remaining", context_remaining::render),
("tokens_raw", tokens_raw::render),
("cache_efficiency", cache_efficiency::render),
("cost", cost::render),
("cost_velocity", cost_velocity::render),
("token_velocity", token_velocity::render),
("cost_trend", cost_trend::render),
("context_trend", context_trend::render),
("lines_changed", lines_changed::render),
("duration", duration::render),
("tools", tools::render),
("turns", turns::render),
("load", load::render),
("version", version::render),
("time", time::render),
("output_style", output_style::render),
("hostname", hostname::render),
]
}
/// Dispatch: look up section by ID and render it.
pub fn render_section(id: &str, ctx: &RenderContext) -> Option<SectionOutput> {
if is_spacer(id) {
return Some(SectionOutput {
raw: " ".into(),
ansi: " ".into(),
});
}
for (name, render_fn) in registry() {
if name == id {
return render_fn(ctx);
}
}
custom::render(id, ctx)
}
pub fn is_spacer(id: &str) -> bool {
id == "spacer" || id.starts_with("_spacer")
}

68
src/section/model.rs Normal file
View File

@@ -0,0 +1,68 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.model.enabled {
return None;
}
let model = ctx.input.model.as_ref()?;
let id = model.id.as_deref().unwrap_or("?");
let id_lower = id.to_ascii_lowercase();
let base_name = if id_lower.contains("opus") {
"Opus"
} else if id_lower.contains("sonnet") {
"Sonnet"
} else if id_lower.contains("haiku") {
"Haiku"
} else {
return Some(simple_output(
model.display_name.as_deref().unwrap_or(id),
ctx,
));
};
let version = extract_version(&id_lower, &base_name.to_ascii_lowercase());
let name = match version {
Some(v) => format!("{base_name} {v}"),
None => base_name.to_string(),
};
let raw = format!("[{name}]");
let ansi = if ctx.color_enabled {
format!("{}[{name}]{}", color::BOLD, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}
fn extract_version(id: &str, family: &str) -> Option<String> {
let parts: Vec<&str> = id.split('-').collect();
for window in parts.windows(3) {
if window[0] == family {
if let (Ok(a), Ok(b)) = (window[1].parse::<u8>(), window[2].parse::<u8>()) {
return Some(format!("{a}.{b}"));
}
}
if window[2] == family {
if let (Ok(a), Ok(b)) = (window[0].parse::<u8>(), window[1].parse::<u8>()) {
return Some(format!("{a}.{b}"));
}
}
}
None
}
fn simple_output(name: &str, ctx: &RenderContext) -> SectionOutput {
let raw = format!("[{name}]");
let ansi = if ctx.color_enabled {
format!("{}[{name}]{}", color::BOLD, color::RESET)
} else {
raw.clone()
};
SectionOutput { raw, ansi }
}

View File

@@ -0,0 +1,19 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.output_style.enabled {
return None;
}
let style_name = ctx.input.output_style.as_ref()?.name.as_deref()?;
let raw = style_name.to_string();
let ansi = if ctx.color_enabled {
format!("{}{raw}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

36
src/section/project.rs Normal file
View File

@@ -0,0 +1,36 @@
use crate::color;
use crate::format;
use crate::glyph;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.project.base.enabled {
return None;
}
let dir = ctx.input.workspace.as_ref()?.project_dir.as_deref()?;
let name = std::path::Path::new(dir).file_name()?.to_str()?;
let truncated = if ctx.config.sections.project.truncate.enabled
&& ctx.config.sections.project.truncate.max > 0
{
format::truncate(
name,
ctx.config.sections.project.truncate.max,
&ctx.config.sections.project.truncate.style,
)
} else {
name.to_string()
};
let folder_glyph = glyph::glyph("folder", &ctx.config.glyphs);
let raw = format!("{folder_glyph}{truncated}");
let ansi = if ctx.color_enabled {
format!("{}{folder_glyph}{truncated}{}", color::CYAN, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

35
src/section/provider.rs Normal file
View File

@@ -0,0 +1,35 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.provider.enabled {
return None;
}
let model = ctx.input.model.as_ref()?;
let id = model.id.as_deref().unwrap_or("");
let id_lower = id.to_ascii_lowercase();
let provider = if id_lower.contains("claude")
|| id_lower.contains("opus")
|| id_lower.contains("sonnet")
|| id_lower.contains("haiku")
{
"Anthropic"
} else if id_lower.contains("gpt") || id_lower.contains("o1") || id_lower.contains("o3") {
"OpenAI"
} else if id_lower.contains("gemini") {
"Google"
} else {
return None;
};
let raw = provider.to_string();
let ansi = if ctx.color_enabled {
format!("{}{provider}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

37
src/section/time.rs Normal file
View File

@@ -0,0 +1,37 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.time.base.enabled {
return None;
}
// Simple HH:MM format without chrono dependency
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()?;
let secs = now.as_secs();
// Get local time offset using libc
let (hour, minute) = local_time(secs)?;
let raw = format!("{hour:02}:{minute:02}");
let ansi = if ctx.color_enabled {
format!("{}{raw}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}
fn local_time(epoch_secs: u64) -> Option<(u32, u32)> {
let time_t = epoch_secs as libc::time_t;
let mut tm = std::mem::MaybeUninit::<libc::tm>::uninit();
let result = unsafe { libc::localtime_r(&time_t, tm.as_mut_ptr()) };
if result.is_null() {
return None;
}
let tm = unsafe { tm.assume_init() };
Some((tm.tm_hour as u32, tm.tm_min as u32))
}

View File

@@ -0,0 +1,20 @@
use crate::color;
use crate::format;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.token_velocity.enabled {
return None;
}
let velocity = ctx.metrics.token_velocity?;
let raw = format!("{} tok/min", format::human_tokens(velocity as u64));
let ansi = if ctx.color_enabled {
format!("{}{raw}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

33
src/section/tokens_raw.rs Normal file
View File

@@ -0,0 +1,33 @@
use crate::color;
use crate::format;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.tokens_raw.base.enabled {
return None;
}
let cw = ctx.input.context_window.as_ref()?;
let input_tok = cw.total_input_tokens.unwrap_or(0);
let output_tok = cw.total_output_tokens.unwrap_or(0);
if input_tok == 0 && output_tok == 0 {
return None;
}
let raw = ctx
.config
.sections
.tokens_raw
.format
.replace("{input}", &format::human_tokens(input_tok))
.replace("{output}", &format::human_tokens(output_tok));
let ansi = if ctx.color_enabled {
format!("{}{raw}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

33
src/section/tools.rs Normal file
View File

@@ -0,0 +1,33 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.tools.base.enabled {
return None;
}
let cost = ctx.input.cost.as_ref()?;
let count = cost.total_tool_uses.unwrap_or(0);
if count == 0 {
return None;
}
let last = if ctx.config.sections.tools.show_last_name {
cost.last_tool_name
.as_deref()
.map(|n| format!(" ({n})"))
.unwrap_or_default()
} else {
String::new()
};
let raw = format!("{count} tools{last}");
let ansi = if ctx.color_enabled {
format!("{}{raw}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

24
src/section/turns.rs Normal file
View File

@@ -0,0 +1,24 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.turns.base.enabled {
return None;
}
let count = ctx.input.cost.as_ref()?.total_turns?;
if count == 0 {
return None;
}
let label = if count == 1 { "turn" } else { "turns" };
let raw = format!("{count} {label}");
let ansi = if ctx.color_enabled {
format!("{}{raw}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

166
src/section/vcs.rs Normal file
View File

@@ -0,0 +1,166 @@
use crate::color;
use crate::glyph;
use crate::section::{RenderContext, SectionOutput, VcsType};
use crate::shell::{self, GitStatusV2};
use crate::width::WidthTier;
use std::time::Duration;
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.vcs.base.enabled {
return None;
}
if ctx.vcs_type == VcsType::None {
return None;
}
let dir = ctx.project_dir.to_str()?;
let ttl = &ctx.config.sections.vcs.ttl;
let glyphs = &ctx.config.glyphs;
match ctx.vcs_type {
VcsType::Git => render_git(ctx, dir, ttl, glyphs),
VcsType::Jj => render_jj(ctx, dir, ttl, glyphs),
VcsType::None => None,
}
}
fn render_git(
ctx: &RenderContext,
dir: &str,
ttl: &crate::config::VcsTtl,
glyphs: &crate::config::GlyphConfig,
) -> Option<SectionOutput> {
let branch_ttl = Duration::from_secs(ttl.branch);
let dirty_ttl = Duration::from_secs(ttl.dirty);
let ab_ttl = Duration::from_secs(ttl.ahead_behind);
let timeout = Duration::from_millis(200);
let branch_cached = ctx.cache.get("vcs_branch", branch_ttl);
let dirty_cached = ctx.cache.get("vcs_dirty", dirty_ttl);
let ab_cached = ctx.cache.get("vcs_ab", ab_ttl);
let status = if branch_cached.is_none() || dirty_cached.is_none() || ab_cached.is_none() {
let output = shell::exec_with_timeout(
"git",
&["-C", dir, "status", "--porcelain=v2", "--branch"],
None,
timeout,
);
match output {
Some(ref out) => {
let s = shell::parse_git_status_v2(out);
if let Some(ref b) = s.branch {
ctx.cache.set("vcs_branch", b);
}
ctx.cache
.set("vcs_dirty", if s.is_dirty { "1" } else { "" });
ctx.cache
.set("vcs_ab", &format!("{} {}", s.ahead, s.behind));
s
}
None => GitStatusV2 {
branch: branch_cached.or_else(|| ctx.cache.get_stale("vcs_branch")),
is_dirty: dirty_cached
.or_else(|| ctx.cache.get_stale("vcs_dirty"))
.is_some_and(|v| !v.is_empty()),
ahead: 0,
behind: 0,
},
}
} else {
GitStatusV2 {
branch: branch_cached,
is_dirty: dirty_cached.is_some_and(|v| !v.is_empty()),
ahead: ab_cached
.as_ref()
.and_then(|s| s.split_whitespace().next()?.parse().ok())
.unwrap_or(0),
behind: ab_cached
.as_ref()
.and_then(|s| s.split_whitespace().nth(1)?.parse().ok())
.unwrap_or(0),
}
};
let branch = status.branch.as_deref().unwrap_or("?");
let branch_glyph = glyph::glyph("branch", glyphs);
let dirty_glyph = if status.is_dirty && ctx.config.sections.vcs.show_dirty {
glyph::glyph("dirty", glyphs)
} else {
""
};
let mut raw = format!("{branch_glyph}{branch}{dirty_glyph}");
let mut ansi = if ctx.color_enabled {
let mut s = format!("{}{branch_glyph}{branch}{}", color::GREEN, color::RESET);
if !dirty_glyph.is_empty() {
s.push_str(&format!("{}{dirty_glyph}{}", color::YELLOW, color::RESET));
}
s
} else {
raw.clone()
};
// Ahead/behind: medium+ width only
if ctx.config.sections.vcs.show_ahead_behind
&& ctx.width_tier != WidthTier::Narrow
&& (status.ahead > 0 || status.behind > 0)
{
let ahead_g = glyph::glyph("ahead", glyphs);
let behind_g = glyph::glyph("behind", glyphs);
let mut ab = String::new();
if status.ahead > 0 {
ab.push_str(&format!("{ahead_g}{}", status.ahead));
}
if status.behind > 0 {
ab.push_str(&format!("{behind_g}{}", status.behind));
}
raw.push_str(&format!(" {ab}"));
if ctx.color_enabled {
ansi.push_str(&format!(" {}{ab}{}", color::DIM, color::RESET));
} else {
ansi = raw.clone();
}
}
Some(SectionOutput { raw, ansi })
}
fn render_jj(
ctx: &RenderContext,
_dir: &str,
ttl: &crate::config::VcsTtl,
glyphs: &crate::config::GlyphConfig,
) -> Option<SectionOutput> {
let branch_ttl = Duration::from_secs(ttl.branch);
let timeout = Duration::from_millis(200);
let branch = ctx.cache.get("vcs_branch", branch_ttl).or_else(|| {
let out = shell::exec_with_timeout(
"jj",
&[
"log",
"-r",
"@",
"--no-graph",
"-T",
"if(bookmarks, bookmarks.join(\",\"), change_id.shortest(8))",
"--color=never",
],
None,
timeout,
)?;
ctx.cache.set("vcs_branch", &out);
Some(out)
})?;
let branch_glyph = glyph::glyph("branch", glyphs);
let raw = format!("{branch_glyph}{branch}");
let ansi = if ctx.color_enabled {
format!("{}{branch_glyph}{branch}{}", color::GREEN, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}

19
src/section/version.rs Normal file
View File

@@ -0,0 +1,19 @@
use crate::color;
use crate::section::{RenderContext, SectionOutput};
pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
if !ctx.config.sections.version.enabled {
return None;
}
let ver = ctx.input.version.as_deref()?;
let raw = format!("v{ver}");
let ansi = if ctx.color_enabled {
format!("{}{raw}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}