1mod background;
11pub(crate) mod event;
12mod handlers;
13mod render;
14mod rom_load;
15mod run;
16mod update;
17
18#[cfg(test)]
19mod tests;
20
21use anyhow::Result;
22use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
23use std::collections::{HashSet, VecDeque};
24
25use crate::client::RommClient;
26use crate::commands::library_scan::ScanCacheInvalidate;
27use crate::config::Config;
28use crate::core::cache::{RomCache, RomCacheKey};
29use crate::core::download::DownloadManager;
30use crate::endpoints::roms::GetRoms;
31use crate::feature_compat::SaveSyncCompatibility;
32use crate::update::UpdateStatus;
33
34use super::screens::connected_splash::StartupSplash;
35use super::screens::{
36 DownloadScreen, ExtrasPickerScreen, GameDetailScreen, LibraryBrowseScreen, MainMenuScreen,
37 SearchScreen, SettingsScreen,
38};
39use super::theme::resolve_theme_or_default;
40use ratatui_themekit::Theme;
41
42use background::types::{
43 CollectionPrefetchDone, CoverLoadDone, DeferredLoadRoms, DeviceListDone,
44 LibraryMetadataRefreshDone, LibraryUploadComplete, PlatformListDone, RomLoadDone,
45 SaveDownloadDone, SaveListDone, SaveUploadDone, SearchLoadDone, StartupUpdatePrompt,
46 SyncPushPullDone,
47};
48
49pub enum AppScreen {
54 MainMenu(MainMenuScreen),
55 LibraryBrowse(Box<LibraryBrowseScreen>),
56 Search(SearchScreen),
57 Settings(Box<SettingsScreen>),
58 GameDetail(Box<GameDetailScreen>),
59 ExtrasPicker(Box<ExtrasPickerScreen>),
60 Download(DownloadScreen),
61 SetupWizard(Box<crate::tui::screens::setup_wizard::SetupWizard>),
62}
63
64pub struct App {
69 pub screen: AppScreen,
70 client: RommClient,
71 config: Config,
72 server_version: Option<String>,
74 save_sync_compat: SaveSyncCompatibility,
75 rom_cache: RomCache,
76 downloads: DownloadManager,
77 screen_before_download: Option<AppScreen>,
79 deferred_load_roms: Option<DeferredLoadRoms>,
81 startup_splash: Option<StartupSplash>,
83 pub global_error: Option<String>,
84 pub global_notice: Option<String>,
85 show_keyboard_help: bool,
86 startup_update_prompt: Option<StartupUpdatePrompt>,
87 library_metadata_rx: Option<tokio::sync::mpsc::UnboundedReceiver<LibraryMetadataRefreshDone>>,
89 library_metadata_refresh_gen: u64,
91 collection_prefetch_rx: tokio::sync::mpsc::UnboundedReceiver<CollectionPrefetchDone>,
92 collection_prefetch_tx: tokio::sync::mpsc::UnboundedSender<CollectionPrefetchDone>,
93 collection_prefetch_queue: VecDeque<(RomCacheKey, GetRoms, u64)>,
94 collection_prefetch_queued_keys: HashSet<RomCacheKey>,
95 collection_prefetch_inflight_keys: HashSet<RomCacheKey>,
96 rom_load_gen: u64,
98 rom_load_rx: tokio::sync::mpsc::UnboundedReceiver<RomLoadDone>,
99 rom_load_tx: tokio::sync::mpsc::UnboundedSender<RomLoadDone>,
100 rom_load_task: Option<tokio::task::JoinHandle<()>>,
101 search_load_rx: tokio::sync::mpsc::UnboundedReceiver<SearchLoadDone>,
102 search_load_tx: tokio::sync::mpsc::UnboundedSender<SearchLoadDone>,
103 search_load_task: Option<tokio::task::JoinHandle<()>>,
104 cover_load_rx: tokio::sync::mpsc::UnboundedReceiver<CoverLoadDone>,
105 cover_load_tx: tokio::sync::mpsc::UnboundedSender<CoverLoadDone>,
106 cover_load_task: Option<tokio::task::JoinHandle<()>>,
107 library_scan_rx: Option<tokio::sync::mpsc::UnboundedReceiver<Result<(), String>>>,
109 library_scan_inflight: bool,
110 library_scan_pending_invalidate: Option<ScanCacheInvalidate>,
112 force_rom_reload_after_metadata: bool,
114 library_upload_inflight: bool,
116 library_upload_progress_rx: Option<tokio::sync::mpsc::UnboundedReceiver<(u64, u64)>>,
117 library_upload_done_rx:
118 Option<tokio::sync::mpsc::UnboundedReceiver<Result<LibraryUploadComplete, String>>>,
119 save_list_rx: tokio::sync::mpsc::UnboundedReceiver<SaveListDone>,
120 save_list_tx: tokio::sync::mpsc::UnboundedSender<SaveListDone>,
121 save_upload_rx: tokio::sync::mpsc::UnboundedReceiver<SaveUploadDone>,
122 save_upload_tx: tokio::sync::mpsc::UnboundedSender<SaveUploadDone>,
123 save_download_rx: tokio::sync::mpsc::UnboundedReceiver<SaveDownloadDone>,
124 save_download_tx: tokio::sync::mpsc::UnboundedSender<SaveDownloadDone>,
125 device_list_rx: tokio::sync::mpsc::UnboundedReceiver<DeviceListDone>,
126 device_list_tx: tokio::sync::mpsc::UnboundedSender<DeviceListDone>,
127 platform_list_rx: tokio::sync::mpsc::UnboundedReceiver<PlatformListDone>,
128 platform_list_tx: tokio::sync::mpsc::UnboundedSender<PlatformListDone>,
129 sync_push_pull_rx: tokio::sync::mpsc::UnboundedReceiver<SyncPushPullDone>,
130 sync_push_pull_tx: tokio::sync::mpsc::UnboundedSender<SyncPushPullDone>,
131 theme: Box<dyn Theme>,
132}
133
134impl App {
135 fn blocks_global_chord_shortcuts(&self) -> bool {
136 self.startup_splash.is_some()
137 || self.startup_update_prompt.is_some()
138 || self.global_error.is_some()
139 || self.global_notice.is_some()
140 }
141
142 fn blocks_global_d_shortcut(&self) -> bool {
143 let base = match &self.screen {
144 AppScreen::Search(_) | AppScreen::Settings(_) | AppScreen::SetupWizard(_) => true,
145 AppScreen::LibraryBrowse(lib) => {
146 lib.any_search_bar_open() || lib.any_upload_prompt_open()
147 }
148 _ => false,
149 };
150 base || self.library_upload_inflight || self.blocks_global_chord_shortcuts()
151 }
152
153 fn allows_global_question_help(&self) -> bool {
154 match &self.screen {
155 AppScreen::Search(_) | AppScreen::SetupWizard(_) => false,
156 AppScreen::LibraryBrowse(lib)
157 if lib.any_search_bar_open() || lib.any_upload_prompt_open() =>
158 {
159 false
160 }
161 AppScreen::Settings(s) if s.editing || s.path_picker.is_some() => false,
162 _ => true,
163 }
164 }
165
166 pub(crate) fn is_force_quit_key(key: &crossterm::event::KeyEvent) -> bool {
167 key.kind == KeyEventKind::Press
168 && key.modifiers.contains(KeyModifiers::CONTROL)
169 && matches!(key.code, KeyCode::Char('c') | KeyCode::Char('C'))
170 }
171 pub fn new(
173 client: RommClient,
174 config: Config,
175 save_sync_compat: SaveSyncCompatibility,
176 server_version: Option<String>,
177 startup_splash: Option<StartupSplash>,
178 startup_update: Option<UpdateStatus>,
179 ) -> Self {
180 let (prefetch_tx, prefetch_rx) = tokio::sync::mpsc::unbounded_channel();
181 let (rom_load_tx, rom_load_rx) = tokio::sync::mpsc::unbounded_channel();
182 let (search_load_tx, search_load_rx) = tokio::sync::mpsc::unbounded_channel();
183 let (cover_load_tx, cover_load_rx) = tokio::sync::mpsc::unbounded_channel();
184 let (save_list_tx, save_list_rx) = tokio::sync::mpsc::unbounded_channel();
185 let (save_upload_tx, save_upload_rx) = tokio::sync::mpsc::unbounded_channel();
186 let (save_download_tx, save_download_rx) = tokio::sync::mpsc::unbounded_channel();
187 let (device_list_tx, device_list_rx) = tokio::sync::mpsc::unbounded_channel();
188 let (platform_list_tx, platform_list_rx) = tokio::sync::mpsc::unbounded_channel();
189 let (sync_push_pull_tx, sync_push_pull_rx) = tokio::sync::mpsc::unbounded_channel();
190 let theme = resolve_theme_or_default(&config.theme);
191 Self {
192 screen: AppScreen::MainMenu(MainMenuScreen::new()),
193 client,
194 config,
195 server_version,
196 save_sync_compat,
197 rom_cache: RomCache::load(),
198 downloads: DownloadManager::new(),
199 screen_before_download: None,
200 deferred_load_roms: None,
201 startup_splash,
202 global_error: None,
203 global_notice: None,
204 show_keyboard_help: false,
205 startup_update_prompt: startup_update.map(|status| StartupUpdatePrompt {
206 status,
207 updating: false,
208 }),
209 library_metadata_rx: None,
210 library_metadata_refresh_gen: 0,
211 collection_prefetch_rx: prefetch_rx,
212 collection_prefetch_tx: prefetch_tx,
213 collection_prefetch_queue: VecDeque::new(),
214 collection_prefetch_queued_keys: HashSet::new(),
215 collection_prefetch_inflight_keys: HashSet::new(),
216 rom_load_gen: 0,
217 rom_load_rx,
218 rom_load_tx,
219 rom_load_task: None,
220 search_load_rx,
221 search_load_tx,
222 search_load_task: None,
223 cover_load_rx,
224 cover_load_tx,
225 cover_load_task: None,
226 library_scan_rx: None,
227 library_scan_inflight: false,
228 library_scan_pending_invalidate: None,
229 force_rom_reload_after_metadata: false,
230 library_upload_inflight: false,
231 library_upload_progress_rx: None,
232 library_upload_done_rx: None,
233 save_list_rx,
234 save_list_tx,
235 save_upload_rx,
236 save_upload_tx,
237 save_download_rx,
238 save_download_tx,
239 device_list_rx,
240 device_list_tx,
241 platform_list_rx,
242 platform_list_tx,
243 sync_push_pull_rx,
244 sync_push_pull_tx,
245 theme,
246 }
247 }
248 pub fn set_error(&mut self, err: crate::error::RommError) {
249 self.global_error = Some(crate::error::user_message(&err));
250 }
251
252 pub(in crate::tui::app) fn apply_saved_theme(&mut self) {
254 self.theme = resolve_theme_or_default(&self.config.theme);
255 }
256
257 #[cfg(test)]
258 pub(crate) fn theme_id(&self) -> &str {
259 self.theme.id()
260 }
261 pub async fn handle_key_event(&mut self, key: &KeyEvent) -> Result<bool> {
263 for action in event::map_key_to_actions(self, key) {
264 if self.update(action).await? {
265 return Ok(true);
266 }
267 }
268 Ok(false)
269 }
270}