shuire 0.1.1

Vim-like TUI git diff viewer
use ratatui::{
    Frame,
    layout::Rect,
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::Paragraph,
};
use unicode_width::UnicodeWidthStr;

use crate::{
    cli::DiffRange,
    state::App,
    theme::{ChromeStyle, Theme},
};

/// Top 朱入レ header bar rendered as the top border of the UI frame:
///
/// ```text
/// ┌━ 朱入レ ──────────────────────── base → target ━┐
/// ```
///
/// For the sparse (sumi) theme we drop the corner chars and
/// use whitespace + heavy rule.
pub fn render(f: &mut Frame, app: &App, area: Rect, theme: &Theme) {
    let width = area.width as usize;
    if width < 6 {
        return;
    }
    let bar_bg = theme.bg;
    let corner_fg = Style::default().fg(theme.shuire).bg(bar_bg);
    let rule_fg = Style::default().fg(theme.border_unfocused).bg(bar_bg);
    let fg_dim = Style::default().fg(theme.dim_fg).bg(bar_bg);
    let brand_style = Style::default()
        .fg(theme.shuire)
        .bg(bar_bg)
        .add_modifier(Modifier::BOLD);

    let mut spans: Vec<Span> = Vec::new();

    let sparse = matches!(theme.chrome, ChromeStyle::Sparse);
    if !sparse {
        spans.push(Span::styled(theme.chrome.top_left().to_string(), corner_fg));
        spans.push(Span::styled("".to_string(), rule_fg));
    } else {
        spans.push(Span::styled("  ", fg_dim));
    }
    spans.push(Span::styled(" 朱入レ ", brand_style));

    let range_spans = range_spans(&app.range, theme);
    let range_w: usize = range_spans.iter().map(|s| s.content.width()).sum();

    let used = spans.iter().map(|s| s.content.width()).sum::<usize>() + range_w + 2;
    let fill = width.saturating_sub(used);
    let fill_char = if sparse { ' ' } else { '' };
    spans.push(Span::styled(
        fill_char.to_string().repeat(fill),
        if sparse { fg_dim } else { rule_fg },
    ));
    for s in range_spans {
        spans.push(s);
    }
    if !sparse {
        spans.push(Span::styled("".to_string(), rule_fg));
        spans.push(Span::styled(
            theme.chrome.top_right().to_string(),
            corner_fg,
        ));
    } else {
        spans.push(Span::styled("  ", fg_dim));
    }

    let line = Line::from(spans).style(Style::default().bg(bar_bg));
    f.render_widget(Paragraph::new(line), area);
}

/// Right-side range indicator. For branch/target pairs the arrow is shuire.
fn range_spans(range: &DiffRange, theme: &Theme) -> Vec<Span<'static>> {
    let bg = theme.bg;
    let fg_dim = Style::default().fg(theme.dim_fg).bg(bg);
    let shuire = Style::default()
        .fg(theme.shuire)
        .bg(bg)
        .add_modifier(Modifier::BOLD);
    let pair = |base: String, target: String| -> Vec<Span<'static>> {
        vec![
            Span::styled(" ", fg_dim),
            Span::styled(base, fg_dim),
            Span::styled("", shuire),
            Span::styled(target, fg_dim),
            Span::styled(" ", fg_dim),
        ]
    };
    let single = |s: String| -> Vec<Span<'static>> { vec![Span::styled(format!(" {s} "), fg_dim)] };

    match range {
        DiffRange::Working => single("working tree".to_string()),
        DiffRange::Staged => single("staged".to_string()),
        DiffRange::StagedAgainst { base } => pair(base.clone(), "staged".to_string()),
        DiffRange::Uncommitted => single("HEAD..".to_string()),
        DiffRange::UncommittedAgainst { base } => pair(base.clone(), "uncommitted".to_string()),
        DiffRange::Range { base, target } => pair(base.clone(), target.clone()),
        DiffRange::MergeBase { base, target } => {
            pair(format!("merge-base({base})"), target.clone())
        }
        DiffRange::PullRequest { url, .. } => single(format!("PR: {url}")),
        DiffRange::Stdin => single("stdin".to_string()),
    }
}