use anyhow::Result;
use crossterm::event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind,
};
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use ratatui::style::Color;
use ratatui::Terminal;
use std::time::Duration;
use crate::client::RommClient;
use crate::config::{auth_for_persist_merge, Config};
use crate::core::cache::{RomCache, RomCacheKey};
use crate::core::download::DownloadManager;
use crate::endpoints::collections::{
merge_all_collection_sources, ListCollections, ListSmartCollections, ListVirtualCollections,
};
use crate::endpoints::{platforms::ListPlatforms, roms::GetRoms};
use crate::types::RomList;
use super::keyboard_help;
use super::openapi::{resolve_path_template, EndpointRegistry};
use super::screens::connected_splash::{self, StartupSplash};
use super::screens::setup_wizard::SetupWizard;
use super::screens::{
BrowseScreen, DownloadScreen, ExecuteScreen, GameDetailPrevious, GameDetailScreen,
LibraryBrowseScreen, MainMenuScreen, ResultDetailScreen, ResultScreen, SearchScreen,
SettingsScreen,
};
pub enum AppScreen {
MainMenu(MainMenuScreen),
LibraryBrowse(LibraryBrowseScreen),
Search(SearchScreen),
Settings(SettingsScreen),
Browse(BrowseScreen),
Execute(ExecuteScreen),
Result(ResultScreen),
ResultDetail(ResultDetailScreen),
GameDetail(Box<GameDetailScreen>),
Download(DownloadScreen),
SetupWizard(Box<crate::tui::screens::setup_wizard::SetupWizard>),
}
fn blocks_global_d_shortcut(screen: &AppScreen) -> bool {
match screen {
AppScreen::Search(_) | AppScreen::Settings(_) | AppScreen::SetupWizard(_) => true,
AppScreen::LibraryBrowse(lib) => lib.any_search_bar_open(),
_ => false,
}
}
fn allows_global_question_help(screen: &AppScreen) -> bool {
match screen {
AppScreen::Search(_) | AppScreen::SetupWizard(_) | AppScreen::Execute(_) => false,
AppScreen::LibraryBrowse(lib) if lib.any_search_bar_open() => false,
AppScreen::Settings(s) if s.editing => false,
_ => true,
}
}
pub struct App {
pub screen: AppScreen,
client: RommClient,
config: Config,
registry: EndpointRegistry,
server_version: Option<String>,
rom_cache: RomCache,
downloads: DownloadManager,
screen_before_download: Option<AppScreen>,
deferred_load_roms: Option<(Option<RomCacheKey>, Option<GetRoms>, u64)>,
startup_splash: Option<StartupSplash>,
pub global_error: Option<String>,
show_keyboard_help: bool,
}
impl App {
pub fn new(
client: RommClient,
config: Config,
registry: EndpointRegistry,
server_version: Option<String>,
startup_splash: Option<StartupSplash>,
) -> Self {
Self {
screen: AppScreen::MainMenu(MainMenuScreen::new()),
client,
config,
registry,
server_version,
rom_cache: RomCache::load(),
downloads: DownloadManager::new(),
screen_before_download: None,
deferred_load_roms: None,
startup_splash,
global_error: None,
show_keyboard_help: false,
}
}
pub fn set_error(&mut self, err: anyhow::Error) {
self.global_error = Some(format!("{:#}", err));
}
pub async fn run(&mut self) -> Result<()> {
enable_raw_mode()?;
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
loop {
if self
.startup_splash
.as_ref()
.is_some_and(|s| s.should_auto_dismiss())
{
self.startup_splash = None;
}
terminal.draw(|f| self.render(f))?;
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && self.handle_key(key.code).await? {
break;
}
}
}
if let Some((key, req, expected)) = self.deferred_load_roms.take() {
if let Ok(Some(roms)) = self.load_roms_cached(key, req, expected).await {
if let AppScreen::LibraryBrowse(ref mut lib) = self.screen {
lib.set_roms(roms);
}
}
}
}
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
}
async fn load_roms_cached(
&mut self,
key: Option<RomCacheKey>,
req: Option<GetRoms>,
expected_count: u64,
) -> Result<Option<RomList>> {
if let Some(ref k) = key {
if let Some(cached) = self.rom_cache.get_valid(k, expected_count) {
return Ok(Some(cached.clone()));
}
}
if let Some(r) = req {
let mut roms = self.client.call(&r).await?;
let total = roms.total;
let ceiling = 20000;
while (roms.items.len() as u64) < total && (roms.items.len() as u64) < ceiling {
let mut next_req = r.clone();
next_req.offset = Some(roms.items.len() as u32);
let next_batch = self.client.call(&next_req).await?;
if next_batch.items.is_empty() {
break;
}
roms.items.extend(next_batch.items);
}
if let Some(k) = key {
self.rom_cache.insert(k, roms.clone(), expected_count); }
return Ok(Some(roms));
}
Ok(None)
}
pub async fn handle_key(&mut self, key: KeyCode) -> Result<bool> {
if self.global_error.is_some() {
if key == KeyCode::Esc || key == KeyCode::Enter {
self.global_error = None;
}
return Ok(false);
}
if self.startup_splash.is_some() {
self.startup_splash = None;
return Ok(false);
}
if self.show_keyboard_help {
if matches!(
key,
KeyCode::Esc | KeyCode::Enter | KeyCode::F(1) | KeyCode::Char('?')
) {
self.show_keyboard_help = false;
}
return Ok(false);
}
if key == KeyCode::F(1) {
self.show_keyboard_help = true;
return Ok(false);
}
if key == KeyCode::Char('?') && allows_global_question_help(&self.screen) {
self.show_keyboard_help = true;
return Ok(false);
}
if key == KeyCode::Char('d') && !blocks_global_d_shortcut(&self.screen) {
self.toggle_download_screen();
return Ok(false);
}
match &self.screen {
AppScreen::MainMenu(_) => self.handle_main_menu(key).await,
AppScreen::LibraryBrowse(_) => self.handle_library_browse(key).await,
AppScreen::Search(_) => self.handle_search(key).await,
AppScreen::Settings(_) => self.handle_settings(key),
AppScreen::Browse(_) => self.handle_browse(key),
AppScreen::Execute(_) => self.handle_execute(key).await,
AppScreen::Result(_) => self.handle_result(key),
AppScreen::ResultDetail(_) => self.handle_result_detail(key),
AppScreen::GameDetail(_) => self.handle_game_detail(key),
AppScreen::Download(_) => self.handle_download(key),
AppScreen::SetupWizard(_) => self.handle_setup_wizard(key).await,
}
}
fn toggle_download_screen(&mut self) {
let current =
std::mem::replace(&mut self.screen, AppScreen::MainMenu(MainMenuScreen::new()));
match current {
AppScreen::Download(_) => {
self.screen = self
.screen_before_download
.take()
.unwrap_or_else(|| AppScreen::MainMenu(MainMenuScreen::new()));
}
other => {
self.screen_before_download = Some(other);
self.screen = AppScreen::Download(DownloadScreen::new(self.downloads.shared()));
}
}
}
fn handle_download(&mut self, key: KeyCode) -> Result<bool> {
if key == KeyCode::Esc || key == KeyCode::Char('d') {
self.screen = self
.screen_before_download
.take()
.unwrap_or_else(|| AppScreen::MainMenu(MainMenuScreen::new()));
}
Ok(false)
}
async fn handle_main_menu(&mut self, key: KeyCode) -> Result<bool> {
let menu = match &mut self.screen {
AppScreen::MainMenu(m) => m,
_ => return Ok(false),
};
match key {
KeyCode::Up | KeyCode::Char('k') => menu.previous(),
KeyCode::Down | KeyCode::Char('j') => menu.next(),
KeyCode::Enter => match menu.selected {
0 => {
let platforms = match self.client.call(&ListPlatforms).await {
Ok(p) => p,
Err(e) => {
self.set_error(e);
return Ok(false);
}
};
let mut collection_errors = Vec::new();
let manual = match self.client.call(&ListCollections).await {
Ok(c) => c.into_vec(),
Err(e) => {
collection_errors.push(format!("GET /api/collections: {e:#}"));
Vec::new()
}
};
let smart = match self.client.call(&ListSmartCollections).await {
Ok(c) => c.into_vec(),
Err(e) => {
collection_errors.push(format!("GET /api/collections/smart: {e:#}"));
Vec::new()
}
};
let virtual_rows = match self.client.call(&ListVirtualCollections).await {
Ok(v) => v,
Err(e) => {
collection_errors
.push(format!("GET /api/collections/virtual?type=all: {e:#}"));
Vec::new()
}
};
let collections = merge_all_collection_sources(manual, smart, virtual_rows);
if !collection_errors.is_empty() {
self.set_error(anyhow::anyhow!("{}", collection_errors.join("\n")));
}
let mut lib = LibraryBrowseScreen::new(platforms, collections);
if lib.list_len() > 0 {
let key = lib.cache_key();
let expected = lib.expected_rom_count();
let req = lib
.get_roms_request_platform()
.or_else(|| lib.get_roms_request_collection());
if let Ok(Some(roms)) = self.load_roms_cached(key, req, expected).await {
lib.set_roms(roms);
}
}
self.screen = AppScreen::LibraryBrowse(lib);
}
1 => self.screen = AppScreen::Search(SearchScreen::new()),
2 => {
self.screen_before_download = Some(AppScreen::MainMenu(MainMenuScreen::new()));
self.screen = AppScreen::Download(DownloadScreen::new(self.downloads.shared()));
}
3 => {
self.screen = AppScreen::Settings(SettingsScreen::new(
&self.config,
self.server_version.as_deref(),
))
}
4 => return Ok(true),
_ => {}
},
KeyCode::Esc | KeyCode::Char('q') => return Ok(true),
_ => {}
}
Ok(false)
}
async fn handle_library_browse(&mut self, key: KeyCode) -> Result<bool> {
use super::screens::library_browse::{LibrarySearchMode, LibraryViewMode};
let lib = match &mut self.screen {
AppScreen::LibraryBrowse(l) => l,
_ => return Ok(false),
};
if lib.view_mode == LibraryViewMode::List {
if let Some(mode) = lib.list_search.mode {
match key {
KeyCode::Esc => lib.clear_list_search(),
KeyCode::Backspace => lib.delete_list_search_char(),
KeyCode::Char(c) => lib.add_list_search_char(c),
KeyCode::Tab if mode == LibrarySearchMode::Jump => lib.list_jump_match(true),
KeyCode::Enter => lib.commit_list_filter_bar(),
_ => {}
}
return Ok(false);
}
}
if lib.view_mode == LibraryViewMode::Roms {
if let Some(mode) = lib.rom_search.mode {
match key {
KeyCode::Esc => lib.clear_rom_search(),
KeyCode::Backspace => lib.delete_rom_search_char(),
KeyCode::Char(c) => lib.add_rom_search_char(c),
KeyCode::Tab if mode == LibrarySearchMode::Jump => lib.jump_rom_match(true),
KeyCode::Enter => lib.commit_rom_filter_bar(),
_ => {}
}
return Ok(false);
}
}
match key {
KeyCode::Up | KeyCode::Char('k') => {
if lib.view_mode == LibraryViewMode::List {
lib.list_previous();
if lib.list_len() > 0 {
lib.clear_roms(); let key = lib.cache_key();
let expected = lib.expected_rom_count();
let req = lib
.get_roms_request_platform()
.or_else(|| lib.get_roms_request_collection());
self.deferred_load_roms = Some((key, req, expected));
}
} else {
lib.rom_previous();
}
}
KeyCode::Down | KeyCode::Char('j') => {
if lib.view_mode == LibraryViewMode::List {
lib.list_next();
if lib.list_len() > 0 {
lib.clear_roms(); let key = lib.cache_key();
let expected = lib.expected_rom_count();
let req = lib
.get_roms_request_platform()
.or_else(|| lib.get_roms_request_collection());
self.deferred_load_roms = Some((key, req, expected));
}
} else {
lib.rom_next();
}
}
KeyCode::Left | KeyCode::Char('h') if lib.view_mode == LibraryViewMode::Roms => {
lib.back_to_list();
}
KeyCode::Right | KeyCode::Char('l') => lib.switch_view(),
KeyCode::Tab => {
if lib.view_mode == LibraryViewMode::List {
lib.switch_view();
} else {
lib.switch_view(); }
}
KeyCode::Char('/') => match lib.view_mode {
LibraryViewMode::List => lib.enter_list_search(LibrarySearchMode::Filter),
LibraryViewMode::Roms => lib.enter_rom_search(LibrarySearchMode::Filter),
},
KeyCode::Char('f') => match lib.view_mode {
LibraryViewMode::List => lib.enter_list_search(LibrarySearchMode::Jump),
LibraryViewMode::Roms => lib.enter_rom_search(LibrarySearchMode::Jump),
},
KeyCode::Enter => {
if lib.view_mode == LibraryViewMode::List {
lib.switch_view();
} else if let Some((primary, others)) = lib.get_selected_group() {
let lib_screen = std::mem::replace(
&mut self.screen,
AppScreen::MainMenu(MainMenuScreen::new()),
);
if let AppScreen::LibraryBrowse(l) = lib_screen {
self.screen = AppScreen::GameDetail(Box::new(GameDetailScreen::new(
primary,
others,
GameDetailPrevious::Library(l),
self.downloads.shared(),
)));
}
}
}
KeyCode::Char('t') => lib.switch_subsection(),
KeyCode::Esc => {
if lib.view_mode == LibraryViewMode::Roms {
if lib.rom_search.filter_browsing {
lib.clear_rom_search();
} else {
lib.back_to_list();
}
} else if lib.list_search.filter_browsing {
lib.clear_list_search();
} else {
self.screen = AppScreen::MainMenu(MainMenuScreen::new());
}
}
KeyCode::Char('q') => return Ok(true),
_ => {}
}
Ok(false)
}
async fn handle_search(&mut self, key: KeyCode) -> Result<bool> {
let search = match &mut self.screen {
AppScreen::Search(s) => s,
_ => return Ok(false),
};
match key {
KeyCode::Backspace => search.delete_char(),
KeyCode::Left => search.cursor_left(),
KeyCode::Right => search.cursor_right(),
KeyCode::Up => search.previous(),
KeyCode::Down => search.next(),
KeyCode::Char(c) => search.add_char(c),
KeyCode::Enter => {
if search.query.is_empty() {
} else if search.result_groups.is_some() && search.results_match_current_query() {
if let Some((primary, others)) = search.get_selected_group() {
let prev = std::mem::replace(
&mut self.screen,
AppScreen::MainMenu(MainMenuScreen::new()),
);
if let AppScreen::Search(s) = prev {
self.screen = AppScreen::GameDetail(Box::new(GameDetailScreen::new(
primary,
others,
GameDetailPrevious::Search(s),
self.downloads.shared(),
)));
}
}
} else {
let req = GetRoms {
search_term: Some(search.query.clone()),
limit: Some(50),
..Default::default()
};
if let Ok(roms) = self.client.call(&req).await {
search.set_results(roms);
}
}
}
KeyCode::Esc => {
if search.results.is_some() {
search.clear_results();
} else {
self.screen = AppScreen::MainMenu(MainMenuScreen::new());
}
}
_ => {}
}
Ok(false)
}
fn handle_settings(&mut self, key: KeyCode) -> Result<bool> {
let settings = match &mut self.screen {
AppScreen::Settings(s) => s,
_ => return Ok(false),
};
if settings.editing {
match key {
KeyCode::Enter => {
settings.save_edit();
}
KeyCode::Esc => settings.cancel_edit(),
KeyCode::Backspace => settings.delete_char(),
KeyCode::Left => settings.move_cursor_left(),
KeyCode::Right => settings.move_cursor_right(),
KeyCode::Char(c) => settings.add_char(c),
_ => {}
}
return Ok(false);
}
match key {
KeyCode::Up | KeyCode::Char('k') => settings.previous(),
KeyCode::Down | KeyCode::Char('j') => settings.next(),
KeyCode::Enter => {
if settings.selected_index == 3 {
self.screen =
AppScreen::SetupWizard(Box::new(SetupWizard::new_auth_only(&self.config)));
} else {
settings.enter_edit();
}
}
KeyCode::Char('s' | 'S') => {
use crate::config::persist_user_config;
let auth = auth_for_persist_merge(self.config.auth.clone());
if let Err(e) = persist_user_config(
&settings.base_url,
&settings.download_dir,
settings.use_https,
auth,
) {
settings.message = Some((format!("Error saving: {e}"), Color::Red));
} else {
settings.message = Some(("Saved to config.json".to_string(), Color::Green));
self.config.base_url = settings.base_url.clone();
self.config.download_dir = settings.download_dir.clone();
self.config.use_https = settings.use_https;
if let Ok(new_client) = RommClient::new(&self.config, self.client.verbose()) {
self.client = new_client;
}
}
}
KeyCode::Esc => self.screen = AppScreen::MainMenu(MainMenuScreen::new()),
KeyCode::Char('q') => return Ok(true),
_ => {}
}
Ok(false)
}
fn handle_browse(&mut self, key: KeyCode) -> Result<bool> {
use super::screens::browse::ViewMode;
let browse = match &mut self.screen {
AppScreen::Browse(b) => b,
_ => return Ok(false),
};
match key {
KeyCode::Up | KeyCode::Char('k') => browse.previous(),
KeyCode::Down | KeyCode::Char('j') => browse.next(),
KeyCode::Left | KeyCode::Char('h') if browse.view_mode == ViewMode::Endpoints => {
browse.switch_view();
}
KeyCode::Right | KeyCode::Char('l') if browse.view_mode == ViewMode::Sections => {
browse.switch_view();
}
KeyCode::Tab => browse.switch_view(),
KeyCode::Enter => {
if browse.view_mode == ViewMode::Endpoints {
if let Some(ep) = browse.get_selected_endpoint() {
self.screen = AppScreen::Execute(ExecuteScreen::new(ep.clone()));
}
} else {
browse.switch_view();
}
}
KeyCode::Esc => self.screen = AppScreen::MainMenu(MainMenuScreen::new()),
_ => {}
}
Ok(false)
}
async fn handle_execute(&mut self, key: KeyCode) -> Result<bool> {
let execute = match &mut self.screen {
AppScreen::Execute(e) => e,
_ => return Ok(false),
};
match key {
KeyCode::Tab => execute.next_field(),
KeyCode::BackTab => execute.previous_field(),
KeyCode::Char(c) => execute.add_char_to_focused(c),
KeyCode::Backspace => execute.delete_char_from_focused(),
KeyCode::Enter => {
let endpoint = execute.endpoint.clone();
let query = execute.get_query_params();
let body = if endpoint.has_body && !execute.body_text.is_empty() {
Some(serde_json::from_str(&execute.body_text)?)
} else {
None
};
let resolved_path =
match resolve_path_template(&endpoint.path, &execute.get_path_params()) {
Ok(p) => p,
Err(e) => {
self.screen = AppScreen::Result(ResultScreen::new(
serde_json::json!({ "error": format!("{e}") }),
None,
None,
));
return Ok(false);
}
};
match self
.client
.request_json(&endpoint.method, &resolved_path, &query, body)
.await
{
Ok(result) => {
self.screen = AppScreen::Result(ResultScreen::new(
result,
Some(&endpoint.method),
Some(resolved_path.as_str()),
));
}
Err(e) => {
self.screen = AppScreen::Result(ResultScreen::new(
serde_json::json!({ "error": format!("{e}") }),
None,
None,
));
}
}
}
KeyCode::Esc => {
self.screen = AppScreen::Browse(BrowseScreen::new(self.registry.clone()));
}
_ => {}
}
Ok(false)
}
fn handle_result(&mut self, key: KeyCode) -> Result<bool> {
use super::screens::result::ResultViewMode;
let result = match &mut self.screen {
AppScreen::Result(r) => r,
_ => return Ok(false),
};
match key {
KeyCode::Up | KeyCode::Char('k') => {
if result.view_mode == ResultViewMode::Json {
result.scroll_up(1);
} else {
result.table_previous();
}
}
KeyCode::Down => {
if result.view_mode == ResultViewMode::Json {
result.scroll_down(1);
} else {
result.table_next();
}
}
KeyCode::Char('j') if result.view_mode == ResultViewMode::Json => {
result.scroll_down(1);
}
KeyCode::PageUp => {
if result.view_mode == ResultViewMode::Table {
result.table_page_up();
} else {
result.scroll_up(10);
}
}
KeyCode::PageDown => {
if result.view_mode == ResultViewMode::Table {
result.table_page_down();
} else {
result.scroll_down(10);
}
}
KeyCode::Char('t') if result.table_row_count > 0 => {
result.switch_view_mode();
}
KeyCode::Enter
if result.view_mode == ResultViewMode::Table && result.table_row_count > 0 =>
{
if let Some(item) = result.get_selected_item_value() {
let prev = std::mem::replace(
&mut self.screen,
AppScreen::MainMenu(MainMenuScreen::new()),
);
if let AppScreen::Result(rs) = prev {
self.screen = AppScreen::ResultDetail(ResultDetailScreen::new(rs, item));
}
}
}
KeyCode::Esc => {
result.clear_message();
self.screen = AppScreen::Browse(BrowseScreen::new(self.registry.clone()));
}
KeyCode::Char('q') => return Ok(true),
_ => {}
}
Ok(false)
}
fn handle_result_detail(&mut self, key: KeyCode) -> Result<bool> {
let detail = match &mut self.screen {
AppScreen::ResultDetail(d) => d,
_ => return Ok(false),
};
match key {
KeyCode::Up | KeyCode::Char('k') => detail.scroll_up(1),
KeyCode::Down | KeyCode::Char('j') => detail.scroll_down(1),
KeyCode::PageUp => detail.scroll_up(10),
KeyCode::PageDown => detail.scroll_down(10),
KeyCode::Char('o') => detail.open_image_url(),
KeyCode::Esc => {
detail.clear_message();
let prev =
std::mem::replace(&mut self.screen, AppScreen::MainMenu(MainMenuScreen::new()));
if let AppScreen::ResultDetail(d) = prev {
self.screen = AppScreen::Result(d.parent);
}
}
KeyCode::Char('q') => return Ok(true),
_ => {}
}
Ok(false)
}
fn handle_game_detail(&mut self, key: KeyCode) -> Result<bool> {
let detail = match &mut self.screen {
AppScreen::GameDetail(d) => d,
_ => return Ok(false),
};
if !detail.download_completion_acknowledged {
if let Ok(list) = detail.downloads.lock() {
let has_completed = list.iter().any(|j| {
j.rom_id == detail.rom.id
&& matches!(
j.status,
crate::core::download::DownloadStatus::Done
| crate::core::download::DownloadStatus::Error(_)
)
});
let is_still_downloading = list.iter().any(|j| {
j.rom_id == detail.rom.id
&& matches!(j.status, crate::core::download::DownloadStatus::Downloading)
});
if has_completed && !is_still_downloading {
detail.download_completion_acknowledged = true;
}
}
}
match key {
KeyCode::Enter if !detail.has_started_download => {
detail.has_started_download = true;
self.downloads
.start_download(&detail.rom, self.client.clone());
}
KeyCode::Char('o') => detail.open_cover(),
KeyCode::Char('m') => detail.toggle_technical(),
KeyCode::Esc => {
detail.clear_message();
let prev =
std::mem::replace(&mut self.screen, AppScreen::MainMenu(MainMenuScreen::new()));
if let AppScreen::GameDetail(g) = prev {
self.screen = match g.previous {
GameDetailPrevious::Library(l) => AppScreen::LibraryBrowse(l),
GameDetailPrevious::Search(s) => AppScreen::Search(s),
};
}
}
KeyCode::Char('q') => return Ok(true),
_ => {}
}
Ok(false)
}
async fn handle_setup_wizard(&mut self, key: KeyCode) -> Result<bool> {
let wizard = match &mut self.screen {
AppScreen::SetupWizard(w) => w,
_ => return Ok(false),
};
let event = crossterm::event::KeyEvent::new(key, crossterm::event::KeyModifiers::empty());
if wizard.handle_key(event)? {
self.screen = AppScreen::Settings(SettingsScreen::new(
&self.config,
self.server_version.as_deref(),
));
return Ok(false);
}
if wizard.testing {
let result = wizard.try_connect_and_persist(self.client.verbose()).await;
wizard.testing = false;
match result {
Ok(cfg) => {
let auth_ok = cfg.auth.is_some();
self.config = cfg;
if let Ok(new_client) = RommClient::new(&self.config, self.client.verbose()) {
self.client = new_client;
}
let mut settings =
SettingsScreen::new(&self.config, self.server_version.as_deref());
if auth_ok {
settings.message = Some((
"Authentication updated successfully".to_string(),
Color::Green,
));
} else {
settings.message = Some((
"Saved configuration but credentials could not be loaded from the OS keyring (see logs)."
.to_string(),
Color::Yellow,
));
}
self.screen = AppScreen::Settings(settings);
}
Err(e) => {
wizard.error = Some(format!("{e:#}"));
}
}
}
Ok(false)
}
fn render(&mut self, f: &mut ratatui::Frame) {
let area = f.size();
if let Some(ref splash) = self.startup_splash {
connected_splash::render(f, area, splash);
return;
}
match &mut self.screen {
AppScreen::MainMenu(menu) => menu.render(f, area),
AppScreen::LibraryBrowse(lib) => lib.render(f, area),
AppScreen::Search(search) => {
search.render(f, area);
if let Some((x, y)) = search.cursor_position(area) {
f.set_cursor(x, y);
}
}
AppScreen::Settings(settings) => {
settings.render(f, area);
if let Some((x, y)) = settings.cursor_position(area) {
f.set_cursor(x, y);
}
}
AppScreen::Browse(browse) => browse.render(f, area),
AppScreen::Execute(execute) => {
execute.render(f, area);
if let Some((x, y)) = execute.cursor_position(area) {
f.set_cursor(x, y);
}
}
AppScreen::Result(result) => result.render(f, area),
AppScreen::ResultDetail(detail) => detail.render(f, area),
AppScreen::GameDetail(detail) => detail.render(f, area),
AppScreen::Download(d) => d.render(f, area),
AppScreen::SetupWizard(wizard) => {
wizard.render(f, area);
if let Some((x, y)) = wizard.cursor_pos(area) {
f.set_cursor(x, y);
}
}
}
if self.show_keyboard_help {
keyboard_help::render_keyboard_help(f, area);
}
if let Some(ref err) = self.global_error {
let popup_area = ratatui::layout::Rect {
x: area.width.saturating_sub(60) / 2,
y: area.height.saturating_sub(10) / 2,
width: 60.min(area.width),
height: 10.min(area.height),
};
f.render_widget(ratatui::widgets::Clear, popup_area);
let block = ratatui::widgets::Block::default()
.title("Error")
.borders(ratatui::widgets::Borders::ALL)
.style(ratatui::style::Style::default().fg(ratatui::style::Color::Red));
let text = format!("{}\n\nPress Esc to dismiss", err);
let paragraph = ratatui::widgets::Paragraph::new(text)
.block(block)
.wrap(ratatui::widgets::Wrap { trim: true });
f.render_widget(paragraph, popup_area);
}
}
}