diffler 0.16.1

Standalone magit-style git TUI and code reviewer, on its own or with your coding agent
Documentation
//! The pull-request list: open PRs of the repo's forge. Enter reviews the
//! selected PR in place (no checkout needed); `b` checks its branch out.

use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;

use crate::app::App;
use crate::keymap::Action;
use crate::ui::Hint;

const HINTS: &[Hint] = &[
    Hint::Leaf(&[Action::Open], "review"),
    Hint::Leaf(&[Action::BranchCheckout], "checkout"),
    Hint::Leaf(&[Action::CopyUrl], "yank url"),
    Hint::Leaf(&[Action::Search], "search"),
    Hint::Leaf(&[Action::Help], "help"),
];

pub(super) fn draw(frame: &mut Frame<'_>, app: &mut App) {
    let (body, bar) = super::screen_chrome(frame, app, HINTS);
    draw_list(frame, app, body);
    frame.render_widget(Paragraph::new(super::status_bar(app, bar.width)), bar);
}

fn draw_list(frame: &mut Frame<'_>, app: &mut App, area: Rect) {
    if app.prs.is_empty() {
        frame.render_widget(
            Paragraph::new(Line::styled(
                "  no open pull requests…",
                app.theme.dim_style(),
            )),
            area,
        );
        return;
    }
    let height = area.height.max(1) as usize;
    let scroll = super::scroll_to_cursor(app.prs_cursor, app.prs_scroll, height, app.prs.len());
    app.prs_scroll = scroll;
    let rows: Vec<Line<'static>> = app
        .prs
        .iter()
        .enumerate()
        .skip(scroll)
        .take(height)
        .map(|(i, pr)| {
            let selected = i == app.prs_cursor;
            let ranges = app
                .search
                .as_ref()
                .map(|s| s.ranges_for(i))
                .unwrap_or_default();
            let mut spans = vec![Span::raw("  ")];
            spans.extend(super::highlight_spans(
                &format!("#{} {} {}", pr.number, pr.title, pr.author),
                Style::new().fg(app.theme.fg),
                &ranges,
                &app.theme,
            ));
            spans.push(Span::styled(
                format!(" · {} → {}", pr.head_ref, pr.base_ref),
                Style::new().fg(app.theme.purple),
            ));
            let line = Line::from(spans);
            if selected {
                super::cursor_line(line, &app.theme, area.width)
            } else {
                line
            }
        })
        .collect();
    frame.render_widget(Paragraph::new(rows), area);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ci::PullRequest;
    use ratatui::Terminal;
    use ratatui::backend::TestBackend;

    use crate::config::LoadedConfig;
    use crate::test_support::standard_fixture;

    fn pr(number: u64, title: &str, author: &str, head_ref: &str, base_ref: &str) -> PullRequest {
        PullRequest {
            number,
            title: title.to_owned(),
            url: None,
            base_ref: base_ref.to_owned(),
            head_ref: head_ref.to_owned(),
            head_oid: "0".repeat(40),
            author: author.to_owned(),
        }
    }

    /// The hint line reads the active screen's keymap, so a test that draws
    /// this pane while the app still sits on the status screen renders the
    /// wrong keys.
    fn prs_app(fixture: &crate::test_support::Fixture) -> App {
        let mut app = App::new(fixture.review(), LoadedConfig::default());
        app.push_screen(crate::app::Screen::Prs);
        app
    }

    #[test]
    fn renders_the_pr_list_with_a_selection() {
        let fixture = standard_fixture();
        let mut app = prs_app(&fixture);
        app.prs = vec![
            pr(12, "Add widgets", "alice", "feat/widgets", "main"),
            pr(9, "Fix flaky test", "bob", "fix/flaky", "main"),
        ];
        app.prs_cursor = 1;
        let backend = TestBackend::new(80, 8);
        let mut terminal = Terminal::new(backend).expect("terminal");
        terminal.draw(|f| draw(f, &mut app)).expect("draw");
        insta::assert_snapshot!(terminal.backend());
    }

    #[test]
    fn renders_no_open_pull_requests() {
        let fixture = standard_fixture();
        let mut app = prs_app(&fixture);
        let backend = TestBackend::new(80, 6);
        let mut terminal = Terminal::new(backend).expect("terminal");
        terminal.draw(|f| draw(f, &mut app)).expect("draw");
        insta::assert_snapshot!(terminal.backend());
    }
}