clap-tui 0.1.1

Auto-generate a TUI from clap commands
Documentation
use crate::controller::navigation;
use crate::frame_snapshot::FrameSnapshot;
use crate::input::AppState;
use crate::query::selectors;

use super::{Action, Effect};

pub(crate) fn apply(
    action: &Action,
    state: &mut AppState,
    frame_snapshot: &FrameSnapshot,
) -> Effect {
    match action {
        Action::MoveSidebarSelection(delta) => {
            navigation::move_sidebar_selection(state, frame_snapshot, *delta);
        }
        Action::SidebarRight => navigation::sidebar_right(state, frame_snapshot),
        Action::CollapseSelected => navigation::collapse_selected(state, frame_snapshot),
        Action::SelectSidebar => navigation::select_sidebar(state, frame_snapshot),
        Action::ClickSidebar { x, y } => apply_sidebar_click(*x, *y, state, frame_snapshot),
        Action::ScrollSidebar(delta) => navigation::scroll_sidebar(state, frame_snapshot, *delta),
        _ => {}
    }
    Effect::None
}

fn apply_sidebar_click(x: u16, y: u16, state: &mut AppState, frame_snapshot: &FrameSnapshot) {
    if let Some(sidebar_area) = frame_snapshot.layout.sidebar
        && y == sidebar_area.y
    {
        navigation::select_root(state);
        state.ui.focus_sidebar();
        return;
    }

    let Some((path, caret_hit, has_children)) = frame_snapshot.sidebar_item_at(x, y).map(|item| {
        (
            item.path.clone(),
            FrameSnapshot::sidebar_caret_contains(item, x, y),
            item.has_children,
        )
    }) else {
        return;
    };

    if *state.domain.selected_path() != path && state.select_command_path(path.as_slice()).is_ok() {
        let root = state.domain.root.clone();
        let selected_path = state.domain.selected_path().clone();
        let args = selectors::visible_form_args(&root, &selected_path, state.ui.active_tab);
        state
            .ui
            .focus_first_tab(&selectors::visible_form_arg_pairs(&args));
    }
    if caret_hit && has_children {
        let items = selectors::visible_sidebar_items(
            &state.domain.root,
            &state.domain.expanded,
            &state.ui.search_query,
        );
        if let Some(item) = items.iter().find(|item| item.path == path) {
            if item.expanded {
                navigation::collapse_selected(state, frame_snapshot);
            } else {
                navigation::expand_selected(state, frame_snapshot);
            }
        }
    }
    navigation::ensure_selected_sidebar_visible(state, frame_snapshot);
    state.ui.focus_sidebar();
}

#[cfg(test)]
mod tests {
    use crate::frame_snapshot::FrameSnapshot;
    use crate::spec::CommandSpec;

    use super::super::{Action, Effect, apply_action};

    #[test]
    fn sidebar_title_click_reselects_root() {
        let mut state = crate::input::AppState::new(CommandSpec {
            name: "tool".to_string(),
            version: Some("1.0.0".to_string()),
            about: None,
            help: String::new(),
            args: Vec::new(),
            subcommands: vec![CommandSpec {
                name: "build".to_string(),
                version: None,
                about: None,
                help: String::new(),
                args: Vec::new(),
                subcommands: Vec::new(),
                ..CommandSpec::default()
            }],
            ..CommandSpec::default()
        });
        state
            .select_command_path(&["build".to_string()])
            .expect("valid path");
        let mut snapshot = FrameSnapshot::default();
        snapshot.layout.sidebar = Some(ratatui::layout::Rect::new(0, 0, 20, 8));

        let action = Action::ClickSidebar { x: 2, y: 0 };
        let effect = apply_action(&action, &mut state, &snapshot);

        assert_eq!(effect, Effect::None);
        assert!(state.domain.selected_path().is_empty());
        assert_eq!(state.domain.current_command().name, "tool");
        assert!(matches!(state.ui.focus, crate::input::Focus::Sidebar));
    }
}