romm-cli 0.37.0

Rust-based CLI and TUI for the ROMM API
Documentation
use std::sync::{Arc, Mutex};

use ratatui_image::picker::ProtocolType;

use super::cover::detect_cover_protocol_from_env;
use super::saves::save_lines;
use super::types::{CoverState, GameDetailPrevious, GameDetailScreen, SaveListState};
use crate::tui::screens::SearchScreen;
use crate::types::SaveMetadata;

#[test]
fn detect_cover_protocol_prefers_kitty_hint() {
    let protocol = detect_cover_protocol_from_env(
        Some("iTerm.app".into()),
        Some("xterm-256color".into()),
        Some("123".into()),
    );
    assert_eq!(protocol, Some(ProtocolType::Kitty));
}

#[test]
fn detect_cover_protocol_supports_sixel_term() {
    let protocol =
        detect_cover_protocol_from_env(None, Some("xterm+sixel".into()), Some(String::new()));
    assert_eq!(protocol, Some(ProtocolType::Sixel));
}

#[test]
fn missing_protocol_still_requests_cover_load() {
    let rom = crate::types::Rom {
        id: 5,
        platform_id: 1,
        platform_slug: None,
        platform_fs_slug: None,
        platform_custom_name: None,
        platform_display_name: None,
        fs_name: "game.zip".to_string(),
        fs_name_no_tags: "game".to_string(),
        fs_name_no_ext: "game".to_string(),
        fs_extension: "zip".to_string(),
        fs_path: "/game.zip".to_string(),
        fs_size_bytes: 10,
        name: "game".to_string(),
        slug: None,
        summary: None,
        path_cover_small: None,
        path_cover_large: None,
        url_cover: Some("http://example.com/cover.png".to_string()),
        has_manual: false,
        path_manual: None,
        url_manual: None,
        is_unidentified: false,
        is_identified: true,
        files: Vec::new(),
    };
    let previous = GameDetailPrevious::Search(SearchScreen::new());
    let downloads = Arc::new(Mutex::new(Vec::new()));
    let mut detail = GameDetailScreen::new(rom, Vec::new(), previous, downloads);
    detail.cover_protocol = None;
    assert!(detail.should_request_cover_load());
    detail.set_cover_loading();
    assert_eq!(detail.cover_state, CoverState::Loading);
}

#[test]
fn has_any_extras_false_when_no_assets() {
    let rom = test_rom(1, None);
    let detail = new_detail(rom);
    assert!(!detail.has_any_extras());
}

#[test]
fn footer_help_text_mentions_extras_shortcut() {
    let detail = new_detail(test_rom(1, None));
    assert!(detail.footer_help_text().contains("e: Extras"));
}

#[test]
fn footer_help_text_tracks_technical_mode() {
    let mut detail = new_detail(test_rom(1, None));
    let non_technical = detail.footer_help_text();
    assert!(non_technical.contains("More technical details"));

    detail.show_technical = true;
    let technical = detail.footer_help_text();
    assert!(technical.contains("Hide technical"));
}

#[test]
fn cover_state_transitions_to_ready_and_error() {
    let mut detail = new_detail(test_rom(1, Some("http://example.com/cover.png".into())));
    detail.set_cover_loading();
    assert_eq!(detail.cover_state, CoverState::Loading);

    detail.apply_cover_image(image::DynamicImage::new_rgba8(4, 4));
    assert_eq!(detail.cover_state, CoverState::Ready);
    assert!(detail.cover_image.is_some());

    detail.apply_cover_error("oops".to_string());
    assert_eq!(detail.cover_state, CoverState::Failed("oops".to_string()));
}

#[test]
fn save_list_formatting_handles_states() {
    assert!(save_lines(&SaveListState::Loading, 0)[0]
        .to_string()
        .contains("Loading"));
    assert!(save_lines(&SaveListState::Failed("boom".into()), 0)[0]
        .to_string()
        .contains("Error"));
    assert!(save_lines(&SaveListState::Loaded(vec![]), 0)[0]
        .to_string()
        .contains("No remote saves"));

    let rows = vec![SaveMetadata {
        id: 7,
        file_name: "game.sav".into(),
        emulator: Some("duckstation".into()),
        slot: Some("1".into()),
        updated_at: Some("2026-05-12T00:00:00Z".into()),
        hash: Some("abcdef1234567890".into()),
        size_bytes: Some(1024),
        device_id: Some("dev1".into()),
        device_name: None,
    }];
    let line = save_lines(&SaveListState::Loaded(rows), 0)[0].to_string();
    assert!(line.contains("> game.sav"));
    assert!(line.contains("emu=duckstation"));
    assert!(line.contains("slot=1"));
}

fn test_rom(id: u64, url_cover: Option<String>) -> crate::types::Rom {
    crate::types::Rom {
        id,
        platform_id: 1,
        platform_slug: None,
        platform_fs_slug: None,
        platform_custom_name: None,
        platform_display_name: None,
        fs_name: "game.zip".to_string(),
        fs_name_no_tags: "game".to_string(),
        fs_name_no_ext: "game".to_string(),
        fs_extension: "zip".to_string(),
        fs_path: "/game.zip".to_string(),
        fs_size_bytes: 10,
        name: "game".to_string(),
        slug: None,
        summary: None,
        path_cover_small: None,
        path_cover_large: None,
        url_cover,
        has_manual: false,
        path_manual: None,
        url_manual: None,
        is_unidentified: false,
        is_identified: true,
        files: Vec::new(),
    }
}

fn new_detail(rom: crate::types::Rom) -> GameDetailScreen {
    GameDetailScreen::new(
        rom,
        Vec::new(),
        GameDetailPrevious::Search(SearchScreen::new()),
        Arc::new(Mutex::new(Vec::new())),
    )
}