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 openapi;
15pub mod openapi_sync;
16pub mod screens;
17pub mod utils;
18
19use anyhow::Result;
20
21use crate::client::RommClient;
22use crate::config::{openapi_cache_path, Config};
23
24use self::app::App;
25use self::openapi_sync::sync_openapi_registry;
26use self::screens::connected_splash::StartupSplash;
27use self::screens::setup_wizard::SetupWizard;
28
29fn install_panic_hook() {
30    let original_hook = std::panic::take_hook();
31    std::panic::set_hook(Box::new(move |panic| {
32        let _ = crossterm::terminal::disable_raw_mode();
33        let _ = crossterm::execute!(
34            std::io::stdout(),
35            crossterm::terminal::LeaveAlternateScreen,
36            crossterm::event::DisableMouseCapture
37        );
38        original_hook(panic);
39    }));
40}
41
42fn startup_splash_for(
43    from_setup_wizard: bool,
44    config: &Config,
45    server_version: &Option<String>,
46) -> Option<StartupSplash> {
47    if from_setup_wizard {
48        return Some(StartupSplash::new(
49            config.base_url.clone(),
50            server_version.clone(),
51        ));
52    }
53    if server_version.is_some() {
54        return Some(StartupSplash::new(
55            config.base_url.clone(),
56            server_version.clone(),
57        ));
58    }
59    None
60}
61
62async fn run_started(client: RommClient, config: Config, from_setup_wizard: bool) -> Result<()> {
63    install_panic_hook();
64    let cache_path = openapi_cache_path()?;
65    let (registry, server_version) = sync_openapi_registry(&client, &cache_path).await?;
66
67    let splash = startup_splash_for(from_setup_wizard, &config, &server_version);
68    let mut app = App::new(client, config, registry, server_version, splash);
69    app.run().await
70}
71
72/// Launch the TUI when the caller already has a [`RommClient`] and [`Config`].
73pub async fn run(client: RommClient, config: Config) -> Result<()> {
74    run_started(client, config, false).await
75}
76
77/// Load config, run first-time setup in the terminal if `API_BASE_URL` is missing, then start the TUI.
78pub async fn run_interactive(verbose: bool) -> Result<()> {
79    crate::config::load_layered_env();
80    let (from_wizard, config) = match crate::config::load_config() {
81        Ok(c) => (false, c),
82        Err(_) => (true, SetupWizard::new().run(verbose).await?),
83    };
84    let client = RommClient::new(&config, verbose)?;
85    run_started(client, config, from_wizard).await
86}