osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
use super::{
    MenuAction, MenuCore, color_to_string, display_text, indent_lines, marker_prefix,
    marker_width_for_layout, style_to_debug, truncate_to_width,
};
use nu_ansi_term::{Color, Style};
use reedline::{MenuEvent, MenuTextStyle, Span, Suggestion};

fn suggestion(value: &str) -> Suggestion {
    Suggestion {
        value: value.to_string(),
        span: Span { start: 0, end: 0 },
        ..Suggestion::default()
    }
}

fn described_suggestion(value: &str, description: &str) -> Suggestion {
    let mut suggestion = suggestion(value);
    suggestion.description = Some(description.to_string());
    suggestion
}

fn numbered_suggestions(count: usize) -> Vec<Suggestion> {
    (0..count)
        .map(|idx| suggestion(&format!("item{idx}")))
        .collect()
}

#[test]
fn event_state_machine_covers_reactivate_edit_and_deactivate() {
    let values = vec![
        suggestion("config"),
        suggestion("doctor"),
        suggestion("history"),
    ];
    let mut core = MenuCore::default();
    core.set_values(values.clone());

    core.pre_event(&MenuEvent::Activate(false));
    assert!(core.is_active());
    assert!(core.values().is_empty());
    assert_eq!(
        core.handle_event(MenuEvent::Activate(false)),
        MenuAction::UpdateValues
    );
    assert!(core.just_activated());

    core.set_values(values.clone());
    assert_eq!(core.selected_index(), Some(0));

    core.pre_event(&MenuEvent::Activate(false));
    assert_eq!(core.values().len(), 3);
    assert_eq!(
        core.handle_event(MenuEvent::Activate(false)),
        MenuAction::ApplySelection
    );
    assert!(!core.just_activated());
    assert_eq!(core.selected_index(), Some(0));

    assert_eq!(
        core.handle_event(MenuEvent::NextElement),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_index(), Some(1));

    core.pre_event(&MenuEvent::Activate(false));
    assert_eq!(
        core.handle_event(MenuEvent::Activate(false)),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_index(), Some(2));

    assert_eq!(
        core.handle_event(MenuEvent::PreviousElement),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_index(), Some(1));

    assert_eq!(
        core.handle_event(MenuEvent::Edit(false)),
        MenuAction::UpdateValues
    );
    assert!(core.just_activated());
    assert_eq!(core.selected_index(), Some(0));

    core.pre_event(&MenuEvent::Deactivate);
    assert!(!core.is_active());
    assert!(core.values().is_empty());
    assert_eq!(core.handle_event(MenuEvent::Deactivate), MenuAction::None);
}

#[test]
fn navigation_wraps_and_clamps_on_sparse_last_row() {
    let mut core = MenuCore::default();
    core.set_columns(4);
    core.set_values(numbered_suggestions(6));
    core.update_layout(80, 0);

    assert_eq!(
        core.handle_event(MenuEvent::PreviousElement),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_row(), 1);
    assert_eq!(core.selected_col(), 1);
    assert_eq!(core.selected_index(), Some(5));

    assert_eq!(
        core.handle_event(MenuEvent::MoveRight),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_row(), 1);
    assert_eq!(core.selected_col(), 0);
    assert_eq!(core.selected_index(), Some(4));

    assert_eq!(
        core.handle_event(MenuEvent::NextElement),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_row(), 1);
    assert_eq!(core.selected_col(), 1);
    assert_eq!(core.selected_index(), Some(5));

    assert_eq!(
        core.handle_event(MenuEvent::NextElement),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_row(), 0);
    assert_eq!(core.selected_col(), 0);
    assert_eq!(core.selected_index(), Some(0));

    assert_eq!(
        core.handle_event(MenuEvent::MoveRight),
        MenuAction::ApplySelection
    );
    assert_eq!(
        core.handle_event(MenuEvent::MoveRight),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_row(), 0);
    assert_eq!(core.selected_col(), 2);
    assert_eq!(core.selected_index(), Some(2));

    assert_eq!(
        core.handle_event(MenuEvent::MoveDown),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_row(), 0);
    assert_eq!(core.selected_col(), 2);
    assert_eq!(core.selected_index(), Some(2));

    assert_eq!(
        core.handle_event(MenuEvent::MoveUp),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_row(), 0);
    assert_eq!(core.selected_col(), 2);
    assert_eq!(core.selected_index(), Some(2));

    assert_eq!(
        core.handle_event(MenuEvent::MoveLeft),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_row(), 0);
    assert_eq!(core.selected_col(), 1);
    assert!(core.selected_value().is_some());
}

