use ratatui::prelude::*;
use ratatui::widgets::Paragraph;
use crate::tui::app::App;
pub fn render(frame: &mut Frame, area: Rect, app: &App) {
let line = build_line(app);
let paragraph =
Paragraph::new(line).style(Style::default().fg(Color::White).bg(Color::DarkGray));
frame.render_widget(paragraph, area);
}
pub fn build_line(app: &App) -> Line<'static> {
let mut spans: Vec<Span<'static>> = Vec::new();
spans.push(Span::raw(" "));
spans.push(Span::styled(
"local".to_string(),
Style::default()
.fg(Color::Green)
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD),
));
spans.push(separator());
spans.push(Span::styled(
app.model_name.clone(),
Style::default().fg(Color::Cyan).bg(Color::DarkGray),
));
spans.push(separator());
spans.push(Span::styled(
format!(
"↑{} ↓{}",
human_count(app.usage.total_input),
human_count(app.usage.total_output),
),
Style::default().fg(Color::White).bg(Color::DarkGray),
));
if let Some(cost) = crate::tui::app::estimate_cost(
&app.model_name,
app.usage.total_input,
app.usage.total_output,
) {
spans.push(Span::raw(" "));
spans.push(Span::styled(
format!("${cost:.4}"),
Style::default().fg(Color::Yellow).bg(Color::DarkGray),
));
}
spans.push(separator());
spans.push(Span::styled(
format!("turn {}", app.turn_count),
Style::default().fg(Color::White).bg(Color::DarkGray),
));
if app.plan_awaiting_approval {
spans.push(separator());
spans.push(Span::styled(
"plan: y/n".to_string(),
Style::default()
.fg(Color::Black)
.bg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
} else if app.planning_mode_on {
spans.push(separator());
spans.push(Span::styled(
"plan-first".to_string(),
Style::default().fg(Color::Yellow).bg(Color::DarkGray),
));
}
if let Some(started) = app.turn.started_at {
let elapsed = started.elapsed().as_secs_f64();
spans.push(separator());
spans.push(Span::styled(
format!("⏱ {:.1}s", elapsed),
Style::default().fg(Color::Magenta).bg(Color::DarkGray),
));
}
Line::from(spans)
}
fn separator() -> Span<'static> {
Span::styled(
" │ ".to_string(),
Style::default().fg(Color::DarkGray).bg(Color::DarkGray),
)
}
fn human_count(n: u64) -> String {
if n < 1000 {
n.to_string()
} else if n < 1_000_000 {
format!("{:.1}k", n as f64 / 1000.0)
} else {
format!("{:.1}M", n as f64 / 1_000_000.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn line_text(line: &Line) -> String {
line.spans.iter().map(|s| s.content.as_ref()).collect()
}
#[test]
fn human_count_formats_thresholds() {
assert_eq!(human_count(0), "0");
assert_eq!(human_count(999), "999");
assert_eq!(human_count(1234), "1.2k");
assert_eq!(human_count(1_500_000), "1.5M");
}
#[test]
fn status_bar_includes_model_and_tokens() {
let mut app = App::new();
app.model_name = "deepseek-chat".to_string();
app.usage.total_input = 1234;
app.usage.total_output = 342;
let line = build_line(&app);
let text = line_text(&line);
assert!(text.contains("local"));
assert!(text.contains("deepseek-chat"));
assert!(text.contains("↑1.2k"));
assert!(text.contains("↓342"));
assert!(text.contains("turn"));
}
#[test]
fn status_bar_includes_cost_for_known_model() {
let mut app = App::new();
app.model_name = "gpt-4o-mini".to_string();
app.usage.total_input = 1000;
app.usage.total_output = 1000;
let text = line_text(&build_line(&app));
assert!(text.contains("$"));
}
#[test]
fn status_bar_omits_cost_for_unknown_model() {
let mut app = App::new();
app.model_name = "totally-bogus-model".to_string();
app.usage.total_input = 1000;
app.usage.total_output = 1000;
let text = line_text(&build_line(&app));
assert!(!text.contains("$"));
}
#[test]
fn status_bar_shows_elapsed_only_when_turn_running() {
let mut app = App::new();
let no_turn = line_text(&build_line(&app));
assert!(!no_turn.contains("⏱"));
app.turn.start();
let with_turn = line_text(&build_line(&app));
assert!(with_turn.contains("⏱"));
}
#[test]
fn status_bar_shows_plan_awaiting_indicator() {
let mut app = App::new();
let no_plan = line_text(&build_line(&app));
assert!(!no_plan.contains("plan:"));
app.plan_awaiting_approval = true;
let with_plan = line_text(&build_line(&app));
assert!(with_plan.contains("plan: y/n"));
}
#[test]
fn status_bar_shows_plan_first_mode() {
let mut app = App::new();
let no_mode = line_text(&build_line(&app));
assert!(!no_mode.contains("plan-first"));
app.planning_mode_on = true;
let with_mode = line_text(&build_line(&app));
assert!(with_mode.contains("plan-first"));
}
}