use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::Widget,
};
pub struct StatusBar<'a> {
model: &'a str,
mode_label: &'a str,
context_pct: u32,
queue_len: usize,
elapsed_secs: u64,
last_turn: Option<&'a TurnStats>,
scroll_info: Option<(usize, usize)>,
}
#[derive(Debug, Clone, Default)]
pub struct TurnStats {
pub tokens_in: i64,
pub tokens_out: i64,
pub cache_read: i64,
pub elapsed_ms: u64,
pub rate: f64,
}
impl<'a> StatusBar<'a> {
pub fn new(model: &'a str, mode_label: &'a str, context_pct: u32) -> Self {
Self {
model,
mode_label,
context_pct,
queue_len: 0,
elapsed_secs: 0,
last_turn: None,
scroll_info: None,
}
}
pub fn with_queue(mut self, queue_len: usize) -> Self {
self.queue_len = queue_len;
self
}
pub fn with_elapsed(mut self, secs: u64) -> Self {
self.elapsed_secs = secs;
self
}
pub fn with_last_turn(mut self, stats: &'a TurnStats) -> Self {
self.last_turn = Some(stats);
self
}
pub fn with_scroll_info(mut self, offset: usize, total: usize) -> Self {
self.scroll_info = Some((offset, total));
self
}
}
impl Widget for StatusBar<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let mode_color = match self.mode_label {
"auto" => Color::Green,
"strict" => Color::Cyan,
"safe" => Color::Yellow,
_ => Color::DarkGray,
};
let bar_width: u32 = 10;
let filled = (self.context_pct * bar_width / 100).min(bar_width);
let empty = bar_width - filled;
let ctx_color = if self.context_pct >= 90 {
Color::Red
} else if self.context_pct >= 75 {
Color::Yellow
} else {
Color::DarkGray
};
let model_display = if self.model.len() > 32 {
format!("{}…", &self.model[..31])
} else {
self.model.to_string()
};
let mut spans = vec![
Span::styled(
format!(" {model_display} "),
Style::default().fg(Color::DarkGray),
),
Span::styled("\u{2502}", Style::default().fg(Color::Rgb(60, 60, 60))),
Span::styled(
format!(" {} ", self.mode_label),
Style::default().fg(mode_color),
),
Span::styled("\u{2502}", Style::default().fg(Color::Rgb(60, 60, 60))),
Span::styled(
format!(
" {}{} {}%",
"\u{2588}".repeat(filled as usize),
"\u{2591}".repeat(empty as usize),
self.context_pct,
),
Style::default().fg(ctx_color),
),
];
if self.elapsed_secs > 0 {
spans.push(Span::styled(
"\u{2502}",
Style::default().fg(Color::Rgb(60, 60, 60)),
));
spans.push(Span::styled(
format!(" \u{23f3} {}s ", self.elapsed_secs),
Style::default().fg(Color::Cyan),
));
}
if self.queue_len > 0 {
spans.push(Span::styled(
"\u{2502}",
Style::default().fg(Color::Rgb(60, 60, 60)),
));
spans.push(Span::styled(
format!(" \u{1f4cb} {} queued ", self.queue_len),
Style::default().fg(Color::Yellow),
));
spans.push(Span::styled(
"^U clear ",
Style::default().fg(Color::Rgb(100, 100, 100)),
));
}
if let Some(stats) = self.last_turn {
spans.push(Span::styled(
"\u{2502}",
Style::default().fg(Color::Rgb(60, 60, 60)),
));
let time = if stats.elapsed_ms >= 1000 {
format!("{:.1}s", stats.elapsed_ms as f64 / 1000.0)
} else {
format!("{}ms", stats.elapsed_ms)
};
let mut stat_str = format!(
" ↑{} ↓{} · {} · {:.0} t/s ",
stats.tokens_in, stats.tokens_out, time, stats.rate
);
if stats.cache_read > 0 && stats.tokens_in > 0 {
let pct = (stats.cache_read * 100) / stats.tokens_in;
stat_str = format!(
" ↑{} ↓{} 🗄{pct}% · {} · {:.0} t/s ",
stats.tokens_in, stats.tokens_out, time, stats.rate
);
}
spans.push(Span::styled(stat_str, Style::default().fg(Color::DarkGray)));
}
if let Some((offset, total)) = self.scroll_info {
spans.push(Span::styled(
"\u{2502}",
Style::default().fg(Color::Rgb(60, 60, 60)),
));
spans.push(Span::styled(
format!(" \u{2191}{offset}/{total} "),
Style::default().fg(Color::Yellow),
));
}
Line::from(spans).render(area, buf);
}
}