eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Core renderers: feedback, palette, theme picker, operation log.

use crate::components::manager::ComponentManager;
use crate::app::AppState;
use crate::ui::style::{self, Emphasis};
use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::widgets::{Paragraph, List, ListItem, Clear, ListState};
use ratatui::text::{Line, Span};
use crate::components::manager::utils;

pub fn render_feedback(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
    // Show feedback messages (success) or errors with guidance
    if let Some(ref feedback) = state.feedback_message {
        let footer_style = style::text(&state.theme, Emphasis::Success);
        let block = style::pane_block(&state.theme, "Status", false);
        let paragraph = Paragraph::new(feedback.clone())
            .style(footer_style)
            .block(block);

        let footer = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Min(0), Constraint::Length(3)])
            .split(area);
        frame.render_widget(paragraph, footer[1]);
        return;
    }

    // Show error with guidance if available
    if let Some(ref error_guidance) = state.error_guidance {
        let formatted = error_guidance.format_display();
        let lines: Vec<Line> = formatted
            .lines()
            .map(|line| {
                if line.starts_with("  [") && line.contains("]") {
                    let parts: Vec<&str> = line.splitn(2, "] ").collect();
                    if parts.len() == 2 {
                        let key_part = parts[0].trim_start_matches("  [");
                        let label = parts[1];
                        Line::from(vec![
                            Span::styled("  [", style::text(&state.theme, Emphasis::Normal)),
                            Span::styled(key_part, style::text(&state.theme, Emphasis::Warning)),
                            Span::styled("] ", style::text(&state.theme, Emphasis::Normal)),
                            Span::styled(label, style::text(&state.theme, Emphasis::Normal)),
                        ])
                    } else {
                        Line::from(line)
                    }
                } else if line.starts_with("") {
                    let prefix = "";
                    let char_count = prefix.chars().count();
                    let rest: String = line.chars().skip(char_count).collect();
                    Line::from(vec![
                        Span::styled(prefix, style::text(&state.theme, Emphasis::Muted)),
                        Span::styled(rest, style::text(&state.theme, Emphasis::Normal)),
                    ])
                } else if line == "Suggestions:" || line == "Quick actions:" {
                    Line::from(vec![
                        Span::styled(line, style::text(&state.theme, Emphasis::Header)),
                    ])
                } else {
                    Line::from(line)
                }
            })
            .collect();

        let error_style = style::text(&state.theme, Emphasis::Error);
        let block = style::pane_block(&state.theme, "Error", false);
        let paragraph = Paragraph::new(lines)
            .style(error_style)
            .block(block);

        let content_height = formatted.lines().count().max(3);
        let height = (content_height + 2).min(area.height as usize) as u16;

        let footer = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Min(0), Constraint::Length(height)])
            .split(area);
        frame.render_widget(paragraph, footer[1]);
        return;
    }

    // Fallback to simple error message
    if let Some(ref error) = state.last_status_error {
        let footer_style = style::text(&state.theme, Emphasis::Error);
        let block = style::pane_block(&state.theme, "Status", false);
        let paragraph = Paragraph::new(error.clone())
            .style(footer_style)
            .block(block);

        let footer = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Min(0), Constraint::Length(3)])
            .split(area);
        frame.render_widget(paragraph, footer[1]);
    }
}

pub fn render_palette(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
    let (items, selected, title) = if state.theme_picker {
        let max_idx = state.available_themes.len().saturating_sub(1);
        let selected = state.theme_picker_selected.min(max_idx);
        let items: Vec<ListItem> = state
            .available_themes
            .iter()
            .enumerate()
            .map(|(i, name)| {
                let style = if i == selected {
                    style::selection(&state.theme)
                } else {
                    style::text(&state.theme, Emphasis::Header)
                };
                ListItem::new(name.clone()).style(style)
            })
            .collect();
        let title = format!(
            "Themes [{} / {}]",
            selected.saturating_add(1),
            state.available_themes.len().max(1)
        );
        (items, selected, title)
    } else {
        (Vec::new(), 0, "Palette".to_string())
    };

    let popup = utils::center_rect(60, 40, area);
    frame.render_widget(Clear, popup);

    let mut list_state = ListState::default();
    list_state.select(Some(selected));

    let list = List::new(items)
        .style(style::body_style(&state.theme))
        .block(style::pane_block(&state.theme, title, true));

    frame.render_stateful_widget(list, popup, &mut list_state);
}

pub fn render_theme_picker(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
    let max_idx = state.available_themes.len().saturating_sub(1);
    let selected = state.theme_picker_selected.min(max_idx);

    let items: Vec<ListItem> = state
        .available_themes
        .iter()
        .enumerate()
        .map(|(i, name)| {
            let style = if i == selected {
                style::selection(&state.theme)
            } else {
                style::body_style(&state.theme)
            };
            ListItem::new(name.clone()).style(style)
        })
        .collect();

    let title = format!(
        "Themes [{} / {}]",
        selected.saturating_add(1),
        state.available_themes.len().max(1)
    );

    let popup = utils::center_rect(40, 60, area);
    frame.render_widget(Clear, popup);

    let list = List::new(items)
        .style(style::body_style(&state.theme))
        .block(style::pane_block(&state.theme, title, true));

    frame.render_widget(list, popup);
}

pub fn render_op_log(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
    let popup = utils::center_rect(60, 30, area);
    frame.render_widget(Clear, popup);

    let lines: Vec<ratatui::widgets::ListItem> = state
        .op_log
        .iter()
        .rev()
        .take(10)
        .rev()
        .map(|l| ratatui::widgets::ListItem::new(l.clone()).style(style::text(&state.theme, style::Emphasis::Muted)))
        .collect();

    let title = "Op log (last 10) (Esc/q/Enter=close)";
    let list = ratatui::widgets::List::new(lines)
        .style(style::body_style(&state.theme))
        .block(style::pane_block(&state.theme, title.to_string(), true));

    frame.render_widget(list, popup);
}