use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::Modifier;
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use super::super::panel;
use super::{draw_stats, draw_table, verdict_line};
use crate::tui::app::App;
use crate::tui::theme;
pub fn draw(f: &mut Frame, app: &App, area: Rect) {
if app.bench.history.is_empty() {
let block = panel("HISTORY ─".into(), false);
let inner = block.inner(area);
f.render_widget(block, area);
f.render_widget(
Paragraph::new(vec![
Line::default(),
Line::from(Span::styled(" No runs recorded yet.", theme::text2())),
Line::from(Span::styled(
" Every completed run is written to ~/.metrale/runs and appears here.",
theme::dim(),
)),
]),
inner,
);
return;
}
let cols = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(34), Constraint::Min(30)])
.split(area);
draw_list(f, app, cols[0]);
draw_detail(f, app, cols[1]);
}
fn draw_list(f: &mut Frame, app: &App, area: Rect) {
let block = panel(format!("RUNS ─ {} ─", app.bench.history.len()), true);
let inner = block.inner(area);
f.render_widget(block, area);
let visible = inner.height as usize;
let offset = app
.bench
.history_row
.saturating_sub(visible.saturating_sub(1));
let mut lines: Vec<Line> = Vec::new();
let mut prev_basis: Option<String> = None;
for (i, entry) in app
.bench
.history
.iter()
.enumerate()
.skip(offset)
.take(visible)
{
let basis = basis_of(&entry.target_model, &entry.params, &entry.serve_overrides);
if let Some(prev) = &prev_basis
&& *prev != basis
&& !lines.is_empty()
{
lines.push(Line::from(Span::styled(
format!(" ┄┄ {basis} ┄┄"),
theme::dim(),
)));
}
prev_basis = Some(basis);
let selected = i == app.bench.history_row;
let mark = match entry.frame.verdict.as_ref().map(|v| v.kind) {
Some(metrale_bench::VerdictKind::Pass) => Span::styled("✓", theme::brand_green()),
Some(metrale_bench::VerdictKind::Fail) => Span::styled("✗", theme::error()),
_ => Span::styled("·", theme::dim()),
};
let mut line = Line::from(vec![
Span::styled(if selected { "▌" } else { " " }, theme::brand_purple()),
mark,
Span::styled(
format!(" {:<20}", entry.benchmark_id),
if selected {
theme::text().add_modifier(Modifier::BOLD)
} else {
theme::text2()
},
),
Span::styled(entry.age_text(), theme::dim()),
]);
if selected {
line = line.style(theme::selected());
}
lines.push(line);
}
f.render_widget(Paragraph::new(lines), inner);
}
pub(super) fn basis_of(
target_model: &str,
params: &std::collections::BTreeMap<String, String>,
serve_overrides: &std::collections::BTreeMap<String, String>,
) -> String {
let model = target_model.rsplit('/').next().unwrap_or(target_model);
let params = joined(params);
format!(
"{model} · {} · {}",
regime_of(serve_overrides),
short_hash(¶ms)
)
}
fn regime_of(serve_overrides: &std::collections::BTreeMap<String, String>) -> String {
if serve_overrides.is_empty() {
return "unpinned".into();
}
let keys: Vec<&str> = serve_overrides.keys().map(String::as_str).collect();
let named = match keys.len() {
1..=2 => keys.join("+"),
n => format!("{}+{}", keys[..2].join("+"), n - 2),
};
format!("{named}:{}", short_hash(&joined(serve_overrides)))
}
fn joined(map: &std::collections::BTreeMap<String, String>) -> String {
map.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join(",")
}
fn short_hash(s: &str) -> String {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in s.as_bytes() {
h ^= *b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{:04x}", (h ^ (h >> 32)) as u16)
}
fn draw_detail(f: &mut Frame, app: &App, area: Rect) {
let Some(entry) = app.bench.history.get(app.bench.history_row) else {
return;
};
let frame = &entry.frame;
let has_stats = !frame.summary.is_empty();
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(if has_stats { 3 } else { 0 }),
Constraint::Min(6),
Constraint::Length(2),
])
.split(area);
f.render_widget(
Paragraph::new(Line::from(vec![
Span::styled(
format!(" {} ", entry.benchmark_id),
theme::text().add_modifier(Modifier::BOLD),
),
Span::styled(
format!(
"· {} · {:.0}s · {}",
frame.phase,
frame.elapsed.as_secs_f64(),
entry.age_text()
),
theme::text2(),
),
])),
rows[0],
);
if has_stats {
draw_stats(f, &frame.summary, rows[1]);
}
if let Some(table) = &frame.table {
let max = draw_table(f, table, app.bench.history_table_scroll, rows[2]);
app.bench.history_table_scroll_max.set(max);
}
if let Some(verdict) = &frame.verdict {
f.render_widget(Paragraph::new(verdict_line(verdict)), rows[3]);
}
}