mod background;
pub(crate) mod event;
mod handlers;
mod render;
mod rom_load;
mod run;
mod update;
#[cfg(test)]
mod tests;
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use std::collections::{HashSet, VecDeque};
use crate::client::RommClient;
use crate::commands::library_scan::ScanCacheInvalidate;
use crate::config::Config;
use crate::core::cache::{RomCache, RomCacheKey};
use crate::core::download::DownloadManager;
use crate::endpoints::roms::GetRoms;
use crate::feature_compat::SaveSyncCompatibility;
use crate::update::UpdateStatus;
use super::screens::connected_splash::StartupSplash;
use super::screens::{
DownloadScreen, ExtrasPickerScreen, GameDetailScreen, LibraryBrowseScreen, MainMenuScreen,
SearchScreen, SettingsScreen,
};
use super::theme::resolve_theme_or_default;
use ratatui_themekit::Theme;
use background::types::{
CollectionPrefetchDone, CoverLoadDone, DeferredLoadRoms, DeviceListDone,
LibraryMetadataRefreshDone, LibraryUploadComplete, PlatformListDone, RomLoadDone,
SaveDownloadDone, SaveListDone, SaveUploadDone, SearchLoadDone, StartupUpdatePrompt,
SyncPushPullDone,
};
pub enum AppScreen {
MainMenu(MainMenuScreen),
LibraryBrowse(Box<LibraryBrowseScreen>),
Search(SearchScreen),
Settings(Box<SettingsScreen>),
GameDetail(Box<GameDetailScreen>),
ExtrasPicker(Box<ExtrasPickerScreen>),
Download(DownloadScreen),
SetupWizard(Box<crate::tui::screens::setup_wizard::SetupWizard>),
}
pub struct App {
pub screen: AppScreen,
client: RommClient,
config: Config,
server_version: Option<String>,
save_sync_compat: SaveSyncCompatibility,
rom_cache: RomCache,
downloads: DownloadManager,
screen_before_download: Option<AppScreen>,
deferred_load_roms: Option<DeferredLoadRoms>,
startup_splash: Option<StartupSplash>,
pub global_error: Option<String>,
pub global_notice: Option<String>,
show_keyboard_help: bool,
startup_update_prompt: Option<StartupUpdatePrompt>,
library_metadata_rx: Option<tokio::sync::mpsc::UnboundedReceiver<LibraryMetadataRefreshDone>>,
library_metadata_refresh_gen: u64,
collection_prefetch_rx: tokio::sync::mpsc::UnboundedReceiver<CollectionPrefetchDone>,
collection_prefetch_tx: tokio::sync::mpsc::UnboundedSender<CollectionPrefetchDone>,
collection_prefetch_queue: VecDeque<(RomCacheKey, GetRoms, u64)>,
collection_prefetch_queued_keys: HashSet<RomCacheKey>,
collection_prefetch_inflight_keys: HashSet<RomCacheKey>,
rom_load_gen: u64,
rom_load_rx: tokio::sync::mpsc::UnboundedReceiver<RomLoadDone>,
rom_load_tx: tokio::sync::mpsc::UnboundedSender<RomLoadDone>,
rom_load_task: Option<tokio::task::JoinHandle<()>>,
search_load_rx: tokio::sync::mpsc::UnboundedReceiver<SearchLoadDone>,
search_load_tx: tokio::sync::mpsc::UnboundedSender<SearchLoadDone>,
search_load_task: Option<tokio::task::JoinHandle<()>>,
cover_load_rx: tokio::sync::mpsc::UnboundedReceiver<CoverLoadDone>,
cover_load_tx: tokio::sync::mpsc::UnboundedSender<CoverLoadDone>,
cover_load_task: Option<tokio::task::JoinHandle<()>>,
library_scan_rx: Option<tokio::sync::mpsc::UnboundedReceiver<Result<(), String>>>,
library_scan_inflight: bool,
library_scan_pending_invalidate: Option<ScanCacheInvalidate>,
force_rom_reload_after_metadata: bool,
library_upload_inflight: bool,
library_upload_progress_rx: Option<tokio::sync::mpsc::UnboundedReceiver<(u64, u64)>>,
library_upload_done_rx:
Option<tokio::sync::mpsc::UnboundedReceiver<Result<LibraryUploadComplete, String>>>,
save_list_rx: tokio::sync::mpsc::UnboundedReceiver<SaveListDone>,
save_list_tx: tokio::sync::mpsc::UnboundedSender<SaveListDone>,
save_upload_rx: tokio::sync::mpsc::UnboundedReceiver<SaveUploadDone>,
save_upload_tx: tokio::sync::mpsc::UnboundedSender<SaveUploadDone>,
save_download_rx: tokio::sync::mpsc::UnboundedReceiver<SaveDownloadDone>,
save_download_tx: tokio::sync::mpsc::UnboundedSender<SaveDownloadDone>,
device_list_rx: tokio::sync::mpsc::UnboundedReceiver<DeviceListDone>,
device_list_tx: tokio::sync::mpsc::UnboundedSender<DeviceListDone>,
platform_list_rx: tokio::sync::mpsc::UnboundedReceiver<PlatformListDone>,
platform_list_tx: tokio::sync::mpsc::UnboundedSender<PlatformListDone>,
sync_push_pull_rx: tokio::sync::mpsc::UnboundedReceiver<SyncPushPullDone>,
sync_push_pull_tx: tokio::sync::mpsc::UnboundedSender<SyncPushPullDone>,
theme: Box<dyn Theme>,
}
impl App {
fn blocks_global_chord_shortcuts(&self) -> bool {
self.startup_splash.is_some()
|| self.startup_update_prompt.is_some()
|| self.global_error.is_some()
|| self.global_notice.is_some()
}
fn blocks_global_d_shortcut(&self) -> bool {
let base = match &self.screen {
AppScreen::Search(_) | AppScreen::Settings(_) | AppScreen::SetupWizard(_) => true,
AppScreen::LibraryBrowse(lib) => {
lib.any_search_bar_open() || lib.any_upload_prompt_open()
}
_ => false,
};
base || self.library_upload_inflight || self.blocks_global_chord_shortcuts()
}
fn allows_global_question_help(&self) -> bool {
match &self.screen {
AppScreen::Search(_) | AppScreen::SetupWizard(_) => false,
AppScreen::LibraryBrowse(lib)
if lib.any_search_bar_open() || lib.any_upload_prompt_open() =>
{
false
}
AppScreen::Settings(s) if s.editing || s.path_picker.is_some() => false,
_ => true,
}
}
pub(crate) fn is_force_quit_key(key: &crossterm::event::KeyEvent) -> bool {
key.kind == KeyEventKind::Press
&& key.modifiers.contains(KeyModifiers::CONTROL)
&& matches!(key.code, KeyCode::Char('c') | KeyCode::Char('C'))
}
pub fn new(
client: RommClient,
config: Config,
save_sync_compat: SaveSyncCompatibility,
server_version: Option<String>,
startup_splash: Option<StartupSplash>,
startup_update: Option<UpdateStatus>,
) -> Self {
let (prefetch_tx, prefetch_rx) = tokio::sync::mpsc::unbounded_channel();
let (rom_load_tx, rom_load_rx) = tokio::sync::mpsc::unbounded_channel();
let (search_load_tx, search_load_rx) = tokio::sync::mpsc::unbounded_channel();
let (cover_load_tx, cover_load_rx) = tokio::sync::mpsc::unbounded_channel();
let (save_list_tx, save_list_rx) = tokio::sync::mpsc::unbounded_channel();
let (save_upload_tx, save_upload_rx) = tokio::sync::mpsc::unbounded_channel();
let (save_download_tx, save_download_rx) = tokio::sync::mpsc::unbounded_channel();
let (device_list_tx, device_list_rx) = tokio::sync::mpsc::unbounded_channel();
let (platform_list_tx, platform_list_rx) = tokio::sync::mpsc::unbounded_channel();
let (sync_push_pull_tx, sync_push_pull_rx) = tokio::sync::mpsc::unbounded_channel();
let theme = resolve_theme_or_default(&config.theme);
Self {
screen: AppScreen::MainMenu(MainMenuScreen::new()),
client,
config,
server_version,
save_sync_compat,
rom_cache: RomCache::load(),
downloads: DownloadManager::new(),
screen_before_download: None,
deferred_load_roms: None,
startup_splash,
global_error: None,
global_notice: None,
show_keyboard_help: false,
startup_update_prompt: startup_update.map(|status| StartupUpdatePrompt {
status,
updating: false,
}),
library_metadata_rx: None,
library_metadata_refresh_gen: 0,
collection_prefetch_rx: prefetch_rx,
collection_prefetch_tx: prefetch_tx,
collection_prefetch_queue: VecDeque::new(),
collection_prefetch_queued_keys: HashSet::new(),
collection_prefetch_inflight_keys: HashSet::new(),
rom_load_gen: 0,
rom_load_rx,
rom_load_tx,
rom_load_task: None,
search_load_rx,
search_load_tx,
search_load_task: None,
cover_load_rx,
cover_load_tx,
cover_load_task: None,
library_scan_rx: None,
library_scan_inflight: false,
library_scan_pending_invalidate: None,
force_rom_reload_after_metadata: false,
library_upload_inflight: false,
library_upload_progress_rx: None,
library_upload_done_rx: None,
save_list_rx,
save_list_tx,
save_upload_rx,
save_upload_tx,
save_download_rx,
save_download_tx,
device_list_rx,
device_list_tx,
platform_list_rx,
platform_list_tx,
sync_push_pull_rx,
sync_push_pull_tx,
theme,
}
}
pub fn set_error(&mut self, err: crate::error::RommError) {
self.global_error = Some(crate::error::user_message(&err));
}
pub(in crate::tui::app) fn apply_saved_theme(&mut self) {
self.theme = resolve_theme_or_default(&self.config.theme);
}
#[cfg(test)]
pub(crate) fn theme_id(&self) -> &str {
self.theme.id()
}
pub async fn handle_key_event(&mut self, key: &KeyEvent) -> Result<bool> {
for action in event::map_key_to_actions(self, key) {
if self.update(action).await? {
return Ok(true);
}
}
Ok(false)
}
}