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