Skip to main content

romm_cli/tui/
mod.rs

1//! Terminal UI module.
2//!
3//! This module contains all ratatui / crossterm code and is responsible
4//! purely for presentation and interaction. It talks to the rest of the
5//! application through:
6//! - `RommClient` (HTTP / data access),
7//! - `core::cache::RomCache` (disk-backed ROM cache), and
8//! - `core::download::DownloadManager` (background ROM downloads).
9//!
10//! Keeping those \"service\" types UI-agnostic makes it easy to add other
11//! frontends (e.g. a GUI) reusing the same core logic.
12
13pub mod app;
14pub mod keyboard_help;
15pub mod openapi;
16pub mod openapi_sync;
17pub mod path_picker;
18pub mod screens;
19pub mod text_search;
20pub mod utils;
21
22use anyhow::Result;
23use std::time::Duration;
24
25use crate::client::RommClient;
26use crate::config::{openapi_cache_path, should_check_updates, Config};
27
28use self::app::App;
29use self::openapi_sync::sync_openapi_registry;
30use self::screens::connected_splash::StartupSplash;
31use self::screens::setup_wizard::SetupWizard;
32
33fn install_panic_hook() {
34    let original_hook = std::panic::take_hook();
35    std::panic::set_hook(Box::new(move |panic| {
36        let _ = crossterm::terminal::disable_raw_mode();
37        let _ = crossterm::execute!(
38            std::io::stdout(),
39            crossterm::terminal::LeaveAlternateScreen,
40            crossterm::event::DisableMouseCapture
41        );
42        original_hook(panic);
43    }));
44}
45
46fn startup_splash_for(
47    from_setup_wizard: bool,
48    config: &Config,
49    server_version: &Option<String>,
50) -> Option<StartupSplash> {
51    if from_setup_wizard {
52        return Some(StartupSplash::new(
53            config.base_url.clone(),
54            server_version.clone(),
55        ));
56    }
57    if server_version.is_some() {
58        return Some(StartupSplash::new(
59            config.base_url.clone(),
60            server_version.clone(),
61        ));
62    }
63    None
64}
65
66async fn run_started(
67    client: RommClient,
68    config: Config,
69    from_setup_wizard: bool,
70    mock_update: bool,
71) -> Result<()> {
72    install_panic_hook();
73    let cache_path = openapi_cache_path()?;
74    let (registry, server_version) = sync_openapi_registry(&client, &cache_path).await?;
75
76    let startup_update = if mock_update {
77        Some(crate::update::UpdateStatus {
78            current_version: format!("{} (dev)", env!("CARGO_PKG_VERSION")),
79            latest_version: "9.9.9-mock".into(),
80            should_update: true,
81            release_url: "https://github.com/patricksmill/romm-cli".into(),
82            changelog_url: crate::update::changelog_url().to_string(),
83        })
84    } else if should_check_updates() {
85        match tokio::time::timeout(Duration::from_secs(2), crate::update::check_for_update()).await
86        {
87            Ok(Ok(status)) if status.should_update => Some(status),
88            _ => None,
89        }
90    } else {
91        None
92    };
93
94    let splash = startup_splash_for(from_setup_wizard, &config, &server_version);
95    let mut app = App::new(
96        client,
97        config,
98        registry,
99        server_version,
100        splash,
101        startup_update,
102    );
103    app.run().await
104}
105
106/// Launch the TUI when the caller already has a [`RommClient`] and [`Config`].
107pub async fn run(client: RommClient, config: Config, mock_update: bool) -> Result<()> {
108    run_started(client, config, false, mock_update).await
109}
110
111/// Load config, run first-time setup in the terminal if `API_BASE_URL` is missing, then start the TUI.
112pub async fn run_interactive(verbose: bool, mock_update: bool) -> Result<()> {
113    let (from_wizard, config) = match crate::config::load_config() {
114        Ok(c) => (false, c),
115        Err(_) => (true, SetupWizard::new().run(verbose).await?),
116    };
117    let client = RommClient::new(&config, verbose)?;
118    run_started(client, config, from_wizard, mock_update).await
119}