#[test]
fn debug_snapshot_scrolls_descriptions_and_hides_when_inactive() {
    let mut core = MenuCore::default();
    core.set_columns(1);
    core.set_max_rows(2);
    core.set_description_rows(1);

    core.pre_event(&MenuEvent::Activate(false));
    assert_eq!(
        core.handle_event(MenuEvent::Activate(false)),
        MenuAction::UpdateValues
    );
    core.set_values(vec![
        described_suggestion("alpha", "alpha description"),
        described_suggestion("bravo", "bravo description"),
        described_suggestion("charlie", "charlie description"),
        described_suggestion("delta", "delta description"),
        described_suggestion("echo", "echo description"),
    ]);
    core.update_layout(40, 3);

    assert_eq!(
        core.handle_event(MenuEvent::NextElement),
        MenuAction::ApplySelection
    );
    assert_eq!(
        core.handle_event(MenuEvent::MoveDown),
        MenuAction::ApplySelection
    );
    assert_eq!(
        core.handle_event(MenuEvent::MoveDown),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_index(), Some(2));

    let colors = MenuTextStyle::default();
    let snapshot = core.debug_snapshot(&colors, 40, 3, 3, false);
    assert_eq!(snapshot.visible_rows, 2);
    assert_eq!(snapshot.description.as_deref(), Some("charlie description"));
    assert!(snapshot.description_rendered.is_some());
    assert_eq!(snapshot.rendered.len(), 3);
    assert!(snapshot.rendered.iter().any(|line| line.contains("bravo")));
    assert!(
        snapshot
            .rendered
            .iter()
            .any(|line| line.contains("charlie"))
    );
    assert!(!snapshot.rendered.iter().any(|line| line.contains("alpha")));

    let tight = core.debug_snapshot(&colors, 40, 1, 3, false);
    assert_eq!(tight.visible_rows, 1);
    assert!(tight.description.is_none());
    assert!(tight.description_rendered.is_none());

    core.pre_event(&MenuEvent::Deactivate);
    let inactive = core.debug_snapshot(&colors, 40, 3, 3, false);
    assert!(inactive.rendered.is_empty());
    assert_eq!(core.menu_string(3, false, &colors), "");
}

#[test]
fn helper_paths_cover_layout_render_and_style_utilities() {
    let mut decorated = suggestion("value");
    decorated.extra = Some(vec!["shown".to_string()]);
    assert_eq!(display_text(&decorated), "shown");

    assert_eq!(truncate_to_width("alpha", 0), "");
    assert_eq!(truncate_to_width("a界z", 3), "a界");
    assert_eq!(indent_lines("alpha\r\nbeta", 2), "  alpha\r\n  beta");
    assert_eq!(marker_prefix(false, 0), ("", 0));
    assert_eq!(marker_prefix(true, 1), (">", 1));
    assert_eq!(marker_prefix(true, 4), ("> ", 2));
    assert_eq!(marker_width_for_layout(1), 1);
    assert_eq!(marker_width_for_layout(8), 2);
    assert_eq!(color_to_string(Color::Default), "default");
    assert_eq!(color_to_string(Color::Fixed(7)), "fixed:7");
    assert_eq!(color_to_string(Color::Rgb(1, 2, 3)), "rgb:1,2,3");

    let style = Style::new()
        .fg(Color::LightBlue)
        .on(Color::Black)
        .bold()
        .italic()
        .underline()
        .blink()
        .reverse()
        .hidden()
        .strikethrough();
    let style_debug = style_to_debug(style);
    assert_eq!(style_debug.foreground.as_deref(), Some("light_blue"));
    assert_eq!(style_debug.background.as_deref(), Some("black"));
    assert!(style_debug.bold);
    assert!(style_debug.italic);
    assert!(style_debug.underline);
    assert!(style_debug.blink);
    assert!(style_debug.reverse);
    assert!(style_debug.hidden);
    assert!(style_debug.strikethrough);
}

#[test]
fn layout_and_render_helpers_cover_empty_narrow_and_ansi_paths() {
    let mut core = MenuCore::default();
    core.update_layout(1, 99);
    assert_eq!(core.input_indent(), 0);
    assert_eq!(core.columns_for_test(), 1);
    assert_eq!(core.menu_required_lines(), 1);

    let (cols, widths) = core.compute_column_layout(0, marker_width_for_layout(1));
    assert_eq!(cols, 1);
    assert_eq!(widths, vec![1]);

    core.set_columns(4);
    core.set_column_padding(3);
    core.set_values(vec![
        described_suggestion("alphabet", "Inspect runtime configuration"),
        suggestion("bravo"),
    ]);
    core.update_layout(8, 0);
    assert_eq!(core.columns_for_test(), 1);
    assert_eq!(
        core.description_line().as_deref(),
        Some("Inspect runtime configuration")
    );

    let colors = MenuTextStyle {
        text_style: Style::new().fg(Color::Cyan),
        selected_text_style: Style::new().fg(Color::Black).on(Color::White),
        description_style: Style::new().fg(Color::Yellow),
        ..MenuTextStyle::default()
    };

    let ansi_entry = core.create_entry_string(&core.values()[0], 0, 0, 1, true, &colors);
    let ascii_entry = core.create_entry_string(&core.values()[0], 0, 0, 1, false, &colors);
    let ansi_description =
        core.create_description_string("Inspect runtime configuration", 12, true, &colors);
    let ascii_description =
        core.create_description_string("Inspect runtime configuration", 12, false, &colors);

    assert!(ansi_entry.contains("\u{1b}["));
    assert!(ascii_entry.starts_with("> "));
    assert!(ansi_description.contains("\u{1b}["));
    assert_eq!(ascii_description.len(), 12);

    core.set_max_rows(1);
    assert_eq!(
        core.handle_event(MenuEvent::NextPage),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_index(), Some(1));
    assert_eq!(
        core.handle_event(MenuEvent::PreviousPage),
        MenuAction::ApplySelection
    );
    assert_eq!(core.selected_index(), Some(0));
}