quinjet 0.0.77

A fast, live, keyboard-first Git source-control interface for the terminal
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use super::*;

pub(super) fn draw_pr_navigation(
    frame: &mut Frame<'_>,
    area: Rect,
    app: &mut App,
    theme: &Theme,
) -> Vec<SidebarHitArea> {
    let has_stack = app.pull_request_stack.is_some();
    let third = area.width.div_euclid(if has_stack { 4 } else { 3 });
    let first = Rect::new(area.x, area.y, third, 1);
    let second = Rect::new(first.right(), area.y, third, 1);
    let third_tab = Rect::new(
        second.right(),
        area.y,
        if has_stack {
            third
        } else {
            area.width.saturating_sub(third.saturating_mul(2))
        },
        1,
    );
    let overview = if app.selected_pr_commit().is_some() {
        "CI"
    } else {
        "PR"
    };
    draw_pull_request_section_tab(
        frame,
        first,
        overview.to_owned(),
        app.pull_request_section == PullRequestSection::Overview,
        theme,
    );
    draw_pull_request_section_tab(
        frame,
        second,
        format!(
            "Files {}{}",
            app.pull_request_total_files,
            app.search_title_suffix()
        ),
        app.pull_request_section == PullRequestSection::Files,
        theme,
    );
    draw_pull_request_section_tab(
        frame,
        third_tab,
        "Commits".to_owned(),
        app.pull_request_section == PullRequestSection::Commits,
        theme,
    );
    let mut hits = vec![
        SidebarHitArea {
            area: first,
            target: SidebarHit::PullRequestOverview,
        },
        SidebarHitArea {
            area: second,
            target: SidebarHit::PullRequestFiles,
        },
        SidebarHitArea {
            area: third_tab,
            target: SidebarHit::PullRequestCommits,
        },
    ];
    if let Some(stack) = &app.pull_request_stack {
        let stack_tab = Rect::new(
            third_tab.right(),
            area.y,
            area.width.saturating_sub(third.saturating_mul(3)),
            1,
        );
        draw_pull_request_section_tab(
            frame,
            stack_tab,
            format!("Stack {}", stack.size),
            app.pull_request_section == PullRequestSection::Stack,
            theme,
        );
        hits.push(SidebarHitArea {
            area: stack_tab,
            target: SidebarHit::PullRequestStack,
        });
    }
    let scoped = app.selected_pr_commit().is_some();
    if scoped && area.height > 1 {
        let scope = Rect::new(area.x, area.y.saturating_add(1), area.width, 1);
        if let Some(commit) = app.selected_pr_commit() {
            frame.render_widget(
                Paragraph::new(Line::from(vec![
                    Span::styled(
                        format!(" {}", commit.abbreviated_oid),
                        Style::default()
                            .fg(theme.accent)
                            .add_modifier(Modifier::BOLD),
                    ),
                    Span::styled(" · [A All commits]", Style::default().fg(theme.muted)),
                ]))
                .style(Style::default().bg(theme.panel_alt)),
                scope,
            );
        }
        hits.push(SidebarHitArea {
            area: scope,
            target: SidebarHit::PullRequestAllCommits,
        });
    }
    let offset = 1 + u16::from(scoped);
    let list = Rect::new(
        area.x,
        area.y.saturating_add(offset),
        area.width,
        area.height.saturating_sub(offset),
    );
    match app.pull_request_section {
        PullRequestSection::Overview => {
            hits.extend(draw_pull_request_check_list(frame, list, app, theme));
        }
        PullRequestSection::Files => {
            hits.extend(draw_pull_request_file_tree(frame, list, app, theme));
        }
        PullRequestSection::Commits => hits.extend(draw_pr_commit_list(frame, list, app, theme)),
        PullRequestSection::Stack => {}
    }
    hits
}

