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::{auth_for_persist_merge, Config};
28use crate::core::cache::{RomCache, RomCacheKey};
29use crate::core::download::DownloadManager;
30use crate::endpoints::collections::{
31 merge_all_collection_sources, ListCollections, ListSmartCollections, ListVirtualCollections,
32};
33use crate::endpoints::{platforms::ListPlatforms, roms::GetRoms};
34use crate::types::RomList;
35
36use super::keyboard_help;
37use super::openapi::{resolve_path_template, EndpointRegistry};
38use super::screens::connected_splash::{self, StartupSplash};
39use super::screens::setup_wizard::SetupWizard;
40use super::screens::{
41 BrowseScreen, DownloadScreen, ExecuteScreen, GameDetailPrevious, GameDetailScreen,
42 LibraryBrowseScreen, MainMenuScreen, ResultDetailScreen, ResultScreen, SearchScreen,
43 SettingsScreen,
44};
45
46pub enum AppScreen {
55 MainMenu(MainMenuScreen),
56 LibraryBrowse(LibraryBrowseScreen),
57 Search(SearchScreen),
58 Settings(SettingsScreen),
59 Browse(BrowseScreen),
60 Execute(ExecuteScreen),
61 Result(ResultScreen),
62 ResultDetail(ResultDetailScreen),
63 GameDetail(Box<GameDetailScreen>),
64 Download(DownloadScreen),
65 SetupWizard(Box<crate::tui::screens::setup_wizard::SetupWizard>),
66}
67
68fn blocks_global_d_shortcut(screen: &AppScreen) -> bool {
69 match screen {
70 AppScreen::Search(_) | AppScreen::Settings(_) | AppScreen::SetupWizard(_) => true,
71 AppScreen::LibraryBrowse(lib) => lib.any_search_bar_open(),
72 _ => false,
73 }
74}
75
76fn allows_global_question_help(screen: &AppScreen) -> bool {
77 match screen {
78 AppScreen::Search(_) | AppScreen::SetupWizard(_) | AppScreen::Execute(_) => false,
79 AppScreen::LibraryBrowse(lib) if lib.any_search_bar_open() => false,
80 AppScreen::Settings(s) if s.editing => false,
81 _ => true,
82 }
83}
84
85pub struct App {
94 pub screen: AppScreen,
95 client: RommClient,
96 config: Config,
97 registry: EndpointRegistry,
98 server_version: Option<String>,
100 rom_cache: RomCache,
101 downloads: DownloadManager,
102 screen_before_download: Option<AppScreen>,
104 deferred_load_roms: Option<(Option<RomCacheKey>, Option<GetRoms>, u64)>,
106 startup_splash: Option<StartupSplash>,
108 pub global_error: Option<String>,
109 show_keyboard_help: bool,
110}
111
112impl App {
113 pub fn new(
115 client: RommClient,
116 config: Config,
117 registry: EndpointRegistry,
118 server_version: Option<String>,
119 startup_splash: Option<StartupSplash>,
120 ) -> Self {
121 Self {
122 screen: AppScreen::MainMenu(MainMenuScreen::new()),
123 client,
124 config,
125 registry,
126 server_version,
127 rom_cache: RomCache::load(),
128 downloads: DownloadManager::new(),
129 screen_before_download: None,
130 deferred_load_roms: None,
131 startup_splash,
132 global_error: None,
133 show_keyboard_help: false,
134 }
135 }
136
137 pub fn set_error(&mut self, err: anyhow::Error) {
138 self.global_error = Some(format!("{:#}", err));
139 }
140
141 pub async fn run(&mut self) -> Result<()> {
151 enable_raw_mode()?;
152 let mut stdout = std::io::stdout();
153 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
154 let backend = CrosstermBackend::new(stdout);
155 let mut terminal = Terminal::new(backend)?;
156
157 loop {
158 if self
159 .startup_splash
160 .as_ref()
161 .is_some_and(|s| s.should_auto_dismiss())
162 {
163 self.startup_splash = None;
164 }
165 terminal.draw(|f| self.render(f))?;
168
169 if event::poll(Duration::from_millis(100))? {
172 if let Event::Key(key) = event::read()? {
173 if key.kind == KeyEventKind::Press && self.handle_key(key.code).await? {
174 break;
175 }
176 }
177 }
178
179 if let Some((key, req, expected)) = self.deferred_load_roms.take() {
184 if let Ok(Some(roms)) = self.load_roms_cached(key, req, expected).await {
185 if let AppScreen::LibraryBrowse(ref mut lib) = self.screen {
186 lib.set_roms(roms);
187 }
188 }
189 }
190 }
191
192 disable_raw_mode()?;
193 execute!(
194 terminal.backend_mut(),
195 LeaveAlternateScreen,
196 DisableMouseCapture
197 )?;
198 terminal.show_cursor()?;
199 Ok(())
200 }
201
202 async fn load_roms_cached(
209 &mut self,
210 key: Option<RomCacheKey>,
211 req: Option<GetRoms>,
212 expected_count: u64,
213 ) -> Result<Option<RomList>> {
214 if let Some(ref k) = key {
216 if let Some(cached) = self.rom_cache.get_valid(k, expected_count) {
217 return Ok(Some(cached.clone()));
218 }
219 }
220 if let Some(r) = req {
222 let mut roms = self.client.call(&r).await?;
223 let total = roms.total;
224 let ceiling = 20000;
225
226 while (roms.items.len() as u64) < total && (roms.items.len() as u64) < ceiling {
229 let mut next_req = r.clone();
230 next_req.offset = Some(roms.items.len() as u32);
231
232 let next_batch = self.client.call(&next_req).await?;
233 if next_batch.items.is_empty() {
234 break;
235 }
236 roms.items.extend(next_batch.items);
237 }
238
239 if let Some(k) = key {
240 self.rom_cache.insert(k, roms.clone(), expected_count); }
242 return Ok(Some(roms));
243 }
244 Ok(None)
245 }
246
247 pub async fn handle_key(&mut self, key: KeyCode) -> Result<bool> {
252 if self.global_error.is_some() {
253 if key == KeyCode::Esc || key == KeyCode::Enter {
254 self.global_error = None;
255 }
256 return Ok(false);
257 }
258
259 if self.startup_splash.is_some() {
260 self.startup_splash = None;
261 return Ok(false);
262 }
263
264 if self.show_keyboard_help {
265 if matches!(
266 key,
267 KeyCode::Esc | KeyCode::Enter | KeyCode::F(1) | KeyCode::Char('?')
268 ) {
269 self.show_keyboard_help = false;
270 }
271 return Ok(false);
272 }
273
274 if key == KeyCode::F(1) {
275 self.show_keyboard_help = true;
276 return Ok(false);
277 }
278 if key == KeyCode::Char('?') && allows_global_question_help(&self.screen) {
279 self.show_keyboard_help = true;
280 return Ok(false);
281 }
282
283 if key == KeyCode::Char('d') && !blocks_global_d_shortcut(&self.screen) {
285 self.toggle_download_screen();
286 return Ok(false);
287 }
288
289 match &self.screen {
290 AppScreen::MainMenu(_) => self.handle_main_menu(key).await,
291 AppScreen::LibraryBrowse(_) => self.handle_library_browse(key).await,
292 AppScreen::Search(_) => self.handle_search(key).await,
293 AppScreen::Settings(_) => self.handle_settings(key),
294 AppScreen::Browse(_) => self.handle_browse(key),
295 AppScreen::Execute(_) => self.handle_execute(key).await,
296 AppScreen::Result(_) => self.handle_result(key),
297 AppScreen::ResultDetail(_) => self.handle_result_detail(key),
298 AppScreen::GameDetail(_) => self.handle_game_detail(key),
299 AppScreen::Download(_) => self.handle_download(key),
300 AppScreen::SetupWizard(_) => self.handle_setup_wizard(key).await,
301 }
302 }
303
304 fn toggle_download_screen(&mut self) {
307 let current =
308 std::mem::replace(&mut self.screen, AppScreen::MainMenu(MainMenuScreen::new()));
309 match current {
310 AppScreen::Download(_) => {
311 self.screen = self
312 .screen_before_download
313 .take()
314 .unwrap_or_else(|| AppScreen::MainMenu(MainMenuScreen::new()));
315 }
316 other => {
317 self.screen_before_download = Some(other);
318 self.screen = AppScreen::Download(DownloadScreen::new(self.downloads.shared()));
319 }
320 }
321 }
322
323 fn handle_download(&mut self, key: KeyCode) -> Result<bool> {
324 if key == KeyCode::Esc || key == KeyCode::Char('d') {
325 self.screen = self
326 .screen_before_download
327 .take()
328 .unwrap_or_else(|| AppScreen::MainMenu(MainMenuScreen::new()));
329 }
330 Ok(false)
331 }
332
333 async fn handle_main_menu(&mut self, key: KeyCode) -> Result<bool> {
336 let menu = match &mut self.screen {
337 AppScreen::MainMenu(m) => m,
338 _ => return Ok(false),
339 };
340 match key {
341 KeyCode::Up | KeyCode::Char('k') => menu.previous(),
342 KeyCode::Down | KeyCode::Char('j') => menu.next(),
343 KeyCode::Enter => match menu.selected {
344 0 => {
345 let platforms = match self.client.call(&ListPlatforms).await {
346 Ok(p) => p,
347 Err(e) => {
348 self.set_error(e);
349 return Ok(false);
350 }
351 };
352 let mut collection_errors = Vec::new();
353 let manual = match self.client.call(&ListCollections).await {
354 Ok(c) => c.into_vec(),
355 Err(e) => {
356 collection_errors.push(format!("GET /api/collections: {e:#}"));
357 Vec::new()
358 }
359 };
360 let smart = match self.client.call(&ListSmartCollections).await {
361 Ok(c) => c.into_vec(),
362 Err(e) => {
363 collection_errors.push(format!("GET /api/collections/smart: {e:#}"));
364 Vec::new()
365 }
366 };
367 let virtual_rows = match self.client.call(&ListVirtualCollections).await {
368 Ok(v) => v,
369 Err(e) => {
370 collection_errors
371 .push(format!("GET /api/collections/virtual?type=all: {e:#}"));
372 Vec::new()
373 }
374 };
375 let collections = merge_all_collection_sources(manual, smart, virtual_rows);
376 if !collection_errors.is_empty() {
377 self.set_error(anyhow::anyhow!("{}", collection_errors.join("\n")));
378 }
379 let mut lib = LibraryBrowseScreen::new(platforms, collections);
380 if lib.list_len() > 0 {
381 let key = lib.cache_key();
382 let expected = lib.expected_rom_count();
383 let req = lib
384 .get_roms_request_platform()
385 .or_else(|| lib.get_roms_request_collection());
386 if let Ok(Some(roms)) = self.load_roms_cached(key, req, expected).await {
387 lib.set_roms(roms);
388 }
389 }
390 self.screen = AppScreen::LibraryBrowse(lib);
391 }
392 1 => self.screen = AppScreen::Search(SearchScreen::new()),
393 2 => {
394 self.screen_before_download = Some(AppScreen::MainMenu(MainMenuScreen::new()));
395 self.screen = AppScreen::Download(DownloadScreen::new(self.downloads.shared()));
396 }
397 3 => {
398 self.screen = AppScreen::Settings(SettingsScreen::new(
399 &self.config,
400 self.server_version.as_deref(),
401 ))
402 }
403 4 => return Ok(true),
404 _ => {}
405 },
406 KeyCode::Esc | KeyCode::Char('q') => return Ok(true),
407 _ => {}
408 }
409 Ok(false)
410 }
411
412 async fn handle_library_browse(&mut self, key: KeyCode) -> Result<bool> {
415 use super::screens::library_browse::{LibrarySearchMode, LibraryViewMode};
416
417 let lib = match &mut self.screen {
418 AppScreen::LibraryBrowse(l) => l,
419 _ => return Ok(false),
420 };
421
422 if lib.view_mode == LibraryViewMode::List {
424 if let Some(mode) = lib.list_search.mode {
425 match key {
426 KeyCode::Esc => lib.clear_list_search(),
427 KeyCode::Backspace => lib.delete_list_search_char(),
428 KeyCode::Char(c) => lib.add_list_search_char(c),
429 KeyCode::Tab if mode == LibrarySearchMode::Jump => lib.list_jump_match(true),
430 KeyCode::Enter => lib.commit_list_filter_bar(),
431 _ => {}
432 }
433 return Ok(false);
434 }
435 }
436
437 if lib.view_mode == LibraryViewMode::Roms {
439 if let Some(mode) = lib.rom_search.mode {
440 match key {
441 KeyCode::Esc => lib.clear_rom_search(),
442 KeyCode::Backspace => lib.delete_rom_search_char(),
443 KeyCode::Char(c) => lib.add_rom_search_char(c),
444 KeyCode::Tab if mode == LibrarySearchMode::Jump => lib.jump_rom_match(true),
445 KeyCode::Enter => lib.commit_rom_filter_bar(),
446 _ => {}
447 }
448 return Ok(false);
449 }
450 }
451
452 match key {
453 KeyCode::Up | KeyCode::Char('k') => {
454 if lib.view_mode == LibraryViewMode::List {
455 lib.list_previous();
456 if lib.list_len() > 0 {
457 lib.clear_roms(); let key = lib.cache_key();
459 let expected = lib.expected_rom_count();
460 let req = lib
461 .get_roms_request_platform()
462 .or_else(|| lib.get_roms_request_collection());
463 self.deferred_load_roms = Some((key, req, expected));
464 }
465 } else {
466 lib.rom_previous();
467 }
468 }
469 KeyCode::Down | KeyCode::Char('j') => {
470 if lib.view_mode == LibraryViewMode::List {
471 lib.list_next();
472 if lib.list_len() > 0 {
473 lib.clear_roms(); let key = lib.cache_key();
475 let expected = lib.expected_rom_count();
476 let req = lib
477 .get_roms_request_platform()
478 .or_else(|| lib.get_roms_request_collection());
479 self.deferred_load_roms = Some((key, req, expected));
480 }
481 } else {
482 lib.rom_next();
483 }
484 }
485 KeyCode::Left | KeyCode::Char('h') if lib.view_mode == LibraryViewMode::Roms => {
486 lib.back_to_list();
487 }
488 KeyCode::Right | KeyCode::Char('l') => lib.switch_view(),
489 KeyCode::Tab => {
490 if lib.view_mode == LibraryViewMode::List {
491 lib.switch_view();
492 } else {
493 lib.switch_view(); }
495 }
496 KeyCode::Char('/') => match lib.view_mode {
497 LibraryViewMode::List => lib.enter_list_search(LibrarySearchMode::Filter),
498 LibraryViewMode::Roms => lib.enter_rom_search(LibrarySearchMode::Filter),
499 },
500 KeyCode::Char('f') => match lib.view_mode {
501 LibraryViewMode::List => lib.enter_list_search(LibrarySearchMode::Jump),
502 LibraryViewMode::Roms => lib.enter_rom_search(LibrarySearchMode::Jump),
503 },
504 KeyCode::Enter => {
505 if lib.view_mode == LibraryViewMode::List {
506 lib.switch_view();
507 } else if let Some((primary, others)) = lib.get_selected_group() {
508 let lib_screen = std::mem::replace(
509 &mut self.screen,
510 AppScreen::MainMenu(MainMenuScreen::new()),
511 );
512 if let AppScreen::LibraryBrowse(l) = lib_screen {
513 self.screen = AppScreen::GameDetail(Box::new(GameDetailScreen::new(
514 primary,
515 others,
516 GameDetailPrevious::Library(l),
517 self.downloads.shared(),
518 )));
519 }
520 }
521 }
522 KeyCode::Char('t') => lib.switch_subsection(),
523 KeyCode::Esc => {
524 if lib.view_mode == LibraryViewMode::Roms {
525 if lib.rom_search.filter_browsing {
526 lib.clear_rom_search();
527 } else {
528 lib.back_to_list();
529 }
530 } else if lib.list_search.filter_browsing {
531 lib.clear_list_search();
532 } else {
533 self.screen = AppScreen::MainMenu(MainMenuScreen::new());
534 }
535 }
536 KeyCode::Char('q') => return Ok(true),
537 _ => {}
538 }
539 Ok(false)
540 }
541
542 async fn handle_search(&mut self, key: KeyCode) -> Result<bool> {
545 let search = match &mut self.screen {
546 AppScreen::Search(s) => s,
547 _ => return Ok(false),
548 };
549 match key {
550 KeyCode::Backspace => search.delete_char(),
551 KeyCode::Left => search.cursor_left(),
552 KeyCode::Right => search.cursor_right(),
553 KeyCode::Up => search.previous(),
554 KeyCode::Down => search.next(),
555 KeyCode::Char(c) => search.add_char(c),
556 KeyCode::Enter => {
557 if search.query.is_empty() {
558 } else if search.result_groups.is_some() && search.results_match_current_query() {
560 if let Some((primary, others)) = search.get_selected_group() {
561 let prev = std::mem::replace(
562 &mut self.screen,
563 AppScreen::MainMenu(MainMenuScreen::new()),
564 );
565 if let AppScreen::Search(s) = prev {
566 self.screen = AppScreen::GameDetail(Box::new(GameDetailScreen::new(
567 primary,
568 others,
569 GameDetailPrevious::Search(s),
570 self.downloads.shared(),
571 )));
572 }
573 }
574 } else {
575 let req = GetRoms {
576 search_term: Some(search.query.clone()),
577 limit: Some(50),
578 ..Default::default()
579 };
580 if let Ok(roms) = self.client.call(&req).await {
581 search.set_results(roms);
582 }
583 }
584 }
585 KeyCode::Esc => {
586 if search.results.is_some() {
587 search.clear_results();
588 } else {
589 self.screen = AppScreen::MainMenu(MainMenuScreen::new());
590 }
591 }
592 _ => {}
593 }
594 Ok(false)
595 }
596
597 fn handle_settings(&mut self, key: KeyCode) -> Result<bool> {
600 let settings = match &mut self.screen {
601 AppScreen::Settings(s) => s,
602 _ => return Ok(false),
603 };
604
605 if settings.editing {
606 match key {
607 KeyCode::Enter => {
608 settings.save_edit();
609 }
610 KeyCode::Esc => settings.cancel_edit(),
611 KeyCode::Backspace => settings.delete_char(),
612 KeyCode::Left => settings.move_cursor_left(),
613 KeyCode::Right => settings.move_cursor_right(),
614 KeyCode::Char(c) => settings.add_char(c),
615 _ => {}
616 }
617 return Ok(false);
618 }
619
620 match key {
621 KeyCode::Up | KeyCode::Char('k') => settings.previous(),
622 KeyCode::Down | KeyCode::Char('j') => settings.next(),
623 KeyCode::Enter => {
624 if settings.selected_index == 3 {
625 self.screen =
626 AppScreen::SetupWizard(Box::new(SetupWizard::new_auth_only(&self.config)));
627 } else {
628 settings.enter_edit();
629 }
630 }
631 KeyCode::Char('s' | 'S') => {
632 use crate::config::persist_user_config;
634 let auth = auth_for_persist_merge(self.config.auth.clone());
635 if let Err(e) = persist_user_config(
636 &settings.base_url,
637 &settings.download_dir,
638 settings.use_https,
639 auth,
640 ) {
641 settings.message = Some((format!("Error saving: {e}"), Color::Red));
642 } else {
643 settings.message = Some(("Saved to config.json".to_string(), Color::Green));
644 self.config.base_url = settings.base_url.clone();
646 self.config.download_dir = settings.download_dir.clone();
647 self.config.use_https = settings.use_https;
648 if let Ok(new_client) = RommClient::new(&self.config, self.client.verbose()) {
650 self.client = new_client;
651 }
652 }
653 }
654 KeyCode::Esc => self.screen = AppScreen::MainMenu(MainMenuScreen::new()),
655 KeyCode::Char('q') => return Ok(true),
656 _ => {}
657 }
658 Ok(false)
659 }
660
661 fn handle_browse(&mut self, key: KeyCode) -> Result<bool> {
664 use super::screens::browse::ViewMode;
665
666 let browse = match &mut self.screen {
667 AppScreen::Browse(b) => b,
668 _ => return Ok(false),
669 };
670 match key {
671 KeyCode::Up | KeyCode::Char('k') => browse.previous(),
672 KeyCode::Down | KeyCode::Char('j') => browse.next(),
673 KeyCode::Left | KeyCode::Char('h') if browse.view_mode == ViewMode::Endpoints => {
674 browse.switch_view();
675 }
676 KeyCode::Right | KeyCode::Char('l') if browse.view_mode == ViewMode::Sections => {
677 browse.switch_view();
678 }
679 KeyCode::Tab => browse.switch_view(),
680 KeyCode::Enter => {
681 if browse.view_mode == ViewMode::Endpoints {
682 if let Some(ep) = browse.get_selected_endpoint() {
683 self.screen = AppScreen::Execute(ExecuteScreen::new(ep.clone()));
684 }
685 } else {
686 browse.switch_view();
687 }
688 }
689 KeyCode::Esc => self.screen = AppScreen::MainMenu(MainMenuScreen::new()),
690 _ => {}
691 }
692 Ok(false)
693 }
694
695 async fn handle_execute(&mut self, key: KeyCode) -> Result<bool> {
698 let execute = match &mut self.screen {
699 AppScreen::Execute(e) => e,
700 _ => return Ok(false),
701 };
702 match key {
703 KeyCode::Tab => execute.next_field(),
704 KeyCode::BackTab => execute.previous_field(),
705 KeyCode::Char(c) => execute.add_char_to_focused(c),
706 KeyCode::Backspace => execute.delete_char_from_focused(),
707 KeyCode::Enter => {
708 let endpoint = execute.endpoint.clone();
709 let query = execute.get_query_params();
710 let body = if endpoint.has_body && !execute.body_text.is_empty() {
711 Some(serde_json::from_str(&execute.body_text)?)
712 } else {
713 None
714 };
715 let resolved_path =
716 match resolve_path_template(&endpoint.path, &execute.get_path_params()) {
717 Ok(p) => p,
718 Err(e) => {
719 self.screen = AppScreen::Result(ResultScreen::new(
720 serde_json::json!({ "error": format!("{e}") }),
721 None,
722 None,
723 ));
724 return Ok(false);
725 }
726 };
727 match self
728 .client
729 .request_json(&endpoint.method, &resolved_path, &query, body)
730 .await
731 {
732 Ok(result) => {
733 self.screen = AppScreen::Result(ResultScreen::new(
734 result,
735 Some(&endpoint.method),
736 Some(resolved_path.as_str()),
737 ));
738 }
739 Err(e) => {
740 self.screen = AppScreen::Result(ResultScreen::new(
741 serde_json::json!({ "error": format!("{e}") }),
742 None,
743 None,
744 ));
745 }
746 }
747 }
748 KeyCode::Esc => {
749 self.screen = AppScreen::Browse(BrowseScreen::new(self.registry.clone()));
750 }
751 _ => {}
752 }
753 Ok(false)
754 }
755
756 fn handle_result(&mut self, key: KeyCode) -> Result<bool> {
759 use super::screens::result::ResultViewMode;
760
761 let result = match &mut self.screen {
762 AppScreen::Result(r) => r,
763 _ => return Ok(false),
764 };
765 match key {
766 KeyCode::Up | KeyCode::Char('k') => {
767 if result.view_mode == ResultViewMode::Json {
768 result.scroll_up(1);
769 } else {
770 result.table_previous();
771 }
772 }
773 KeyCode::Down => {
774 if result.view_mode == ResultViewMode::Json {
775 result.scroll_down(1);
776 } else {
777 result.table_next();
778 }
779 }
780 KeyCode::Char('j') if result.view_mode == ResultViewMode::Json => {
781 result.scroll_down(1);
782 }
783 KeyCode::PageUp => {
784 if result.view_mode == ResultViewMode::Table {
785 result.table_page_up();
786 } else {
787 result.scroll_up(10);
788 }
789 }
790 KeyCode::PageDown => {
791 if result.view_mode == ResultViewMode::Table {
792 result.table_page_down();
793 } else {
794 result.scroll_down(10);
795 }
796 }
797 KeyCode::Char('t') if result.table_row_count > 0 => {
798 result.switch_view_mode();
799 }
800 KeyCode::Enter
801 if result.view_mode == ResultViewMode::Table && result.table_row_count > 0 =>
802 {
803 if let Some(item) = result.get_selected_item_value() {
804 let prev = std::mem::replace(
805 &mut self.screen,
806 AppScreen::MainMenu(MainMenuScreen::new()),
807 );
808 if let AppScreen::Result(rs) = prev {
809 self.screen = AppScreen::ResultDetail(ResultDetailScreen::new(rs, item));
810 }
811 }
812 }
813 KeyCode::Esc => {
814 result.clear_message();
815 self.screen = AppScreen::Browse(BrowseScreen::new(self.registry.clone()));
816 }
817 KeyCode::Char('q') => return Ok(true),
818 _ => {}
819 }
820 Ok(false)
821 }
822
823 fn handle_result_detail(&mut self, key: KeyCode) -> Result<bool> {
826 let detail = match &mut self.screen {
827 AppScreen::ResultDetail(d) => d,
828 _ => return Ok(false),
829 };
830 match key {
831 KeyCode::Up | KeyCode::Char('k') => detail.scroll_up(1),
832 KeyCode::Down | KeyCode::Char('j') => detail.scroll_down(1),
833 KeyCode::PageUp => detail.scroll_up(10),
834 KeyCode::PageDown => detail.scroll_down(10),
835 KeyCode::Char('o') => detail.open_image_url(),
836 KeyCode::Esc => {
837 detail.clear_message();
838 let prev =
839 std::mem::replace(&mut self.screen, AppScreen::MainMenu(MainMenuScreen::new()));
840 if let AppScreen::ResultDetail(d) = prev {
841 self.screen = AppScreen::Result(d.parent);
842 }
843 }
844 KeyCode::Char('q') => return Ok(true),
845 _ => {}
846 }
847 Ok(false)
848 }
849
850 fn handle_game_detail(&mut self, key: KeyCode) -> Result<bool> {
853 let detail = match &mut self.screen {
854 AppScreen::GameDetail(d) => d,
855 _ => return Ok(false),
856 };
857
858 if !detail.download_completion_acknowledged {
861 if let Ok(list) = detail.downloads.lock() {
862 let has_completed = list.iter().any(|j| {
863 j.rom_id == detail.rom.id
864 && matches!(
865 j.status,
866 crate::core::download::DownloadStatus::Done
867 | crate::core::download::DownloadStatus::Error(_)
868 )
869 });
870 let is_still_downloading = list.iter().any(|j| {
871 j.rom_id == detail.rom.id
872 && matches!(j.status, crate::core::download::DownloadStatus::Downloading)
873 });
874 if has_completed && !is_still_downloading {
876 detail.download_completion_acknowledged = true;
877 }
878 }
879 }
880
881 match key {
882 KeyCode::Enter if !detail.has_started_download => {
885 detail.has_started_download = true;
886 self.downloads
887 .start_download(&detail.rom, self.client.clone());
888 }
889 KeyCode::Char('o') => detail.open_cover(),
890 KeyCode::Char('m') => detail.toggle_technical(),
891 KeyCode::Esc => {
892 detail.clear_message();
893 let prev =
894 std::mem::replace(&mut self.screen, AppScreen::MainMenu(MainMenuScreen::new()));
895 if let AppScreen::GameDetail(g) = prev {
896 self.screen = match g.previous {
897 GameDetailPrevious::Library(l) => AppScreen::LibraryBrowse(l),
898 GameDetailPrevious::Search(s) => AppScreen::Search(s),
899 };
900 }
901 }
902 KeyCode::Char('q') => return Ok(true),
903 _ => {}
904 }
905 Ok(false)
906 }
907
908 async fn handle_setup_wizard(&mut self, key: KeyCode) -> Result<bool> {
911 let wizard = match &mut self.screen {
912 AppScreen::SetupWizard(w) => w,
913 _ => return Ok(false),
914 };
915
916 let event = crossterm::event::KeyEvent::new(key, crossterm::event::KeyModifiers::empty());
918 if wizard.handle_key(event)? {
919 self.screen = AppScreen::Settings(SettingsScreen::new(
921 &self.config,
922 self.server_version.as_deref(),
923 ));
924 return Ok(false);
925 }
926
927 if wizard.testing {
928 let result = wizard.try_connect_and_persist(self.client.verbose()).await;
929 wizard.testing = false;
930 match result {
931 Ok(cfg) => {
932 let auth_ok = cfg.auth.is_some();
933 self.config = cfg;
934 if let Ok(new_client) = RommClient::new(&self.config, self.client.verbose()) {
935 self.client = new_client;
936 }
937 let mut settings =
938 SettingsScreen::new(&self.config, self.server_version.as_deref());
939 if auth_ok {
940 settings.message = Some((
941 "Authentication updated successfully".to_string(),
942 Color::Green,
943 ));
944 } else {
945 settings.message = Some((
946 "Saved configuration but credentials could not be loaded from the OS keyring (see logs)."
947 .to_string(),
948 Color::Yellow,
949 ));
950 }
951 self.screen = AppScreen::Settings(settings);
952 }
953 Err(e) => {
954 wizard.error = Some(format!("{e:#}"));
955 }
956 }
957 }
958 Ok(false)
959 }
960
961 fn render(&mut self, f: &mut ratatui::Frame) {
966 let area = f.size();
967 if let Some(ref splash) = self.startup_splash {
968 connected_splash::render(f, area, splash);
969 return;
970 }
971 match &mut self.screen {
972 AppScreen::MainMenu(menu) => menu.render(f, area),
973 AppScreen::LibraryBrowse(lib) => lib.render(f, area),
974 AppScreen::Search(search) => {
975 search.render(f, area);
976 if let Some((x, y)) = search.cursor_position(area) {
977 f.set_cursor(x, y);
978 }
979 }
980 AppScreen::Settings(settings) => {
981 settings.render(f, area);
982 if let Some((x, y)) = settings.cursor_position(area) {
983 f.set_cursor(x, y);
984 }
985 }
986 AppScreen::Browse(browse) => browse.render(f, area),
987 AppScreen::Execute(execute) => {
988 execute.render(f, area);
989 if let Some((x, y)) = execute.cursor_position(area) {
990 f.set_cursor(x, y);
991 }
992 }
993 AppScreen::Result(result) => result.render(f, area),
994 AppScreen::ResultDetail(detail) => detail.render(f, area),
995 AppScreen::GameDetail(detail) => detail.render(f, area),
996 AppScreen::Download(d) => d.render(f, area),
997 AppScreen::SetupWizard(wizard) => {
998 wizard.render(f, area);
999 if let Some((x, y)) = wizard.cursor_pos(area) {
1000 f.set_cursor(x, y);
1001 }
1002 }
1003 }
1004
1005 if self.show_keyboard_help {
1006 keyboard_help::render_keyboard_help(f, area);
1007 }
1008
1009 if let Some(ref err) = self.global_error {
1010 let popup_area = ratatui::layout::Rect {
1011 x: area.width.saturating_sub(60) / 2,
1012 y: area.height.saturating_sub(10) / 2,
1013 width: 60.min(area.width),
1014 height: 10.min(area.height),
1015 };
1016 f.render_widget(ratatui::widgets::Clear, popup_area);
1017 let block = ratatui::widgets::Block::default()
1018 .title("Error")
1019 .borders(ratatui::widgets::Borders::ALL)
1020 .style(ratatui::style::Style::default().fg(ratatui::style::Color::Red));
1021 let text = format!("{}\n\nPress Esc to dismiss", err);
1022 let paragraph = ratatui::widgets::Paragraph::new(text)
1023 .block(block)
1024 .wrap(ratatui::widgets::Wrap { trim: true });
1025 f.render_widget(paragraph, popup_area);
1026 }
1027 }
1028}