eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
use crate::components::Component;
use crate::app::{AppState, Action};
use crate::errors::ComponentError;
use crate::input::InputEvent;
use crate::ui::style;
use crate::ui::style::DiffKind;
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::widgets::Paragraph;
use ratatui::text::{Line, Span};

pub struct DiffPane;

impl DiffPane {
    pub fn new() -> Self {
        Self
    }
}

impl Component for DiffPane {
    fn handle_event(
        &mut self,
        _event: InputEvent,
        _state: &AppState,
    ) -> Result<Option<Action>, ComponentError> {
        Ok(None)
    }

    fn render(&mut self, frame: &mut Frame, area: Rect, state: &AppState) {
        let theme = &state.theme;
        // Virtualized rendering: show only the visible slice based on scroll offset.
        const MAX_RENDER_LINES: usize = 1000;
        let total_lines = state.last_diff.lines().count();
        let capped_total = if state.load_full_diff { total_lines } else { total_lines.min(MAX_RENDER_LINES) };
        let start = state.detail_scroll.min(capped_total.saturating_sub(1));
        let visible_height = area.height.saturating_sub(1) as usize; // leave 1 line for footer
        let end = (start + visible_height).min(capped_total);

        let visible_lines: Vec<Line> = state
            .last_diff
            .lines()
            .skip(start)
            .take(end.saturating_sub(start))
            .enumerate()
            .map(|(idx, l)| {
                let mut style = if l.starts_with('+') {
                    style::diff_style(theme, DiffKind::Add)
                } else if l.starts_with('-') {
                    style::diff_style(theme, DiffKind::Remove)
                } else if l.starts_with("@@") {
                    style::diff_style(theme, DiffKind::Hunk)
                } else {
                    style::diff_style(theme, DiffKind::Context)
                };
                let absolute = start + idx;
                if state.line_select_mode && state.selected_lines.contains(&absolute) {
                    style = style::selection(theme);
                }
                Line::from(Span::styled(format!("{:>5} {}", absolute + 1, l), style))
            })
            .collect();

        let mut base_footer = if !state.load_full_diff && total_lines > MAX_RENDER_LINES {
            format!(
                "visible {}-{} of {} (truncated to {}) — press L to load full",
                start,
                end.saturating_sub(1),
                total_lines,
                MAX_RENDER_LINES
            )
        } else {
            format!(
                "visible {}-{} of {} (full)",
                start,
                end.saturating_sub(1),
                total_lines
            )
        };
        if state.side_by_side {
            if state.has_delta && state.use_delta {
                base_footer.push_str(" • side-by-side (delta required; inline fallback)");
            } else {
                base_footer.push_str(" • side-by-side requested (delta not available)");
            }
        }
        let footer_text = if state.line_select_mode {
            format!(
                "{} — line select mode (selected: {})",
                base_footer,
                state.selected_lines.len()
            )
        } else {
            base_footer
        };

        let title = if state.diff_focus { "Diff (focused)" } else { "Diff" };
        let block = style::pane_block(theme, title, state.diff_focus);

        let chunks = ratatui::layout::Layout::default()
            .direction(ratatui::layout::Direction::Vertical)
            .constraints([
                ratatui::layout::Constraint::Min(1),
                ratatui::layout::Constraint::Length(1),
            ])
            .split(area);

        let paragraph = Paragraph::new(visible_lines)
            .style(style::body_style(theme))
            .block(block);
        frame.render_widget(paragraph, chunks[0]);

        let footer = Paragraph::new(footer_text).style(style::text(theme, style::Emphasis::Muted));
        frame.render_widget(footer, chunks[1]);
    }

    fn name(&self) -> &'static str {
        "DiffPane"
    }
}