laziergit 1.1.0

an even lazier git implementation than lazygit
use crate::app::App;
use crate::theme;
use crate::tree::{Row, RowKind};
use ratatui::{
    Frame,
    layout::Rect,
    style::{Color, Style},
    text::{Line, Span},
    widgets::{HighlightSpacing, List, ListItem, ListState},
};

pub fn render(f: &mut Frame, area: Rect, app: &App) {
    let focused = app.focus == crate::app::Focus::Sidebar;
    let items: Vec<ListItem> = build_items(app);
    let list = List::new(items)
        .block(super::panel("files", focused))
        .highlight_style(Style::new().bg(theme::SELECTION_BG))
        .highlight_symbol("")
        .highlight_spacing(HighlightSpacing::Always);
    let selected = (!app.rows.is_empty()).then_some(app.selected);
    let mut state = ListState::default()
        .with_offset(app.sidebar_offset)
        .with_selected(selected);
    f.render_stateful_widget(list, area, &mut state);
}

fn build_items(app: &App) -> Vec<ListItem<'static>> {
    let guides = app.theme.guides();
    let mut last_at: Vec<bool> = Vec::new();
    let mut items = Vec::new();

    for row in &app.rows {
        let depth = row.depth as usize;
        last_at.truncate(depth);
        last_at.push(row.is_last);

        let mut spans: Vec<Span> = Vec::new();
        for (column, &ended) in last_at.iter().take(depth).enumerate() {
            let glyph = if column == depth - 1 {
                if row.is_last {
                    guides.last
                } else {
                    guides.branch
                }
            } else if ended {
                guides.blank
            } else {
                guides.vertical
            };
            spans.push(Span::styled(
                glyph.to_string(),
                Style::new().fg(theme::GUIDE),
            ));
        }

        match row.kind {
            RowKind::Dir => push_dir(app, row, &mut spans),
            RowKind::File { index } => push_file(app, row, index, &mut spans),
        }
        items.push(ListItem::new(Line::from(spans)));
    }
    items
}

fn push_dir(app: &App, row: &Row, spans: &mut Vec<Span<'static>>) {
    spans.push(Span::styled(
        format!("{} ", theme::folder_glyph(row.expanded)),
        Style::new().fg(theme::FOLDER),
    ));
    if app.theme.nerd_fonts {
        spans.push(Span::styled(
            format!("{} ", theme::folder_icon(row.expanded)),
            Style::new().fg(theme::FOLDER),
        ));
    }
    spans.push(Span::styled(
        row.name.clone(),
        Style::new().fg(folder_color(app, &row.path)),
    ));
}

fn push_file(app: &App, row: &Row, index: usize, spans: &mut Vec<Span<'static>>) {
    if app.theme.nerd_fonts {
        let (icon, color) = theme::icon_for(&row.path);
        spans.push(Span::styled(format!("{icon} "), Style::new().fg(color)));
    }
    let file = &app.status.files[index];
    spans.push(Span::styled(row.name.clone(), Style::new().fg(theme::TEXT)));
    spans.push(Span::raw(" "));
    spans.push(Span::styled(
        file.kind.letter().to_string(),
        Style::new().fg(theme::status_color(file)),
    ));
}

fn folder_color(app: &App, path: &str) -> Color {
    let prefix = format!("{path}/");
    let (mut staged, mut unstaged) = (false, false);
    for file in &app.status.files {
        if file.path == path || file.path.starts_with(&prefix) {
            staged |= file.staged;
            unstaged |= file.unstaged;
        }
    }
    match (staged, unstaged) {
        (true, true) => theme::YELLOW,
        (true, false) => theme::GREEN,
        (false, true) => theme::RED,
        (false, false) => theme::FOLDER,
    }
}