use std::io::Write;
use std::time::Instant;
use crate::tui::theme::Theme;
pub struct StatusBarState {
pub model: String,
pub permission_mode: String,
pub message_count: usize,
pub cumulative_input_tokens: u64,
pub cumulative_output_tokens: u64,
pub estimated_cost_usd: String,
pub turn_start: Instant,
pub git_branch: Option<String>,
pub terminal_width: u16,
}
pub struct StatusBar;
impl StatusBar {
pub fn render(state: &StatusBarState, out: &mut dyn Write) -> std::io::Result<()> {
let elapsed = state.turn_start.elapsed();
let secs = elapsed.as_secs();
let model_display = truncate_str(&state.model, 18);
let total_tokens = state.cumulative_input_tokens + state.cumulative_output_tokens;
let tokens_display = format_tokens(total_tokens);
let branch_display = state.git_branch.as_deref().unwrap_or("?");
let cost_display = format!(
"{}${}{}",
Theme::ACCENT,
state.estimated_cost_usd,
Theme::TEXT_SECONDARY
);
let content = format!(
" model {} {} tokens {} {}s {} ",
model_display, tokens_display, cost_display, secs, branch_display,
);
let width = state.terminal_width as usize;
let display = if content.chars().count() > width {
truncate_str(&content, width.saturating_sub(1))
} else {
content
};
write!(
out,
"\x1b7\x1b[0G\x1b[2K{}{}{}",
Theme::TEXT_SECONDARY,
display,
Theme::RESET,
)?;
write!(out, "\x1b8")?;
out.flush()
}
pub fn clear(out: &mut dyn Write) -> std::io::Result<()> {
write!(out, "\x1b7\x1b[0G\x1b[2K\x1b8")?;
out.flush()
}
}
fn truncate_str(s: &str, max_len: usize) -> String {
if s.chars().count() <= max_len {
s.to_string()
} else {
let mut result = s
.chars()
.take(max_len.saturating_sub(1))
.collect::<String>();
result.push('…');
result
}
}
fn format_tokens(count: u64) -> String {
if count >= 1_000_000 {
format!("{:.1}M", count as f64 / 1_000_000.0)
} else if count >= 1_000 {
format!("{:.1}k", count as f64 / 1_000.0)
} else {
count.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_str_shorter_than_max() {
assert_eq!(truncate_str("hello", 10), "hello");
}
#[test]
fn truncate_str_at_max() {
assert_eq!(truncate_str("hello", 5), "hello");
}
#[test]
fn truncate_str_longer_than_max() {
let result = truncate_str("hello world", 6);
assert_eq!(result, "hello…");
}
#[test]
fn format_tokens_zero() {
assert_eq!(format_tokens(0), "0");
}
#[test]
fn format_tokens_thousands() {
assert_eq!(format_tokens(3200), "3.2k");
}
#[test]
fn format_tokens_millions() {
assert_eq!(format_tokens(1_500_000), "1.5M");
}
#[test]
fn render_produces_output_without_panicking() {
let state = StatusBarState {
model: "claude-sonnet-4".to_string(),
permission_mode: "read-only".to_string(),
message_count: 5,
cumulative_input_tokens: 3200,
cumulative_output_tokens: 800,
estimated_cost_usd: "0.04".to_string(),
turn_start: Instant::now(),
git_branch: Some("main".to_string()),
terminal_width: 80,
};
let mut buf: Vec<u8> = Vec::new();
let out: &mut dyn Write = &mut buf;
let _ = StatusBar::render(&state, out);
assert!(!buf.is_empty());
}
#[test]
fn render_truncates_to_terminal_width() {
let state = StatusBarState {
model: "claude-sonnet-4-with-a-very-long-name".to_string(),
permission_mode: "read-only".to_string(),
message_count: 5,
cumulative_input_tokens: 3200,
cumulative_output_tokens: 800,
estimated_cost_usd: "0.04".to_string(),
turn_start: Instant::now(),
git_branch: Some("feature/some-long-branch-name".to_string()),
terminal_width: 40,
};
let mut buf: Vec<u8> = Vec::new();
let out: &mut dyn Write = &mut buf;
let _ = StatusBar::render(&state, out);
assert!(!buf.is_empty());
}
#[test]
fn clear_produces_output_without_panicking() {
let mut buf: Vec<u8> = Vec::new();
let out: &mut dyn Write = &mut buf;
let _ = StatusBar::clear(out);
assert!(!buf.is_empty());
}
}