fn draw_pr_commit_list(
    frame: &mut Frame<'_>,
    area: Rect,
    app: &mut App,
    theme: &Theme,
) -> Vec<SidebarHitArea> {
    if area.height == 0 {
        return Vec::new();
    }
    let snapshot = &app.pr_commits.snapshot;
    let label = if snapshot.truncated {
        format!(
            " NEWEST {} OF {} COMMITS",
            snapshot.commits.len(),
            snapshot.total_commits
        )
    } else {
        format!(" COMMITS {} · newest first", snapshot.total_commits)
    };
    frame.render_widget(
        Paragraph::new(label).style(Style::default().fg(theme.muted).bg(theme.panel_alt)),
        Rect::new(area.x, area.y, area.width, 1),
    );
    let height = area.height.saturating_sub(1);
    if snapshot.commits.is_empty() {
        let message = app.pr_commits.error.clone().unwrap_or_else(|| {
            if app.pr_commits.loading {
                " Loading commits…".to_owned()
            } else {
                " No commits reported".to_owned()
            }
        });
        frame.render_widget(
            Paragraph::new(message)
                .wrap(Wrap { trim: false })
                .style(Style::default().fg(theme.muted)),
            Rect::new(area.x, area.y.saturating_add(1), area.width, height),
        );
        return Vec::new();
    }
    app.sidebar_viewport(
        app.pr_commits.cursor.saturating_mul(2),
        usize::from(height),
        app.pr_commits.snapshot.commits.len().saturating_mul(2),
    );
    let list = Rect::new(area.x, area.y.saturating_add(1), area.width, height);
    app.geometry.sidebar_scrollbar = crate::app::Scrollbar::new(
        list,
        app.sidebar_offset,
        app.pr_commits.snapshot.commits.len().saturating_mul(2),
        usize::from(height),
    );
    let width = area
        .width
        .saturating_sub(u16::from(app.geometry.sidebar_scrollbar.is_some()));
    let mut hits = Vec::new();
    for offset in 0..usize::from(height) {
        let row_index = app.sidebar_offset.saturating_add(offset);
        let index = row_index.div_euclid(2);
        let Some(commit) = app.pr_commits.snapshot.commits.get(index) else {
            break;
        };
        let selected = index == app.pr_commits.cursor;
        let row = Rect::new(
            area.x,
            area.y.saturating_add(1).saturating_add(cells(offset)),
            width,
            1,
        );
        let text = if row_index.is_multiple_of(2) {
            format!(" {} {}", if selected { "›" } else { " " }, commit.subject)
        } else {
            format!("   {} · {}", commit.abbreviated_oid, commit.author)
        };
        frame.render_widget(
            Paragraph::new(truncate_end(&text, usize::from(width))).style(
                Style::default()
                    .fg(if row_index.is_multiple_of(2) {
                        theme.text
                    } else {
                        theme.muted
                    })
                    .bg(if selected {
                        theme.selected
                    } else {
                        theme.panel
                    }),
            ),
            row,
        );
        hits.push(SidebarHitArea {
            area: row,
            target: SidebarHit::PullRequestCommit(index),
        });
    }
    draw_scrollbar(frame, app.geometry.sidebar_scrollbar, theme);
    hits
}

pub(super) fn commit_ci_rows(app: &App, theme: &Theme) -> Vec<ContentRow> {
    let Some(commit) = app.selected_pr_commit() else {
        return Vec::new();
    };
    let mut rows = vec![
        ContentRow::text(
            format!("{}  {}", commit.abbreviated_oid, commit.subject),
            Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
        ),
        ContentRow::text(
            format!(
                "{} · {}",
                commit.author,
                format_relative_timestamp(&commit.authored_at)
            ),
            Style::default().fg(theme.muted),
        ),
        ContentRow::blank(),
        ContentRow::plain(check_summary_line(app, theme)),
    ];
    if let Some(error) = &app.pull_request_checks_error {
        rows.push(ContentRow::text(
            error.clone(),
            Style::default().fg(theme.error),
        ));
    }
    rows.push(ContentRow::blank());
    rows.push(ContentRow::text(
        "Select a check in the sidebar to inspect its steps and logs.",
        Style::default().fg(theme.muted),
    ));
    rows
}

pub(super) fn commit_browser_rows(app: &App, theme: &Theme) -> Vec<ContentRow> {
    let Some(commit) = app.pr_commits.snapshot.commits.get(app.pr_commits.cursor) else {
        let message = app.pr_commits.error.clone().unwrap_or_else(|| {
            if app.pr_commits.loading {
                "Loading commits…".to_owned()
            } else {
                "No commits reported".to_owned()
            }
        });
        return vec![ContentRow::text(message, Style::default().fg(theme.muted))];
    };
    vec![
        ContentRow::text(
            commit.subject.clone(),
            Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
        ),
        ContentRow::blank(),
        ContentRow::plain(detail_line("Commit", commit.oid.clone(), theme)),
        ContentRow::plain(detail_line("Author", commit.author.clone(), theme)),
        ContentRow::plain(detail_line(
            "Authored",
            format_relative_timestamp(&commit.authored_at),
            theme,
        )),
        ContentRow::plain(detail_line(
            "Committed",
            format_relative_timestamp(&commit.committed_at),
            theme,
        )),
        ContentRow::blank(),
        ContentRow::text(
            "Complete pushed history up to the PR head. Enter or click a commit for files and CI checks.",
            Style::default().fg(theme.text),
        ),
        ContentRow::text(
            "Files compare the selected commit with its first parent.",
            Style::default().fg(theme.muted),
        ),
        ContentRow::text(
            "Shift+F: Files · Shift+P: CI · Shift+A: All commits",
            Style::default().fg(theme.muted),
        ),
    ]
}

pub(super) fn commit_person(name: &str, email: &str, timestamp: &str) -> String {
    let date = format_relative_timestamp(timestamp);
    if name.is_empty() {
        return date;
    }
    if email.is_empty() {
        format!("{name}  ·  {date}")
    } else {
        format!("{name} <{email}>  ·  {date}")
    }
}