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, SearchScreen,
37 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 LibraryBrowse(Box<LibraryBrowseScreen>),
55 Search(SearchScreen),
56 Settings(Box<SettingsScreen>),
57 GameDetail(Box<GameDetailScreen>),
58 ExtrasPicker(Box<ExtrasPickerScreen>),
59 Download(DownloadScreen),
60 SetupWizard(Box<crate::tui::screens::setup_wizard::SetupWizard>),
61}
62
63pub struct App {
68 pub screen: AppScreen,
69 client: RommClient,
70 config: Config,
71 server_version: Option<String>,
73 save_sync_compat: SaveSyncCompatibility,
74 rom_cache: RomCache,
75 downloads: DownloadManager,
76 screen_before_download: Option<AppScreen>,
78 screen_before_search: Option<AppScreen>,
80 screen_before_settings: Option<AppScreen>,
82 deferred_load_roms: Option<DeferredLoadRoms>,
84 startup_splash: Option<StartupSplash>,
86 pub global_error: Option<String>,
87 pub global_notice: Option<String>,
88 show_keyboard_help: bool,
89 startup_update_prompt: Option<StartupUpdatePrompt>,
90 library_metadata_rx: Option<tokio::sync::mpsc::UnboundedReceiver<LibraryMetadataRefreshDone>>,
92 library_metadata_refresh_gen: u64,
94 collection_prefetch_rx: tokio::sync::mpsc::UnboundedReceiver<CollectionPrefetchDone>,
95 collection_prefetch_tx: tokio::sync::mpsc::UnboundedSender<CollectionPrefetchDone>,
96 collection_prefetch_queue: VecDeque<(RomCacheKey, GetRoms, u64)>,
97 collection_prefetch_queued_keys: HashSet<RomCacheKey>,
98 collection_prefetch_inflight_keys: HashSet<RomCacheKey>,
99 rom_load_gen: u64,
101 rom_load_rx: tokio::sync::mpsc::UnboundedReceiver<RomLoadDone>,
102 rom_load_tx: tokio::sync::mpsc::UnboundedSender<RomLoadDone>,
103 rom_load_task: Option<tokio::task::JoinHandle<()>>,
104 search_load_rx: tokio::sync::mpsc::UnboundedReceiver<SearchLoadDone>,
105 search_load_tx: tokio::sync::mpsc::UnboundedSender<SearchLoadDone>,
106 search_load_task: Option<tokio::task::JoinHandle<()>>,
107 cover_load_rx: tokio::sync::mpsc::UnboundedReceiver<CoverLoadDone>,
108 cover_load_tx: tokio::sync::mpsc::UnboundedSender<CoverLoadDone>,
109 cover_load_task: Option<tokio::task::JoinHandle<()>>,
110 library_scan_rx: Option<tokio::sync::mpsc::UnboundedReceiver<Result<(), String>>>,
112 library_scan_inflight: bool,
113 library_scan_pending_invalidate: Option<ScanCacheInvalidate>,
115 force_rom_reload_after_metadata: bool,
117 library_upload_inflight: bool,
119 library_upload_progress_rx: Option<tokio::sync::mpsc::UnboundedReceiver<(u64, u64)>>,
120 library_upload_done_rx:
121 Option<tokio::sync::mpsc::UnboundedReceiver<Result<LibraryUploadComplete, String>>>,
122 save_list_rx: tokio::sync::mpsc::UnboundedReceiver<SaveListDone>,
123 save_list_tx: tokio::sync::mpsc::UnboundedSender<SaveListDone>,
124 save_upload_rx: tokio::sync::mpsc::UnboundedReceiver<SaveUploadDone>,
125 save_upload_tx: tokio::sync::mpsc::UnboundedSender<SaveUploadDone>,
126 save_download_rx: tokio::sync::mpsc::UnboundedReceiver<SaveDownloadDone>,
127 save_download_tx: tokio::sync::mpsc::UnboundedSender<SaveDownloadDone>,
128 device_list_rx: tokio::sync::mpsc::UnboundedReceiver<DeviceListDone>,
129 device_list_tx: tokio::sync::mpsc::UnboundedSender<DeviceListDone>,
130 platform_list_rx: tokio::sync::mpsc::UnboundedReceiver<PlatformListDone>,
131 platform_list_tx: tokio::sync::mpsc::UnboundedSender<PlatformListDone>,
132 sync_push_pull_rx: tokio::sync::mpsc::UnboundedReceiver<SyncPushPullDone>,
133 sync_push_pull_tx: tokio::sync::mpsc::UnboundedSender<SyncPushPullDone>,
134 theme: Box<dyn Theme>,
135}
136
137impl App {
138 fn blocks_global_chord_shortcuts(&self) -> bool {
139 self.startup_splash.is_some()
140 || self.startup_update_prompt.is_some()
141 || self.global_error.is_some()
142 || self.global_notice.is_some()
143 || self.typing_in_text_field()
144 }
145
146 fn typing_in_text_field(&self) -> bool {
148 match &self.screen {
149 AppScreen::Search(_) | AppScreen::SetupWizard(_) => true,
150 AppScreen::Settings(s) => {
151 s.editing
152 || s.path_picker.is_some()
153 || s.console_path_picker.is_some()
154 || s.console_picker_open
155 || s.device_picker_open
156 || s.confirm.is_some()
157 }
158 AppScreen::LibraryBrowse(lib) => lib.any_upload_prompt_open(),
159 _ => false,
160 }
161 }
162
163 fn blocks_global_d_shortcut(&self) -> bool {
164 self.typing_in_text_field()
165 || self.library_upload_inflight
166 || self.startup_splash.is_some()
167 || self.startup_update_prompt.is_some()
168 || self.global_error.is_some()
169 || self.global_notice.is_some()
170 }
171
172 fn blocks_global_slash_shortcut(&self) -> bool {
173 self.typing_in_text_field()
174 || self.startup_splash.is_some()
175 || self.startup_update_prompt.is_some()
176 || self.global_error.is_some()
177 || self.global_notice.is_some()
178 }
179
180 fn blocks_global_comma_shortcut(&self) -> bool {
181 self.typing_in_text_field()
182 || self.startup_splash.is_some()
183 || self.startup_update_prompt.is_some()
184 || self.global_error.is_some()
185 || self.global_notice.is_some()
186 }
187
188 fn allows_global_question_help(&self) -> bool {
189 !self.typing_in_text_field()
190 && self.startup_splash.is_none()
191 && self.startup_update_prompt.is_none()
192 }
193
194 pub(crate) fn is_force_quit_key(key: &crossterm::event::KeyEvent) -> bool {
195 key.kind == KeyEventKind::Press
196 && key.modifiers.contains(KeyModifiers::CONTROL)
197 && matches!(key.code, KeyCode::Char('c') | KeyCode::Char('C'))
198 }
199 pub fn new(
201 client: RommClient,
202 config: Config,
203 save_sync_compat: SaveSyncCompatibility,
204 server_version: Option<String>,
205 startup_splash: Option<StartupSplash>,
206 startup_update: Option<UpdateStatus>,
207 ) -> Self {
208 let (prefetch_tx, prefetch_rx) = tokio::sync::mpsc::unbounded_channel();
209 let (rom_load_tx, rom_load_rx) = tokio::sync::mpsc::unbounded_channel();
210 let (search_load_tx, search_load_rx) = tokio::sync::mpsc::unbounded_channel();
211 let (cover_load_tx, cover_load_rx) = tokio::sync::mpsc::unbounded_channel();
212 let (save_list_tx, save_list_rx) = tokio::sync::mpsc::unbounded_channel();
213 let (save_upload_tx, save_upload_rx) = tokio::sync::mpsc::unbounded_channel();
214 let (save_download_tx, save_download_rx) = tokio::sync::mpsc::unbounded_channel();
215 let (device_list_tx, device_list_rx) = tokio::sync::mpsc::unbounded_channel();
216 let (platform_list_tx, platform_list_rx) = tokio::sync::mpsc::unbounded_channel();
217 let (sync_push_pull_tx, sync_push_pull_rx) = tokio::sync::mpsc::unbounded_channel();
218 let theme = resolve_theme_or_default(&config.theme);
219 Self {
220 screen: AppScreen::LibraryBrowse(Box::new(LibraryBrowseScreen::new(
221 Vec::new(),
222 Vec::new(),
223 config.tui_layout.library_left_panel_percent,
224 ))),
225 client,
226 config,
227 server_version,
228 save_sync_compat,
229 rom_cache: RomCache::load(),
230 downloads: DownloadManager::new(),
231 screen_before_download: None,
232 screen_before_search: None,
233 screen_before_settings: None,
234 deferred_load_roms: None,
235 startup_splash,
236 global_error: None,
237 global_notice: None,
238 show_keyboard_help: false,
239 startup_update_prompt: startup_update.map(|status| StartupUpdatePrompt {
240 status,
241 updating: false,
242 }),
243 library_metadata_rx: None,
244 library_metadata_refresh_gen: 0,
245 collection_prefetch_rx: prefetch_rx,
246 collection_prefetch_tx: prefetch_tx,
247 collection_prefetch_queue: VecDeque::new(),
248 collection_prefetch_queued_keys: HashSet::new(),
249 collection_prefetch_inflight_keys: HashSet::new(),
250 rom_load_gen: 0,
251 rom_load_rx,
252 rom_load_tx,
253 rom_load_task: None,
254 search_load_rx,
255 search_load_tx,
256 search_load_task: None,
257 cover_load_rx,
258 cover_load_tx,
259 cover_load_task: None,
260 library_scan_rx: None,
261 library_scan_inflight: false,
262 library_scan_pending_invalidate: None,
263 force_rom_reload_after_metadata: false,
264 library_upload_inflight: false,
265 library_upload_progress_rx: None,
266 library_upload_done_rx: None,
267 save_list_rx,
268 save_list_tx,
269 save_upload_rx,
270 save_upload_tx,
271 save_download_rx,
272 save_download_tx,
273 device_list_rx,
274 device_list_tx,
275 platform_list_rx,
276 platform_list_tx,
277 sync_push_pull_rx,
278 sync_push_pull_tx,
279 theme,
280 }
281 }
282 pub fn set_error(&mut self, err: crate::error::RommError) {
283 self.global_error = Some(crate::error::user_message(&err));
284 }
285
286 pub(in crate::tui::app) fn apply_saved_theme(&mut self) {
288 self.theme = resolve_theme_or_default(&self.config.theme);
289 }
290
291 pub(in crate::tui::app) fn persist_tui_layout(&self) {
292 if let Err(e) = crate::config::persist_user_config(&self.config) {
293 tracing::warn!("failed to persist TUI panel layout: {e:#}");
294 }
295 }
296
297 #[cfg(test)]
298 pub(crate) fn theme_id(&self) -> &str {
299 self.theme.id()
300 }
301 pub async fn handle_key_event(&mut self, key: &KeyEvent) -> Result<bool> {
303 for action in event::map_key_to_actions(self, key) {
304 if self.update(action).await? {
305 return Ok(true);
306 }
307 }
308 Ok(false)
309 }
310}