darq 0.1.0

darq CLI + TUI — autonomous issue → PR pipeline with SAT and a learning loop.
Documentation
//! Footer — 1-row keyboard hint strip.
//!
//! Lowercase keybindings, ember highlight on actionable when state matches.
//! Override key deliberately omitted — see design-tui.md §Open questions §3.

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;

/// Footer = status bar. Left: focused-shift identity. Right: live agent
/// status > approval prompt > sock path > replay indicator (priority order).
/// Keybinding reference moved to a `?` help modal (follow-up). The "press ?
/// for keys" hint sits as a tiny corner glyph on the right.
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],
    );
}

/// Left side — focused shift identity, or idle hint.
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)),
        ]
    }
}

/// Right side — priority order: agent thinking > approval prompt > sock/replay.
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
}

/// Compact "Ns / Nm:SSs / NhMMm".
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)
    }
}

// (duplicate helper definitions removed — see top of module)
#[allow(dead_code)]
fn _unused_helper_marker() {}