rdice-tui 0.1.2

Terminal UI for persistent dice trays powered by rdice-core
Documentation
use ratatui::style::{Color, Modifier, Style};
use ratatui::{Terminal, backend::TestBackend};
use rdice_core::DiceEngine;
use rdice_tui::app::{App, Screen};
use rdice_tui::theme;
use rdice_tui::ui::{footer_text, help_text};

#[test]
fn help_text_changes_by_screen() {
    let mut app = App::new(
        DiceEngine::new(),
        std::env::temp_dir().join("rdice-ui-test.toml"),
    );

    app.screen = Screen::Overview;
    assert!(help_text(&app).contains("m trays"));
    assert!(help_text(&app).contains("c dice"));

    app.screen = Screen::TrayDetail("combat".into());
    assert!(help_text(&app).contains("m/c dice"));

    app.screen = Screen::AddDie("combat".into());
    assert!(help_text(&app).contains("Esc tray"));
    assert!(help_text(&app).contains("c dice"));

    app.screen = Screen::TrayManager;
    assert!(help_text(&app).contains("n new"));

    app.screen = Screen::DiceManager;
    assert!(help_text(&app).contains("e<num> edit"));
}

#[test]
fn footer_prioritizes_feedback_on_narrow_width() {
    let mut app = App::new(
        DiceEngine::new(),
        std::env::temp_dir().join("rdice-ui-test.toml"),
    );
    app.message = Some("added D6 to combat".into());

    let footer = footer_text(&app, 10);

    assert_eq!(footer, "added D6 t");
}

#[test]
fn footer_labels_command_mode_with_actions() {
    let mut app = App::new(
        DiceEngine::new(),
        std::env::temp_dir().join("rdice-ui-test.toml"),
    );
    app.command_buffer = Some("manager dice".into());

    let footer = footer_text(&app, 80);

    assert!(footer.contains("COMMAND: manager dice|"));
    assert!(footer.contains("Enter apply"));
    assert!(footer.contains("Esc cancel"));
}

#[test]
fn footer_explains_dice_creation_wizard_steps() {
    let mut app = App::new(
        DiceEngine::new(),
        std::env::temp_dir().join("rdice-ui-test.toml"),
    );
    app.start_dice_creation();

    let footer = footer_text(&app, 80);

    assert_eq!(footer.len(), 80);
    assert!(footer.starts_with("NEW DIE > name: <name>|"));
    assert!(footer.ends_with("Enter next  Esc cancel"));
    assert!(!footer.contains("n new"));
    assert!(!footer.contains(":dice"));

    for ch in "coin".chars() {
        app.push_dice_creation_char(ch);
    }
    let footer = footer_text(&app, 80);
    assert!(footer.contains("NEW DIE > name: coin|"));

    app.advance_dice_creation().unwrap();
    let footer = footer_text(&app, 80);
    assert!(footer.starts_with("NEW DIE > face count: <count>|"));
    assert!(footer.ends_with("Enter next  Esc cancel"));

    app.push_dice_creation_char('2');
    app.advance_dice_creation().unwrap();
    let footer = footer_text(&app, 80);
    assert!(footer.starts_with("NEW DIE > face 1/2: <face>|"));
    assert!(footer.ends_with("Enter next  Esc cancel"));

    for ch in "heads".chars() {
        app.push_dice_creation_char(ch);
    }
    app.advance_dice_creation().unwrap();
    let footer = footer_text(&app, 80);
    assert!(footer.starts_with("NEW DIE > face 2/2: <face>|"));
    assert!(footer.ends_with("Enter create  Esc cancel"));
}

#[test]
fn theme_styles_can_be_disabled_for_no_color_contexts() {
    assert_eq!(theme::border(false), Style::default());
    assert_eq!(theme::content(false), Style::default());
    assert_eq!(theme::footer(false), Style::default());

    let title = theme::title(true);
    assert_eq!(title.fg, Some(Color::Yellow));
    assert!(title.add_modifier.contains(Modifier::BOLD));

    let command_footer = theme::command_footer(true);
    assert_eq!(command_footer.fg, Some(Color::Magenta));
    assert!(command_footer.add_modifier.contains(Modifier::BOLD));
}

#[test]
fn overview_main_content_uses_multiple_colors() {
    let mut app = App::new_with_color(
        DiceEngine::new(),
        std::env::temp_dir().join("rdice-ui-color-test.toml"),
        true,
    );
    app.engine.create_tray("combat").unwrap();
    app.engine.add_die_to_tray("d6", "combat").unwrap();
    app.engine.roll_tray("combat").unwrap();
    app.selected_trays.push("combat".into());

    let backend = TestBackend::new(80, 12);
    let mut terminal = Terminal::new(backend).unwrap();
    terminal
        .draw(|frame| rdice_tui::ui::render(frame, &app))
        .unwrap();

    let mut colors = Vec::new();
    let buffer = terminal.backend().buffer();
    for y in 0..11 {
        for x in 0..80 {
            let cell = buffer.cell((x, y)).unwrap();
            if cell.symbol() != " " && !colors.contains(&cell.fg) {
                colors.push(cell.fg);
            }
        }
    }

    assert!(
        colors.len() >= 3,
        "expected varied overview colors, got {colors:?}"
    );

    let selected_marker = (0..11).find_map(|y| {
        (0..80).find_map(|x| {
            let cell = buffer.cell((x, y)).unwrap();
            (cell.symbol() == "*").then_some(cell)
        })
    });
    let selected_marker = selected_marker.expect("expected selected tray marker");
    assert_eq!(selected_marker.fg, Color::Red);
    assert!(selected_marker.modifier.contains(Modifier::BOLD));
}