use crate::app::App;
use crate::locale::t;
use crate::theme::Theme;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Cell, Paragraph, Row, Table};
use ratatui::Frame;
use super::{
braille_graph_multirow, btop_block, fmt_tokens, grad_at, make_gradient, meter_bar, truncate_str,
};
pub(crate) fn draw_context_panel(f: &mut Frame, app: &App, area: Rect, theme: &Theme) {
let cpu_grad = make_gradient(theme.cpu_grad.start, theme.cpu_grad.mid, theme.cpu_grad.end);
let block = btop_block("context", "¹", theme.cpu_box, theme);
f.render_widget(block, area);
let inner = Rect {
x: area.x + 1,
y: area.y + 1,
width: area.width.saturating_sub(2),
height: area.height.saturating_sub(2),
};
if inner.height <= 1 {
let ticks_per_min = 30usize;
let rates: Vec<f64> = app.token_rates.iter().copied().collect();
let tokens_per_min: f64 = rates.iter().rev().take(ticks_per_min).sum();
let total: u64 = app.sessions.iter().map(|s| s.total_tokens()).sum();
let active = app.sessions.iter().filter(|s| s.status.is_active()).count();
let rate_label = t("context.rate");
let total_label = t("context.total");
let active_label = t("context.active");
let line = Line::from(vec![
Span::styled(
format!(" {} ", rate_label),
Style::default().fg(theme.graph_text),
),
Span::styled(
format!("{}/min", fmt_tokens(tokens_per_min as u64)),
Style::default().fg(grad_at(&cpu_grad, 50.0)),
),
Span::styled(
format!(" {} ", total_label),
Style::default().fg(theme.graph_text),
),
Span::styled(fmt_tokens(total), Style::default().fg(theme.main_fg)),
Span::styled(
format!(" {} {}", active, active_label),
Style::default().fg(theme.proc_misc),
),
]);
f.render_widget(Paragraph::new(line), inner);
return;
}
let halves = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(65), Constraint::Percentage(35)])
.split(inner);
draw_context_sparkline(f, app, halves[0], &cpu_grad, theme);
draw_context_bars(f, app, halves[1], &cpu_grad, theme);
}
fn draw_context_sparkline(
f: &mut Frame,
app: &App,
area: Rect,
cpu_grad: &[Color; 101],
theme: &Theme,
) {
let avail_h = area.height as usize;
let avail_w = area.width as usize;
let mut lines: Vec<Line> = Vec::new();
let spark_w = avail_w.saturating_sub(2).max(4);
let rates: Vec<f64> = app.token_rates.iter().copied().collect();
let max_rate = rates.iter().cloned().fold(1.0_f64, f64::max);
let normalized: Vec<f64> = rates.iter().map(|&v| v / max_rate).collect();
let ticks_per_min = 30usize;
let tokens_per_min: f64 = rates.iter().rev().take(ticks_per_min).sum();
let current_pct = normalized.last().copied().unwrap_or(0.0) * 100.0;
let pct_color = grad_at(cpu_grad, current_pct);
let token_rate_label = t("context.token_rate");
lines.push(Line::from(vec![
Span::styled(token_rate_label, Style::default().fg(theme.graph_text)),
Span::styled(
format!(" {}/min", fmt_tokens(tokens_per_min as u64)),
Style::default().fg(pct_color),
),
]));
let graph_h = avail_h.saturating_sub(2).max(1);
let graph_rows =
braille_graph_multirow(&normalized, spark_w, graph_h, cpu_grad, theme.graph_text);
for row_spans in graph_rows {
let mut line_spans = vec![Span::styled(" ", Style::default())];
line_spans.extend(row_spans);
lines.push(Line::from(line_spans));
}
let total_tokens: u64 = app.sessions.iter().map(|s| s.total_tokens()).sum();
let total_label = t("context.total");
lines.push(Line::from(vec![
Span::styled(
format!(" {}", fmt_tokens(total_tokens)),
Style::default().fg(theme.main_fg),
),
Span::styled(
format!(" {}", total_label),
Style::default().fg(theme.graph_text),
),
]));
f.render_widget(Paragraph::new(lines), area);
}
fn draw_context_bars(f: &mut Frame, app: &App, area: Rect, cpu_grad: &[Color; 101], theme: &Theme) {
let header_style = Style::default()
.fg(theme.main_fg)
.add_modifier(Modifier::BOLD);
let bar_width = (area.width as usize).saturating_sub(30).clamp(4, 20);
let mut rows = Vec::new();
let project_label = t("context.project");
let context_label = t("context.context");
let window_label = t("context.window");
for session in &app.sessions {
let raw_pct = session.context_percent;
let bar_pct = raw_pct.min(100.0);
let warn = if raw_pct >= 90.0 {
"⚠"
} else if raw_pct >= 75.0 {
"!"
} else {
""
};
let pct_color = grad_at(cpu_grad, bar_pct);
let ctx_info = match (session.context_window > 0, session.compaction_count) {
(true, 0) => fmt_tokens(session.context_window),
(true, n) => format!("{} C{}", fmt_tokens(session.context_window), n),
(false, 0) => String::new(),
(false, n) => format!("C{}", n),
};
rows.push(Row::new(vec![
Cell::from(Span::styled(
truncate_str(&session.project_name, 10),
Style::default().fg(theme.title),
)),
Cell::from(Line::from({
let mut spans = meter_bar(bar_pct, bar_width, cpu_grad, theme.meter_bg);
spans.push(Span::styled(
format!(" {:>3.0}%{}", raw_pct, warn),
Style::default().fg(pct_color),
));
spans
})),
Cell::from(Span::styled(
ctx_info,
Style::default().fg(theme.graph_text),
)),
]));
}
if app.sessions.is_empty() {
let no_active = t("context.no_active_sessions");
rows.push(Row::new(vec![
Cell::from(Span::styled(
no_active,
Style::default().fg(theme.inactive_fg),
)),
Cell::from(""),
Cell::from(""),
]));
}
let header = Row::new(vec![
Cell::from(Span::styled(project_label, header_style)),
Cell::from(Span::styled(context_label, header_style)),
Cell::from(Span::styled(window_label, header_style)),
]);
let widths = [
Constraint::Length(10),
Constraint::Min(10),
Constraint::Length(10),
];
let table = Table::new(rows, widths).header(header);
f.render_widget(table, area);
}