use ratatui::Frame;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use crate::tui::app::{App, StationState};
use crate::tui::theme::Palette;
pub fn render(frame: &mut Frame, area: Rect, app: &App, palette: &Palette) {
let left_spans = build_left(app, palette);
let right_spans = build_right(app, palette);
let right_width = right_spans
.iter()
.map(|s| s.content.chars().count())
.sum::<usize>() as u16;
let cols = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(0), Constraint::Length(right_width)])
.split(area);
let bg = Style::new().bg(palette.bg_0);
frame.render_widget(
Paragraph::new(Line::from(left_spans))
.style(bg)
.alignment(Alignment::Left),
cols[0],
);
frame.render_widget(
Paragraph::new(Line::from(right_spans))
.style(bg)
.alignment(Alignment::Right),
cols[1],
);
}
fn build_left(app: &App, palette: &Palette) -> Vec<Span<'static>> {
let dim = Style::new().fg(palette.fg_3);
let val = Style::new().fg(palette.fg_1);
let id_style = Style::new().fg(palette.cyan);
if let Some(rid) = &app.focus_run_id {
let short = rid[..8.min(rid.len())].to_string();
let issue = app
.runs
.iter()
.chain(app.approvals.iter())
.find(|r| r.id == *rid)
.map(|r| r.issue.clone())
.unwrap_or_else(|| "—".into());
let duration = app
.runs
.iter()
.chain(app.approvals.iter())
.find(|r| r.id == *rid)
.map(|r| r.duration.clone())
.unwrap_or_else(|| "—".into());
let done = app
.chain_state
.stations
.iter()
.filter(|s| matches!(s, StationState::Completed))
.count();
let active = app
.chain_state
.stations
.iter()
.any(|s| matches!(s, StationState::Active));
let progress_glyph = if active { "●" } else { "·" };
vec![
Span::raw(" "),
Span::styled(short, id_style),
Span::styled(" · ", dim),
Span::styled(issue, val),
Span::styled(" · ", dim),
Span::styled(duration, val),
Span::styled(" · ", dim),
Span::styled(format!("{progress_glyph} {done}/7 stations"), val),
]
} else {
vec![
Span::raw(" "),
Span::styled("idle", Style::new().fg(palette.fg_3)),
Span::styled(" · no shift focused", Style::new().fg(palette.fg_4)),
]
}
}
fn build_right(app: &App, palette: &Palette) -> Vec<Span<'static>> {
let dim = Style::new().fg(palette.fg_4);
let hint = Style::new().fg(palette.fg_4);
let suffix_hint = vec![
Span::styled(" ", Style::new()),
Span::styled(
"?",
Style::new().fg(palette.cyan).add_modifier(Modifier::BOLD),
),
Span::styled(" keys ", hint),
];
let primary: Vec<Span<'static>> = if let Some(hb) = &app.agent_heartbeat {
let since = hb.last_frame_at.elapsed().as_secs();
let rate = hb.rate_per_sec();
vec![
Span::styled(
"agent thinking ",
Style::new()
.fg(palette.heartbeat)
.add_modifier(Modifier::BOLD),
),
Span::styled("· ", dim),
Span::styled(
format_short_secs(hb.elapsed_secs),
Style::new().fg(palette.fg_0),
),
Span::styled(" · ", dim),
Span::styled(format_chars(hb.output_chars), Style::new().fg(palette.fg_1)),
Span::styled(" · ", dim),
Span::styled(format!("+{rate}/s"), Style::new().fg(palette.cyan)),
Span::styled(" · last ", dim),
Span::styled(format!("{since}s"), Style::new().fg(palette.fg_2)),
]
} else if !app.approvals.is_empty() {
vec![
Span::styled(
"gate awaiting ",
Style::new().fg(palette.ember).add_modifier(Modifier::BOLD),
),
Span::styled("· ", dim),
Span::styled(
"[enter]",
Style::new().fg(palette.ember).add_modifier(Modifier::BOLD),
),
Span::styled(" to approve", Style::new().fg(palette.fg_2)),
]
} else if app.is_replay {
vec![Span::styled(
"replay session",
Style::new().fg(palette.fg_3),
)]
} else if let Ok(home) = std::env::var("HOME") {
vec![Span::styled(
format!("sock · {home}/.darq/daemon.sock"),
dim,
)]
} else {
vec![]
};
let mut out = primary;
out.extend(suffix_hint);
out
}
fn format_short_secs(secs: u64) -> String {
if secs < 60 {
format!("{secs}s")
} else if secs < 3600 {
format!("{}m{:02}s", secs / 60, secs % 60)
} else {
format!("{}h{:02}m", secs / 3600, (secs % 3600) / 60)
}
}
fn format_chars(n: u64) -> String {
if n < 1_000 {
format!("{n}B")
} else if n < 1_000_000 {
format!("{}KB", n / 1_000)
} else {
format!("{:.1}MB", n as f64 / 1_000_000.0)
}
}
#[allow(dead_code)]
fn _unused_helper_marker() {}