cargo-port 0.1.3

A TUI for inspecting and managing Rust projects
use std::time::Duration;

use ratatui::Frame;
use ratatui::layout::Alignment;
use ratatui::layout::Constraint;
use ratatui::layout::Rect;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::Block;
use ratatui::widgets::Cell;
use ratatui::widgets::Row;
use ratatui::widgets::Table;
use ratatui::widgets::TableState;
use tui_pane::ACTIVITY_SPINNER;
use tui_pane::PaneFocusState;
use tui_pane::PaneTitleCount;
use tui_pane::Viewport;
use tui_pane::accent_color;
use tui_pane::error_color;
use tui_pane::label_color;
use tui_pane::render_overflow_affordance;
use tui_pane::success_color;
use tui_pane::title_color;

use super::LintsData;
use crate::lint::LintRun;
use crate::lint::LintRunStatus;
use crate::tui::pane::PaneRenderCtx;
use crate::tui::render;
use crate::tui::state::Lint;
use crate::tui::theme_roles;

fn lints_panel_title(data: &LintsData, focused: bool, cursor: usize) -> String {
    if data.runs.is_empty() {
        let msg = if data.is_rust {
            "No lint runs"
        } else {
            "No lint runs — not a Rust project"
        };
        return format!(" {msg} ");
    }
    tui_pane::pane_title(
        "Lint Runs",
        &PaneTitleCount::Single {
            len:    data.runs.len(),
            cursor: focused.then_some(cursor),
        },
    )
}

fn lints_panel_block(title: String, focused: bool, has_runs: bool) -> Block<'static> {
    if has_runs {
        tui_pane::default_pane_chrome().block(title, focused)
    } else {
        tui_pane::empty_pane_block(title)
    }
}

fn build_lint_rows(
    runs: &[LintRun],
    sizes: &[Option<u64>],
    animation_elapsed: Duration,
    pane: &Viewport,
    focus: PaneFocusState,
) -> Vec<Row<'static>> {
    let date_style = Style::default()
        .fg(title_color())
        .add_modifier(Modifier::BOLD);

    let mut rows = Vec::new();
    let mut current_date = String::new();

    for (row_index, run) in runs.iter().enumerate() {
        let date = super::format_date(&run.started_at);
        let date_cell = if date == current_date {
            Cell::from("")
        } else {
            current_date.clone_from(&date);
            Cell::from(Span::styled(date, date_style))
        };

        let start_time = super::format_time(&run.started_at);
        let end_time = run
            .finished_at
            .as_deref()
            .map_or_else(|| "".to_string(), super::format_time);
        let duration = super::format_duration(run.duration_ms);
        let size = sizes
            .get(row_index)
            .copied()
            .flatten()
            .map_or_else(|| "".to_string(), render::format_bytes);

        let (result_cell, row_style) = match run.status {
            LintRunStatus::Running => {
                let spinner = running_lint_spinner(animation_elapsed);
                (Cell::from(spinner), Style::default().fg(accent_color()))
            },
            LintRunStatus::Passed => (Cell::from("passed"), Style::default().fg(success_color())),
            LintRunStatus::Failed => (Cell::from("failed"), Style::default().fg(error_color())),
        };

        let selection = tui_pane::selection_state(pane, row_index, focus);
        rows.push(
            Row::new(vec![
                date_cell,
                Cell::from(
                    Line::from(Span::styled(start_time, Style::default().fg(label_color())))
                        .alignment(Alignment::Right),
                ),
                Cell::from(
                    Line::from(Span::styled(end_time, Style::default().fg(label_color())))
                        .alignment(Alignment::Right),
                ),
                Cell::from(
                    Line::from(Span::styled(duration, Style::default().fg(label_color())))
                        .alignment(Alignment::Right),
                ),
                Cell::from(
                    Line::from(Span::styled(size, Style::default().fg(label_color())))
                        .alignment(Alignment::Right),
                ),
                result_cell,
            ])
            .style(selection.patch(row_style)),
        );
    }

    rows
}

fn running_lint_spinner(animation_elapsed: Duration) -> &'static str {
    ACTIVITY_SPINNER.frame_at(animation_elapsed)
}

/// Body of `LintsPane::render`. Same as
/// `cpu::render_cpu_pane_body`: typed parameters instead of
/// `&mut App`. Helpers above already operate on `&Viewport`.
pub fn render_lints_pane_body(
    frame: &mut Frame,
    area: Rect,
    pane: &mut Lint,
    ctx: &PaneRenderCtx<'_>,
) {
    let Some(lints_data) = pane.content().cloned() else {
        let block = lints_panel_block(" No Lint Runs ".to_string(), false, false);
        frame.render_widget(block, area);
        return;
    };

    let focused = pane.focus.is_focused;
    let title = lints_panel_title(&lints_data, focused, pane.viewport.pos());
    let block = lints_panel_block(title, focused, !lints_data.runs.is_empty());

    let inner = block.inner(area);
    {
        let viewport = &mut pane.viewport;
        viewport.set_content_area(inner);
        viewport.set_viewport_rows(usize::from(inner.height.saturating_sub(1)));
    }

    if lints_data.runs.is_empty() {
        frame.render_widget(block, area);
        pane.viewport.set_len(0);
        return;
    }

    let viewport_clone = pane.viewport.clone();
    let focus = pane.focus.state;
    let rows = build_lint_rows(
        &lints_data.runs,
        &lints_data.sizes,
        ctx.animation_elapsed,
        &viewport_clone,
        focus,
    );
    pane.viewport.set_len(rows.len());

    let col_header_style = Style::default()
        .fg(theme_roles::column_header_color())
        .add_modifier(Modifier::BOLD);

    let table = Table::new(
        rows,
        [
            Constraint::Length(10),
            Constraint::Length(8),
            Constraint::Length(8),
            Constraint::Length(8),
            Constraint::Length(9),
            Constraint::Length(8),
        ],
    )
    .header(
        Row::new(vec![
            Cell::from(""),
            Cell::from(Line::from("Start").alignment(Alignment::Right)),
            Cell::from(Line::from("End").alignment(Alignment::Right)),
            Cell::from(Line::from("Duration").alignment(Alignment::Right)),
            Cell::from(Line::from("Size").alignment(Alignment::Right)),
            Cell::from("Result"),
        ])
        .style(col_header_style),
    )
    .block(block)
    .column_spacing(2)
    .row_highlight_style(Style::default());

    let mut table_state = TableState::default().with_selected(Some(pane.viewport.pos()));
    *table_state.offset_mut() = pane.viewport.scroll_offset();
    frame.render_stateful_widget(table, area, &mut table_state);
    pane.viewport.set_scroll_offset(table_state.offset());
    render_overflow_affordance(
        frame,
        area,
        pane.viewport.overflow(),
        Style::default().fg(label_color()),
    );

    let _ = ctx;
}