delta-gitu 0.1.6

An experimental fork of gitu (a git client inspired by Magit), published as a library for delta to embed
Documentation
use std::borrow::Cow;

use crate::menu::Menu;
use crate::menu::arg::Arg;
use crate::style::Style;
use crate::ui::layout::opts;
use crate::ui::{self, UiTree, layout_line, layout_span, repeat_chars};
use crate::{app::State, config::Config, ops::Op};
use itertools::Itertools;
use unicode_width::UnicodeWidthStr;

/// The value column of a keybind table row.
enum MenuValue<'a> {
    Text(Cow<'a, str>),
    Arg(&'a Arg),
}

pub(crate) fn layout_menu<'a>(layout: &mut UiTree<'a>, state: &'a State, width: usize) {
    let Some(ref pending) = state.pending_menu else {
        return;
    };

    if pending.is_hidden {
        return;
    }

    if state.picker.is_some() {
        return;
    }

    let config = &state.config;
    let item = state.screens.last().unwrap().get_selected_item();
    let style = &config.style;

    let arg_binds = config.bindings.arg_list(pending).collect::<Vec<_>>();
    let (target_binds, non_target_binds): (Vec<_>, Vec<_>) = config
        .bindings
        .list(&pending.menu)
        .partition(|keybind| keybind.op.clone().implementation().is_target_op());
    let target_binds: Vec<_> = target_binds
        .into_iter()
        .filter(|keybind| {
            keybind
                .op
                .clone()
                .implementation()
                .get_action(&item.data)
                .is_some()
        })
        .collect();
    let (menu_binds, non_menu_binds): (Vec<_>, Vec<_>) = non_target_binds
        .into_iter()
        .chunk_by(|bind| &bind.op)
        .into_iter()
        .map(|(op, binds)| {
            let binds: Vec<_> = binds.collect();
            (op, binds)
        })
        .partition(|(op, _binds)| matches!(op, Op::OpenMenu(_)));

    // Feature keys are top-level keys, so they are listed where those are.
    let feature_binds: Vec<_> = if matches!(pending.menu, Menu::Root | Menu::Help) {
        state
            .feature_keys
            .iter()
            .map(|(feature, key)| {
                (
                    key.to_string().into(),
                    MenuValue::Text(feature.as_str().into()),
                )
            })
            .collect()
    } else {
        vec![]
    };

    let separator_style = Style::from(&style.separator);

    layout.col(opts(), |layout| {
        ui::repeat_chars(layout, width, ui::DASHES, separator_style);

        layout.row(opts().gap(3).pad(1), |layout| {
            // Column 1: Main menu commands
            if !non_menu_binds.is_empty() {
                layout.col(opts(), |layout| {
                    let heading = match pending.menu {
                        Menu::Root | Menu::Help => "Commands".into(),
                        menu => menu.to_string(),
                    };
                    layout_line(layout, heading.into(), Style::from(&style.menu.heading));

                    layout_keybinds_table(
                        layout,
                        config,
                        non_menu_binds
                            .into_iter()
                            .map(|(op, binds)| {
                                (
                                    binds.iter().map(|bind| bind.raw.as_str()).join("/").into(),
                                    MenuValue::Text(
                                        op.clone().implementation().display(state).into(),
                                    ),
                                )
                            })
                            .collect(),
                    );
                });
            }

            // Column 2: Submenus, and the keys toggling renderer features
            if !menu_binds.is_empty() || !feature_binds.is_empty() {
                let has_submenus = !menu_binds.is_empty();
                layout.col(opts(), |layout| {
                    if has_submenus {
                        layout_line(layout, "Submenus".into(), Style::from(&style.menu.heading));

                        layout_keybinds_table(
                            layout,
                            config,
                            menu_binds
                                .into_iter()
                                .map(|(op, binds)| {
                                    let Op::OpenMenu(menu) = op else {
                                        unreachable!();
                                    };

                                    (
                                        binds.iter().map(|bind| bind.raw.as_str()).join("/").into(),
                                        MenuValue::Text(menu.to_string().into()),
                                    )
                                })
                                .collect(),
                        );
                    }

                    if !feature_binds.is_empty() {
                        if has_submenus {
                            layout_line(layout, " ".into(), Style::new());
                        }
                        layout_line(layout, "Features".into(), Style::from(&style.menu.heading));
                        layout_keybinds_table(layout, config, feature_binds);
                    }
                });
            }

            // Column 3: Target commands and arguments
            layout.col(opts(), |layout| {
                if !target_binds.is_empty() {
                    layout_line(
                        layout,
                        item.data.kind().into(),
                        Style::from(&style.menu.heading),
                    );

                    layout_keybinds_table(
                        layout,
                        config,
                        target_binds
                            .into_iter()
                            .map(|bind| {
                                (
                                    bind.raw.as_str().into(),
                                    MenuValue::Text(
                                        bind.op.clone().implementation().display(state).into(),
                                    ),
                                )
                            })
                            .collect(),
                    );
                }

                if !arg_binds.is_empty() {
                    layout_line(layout, "Arguments".into(), Style::from(&style.menu.heading));

                    layout_keybinds_table(
                        layout,
                        config,
                        arg_binds
                            .into_iter()
                            .map(|bind| {
                                let Op::ToggleArg(name) = &bind.op else {
                                    unreachable!();
                                };

                                let arg = pending.args.get(name.as_str()).unwrap();

                                (bind.raw.as_str().into(), MenuValue::Arg(arg))
                            })
                            .collect(),
                    );
                }
            });
        });
    });
}

fn layout_keybinds_table<'a>(
    layout: &mut UiTree<'a>,
    config: &Config,
    rows: Vec<(Cow<'a, str>, MenuValue<'a>)>,
) {
    const SPACES: &str = "                                                                ";
    let key_style = Style::from(&config.style.menu.key);
    let max_width = rows
        .iter()
        .map(|(key, _)| UnicodeWidthStr::width(key.as_ref()))
        .max()
        .unwrap_or(0)
        + 1;

    layout.col(opts(), |layout| {
        for (key, value) in rows {
            let padding = max_width - UnicodeWidthStr::width(key.as_ref());

            layout.row(opts(), |layout| {
                layout_line(layout, key, key_style);
                repeat_chars(layout, padding, SPACES, Style::new());

                layout.row(opts(), |layout| match value {
                    MenuValue::Text(text) => layout_span(layout, (text, Style::new())),
                    MenuValue::Arg(arg) => {
                        layout_span(layout, (arg.display.into(), Style::new()));
                        layout_span(layout, (" (".into(), Style::new()));
                        layout_span(
                            layout,
                            (
                                arg.get_cli_token().into(),
                                if arg.is_active() {
                                    Style::from(&config.style.menu.active_arg)
                                } else {
                                    Style::from(&config.style.menu.inactive_arg)
                                },
                            ),
                        );
                        layout_span(layout, (")".into(), Style::new()));
                    }
                });
            });
        }
    });
}