laziergit 1.0.0

an even lazier git implementation than lazygit
use crate::app::App;
use crate::diff::{Cell, Kind};
use crate::theme;
use crate::tree::RowKind;
use ratatui::{
    Frame,
    buffer::Buffer,
    layout::{Alignment, Constraint, Layout, Rect},
    style::{Color, Modifier, Style},
    text::Line,
    widgets::Paragraph,
};

pub fn render(f: &mut Frame, area: Rect, app: &App) {
    let focused = app.focus == crate::app::Focus::Diff;
    let block = super::panel(&title(app), focused);
    let inner = block.inner(area);
    f.render_widget(block, area);

    if app.diff_rows.is_empty() {
        placeholder(f, inner, "no changes");
        return;
    }

    let [lcol, divcol, rcol] = Layout::horizontal([
        Constraint::Percentage(50),
        Constraint::Length(1),
        Constraint::Percentage(50),
    ])
    .areas(inner);

    let rows = &app.diff_rows;
    let gutter = crate::diff::gutter_width(rows);
    let height = inner.height as usize;
    let start = (app.diff_scroll as usize).min(rows.len().saturating_sub(height));
    let buf = f.buffer_mut();

    for (offset, row) in rows[start..(start + height).min(rows.len())]
        .iter()
        .enumerate()
    {
        let y = inner.y + offset as u16;
        if row.left.kind == Kind::File {
            buf.set_style(line_rect(inner, y), Style::new().bg(theme::SURFACE));
            let label = if app.theme.nerd_fonts {
                let (icon, _) = theme::icon_for(&row.left.text);
                format!("  {icon}  {} ", row.left.text)
            } else {
                format!("  {} ", row.left.text)
            };
            buf.set_stringn(
                inner.x,
                y,
                label,
                inner.width as usize,
                Style::new()
                    .fg(theme::ACCENT)
                    .bg(theme::SURFACE)
                    .add_modifier(Modifier::BOLD),
            );
            continue;
        }
        if row.left.kind == Kind::Hunk {
            buf.set_style(line_rect(inner, y), Style::new().bg(theme::HUNK_BG));
            let label = if row.left.text.is_empty() {
                String::new()
            } else {
                format!("  {} ", row.left.text)
            };
            buf.set_stringn(
                inner.x,
                y,
                label,
                inner.width as usize,
                Style::new().fg(theme::HUNK_FG).bg(theme::HUNK_BG),
            );
            continue;
        }
        paint_cell(buf, lcol, y, &row.left, gutter);
        paint_cell(buf, rcol, y, &row.right, gutter);
        buf.set_stringn(
            divcol.x,
            y,
            "",
            1,
            Style::new().fg(theme::BORDER).bg(theme::BASE),
        );
    }
}

fn paint_cell(buf: &mut Buffer, col: Rect, y: u16, cell: &Cell, gutter: usize) {
    let (bg, fg, gutter_fg) = palette(cell.kind);
    buf.set_style(line_rect(col, y), Style::new().bg(bg));
    if cell.kind == Kind::Empty {
        return;
    }
    let num = cell
        .num
        .map(|n| format!("{n:>gutter$}"))
        .unwrap_or_else(|| " ".repeat(gutter));
    let mut x = buf
        .set_stringn(col.x, y, num, gutter, Style::new().fg(gutter_fg).bg(bg))
        .0;
    x = buf
        .set_stringn(x, y, "", 1, Style::new().fg(theme::GUTTER_SEP_FG).bg(bg))
        .0;
    x = buf.set_stringn(x, y, " ", 1, Style::new().bg(bg)).0;
    let avail = (col.x + col.width).saturating_sub(x) as usize;
    buf.set_stringn(
        x,
        y,
        truncate(&cell.text, avail),
        avail,
        Style::new().fg(fg).bg(bg),
    );
}

fn palette(kind: Kind) -> (Color, Color, Color) {
    match kind {
        Kind::Context => (theme::BASE, theme::CONTEXT_FG, theme::GUTTER_FG),
        Kind::Del => (theme::DEL_BG, theme::DEL_FG, theme::DEL_GUTTER_FG),
        Kind::Add => (theme::ADD_BG, theme::ADD_FG, theme::ADD_GUTTER_FG),
        Kind::Empty => (theme::FILLER_BG, theme::CONTEXT_FG, theme::GUTTER_FG),
        Kind::Hunk | Kind::File => (theme::HUNK_BG, theme::HUNK_FG, theme::HUNK_FG),
    }
}

fn truncate(text: &str, avail: usize) -> String {
    if avail == 0 {
        return String::new();
    }
    if text.chars().count() <= avail {
        return text.to_string();
    }
    let mut out: String = text.chars().take(avail.saturating_sub(1)).collect();
    out.push('');
    out
}

fn line_rect(area: Rect, y: u16) -> Rect {
    Rect {
        x: area.x,
        y,
        width: area.width,
        height: 1,
    }
}

fn title(app: &App) -> String {
    match app
        .rows
        .get(app.selected)
        .map(|r| (r.kind, r.path.as_str()))
    {
        Some((RowKind::File { .. }, path)) => path.to_string(),
        Some((RowKind::Dir, path)) => {
            let prefix = format!("{path}/");
            let count = app
                .status
                .files
                .iter()
                .filter(|f| f.path.starts_with(&prefix))
                .count();
            format!("{path}/  ({count})")
        }
        None => "diff".to_string(),
    }
}

fn placeholder(f: &mut Frame, area: Rect, msg: &str) {
    f.render_widget(
        Paragraph::new(Line::styled(msg, Style::new().fg(theme::OVERLAY)))
            .alignment(Alignment::Center)
            .style(Style::new().bg(theme::BASE)),
        area,
    );
}