govctl 0.9.1

Project governance CLI for RFC, ADR, and Work Item management
use super::super::app::{App, View};
use super::components::ChromeBar;
use ratatui::prelude::*;

fn breadcrumb(app: &App) -> String {
    match app.view {
        View::Dashboard => "Dashboard".to_string(),
        View::RfcList => "Dashboard > RFCs".to_string(),
        View::AdrList => "Dashboard > ADRs".to_string(),
        View::WorkList => "Dashboard > Work".to_string(),
        View::RfcDetail(idx) => app
            .index
            .rfcs
            .get(idx)
            .map(|rfc| format!("Dashboard > RFCs > {}", rfc.rfc.rfc_id))
            .unwrap_or_else(|| "Dashboard > RFCs".to_string()),
        View::AdrDetail(idx) => app
            .index
            .adrs
            .get(idx)
            .map(|adr| format!("Dashboard > ADRs > {}", adr.meta().id))
            .unwrap_or_else(|| "Dashboard > ADRs".to_string()),
        View::WorkDetail(idx) => app
            .index
            .work_items
            .get(idx)
            .map(|item| format!("Dashboard > Work > {}", item.meta().id))
            .unwrap_or_else(|| "Dashboard > Work".to_string()),
        View::ClauseDetail(rfc_idx, clause_idx) => app
            .index
            .rfcs
            .get(rfc_idx)
            .and_then(|rfc| rfc.clauses.get(clause_idx).map(|clause| (rfc, clause)))
            .map(|(rfc, clause)| {
                format!(
                    "Dashboard > RFCs > {} > {}",
                    rfc.rfc.rfc_id, clause.spec.clause_id
                )
            })
            .unwrap_or_else(|| "Dashboard > RFCs".to_string()),
    }
}

fn header_status(app: &mut App) -> String {
    match app.view {
        View::Dashboard => format!(
            "RFC {} | ADR {} | Work {}",
            app.index.rfcs.len(),
            app.index.adrs.len(),
            app.index.work_items.len()
        ),
        View::RfcList | View::AdrList | View::WorkList => {
            let total = app.list_total_len();
            let shown = app.list_len();
            let mut parts = vec![format!("Shown {}/{}", shown, total)];
            if shown > 0 {
                parts.push(format!("Sel {}/{}", app.selected + 1, shown));
            }
            if app.filter_mode {
                parts.push(format!("Filter: /{}_", app.filter_query));
            } else if app.filter_active() {
                parts.push(format!("Filter: {}", app.filter_query));
            }
            parts.join(" | ")
        }
        _ => String::new(),
    }
}

pub(super) struct Header<'a> {
    app: &'a mut App,
}

impl<'a> Header<'a> {
    pub(super) fn new(app: &'a mut App) -> Self {
        Self { app }
    }

    // Implements [[RFC-0003:C-NAV]]
    pub(super) fn render(self, frame: &mut Frame, area: Rect) {
        let app = self.app;
        let left = Line::from(vec![
            Span::styled("govctl", Style::default().fg(Color::Cyan).bold()),
            Span::raw(" "),
            Span::raw(breadcrumb(app)),
        ]);

        ChromeBar::new(Color::Cyan, left, header_status(app)).render(frame, area);
    }
}

fn bindings_for_view(view: View) -> &'static [&'static str] {
    match view {
        View::Dashboard => &[
            "1/r", "RFCs", "2/a", "ADRs", "3/w", "Work", "?", "Help", "q", "Quit",
        ],
        View::RfcList | View::AdrList | View::WorkList => &[
            "j/k", "Navigate", "Enter", "View", "Esc", "Back", "/", "Filter", "g/G", "Jump", "?",
            "Help", "q", "Quit",
        ],
        View::RfcDetail(_) => &[
            "j/k",
            "Navigate",
            "Enter",
            "View Clause",
            "Esc",
            "Back",
            "?",
            "Help",
            "q",
            "Quit",
        ],
        View::AdrDetail(_) | View::WorkDetail(_) | View::ClauseDetail(_, _) => &[
            "j/k", "Scroll", "^d/^u", "Page", "Esc", "Back", "?", "Help", "q", "Quit",
        ],
    }
}

fn keybind_line(bindings: &[&str]) -> Line<'static> {
    let mut spans: Vec<Span<'static>> = vec![Span::raw(" ")];
    for chunk in bindings.chunks(2) {
        if chunk.len() == 2 {
            spans.push(Span::styled("[", Style::default().fg(Color::DarkGray)));
            spans.push(Span::styled(
                chunk[0].to_string(),
                Style::default().fg(Color::Cyan).bold(),
            ));
            spans.push(Span::styled("] ", Style::default().fg(Color::DarkGray)));
            spans.push(Span::styled(
                format!("{}  ", chunk[1]),
                Style::default().fg(Color::DarkGray),
            ));
        }
    }
    Line::from(spans)
}

pub(super) struct Footer<'a> {
    view: View,
    status: Option<&'a str>,
}

impl<'a> Footer<'a> {
    pub(super) fn new(view: View, status: Option<&'a str>) -> Self {
        Self { view, status }
    }

    // Implements [[RFC-0003:C-NAV]]
    pub(super) fn render(self, frame: &mut Frame, area: Rect) {
        ChromeBar::new(
            Color::DarkGray,
            keybind_line(bindings_for_view(self.view)),
            self.status.unwrap_or(""),
        )
        .left_alignment(Alignment::Center)
        .render(frame, area);
    }
}