1use anyhow::Result;
14use crossterm::event::{
15 self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind,
16};
17use crossterm::execute;
18use crossterm::terminal::{
19 disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
20};
21use ratatui::backend::CrosstermBackend;
22use ratatui::style::Color;
23use ratatui::Terminal;
24use std::time::Duration;
25
26use crate::client::RommClient;
27use crate::config::Config;
28use crate::core::cache::{RomCache, RomCacheKey};
29use crate::core::download::DownloadManager;
30use crate::endpoints::{collections::ListCollections, platforms::ListPlatforms, roms::GetRoms};
31use crate::types::RomList;
32
33use super::openapi::{resolve_path_template, EndpointRegistry};
34use super::screens::connected_splash::{self, StartupSplash};
35use super::screens::{
36 BrowseScreen, DownloadScreen, ExecuteScreen, GameDetailPrevious, GameDetailScreen,
37 LibraryBrowseScreen, MainMenuScreen, ResultDetailScreen, ResultScreen, SearchScreen,
38 SettingsScreen,
39};
40
41pub enum AppScreen {
50 MainMenu(MainMenuScreen),
51 LibraryBrowse(LibraryBrowseScreen),
52 Search(SearchScreen),
53 Settings(SettingsScreen),
54 Browse(BrowseScreen),
55 Execute(ExecuteScreen),
56 Result(ResultScreen),
57 ResultDetail(ResultDetailScreen),
58 GameDetail(Box<GameDetailScreen>),
59 Download(DownloadScreen),
60}
61
62pub struct App {
71 screen: AppScreen,
72 client: RommClient,
73 config: Config,
74 registry: EndpointRegistry,
75 server_version: Option<String>,
77 rom_cache: RomCache,
78 downloads: DownloadManager,
79 screen_before_download: Option<AppScreen>,
81 deferred_load_roms: Option<(Option<RomCacheKey>, Option<GetRoms>, u64)>,
83 startup_splash: Option<StartupSplash>,
85}
86
87impl App {
88 pub fn new(
90 client: RommClient,
91 config: Config,
92 registry: EndpointRegistry,
93 server_version: Option<String>,
94 startup_splash: Option<StartupSplash>,
95 ) -> Self {
96 Self {
97 screen: AppScreen::MainMenu(MainMenuScreen::new()),
98 client,
99 config,
100 registry,
101 server_version,
102 rom_cache: RomCache::load(),
103 downloads: DownloadManager::new(),
104 screen_before_download: None,
105 deferred_load_roms: None,
106 startup_splash,
107 }
108 }
109
110 pub async fn run(&mut self) -> Result<()> {
120 enable_raw_mode()?;
121 let mut stdout = std::io::stdout();
122 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
123 let backend = CrosstermBackend::new(stdout);
124 let mut terminal = Terminal::new(backend)?;
125
126 loop {
127 if self
128 .startup_splash
129 .as_ref()
130 .is_some_and(|s| s.should_auto_dismiss())
131 {
132 self.startup_splash = None;
133 }
134 terminal.draw(|f| self.render(f))?;
137
138 if event::poll(Duration::from_millis(100))? {
141 if let Event::Key(key) = event::read()? {
142 if key.kind == KeyEventKind::Press && self.handle_key(key.code).await? {
143 break;
144 }
145 }
146 }
147
148 if let Some((key, req, expected)) = self.deferred_load_roms.take() {
153 if let Ok(Some(roms)) = self.load_roms_cached(key, req, expected).await {
154 if let AppScreen::LibraryBrowse(ref mut lib) = self.screen {
155 lib.set_roms(roms);
156 }
157 }
158 }
159 }
160
161 disable_raw_mode()?;
162 execute!(
163 terminal.backend_mut(),
164 LeaveAlternateScreen,
165 DisableMouseCapture
166 )?;
167 terminal.show_cursor()?;
168 Ok(())
169 }
170
171 async fn load_roms_cached(
178 &mut self,
179 key: Option<RomCacheKey>,
180 req: Option<GetRoms>,
181 expected_count: u64,
182 ) -> Result<Option<RomList>> {
183 if let Some(k) = key {
185 if let Some(cached) = self.rom_cache.get_valid(&k, expected_count) {
186 return Ok(Some(cached.clone()));
187 }
188 }
189 if let Some(r) = req {
191 let mut roms = self.client.call(&r).await?;
192 let total = roms.total;
193 let ceiling = 20000;
194
195 while (roms.items.len() as u64) < total && (roms.items.len() as u64) < ceiling {
198 let mut next_req = r.clone();
199 next_req.offset = Some(roms.items.len() as u32);
200
201 let next_batch = self.client.call(&next_req).await?;
202 if next_batch.items.is_empty() {
203 break;
204 }
205 roms.items.extend(next_batch.items);
206 }
207
208 if let Some(k) = key {
209 self.rom_cache.insert(k, roms.clone(), expected_count); }
211 return Ok(Some(roms));
212 }
213 Ok(None)
214 }
215
216 async fn handle_key(&mut self, key: KeyCode) -> Result<bool> {
221 if self.startup_splash.is_some() {
222 self.startup_splash = None;
223 return Ok(false);
224 }
225
226 if key == KeyCode::Char('d') && !matches!(&self.screen, AppScreen::Search(_)) {
228 self.toggle_download_screen();
229 return Ok(false);
230 }
231
232 match &self.screen {
233 AppScreen::MainMenu(_) => self.handle_main_menu(key).await,
234 AppScreen::LibraryBrowse(_) => self.handle_library_browse(key).await,
235 AppScreen::Search(_) => self.handle_search(key).await,
236 AppScreen::Settings(_) => self.handle_settings(key),
237 AppScreen::Browse(_) => self.handle_browse(key),
238 AppScreen::Execute(_) => self.handle_execute(key).await,
239 AppScreen::Result(_) => self.handle_result(key),
240 AppScreen::ResultDetail(_) => self.handle_result_detail(key),
241 AppScreen::GameDetail(_) => self.handle_game_detail(key),
242 AppScreen::Download(_) => self.handle_download(key),
243 }
244 }
245
246 fn toggle_download_screen(&mut self) {
249 let current =
250 std::mem::replace(&mut self.screen, AppScreen::MainMenu(MainMenuScreen::new()));
251 match current {
252 AppScreen::Download(_) => {
253 self.screen = self
254 .screen_before_download
255 .take()
256 .unwrap_or_else(|| AppScreen::MainMenu(MainMenuScreen::new()));
257 }
258 other => {
259 self.screen_before_download = Some(other);
260 self.screen = AppScreen::Download(DownloadScreen::new(self.downloads.shared()));
261 }
262 }
263 }
264
265 fn handle_download(&mut self, key: KeyCode) -> Result<bool> {
266 if key == KeyCode::Esc || key == KeyCode::Char('d') {
267 self.screen = self
268 .screen_before_download
269 .take()
270 .unwrap_or_else(|| AppScreen::MainMenu(MainMenuScreen::new()));
271 }
272 Ok(false)
273 }
274
275 async fn handle_main_menu(&mut self, key: KeyCode) -> Result<bool> {
278 let menu = match &mut self.screen {
279 AppScreen::MainMenu(m) => m,
280 _ => return Ok(false),
281 };
282 match key {
283 KeyCode::Up | KeyCode::Char('k') => menu.previous(),
284 KeyCode::Down | KeyCode::Char('j') => menu.next(),
285 KeyCode::Enter => match menu.selected {
286 0 => {
287 let platforms = self.client.call(&ListPlatforms).await?;
288 let collections = self.client.call(&ListCollections).await.unwrap_or_default();
289 let mut lib = LibraryBrowseScreen::new(platforms, collections);
290 if lib.list_len() > 0 {
291 let key = lib.cache_key();
292 let expected = lib.expected_rom_count();
293 let req = lib
294 .get_roms_request_platform()
295 .or_else(|| lib.get_roms_request_collection());
296 if let Ok(Some(roms)) = self.load_roms_cached(key, req, expected).await {
297 lib.set_roms(roms);
298 }
299 }
300 self.screen = AppScreen::LibraryBrowse(lib);
301 }
302 1 => self.screen = AppScreen::Search(SearchScreen::new()),
303 2 => {
304 self.screen_before_download = Some(AppScreen::MainMenu(MainMenuScreen::new()));
305 self.screen = AppScreen::Download(DownloadScreen::new(self.downloads.shared()));
306 }
307 3 => {
308 self.screen = AppScreen::Settings(SettingsScreen::new(
309 &self.config,
310 self.server_version.as_deref(),
311 ))
312 }
313 4 => self.screen = AppScreen::Browse(BrowseScreen::new(self.registry.clone())),
314 5 => return Ok(true),
315 _ => {}
316 },
317 KeyCode::Esc | KeyCode::Char('q') => return Ok(true),
318 _ => {}
319 }
320 Ok(false)
321 }
322
323 async fn handle_library_browse(&mut self, key: KeyCode) -> Result<bool> {
326 use super::screens::library_browse::{LibrarySearchMode, LibraryViewMode};
327
328 let lib = match &mut self.screen {
329 AppScreen::LibraryBrowse(l) => l,
330 _ => return Ok(false),
331 };
332
333 if let Some(mode) = lib.search_mode {
335 match key {
336 KeyCode::Esc => lib.clear_search(),
337 KeyCode::Backspace => lib.delete_search_char(),
338 KeyCode::Char(c) => lib.add_search_char(c),
339 KeyCode::Tab if mode == LibrarySearchMode::Jump => lib.jump_to_match(true),
340 KeyCode::Enter => lib.search_mode = None, _ => {}
342 }
343 return Ok(false);
344 }
345
346 match key {
347 KeyCode::Up | KeyCode::Char('k') => {
348 if lib.view_mode == LibraryViewMode::List {
349 lib.list_previous();
350 if lib.list_len() > 0 {
351 lib.clear_roms(); let key = lib.cache_key();
353 let expected = lib.expected_rom_count();
354 let req = lib
355 .get_roms_request_platform()
356 .or_else(|| lib.get_roms_request_collection());
357 self.deferred_load_roms = Some((key, req, expected));
358 }
359 } else {
360 lib.rom_previous();
361 }
362 }
363 KeyCode::Down | KeyCode::Char('j') => {
364 if lib.view_mode == LibraryViewMode::List {
365 lib.list_next();
366 if lib.list_len() > 0 {
367 lib.clear_roms(); let key = lib.cache_key();
369 let expected = lib.expected_rom_count();
370 let req = lib
371 .get_roms_request_platform()
372 .or_else(|| lib.get_roms_request_collection());
373 self.deferred_load_roms = Some((key, req, expected));
374 }
375 } else {
376 lib.rom_next();
377 }
378 }
379 KeyCode::Left | KeyCode::Char('h') => {
380 if lib.view_mode == LibraryViewMode::Roms {
381 lib.back_to_list();
382 }
383 }
384 KeyCode::Right | KeyCode::Char('l') => lib.switch_view(),
385 KeyCode::Tab => {
386 if lib.view_mode == LibraryViewMode::List {
387 lib.switch_view();
388 } else {
389 lib.switch_view(); }
391 }
392 KeyCode::Char('/') if lib.view_mode == LibraryViewMode::Roms => {
393 lib.enter_search(LibrarySearchMode::Filter);
394 }
395 KeyCode::Char('f') if lib.view_mode == LibraryViewMode::Roms => {
396 lib.enter_search(LibrarySearchMode::Jump);
397 }
398 KeyCode::Enter => {
399 if lib.view_mode == LibraryViewMode::List {
400 lib.switch_view();
401 } else if let Some((primary, others)) = lib.get_selected_group() {
402 let lib_screen = std::mem::replace(
403 &mut self.screen,
404 AppScreen::MainMenu(MainMenuScreen::new()),
405 );
406 if let AppScreen::LibraryBrowse(l) = lib_screen {
407 self.screen = AppScreen::GameDetail(Box::new(GameDetailScreen::new(
408 primary,
409 others,
410 GameDetailPrevious::Library(l),
411 self.downloads.shared(),
412 )));
413 }
414 }
415 }
416 KeyCode::Char('t') => lib.switch_subsection(),
417 KeyCode::Esc => {
418 if lib.view_mode == LibraryViewMode::Roms {
419 lib.back_to_list();
420 } else {
421 self.screen = AppScreen::MainMenu(MainMenuScreen::new());
422 }
423 }
424 KeyCode::Char('q') => return Ok(true),
425 _ => {}
426 }
427 Ok(false)
428 }
429
430 async fn handle_search(&mut self, key: KeyCode) -> Result<bool> {
433 let search = match &mut self.screen {
434 AppScreen::Search(s) => s,
435 _ => return Ok(false),
436 };
437 match key {
438 KeyCode::Backspace => search.delete_char(),
439 KeyCode::Left => search.cursor_left(),
440 KeyCode::Right => search.cursor_right(),
441 KeyCode::Up => search.previous(),
442 KeyCode::Down => search.next(),
443 KeyCode::Char(c) => search.add_char(c),
444 KeyCode::Enter => {
445 if search.result_groups.is_some() {
446 if let Some((primary, others)) = search.get_selected_group() {
447 let prev = std::mem::replace(
448 &mut self.screen,
449 AppScreen::MainMenu(MainMenuScreen::new()),
450 );
451 if let AppScreen::Search(s) = prev {
452 self.screen = AppScreen::GameDetail(Box::new(GameDetailScreen::new(
453 primary,
454 others,
455 GameDetailPrevious::Search(s),
456 self.downloads.shared(),
457 )));
458 }
459 }
460 } else if !search.query.is_empty() {
461 let req = GetRoms {
462 search_term: Some(search.query.clone()),
463 limit: Some(50),
464 ..Default::default()
465 };
466 if let Ok(roms) = self.client.call(&req).await {
467 search.set_results(roms);
468 }
469 }
470 }
471 KeyCode::Esc => {
472 if search.results.is_some() {
473 search.clear_results();
474 } else {
475 self.screen = AppScreen::MainMenu(MainMenuScreen::new());
476 }
477 }
478 _ => {}
479 }
480 Ok(false)
481 }
482
483 fn handle_settings(&mut self, key: KeyCode) -> Result<bool> {
486 let settings = match &mut self.screen {
487 AppScreen::Settings(s) => s,
488 _ => return Ok(false),
489 };
490
491 if settings.editing {
492 match key {
493 KeyCode::Enter => {
494 settings.save_edit();
495 }
496 KeyCode::Esc => settings.cancel_edit(),
497 KeyCode::Backspace => settings.delete_char(),
498 KeyCode::Left => settings.move_cursor_left(),
499 KeyCode::Right => settings.move_cursor_right(),
500 KeyCode::Char(c) => settings.add_char(c),
501 _ => {}
502 }
503 return Ok(false);
504 }
505
506 match key {
507 KeyCode::Up | KeyCode::Char('k') => settings.previous(),
508 KeyCode::Down | KeyCode::Char('j') => settings.next(),
509 KeyCode::Enter => settings.enter_edit(),
510 KeyCode::Char('s') => {
511 use crate::config::persist_user_config;
513 if let Err(e) = persist_user_config(
514 &settings.base_url,
515 &settings.download_dir,
516 settings.use_https,
517 self.config.auth.clone(),
518 ) {
519 settings.message = Some((format!("Error saving: {e}"), Color::Red));
520 } else {
521 settings.message = Some(("Saved to .env".to_string(), Color::Green));
522 self.config.base_url = settings.base_url.clone();
524 self.config.download_dir = settings.download_dir.clone();
525 self.config.use_https = settings.use_https;
526 if let Ok(new_client) = RommClient::new(&self.config, self.client.verbose()) {
528 self.client = new_client;
529 }
530 }
531 }
532 KeyCode::Esc => self.screen = AppScreen::MainMenu(MainMenuScreen::new()),
533 KeyCode::Char('q') => return Ok(true),
534 _ => {}
535 }
536 Ok(false)
537 }
538
539 fn handle_browse(&mut self, key: KeyCode) -> Result<bool> {
542 use super::screens::browse::ViewMode;
543
544 let browse = match &mut self.screen {
545 AppScreen::Browse(b) => b,
546 _ => return Ok(false),
547 };
548 match key {
549 KeyCode::Up | KeyCode::Char('k') => browse.previous(),
550 KeyCode::Down | KeyCode::Char('j') => browse.next(),
551 KeyCode::Left | KeyCode::Char('h') => {
552 if browse.view_mode == ViewMode::Endpoints {
553 browse.switch_view();
554 }
555 }
556 KeyCode::Right | KeyCode::Char('l') => {
557 if browse.view_mode == ViewMode::Sections {
558 browse.switch_view();
559 }
560 }
561 KeyCode::Tab => browse.switch_view(),
562 KeyCode::Enter => {
563 if browse.view_mode == ViewMode::Endpoints {
564 if let Some(ep) = browse.get_selected_endpoint() {
565 self.screen = AppScreen::Execute(ExecuteScreen::new(ep.clone()));
566 }
567 } else {
568 browse.switch_view();
569 }
570 }
571 KeyCode::Esc => self.screen = AppScreen::MainMenu(MainMenuScreen::new()),
572 _ => {}
573 }
574 Ok(false)
575 }
576
577 async fn handle_execute(&mut self, key: KeyCode) -> Result<bool> {
580 let execute = match &mut self.screen {
581 AppScreen::Execute(e) => e,
582 _ => return Ok(false),
583 };
584 match key {
585 KeyCode::Tab => execute.next_field(),
586 KeyCode::BackTab => execute.previous_field(),
587 KeyCode::Char(c) => execute.add_char_to_focused(c),
588 KeyCode::Backspace => execute.delete_char_from_focused(),
589 KeyCode::Enter => {
590 let endpoint = execute.endpoint.clone();
591 let query = execute.get_query_params();
592 let body = if endpoint.has_body && !execute.body_text.is_empty() {
593 Some(serde_json::from_str(&execute.body_text)?)
594 } else {
595 None
596 };
597 let resolved_path =
598 match resolve_path_template(&endpoint.path, &execute.get_path_params()) {
599 Ok(p) => p,
600 Err(e) => {
601 self.screen = AppScreen::Result(ResultScreen::new(
602 serde_json::json!({ "error": format!("{e}") }),
603 None,
604 None,
605 ));
606 return Ok(false);
607 }
608 };
609 match self
610 .client
611 .request_json(&endpoint.method, &resolved_path, &query, body)
612 .await
613 {
614 Ok(result) => {
615 self.screen = AppScreen::Result(ResultScreen::new(
616 result,
617 Some(&endpoint.method),
618 Some(resolved_path.as_str()),
619 ));
620 }
621 Err(e) => {
622 self.screen = AppScreen::Result(ResultScreen::new(
623 serde_json::json!({ "error": format!("{e}") }),
624 None,
625 None,
626 ));
627 }
628 }
629 }
630 KeyCode::Esc => {
631 self.screen = AppScreen::Browse(BrowseScreen::new(self.registry.clone()));
632 }
633 _ => {}
634 }
635 Ok(false)
636 }
637
638 fn handle_result(&mut self, key: KeyCode) -> Result<bool> {
641 use super::screens::result::ResultViewMode;
642
643 let result = match &mut self.screen {
644 AppScreen::Result(r) => r,
645 _ => return Ok(false),
646 };
647 match key {
648 KeyCode::Up | KeyCode::Char('k') => {
649 if result.view_mode == ResultViewMode::Json {
650 result.scroll_up(1);
651 } else {
652 result.table_previous();
653 }
654 }
655 KeyCode::Down => {
656 if result.view_mode == ResultViewMode::Json {
657 result.scroll_down(1);
658 } else {
659 result.table_next();
660 }
661 }
662 KeyCode::Char('j') => {
663 if result.view_mode == ResultViewMode::Json {
664 result.scroll_down(1);
665 }
666 }
667 KeyCode::PageUp => {
668 if result.view_mode == ResultViewMode::Table {
669 result.table_page_up();
670 } else {
671 result.scroll_up(10);
672 }
673 }
674 KeyCode::PageDown => {
675 if result.view_mode == ResultViewMode::Table {
676 result.table_page_down();
677 } else {
678 result.scroll_down(10);
679 }
680 }
681 KeyCode::Char('t') => {
682 if result.table_row_count > 0 {
683 result.switch_view_mode();
684 }
685 }
686 KeyCode::Enter => {
687 if result.view_mode == ResultViewMode::Table && result.table_row_count > 0 {
688 if let Some(item) = result.get_selected_item_value() {
689 let prev = std::mem::replace(
690 &mut self.screen,
691 AppScreen::MainMenu(MainMenuScreen::new()),
692 );
693 if let AppScreen::Result(rs) = prev {
694 self.screen =
695 AppScreen::ResultDetail(ResultDetailScreen::new(rs, item));
696 }
697 }
698 }
699 }
700 KeyCode::Esc => {
701 result.clear_message();
702 self.screen = AppScreen::Browse(BrowseScreen::new(self.registry.clone()));
703 }
704 KeyCode::Char('q') => return Ok(true),
705 _ => {}
706 }
707 Ok(false)
708 }
709
710 fn handle_result_detail(&mut self, key: KeyCode) -> Result<bool> {
713 let detail = match &mut self.screen {
714 AppScreen::ResultDetail(d) => d,
715 _ => return Ok(false),
716 };
717 match key {
718 KeyCode::Up | KeyCode::Char('k') => detail.scroll_up(1),
719 KeyCode::Down | KeyCode::Char('j') => detail.scroll_down(1),
720 KeyCode::PageUp => detail.scroll_up(10),
721 KeyCode::PageDown => detail.scroll_down(10),
722 KeyCode::Char('o') => detail.open_image_url(),
723 KeyCode::Esc => {
724 detail.clear_message();
725 let prev =
726 std::mem::replace(&mut self.screen, AppScreen::MainMenu(MainMenuScreen::new()));
727 if let AppScreen::ResultDetail(d) = prev {
728 self.screen = AppScreen::Result(d.parent);
729 }
730 }
731 KeyCode::Char('q') => return Ok(true),
732 _ => {}
733 }
734 Ok(false)
735 }
736
737 fn handle_game_detail(&mut self, key: KeyCode) -> Result<bool> {
740 let detail = match &mut self.screen {
741 AppScreen::GameDetail(d) => d,
742 _ => return Ok(false),
743 };
744
745 if !detail.download_completion_acknowledged {
748 if let Ok(list) = detail.downloads.lock() {
749 let has_completed = list.iter().any(|j| {
750 j.rom_id == detail.rom.id
751 && matches!(
752 j.status,
753 crate::core::download::DownloadStatus::Done
754 | crate::core::download::DownloadStatus::Error(_)
755 )
756 });
757 let is_still_downloading = list.iter().any(|j| {
758 j.rom_id == detail.rom.id
759 && matches!(j.status, crate::core::download::DownloadStatus::Downloading)
760 });
761 if has_completed && !is_still_downloading {
763 detail.download_completion_acknowledged = true;
764 }
765 }
766 }
767
768 match key {
769 KeyCode::Enter => {
770 if !detail.has_started_download {
773 detail.has_started_download = true;
774 self.downloads
775 .start_download(&detail.rom, self.client.clone());
776 }
777 }
778 KeyCode::Char('o') => detail.open_cover(),
779 KeyCode::Char('m') => detail.toggle_technical(),
780 KeyCode::Esc => {
781 detail.clear_message();
782 let prev =
783 std::mem::replace(&mut self.screen, AppScreen::MainMenu(MainMenuScreen::new()));
784 if let AppScreen::GameDetail(g) = prev {
785 self.screen = match g.previous {
786 GameDetailPrevious::Library(l) => AppScreen::LibraryBrowse(l),
787 GameDetailPrevious::Search(s) => AppScreen::Search(s),
788 };
789 }
790 }
791 KeyCode::Char('q') => return Ok(true),
792 _ => {}
793 }
794 Ok(false)
795 }
796
797 fn render(&mut self, f: &mut ratatui::Frame) {
802 let area = f.size();
803 if let Some(ref splash) = self.startup_splash {
804 connected_splash::render(f, area, splash);
805 return;
806 }
807 match &mut self.screen {
808 AppScreen::MainMenu(menu) => menu.render(f, area),
809 AppScreen::LibraryBrowse(lib) => lib.render(f, area),
810 AppScreen::Search(search) => {
811 search.render(f, area);
812 if let Some((x, y)) = search.cursor_position(area) {
813 f.set_cursor(x, y);
814 }
815 }
816 AppScreen::Settings(settings) => {
817 settings.render(f, area);
818 if let Some((x, y)) = settings.cursor_position(area) {
819 f.set_cursor(x, y);
820 }
821 }
822 AppScreen::Browse(browse) => browse.render(f, area),
823 AppScreen::Execute(execute) => {
824 execute.render(f, area);
825 if let Some((x, y)) = execute.cursor_position(area) {
826 f.set_cursor(x, y);
827 }
828 }
829 AppScreen::Result(result) => result.render(f, area),
830 AppScreen::ResultDetail(detail) => detail.render(f, area),
831 AppScreen::GameDetail(detail) => detail.render(f, area),
832 AppScreen::Download(d) => d.render(f, area),
833 }
834 }
835}