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:
48
src/section/beads.rs
Normal file
48
src/section/beads.rs
Normal 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 })
|
||||
}
|
||||
29
src/section/cache_efficiency.rs
Normal file
29
src/section/cache_efficiency.rs
Normal 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 })
|
||||
}
|
||||
45
src/section/context_bar.rs
Normal file
45
src/section/context_bar.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
43
src/section/context_remaining.rs
Normal file
43
src/section/context_remaining.rs
Normal 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 })
|
||||
}
|
||||
47
src/section/context_trend.rs
Normal file
47
src/section/context_trend.rs
Normal 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 })
|
||||
}
|
||||
49
src/section/context_usage.rs
Normal file
49
src/section/context_usage.rs
Normal 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
39
src/section/cost.rs
Normal 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
36
src/section/cost_trend.rs
Normal 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 })
|
||||
}
|
||||
19
src/section/cost_velocity.rs
Normal file
19
src/section/cost_velocity.rs
Normal 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
66
src/section/custom.rs
Normal 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
23
src/section/duration.rs
Normal 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
34
src/section/hostname.rs
Normal 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()
|
||||
}
|
||||
32
src/section/lines_changed.rs
Normal file
32
src/section/lines_changed.rs
Normal 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
53
src/section/load.rs
Normal 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
118
src/section/mod.rs
Normal 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
68
src/section/model.rs
Normal 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 }
|
||||
}
|
||||
19
src/section/output_style.rs
Normal file
19
src/section/output_style.rs
Normal 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
36
src/section/project.rs
Normal 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
35
src/section/provider.rs
Normal 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
37
src/section/time.rs
Normal 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))
|
||||
}
|
||||
20
src/section/token_velocity.rs
Normal file
20
src/section/token_velocity.rs
Normal 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
33
src/section/tokens_raw.rs
Normal 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
33
src/section/tools.rs
Normal 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
24
src/section/turns.rs
Normal 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
166
src/section/vcs.rs
Normal 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
19
src/section/version.rs
Normal 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 })
|
||||
}
|
||||
Reference in New Issue
Block a user