1use anyhow::Result;
4use crossterm::event::{
5 self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers,
6};
7use crossterm::execute;
8use crossterm::terminal::{
9 disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
10};
11use ratatui::backend::CrosstermBackend;
12use ratatui::Terminal;
13use std::time::Duration;
14
15use crate::commands::library_scan::ScanCacheInvalidate;
16use crate::types::RomList;
17
18use super::background::types::{RomLoadDone, RomLoadEvent};
19use super::AppScreen;
20
21impl super::App {
22 pub async fn run(&mut self) -> Result<()> {
23 enable_raw_mode()?;
24 let mut stdout = std::io::stdout();
25 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
26 let backend = CrosstermBackend::new(stdout);
27 let mut terminal = Terminal::new(backend)?;
28
29 loop {
30 self.poll_background_tasks();
31 if self
32 .startup_splash
33 .as_ref()
34 .is_some_and(|s| s.should_auto_dismiss())
35 {
36 self.startup_splash = None;
37 }
38 terminal.draw(|f| self.render(f))?;
41
42 if let Some(ref mut prompt) = self.startup_update_prompt {
44 if prompt.updating {
45 if prompt.status.latest_version == "9.9.9-mock" {
47 tokio::time::sleep(std::time::Duration::from_secs(2)).await; self.global_notice =
49 Some("Mock update successful! (No files were changed)".into());
50 self.startup_update_prompt = None;
51 } else {
52 let options = crate::update::ApplyUpdateOptions {
53 show_progress: false,
54 show_output: false,
55 no_confirm: true,
56 target_version_tag: Some(prompt.status.release_tag.clone()),
57 };
58 match crate::update::apply_update(None, options).await {
59 Ok(crate::update::ApplyUpdateOutcome::Updated(version)) => {
60 self.global_notice = Some(format!(
61 "Updated to {version}. Restart romm-cli to use the new version."
62 ));
63 }
64 Ok(crate::update::ApplyUpdateOutcome::UpToDate(version)) => {
65 self.global_notice =
66 Some(format!("Already up to date (`{version}`)."));
67 }
68 Err(err) => {
69 self.global_error = Some(format!("Update failed: {err:#}"));
70 }
71 }
72 self.startup_update_prompt = None;
73 }
74 continue;
75 }
76 }
77
78 if event::poll(Duration::from_millis(100))? {
81 if let Event::Key(key_event) = event::read()? {
82 if Self::is_force_quit_key(&key_event) {
83 break;
84 }
85 if key_event.kind == KeyEventKind::Press
86 && key_event.modifiers.contains(KeyModifiers::CONTROL)
87 && matches!(key_event.code, KeyCode::Char('r') | KeyCode::Char('R'))
88 && !self.blocks_global_chord_shortcuts()
89 {
90 if let AppScreen::LibraryBrowse(ref lib) = self.screen {
91 if !lib.any_search_bar_open()
92 && !lib.any_upload_prompt_open()
93 && !self.library_upload_inflight
94 && !self.library_scan_inflight
95 {
96 self.spawn_library_rescan_worker(ScanCacheInvalidate::AllPlatforms);
97 }
98 }
99 continue;
100 }
101 if key_event.kind == KeyEventKind::Press
102 && key_event.modifiers.contains(KeyModifiers::CONTROL)
103 && matches!(key_event.code, KeyCode::Char('u') | KeyCode::Char('U'))
104 && !self.blocks_global_chord_shortcuts()
105 {
106 if let AppScreen::LibraryBrowse(ref mut lib) = self.screen {
107 if lib.any_upload_prompt_open() {
108 lib.close_upload_prompt();
109 } else if !lib.any_search_bar_open()
110 && !self.library_upload_inflight
111 && !self.library_scan_inflight
112 {
113 if lib.subsection
114 == crate::tui::screens::library_browse::LibrarySubsection::ByConsole
115 {
116 lib.open_upload_prompt();
117 } else {
118 lib.set_metadata_footer(Some(
119 "Upload requires Consoles view — press t".into(),
120 ));
121 }
122 }
123 }
124 continue;
125 }
126 if key_event.kind == KeyEventKind::Press
127 && self.handle_key_event(&key_event).await?
128 {
129 break;
130 }
131 }
132 }
133
134 if let Some((key, req, expected, context, started)) = self.deferred_load_roms.take() {
138 if let Some(ref k) = key {
140 if let Some(cached) = self.rom_cache.get_valid(k, expected) {
141 if let AppScreen::LibraryBrowse(ref mut lib) = self.screen {
142 if crate::tui::app::rom_load::primary_rom_load_result_matches_selection(
143 lib, &key,
144 ) {
145 lib.set_roms(cached.clone());
146 lib.set_rom_loading(false);
147 tracing::debug!(
148 "rom-list-render context={} latency_ms={} (cache_hit)",
149 context,
150 started.elapsed().as_millis()
151 );
152 } else {
153 lib.set_rom_loading(false);
154 tracing::debug!(
155 "rom-list-render context={} skipped stale cache hit",
156 context
157 );
158 }
159 }
160 continue;
161 }
162 }
163
164 if started.elapsed() < std::time::Duration::from_millis(250) {
166 self.deferred_load_roms = Some((key, req, expected, context, started));
168 continue;
169 }
170
171 let gen = self.rom_load_gen;
172 if let AppScreen::LibraryBrowse(ref mut lib) = self.screen {
173 lib.set_rom_loading(expected > 0);
174 }
175 if expected == 0 {
176 if let AppScreen::LibraryBrowse(ref mut lib) = self.screen {
177 lib.set_rom_loading(false);
178 }
179 continue;
180 }
181
182 let Some(r) = req else {
183 if let AppScreen::LibraryBrowse(ref mut lib) = self.screen {
184 lib.set_rom_loading(false);
185 }
186 continue;
187 };
188 let client = self.client.clone();
189 let tx = self.rom_load_tx.clone();
190
191 self.rom_load_task = Some(tokio::spawn(async move {
192 let mut req = r;
193 let mut aggregated: Option<RomList> = None;
194
195 loop {
196 match client.call(&req).await {
197 Ok(mut batch) => {
198 if let Some(ref mut all) = aggregated {
199 if batch.items.is_empty() {
200 break;
201 }
202 all.items.append(&mut batch.items);
203 let _ = tx.send(RomLoadDone {
204 gen,
205 key: key.clone(),
206 expected,
207 event: RomLoadEvent::Batch(all.clone()),
208 context,
209 started,
210 });
211 if all.items.len() as u64 >= all.total {
212 break;
213 }
214 req.offset = Some(all.items.len() as u32);
215 } else {
216 let loaded = batch.items.len() as u64;
217 let total = batch.total;
218 let _ = tx.send(RomLoadDone {
219 gen,
220 key: key.clone(),
221 expected,
222 event: RomLoadEvent::Batch(batch.clone()),
223 context,
224 started,
225 });
226 req.offset = Some(loaded as u32);
227 aggregated = Some(batch);
228 if loaded >= total {
229 break;
230 }
231 }
232 }
233 Err(e) => {
234 let _ = tx.send(RomLoadDone {
235 gen,
236 key: key.clone(),
237 expected,
238 event: RomLoadEvent::Failed(format!("{e:#}")),
239 context,
240 started,
241 });
242 return;
243 }
244 }
245 if let Some(ref all) = aggregated {
247 if all.items.len() >= 20000 {
248 break;
249 }
250 }
251 }
252
253 let _ = tx.send(RomLoadDone {
254 gen,
255 key,
256 expected,
257 event: RomLoadEvent::Complete,
258 context,
259 started,
260 });
261 }));
262 }
263 }
264
265 disable_raw_mode()?;
266 execute!(
267 terminal.backend_mut(),
268 LeaveAlternateScreen,
269 DisableMouseCapture
270 )?;
271 terminal.show_cursor()?;
272 Ok(())
273 }
274}