gitui-view 0.1.1

A terminal-based Git repository browser with inline and side-by-side diff views
use crate::git::{GitRepo, StatusEntry, StatusType};
use crate::theme::Theme;
use ratatui::{
    layout::Rect,
    style::Style,
    text::{Line, Span},
    widgets::{Block, Borders, Paragraph},
    Frame,
};
use std::cmp;

pub struct StatusTab {
    pub staged: Vec<StatusEntry>,
    pub unstaged: Vec<StatusEntry>,
    pub selected: usize,
    pub scroll: usize,
}

impl StatusTab {
    pub fn new() -> Self {
        Self {
            staged: Vec::new(),
            unstaged: Vec::new(),
            selected: 0,
            scroll: 0,
        }
    }

    pub fn refresh(&mut self, repo: &GitRepo) {
        if let Ok((staged, unstaged)) = repo.get_status() {
            self.staged = staged;
            self.unstaged = unstaged;
            self.selected = 0;
            self.scroll = 0;
        }
    }

    pub fn total_items(&self) -> usize {
        self.staged.len() + self.unstaged.len()
    }

    pub fn current_file(&self) -> Option<String> {
        let all = self.all_items();
        all.get(self.selected).map(|e| e.path.clone())
    }

    pub fn current_staged(&self) -> bool {
        if self.selected < self.staged.len() {
            true
        } else {
            false
        }
    }

    fn all_items(&self) -> Vec<&StatusEntry> {
        let mut items: Vec<&StatusEntry> = self.staged.iter().collect();
        items.extend(self.unstaged.iter());
        items
    }

    pub fn move_down(&mut self) {
        let max = self.total_items().saturating_sub(1);
        self.selected = cmp::min(self.selected + 1, max);
    }

    pub fn move_up(&mut self) {
        self.selected = self.selected.saturating_sub(1);
    }

    pub fn render(&self, f: &mut Frame, area: Rect, theme: &Theme) {
        let block = Block::default()
            .title(" Status ")
            .borders(Borders::ALL)
            .border_style(theme.border_style());
        let inner = block.inner(area);
        f.render_widget(block, area);

        let mut lines: Vec<Line> = Vec::new();

        if !self.staged.is_empty() {
            lines.push(Line::from(Span::styled(
                " Staged:",
                theme.title(),
            )));
            for (i, entry) in self.staged.iter().enumerate() {
                let is_selected = i == self.selected;
                let style = status_style(entry, is_selected, theme);
                let marker = if is_selected { ">" } else { " " };
                let status_char = status_char(&entry.status);
                lines.push(Line::from(Span::styled(
                    format!(" {} {} {}", marker, status_char, entry.path),
                    style,
                )));
            }
        }

        if !self.unstaged.is_empty() {
            lines.push(Line::from(Span::styled(
                " Unstaged:",
                theme.title(),
            )));
            for (i, entry) in self.unstaged.iter().enumerate() {
                let idx = self.staged.len() + i;
                let is_selected = idx == self.selected;
                let style = status_style(entry, is_selected, theme);
                let marker = if is_selected { ">" } else { " " };
                let status_char = status_char(&entry.status);
                lines.push(Line::from(Span::styled(
                    format!(" {} {} {}", marker, status_char, entry.path),
                    style,
                )));
            }
        }

        if lines.is_empty() {
            lines.push(Line::from(Span::styled(
                " (clean)",
                theme.dim_text(),
            )));
        }

        let visible: Vec<Line> = lines
            .into_iter()
            .skip(self.scroll)
            .take(inner.height as usize)
            .collect();

        f.render_widget(Paragraph::new(visible), inner);
    }
}

fn status_char(st: &StatusType) -> &'static str {
    match st {
        StatusType::Added => "A",
        StatusType::Modified => "M",
        StatusType::Deleted => "D",
        StatusType::Renamed => "R",
        StatusType::Copied => "C",
        StatusType::Untracked => "?",
        StatusType::TypeChange => "T",
    }
}

fn status_style(entry: &StatusEntry, selected: bool, theme: &Theme) -> Style {
    if selected {
        return theme.selected();
    }
    if entry.staged {
        Style::default().fg(theme.file_entry_staged).bg(theme.light_bg)
    } else {
        let fg = match entry.status {
            StatusType::Untracked => theme.file_entry_untracked,
            StatusType::Modified => theme.file_entry_modified,
            _ => theme.file_entry,
        };
        Style::default().fg(fg).bg(theme.light_bg)
    }
}