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