cargo-port 0.0.3

A TUI for inspecting and managing Rust projects
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 super::PaneTitleCount;
use super::default_pane_chrome;
use super::empty_pane_block;
use super::pane_title;
use crate::lint::LintRun;
use crate::lint::LintRunStatus;
use crate::tui::LINT_SPINNER;
use crate::tui::app::App;
use crate::tui::constants::ACCENT_COLOR;
use crate::tui::constants::COLUMN_HEADER_COLOR;
use crate::tui::constants::ERROR_COLOR;
use crate::tui::constants::LABEL_COLOR;
use crate::tui::constants::SUCCESS_COLOR;
use crate::tui::constants::TITLE_COLOR;
use crate::tui::detail;
use crate::tui::detail::LintsData;
use crate::tui::interaction;
use crate::tui::interaction::UiSurface::Content;
use crate::tui::types::Pane;
use crate::tui::types::PaneFocusState;
use crate::tui::types::PaneId;

fn lints_panel_title(data: &LintsData, focused: bool, cursor: usize) -> String {
    if data.runs.is_empty() {
        let msg = if data.is_cargo_active {
            crate::constants::NO_LINT_RUNS
        } else {
            crate::constants::NO_LINT_RUNS_NOT_RUST
        };
        return format!(" {msg} ");
    }
    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 {
        default_pane_chrome().block(title, focused)
    } else {
        empty_pane_block(title)
    }
}

fn build_lint_rows(
    runs: &[LintRun],
    animation_elapsed: std::time::Duration,
    pane: &Pane,
    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 = detail::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 = detail::format_time(&run.started_at);
        let end_time = run
            .finished_at
            .as_deref()
            .map_or_else(|| "".to_string(), detail::format_time);
        let duration = detail::format_duration(run.duration_ms);

        let (result_cell, row_style) = match run.status {
            LintRunStatus::Running => {
                let spinner = LINT_SPINNER.frame_at(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 = pane.selection_state(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),
                ),
                result_cell,
            ])
            .style(selection.patch(row_style)),
        );
    }

    rows
}

pub fn render_lints_panel(frame: &mut Frame, app: &mut App, area: Rect) {
    let Some(lints_data) = app.pane_manager().lints_data.clone() else {
        let block = lints_panel_block(" No Lint Runs ".to_string(), false, false);
        frame.render_widget(block, area);
        return;
    };

    let focused = app.is_focused(PaneId::Lints);
    let title = lints_panel_title(
        &lints_data,
        focused,
        app.pane_manager().pane(PaneId::Lints).pos(),
    );
    let block = lints_panel_block(title, focused, !lints_data.runs.is_empty());

    let inner = block.inner(area);
    app.pane_manager_mut()
        .pane_mut(PaneId::Lints)
        .set_content_area(inner);

    if lints_data.runs.is_empty() {
        frame.render_widget(block, area);
        app.pane_manager_mut().pane_mut(PaneId::Lints).set_len(0);
        return;
    }

    let pane = app.pane_manager().pane(PaneId::Lints).clone();
    let focus = app.pane_focus_state(PaneId::Lints);
    let rows = build_lint_rows(&lints_data.runs, app.animation_elapsed(), &pane, focus);
    app.pane_manager_mut()
        .pane_mut(PaneId::Lints)
        .set_len(rows.len());

    let col_header_style = Style::default()
        .fg(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(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("Result"),
        ])
        .style(col_header_style),
    )
    .block(block)
    .column_spacing(2)
    .row_highlight_style(Style::default());

    let mut table_state =
        TableState::default().with_selected(Some(app.pane_manager().pane(PaneId::Lints).pos()));
    frame.render_stateful_widget(table, area, &mut table_state);
    app.pane_manager_mut()
        .pane_mut(PaneId::Lints)
        .set_scroll_offset(table_state.offset());

    let visible_height = usize::from(inner.height.saturating_sub(1));
    let visible_start = table_state.offset();
    let visible_end = app
        .pane_manager()
        .pane(PaneId::Lints)
        .len()
        .min(visible_start.saturating_add(visible_height));

    for (screen_row, row_index) in (visible_start..visible_end).enumerate() {
        let row_y = inner
            .y
            .saturating_add(1)
            .saturating_add(u16::try_from(screen_row).unwrap_or(u16::MAX));
        interaction::register_pane_row_hitbox(
            app,
            Rect::new(inner.x, row_y, inner.width, 1),
            PaneId::Lints,
            row_index,
            Content,
        );
    }
}