gitu 0.41.0

A git client inspired by Magit
Documentation
use std::borrow::Cow;

use crate::app::State;
use crate::screen;
use crate::ui::layout::LayoutItem;
use itertools::Itertools;
use layout::LayoutTree;
use layout::OPTS;
use ratatui::Frame;
use ratatui::prelude::*;
use tui_prompts::State as _;
use unicode_segmentation::UnicodeSegmentation;

pub(crate) mod layout;
mod menu;
pub mod picker;

const CARET: &str = "\u{2588}";
const DASHES: &str = "────────────────────────────────────────────────────────────────";

pub(crate) type UiTree<'a> = LayoutTree<(Cow<'a, str>, Style)>;

pub(crate) fn ui(frame: &mut Frame, state: &mut State) {
    let size = frame.area().as_size();
    let mut layout = UiTree::new();

    layout.vertical(None, OPTS, |layout| {
        layout.vertical(None, OPTS.grow(), |layout| {
            let hide_cursor = state.picker.is_some();
            screen::layout_screen(layout, size, state.screens.last().unwrap(), hide_cursor);
        });

        layout.vertical(None, OPTS, |layout| {
            menu::layout_menu(layout, state, size.width as usize);
            layout_command_log(layout, state, size.width as usize);
            layout_prompt(layout, state, size.width as usize);
            layout_picker(layout, state, size.width as usize);
        });
    });

    layout.compute([frame.area().width, frame.area().height]);

    for item in layout.iter() {
        let LayoutItem { data, pos, size } = item;
        let area = Rect::new(pos[0], pos[1], size[0], size[1]);
        let (text, style) = data;
        frame.render_widget(SpanRef(text, *style), area);
    }

    layout.clear();

    state.screens.last_mut().unwrap().size = frame.area().as_size();
}

struct SpanRef<'a>(&'a Cow<'a, str>, Style);

impl<'a> Widget for SpanRef<'a> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let SpanRef(text, style) = self;
        buf.set_string(area.x, area.y, text, style);
    }
}

fn layout_command_log<'a>(layout: &mut UiTree<'a>, state: &State, width: usize) {
    if !state.current_cmd_log.is_empty() {
        let separator_style = Style::from(&state.config.style.separator);
        repeat_chars(layout, width, DASHES, separator_style);
        layout_text(layout, state.current_cmd_log.format_log(&state.config));
    }
}

fn layout_prompt<'a>(layout: &mut UiTree<'a>, state: &'a State, width: usize) {
    let Some(ref prompt_data) = state.prompt.data else {
        return;
    };

    let prompt_symbol = state.prompt.state.status().symbol();
    let separator_style = Style::from(&state.config.style.separator);
    let prompt_style = Style::from(&state.config.style.prompt);

    repeat_chars(layout, width, DASHES, separator_style);
    layout.horizontal(None, OPTS, |layout| {
        layout_span(layout, (prompt_symbol.content, prompt_symbol.style));
        layout_span(layout, (" ".into(), Style::new()));
        layout_span(
            layout,
            (prompt_data.prompt_text.as_ref().into(), prompt_style),
        );
        layout_span(layout, ("".into(), prompt_style));
        layout_span(layout, (state.prompt.state.value().into(), Style::new()));
        layout_span(layout, (CARET.into(), Style::new()));
    });
}

fn layout_picker<'a>(layout: &mut UiTree<'a>, state: &'a State, width: usize) {
    if let Some(ref picker_state) = state.picker {
        picker::layout_picker(layout, picker_state, &state.config, width);
    }
}

pub(crate) fn layout_text<'a>(layout: &mut UiTree<'a>, text: Text<'a>) {
    layout.vertical(None, OPTS, |layout| {
        for line in text {
            layout_line(layout, line);
        }
    });
}

pub(crate) fn layout_line<'a>(layout: &mut UiTree<'a>, line: Line<'a>) {
    let line_style = line.style;
    layout.horizontal(None, OPTS, |layout| {
        for span in line {
            // Merge line.style with span.style
            let merged_style = line_style.patch(span.style);
            layout_span(layout, (span.content, merged_style));
        }
    });
}

pub(crate) fn layout_span<'a>(layout: &mut UiTree<'a>, span: (Cow<'a, str>, Style)) {
    let width = span.0.graphemes(true).count() as u16;
    layout.leaf_with_size(span, [width, 1]);
}

pub(crate) fn repeat_chars(layout: &mut UiTree, count: usize, chars: &'static str, style: Style) {
    let grapheme_count = chars.grapheme_indices(true).count();
    let full = count / grapheme_count;
    let partial = count % grapheme_count;

    layout.horizontal(None, OPTS, |layout| {
        for _ in 0..full {
            layout_span(layout, (chars.into(), style));
        }

        if partial > 0 {
            let end = chars
                .grapheme_indices(true)
                .tuple_windows()
                .take(partial)
                .last()
                .map(|((_, _), (end, _))| end)
                .unwrap_or(chars.len());

            layout_span(layout, (chars[..end].into(), style));
        }
    });
}