git-igitt 0.1.18

Interactive Git terminal application to browse and visualize Git history graphs arranged for your branching model
Documentation
use crate::app::DiffType;
use crate::util::ctrl_chars::CtrlChars;
use crate::widgets::list::{ListItem, StatefulList};
use git2::Oid;
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::style::Style;
use tui::widgets::{Block, StatefulWidget, Widget};

#[derive(Default)]
pub struct CommitViewState {
    pub content: Option<CommitViewInfo>,
}

pub struct DiffItem {
    pub(crate) file: String,
    pub(crate) diff_type: DiffType,
}

impl ListItem for DiffItem {
    fn is_selectable(&self) -> bool {
        true
    }
}

pub struct CommitViewInfo {
    pub text: Vec<String>,
    pub diffs: StatefulList<DiffItem>,
    pub oid: Oid,
    pub compare_oid: Oid,
    pub scroll: u16,
}
impl CommitViewInfo {
    pub fn new(
        text: Vec<String>,
        diffs: StatefulList<DiffItem>,
        oid: Oid,
        compare_oid: Oid,
    ) -> Self {
        Self {
            text,
            diffs,
            oid,
            compare_oid,
            scroll: 0,
        }
    }
}

#[derive(Default)]
pub struct CommitView<'a> {
    block: Option<Block<'a>>,
    highlight_symbol: Option<&'a str>,
    style: Style,
}

impl<'a> CommitView<'a> {
    pub fn block(mut self, block: Block<'a>) -> CommitView<'a> {
        self.block = Some(block);
        self
    }

    pub fn style(mut self, style: Style) -> CommitView<'a> {
        self.style = style;
        self
    }

    pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> CommitView<'a> {
        self.highlight_symbol = Some(highlight_symbol);
        self
    }
}

impl<'a> StatefulWidget for CommitView<'a> {
    type State = CommitViewState;

    fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
        buf.set_style(area, self.style);
        let list_area = match self.block.take() {
            Some(b) => {
                let inner_area = b.inner(area);
                b.render(area, buf);
                inner_area
            }
            None => area,
        };

        if list_area.width < 1 || list_area.height < 1 {
            return;
        }

        let (x_start, y0) = (list_area.left(), list_area.top());
        let list_bottom = list_area.top() + list_area.height;

        let max_element_width = list_area.width;
        if let Some(commit_info) = &state.content {
            let scroll = commit_info.scroll;
            let y_start = y0 as i32 - scroll as i32;

            let wrapping =
                textwrap::Options::new(list_area.width as usize).subsequent_indent("        ");
            let ellipsis = &format!(
                "    ...{}",
                " ".repeat(max_element_width.saturating_sub(7) as usize)
            )[..max_element_width as usize];

            let mut y = y_start;
            for (line_idx, text_line) in commit_info.text.iter().enumerate() {
                if text_line.is_empty() {
                    y += 1;
                    if y >= list_bottom as i32 {
                        buf.set_string(x_start, (y - 1) as u16, ellipsis, self.style);
                        break;
                    }
                } else {
                    let wrapped = if line_idx > 1 {
                        #[allow(clippy::needless_borrow)]
                        textwrap::fill(text_line, &wrapping)
                    } else {
                        text_line.clone()
                    };

                    for line in wrapped.lines() {
                        let mut x = x_start;
                        let mut remaining_width = max_element_width;

                        let line_span = CtrlChars::parse(line).into_text();
                        if y >= y0 as i32 {
                            for txt in line_span {
                                for line in txt.lines {
                                    if remaining_width == 0 {
                                        break;
                                    }
                                    let pos = buf.set_spans(x, y as u16, &line, remaining_width);
                                    let w = pos.0.saturating_sub(x);
                                    x = pos.0;
                                    y = pos.1 as i32;
                                    remaining_width = remaining_width.saturating_sub(w);
                                }
                            }
                        }
                        y += 1;
                        if y >= list_bottom as i32 {
                            break;
                        }
                    }
                    if y >= list_bottom as i32 {
                        buf.set_string(x_start, (y - 1) as u16, ellipsis, self.style);
                        break;
                    }
                }
            }
        }
    }
}

impl<'a> Widget for CommitView<'a> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let mut state = CommitViewState::default();
        StatefulWidget::render(self, area, buf, &mut state);
    }
}