diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index 9ebf397..b6857ef 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -7,14 +7,15 @@ use crate::tui::{ lib::IMusicHoard, }; -pub enum AppState { +pub enum AppState { Browse(BS), Info(IS), Reload(RS), Error(ES), + Critical(CS), } -impl AppState { +impl AppState { fn is_browse(&self) -> bool { matches!(self, AppState::Browse(_)) } @@ -37,17 +38,21 @@ pub trait IAppInteract { type IS: IAppInteractInfo; type RS: IAppInteractReload; type ES: IAppInteractError; + type CS: IAppInteractCritical; fn is_running(&self) -> bool; - fn quit(&mut self); fn force_quit(&mut self); - fn save(&mut self); - - fn state(&mut self) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES>; + #[allow(clippy::type_complexity)] + fn state( + &mut self, + ) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES, &mut Self::CS>; } pub trait IAppInteractBrowse { + fn save(&mut self); + fn quit(&mut self); + fn increment_category(&mut self); fn decrement_category(&mut self); fn increment_selection(&mut self, delta: Delta); @@ -72,6 +77,8 @@ pub trait IAppInteractError { fn dismiss_error(&mut self); } +pub trait IAppInteractCritical {} + // It would be preferable to have a getter for each field separately. However, the selection field // needs to be mutably accessible requiring a mutable borrow of the entire struct if behind a trait. // This in turn complicates simultaneous field access since only a single mutable borrow is allowed. @@ -83,21 +90,21 @@ pub trait IAppAccess { pub struct AppPublic<'app> { pub collection: &'app Collection, pub selection: &'app mut Selection, - pub state: &'app AppState<(), (), (), String>, + pub state: &'app AppState<(), (), (), String, String>, } pub struct App { running: bool, music_hoard: MH, selection: Selection, - state: AppState<(), (), (), String>, + state: AppState<(), (), (), String, String>, } impl App { pub fn new(mut music_hoard: MH) -> Self { let state = match Self::init(&mut music_hoard) { Ok(()) => AppState::Browse(()), - Err(err) => AppState::Error(err.to_string()), + Err(err) => AppState::Critical(err.to_string()), }; let selection = Selection::new(music_hoard.get_collection()); App { @@ -120,18 +127,31 @@ impl IAppInteract for App { type IS = Self; type RS = Self; type ES = Self; + type CS = Self; fn is_running(&self) -> bool { self.running } - fn quit(&mut self) { - if !self.state.is_error() { - self.running = false; - } + fn force_quit(&mut self) { + self.running = false; } - fn force_quit(&mut self) { + fn state( + &mut self, + ) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES, &mut Self::CS> { + match self.state { + AppState::Browse(_) => AppState::Browse(self), + AppState::Info(_) => AppState::Info(self), + AppState::Reload(_) => AppState::Reload(self), + AppState::Error(_) => AppState::Error(self), + AppState::Critical(_) => AppState::Critical(self), + } + } +} + +impl IAppInteractBrowse for App { + fn quit(&mut self) { self.running = false; } @@ -141,17 +161,6 @@ impl IAppInteract for App { } } - fn state(&mut self) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES> { - match self.state { - AppState::Browse(_) => AppState::Browse(self), - AppState::Info(_) => AppState::Info(self), - AppState::Reload(_) => AppState::Reload(self), - AppState::Error(_) => AppState::Error(self), - } - } -} - -impl IAppInteractBrowse for App { fn increment_category(&mut self) { self.selection.increment_category(); } @@ -232,6 +241,8 @@ impl IAppInteractError for App { } } +impl IAppInteractCritical for App {} + impl IAppAccess for App { fn get(&mut self) -> AppPublic { AppPublic { @@ -280,9 +291,6 @@ mod tests { app.state = AppState::Error(String::from("get rekt")); - app.quit(); - assert!(app.is_running()); - app.dismiss_error(); app.quit(); @@ -350,10 +358,10 @@ mod tests { .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); music_hoard.expect_get_collection().return_const(vec![]); - let app = App::new(music_hoard); + let mut app = App::new(music_hoard); assert!(app.is_running()); - assert!(app.state.is_error()); + assert!(matches!(app.state(), AppState::Critical(_))); } #[test] diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 4141a23..a0352c6 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -25,6 +25,7 @@ trait IEventHandlerPrivate { fn handle_info_key_event(app: &mut ::IS, key_event: KeyEvent); fn handle_reload_key_event(app: &mut ::RS, key_event: KeyEvent); fn handle_error_key_event(app: &mut ::ES, key_event: KeyEvent); + fn handle_critical_key_event(app: &mut ::CS, key_event: KeyEvent); } pub struct EventHandler { @@ -52,11 +53,6 @@ impl IEventHandler for EventHandler { impl IEventHandlerPrivate for EventHandler { fn handle_key_event(app: &mut APP, key_event: KeyEvent) { match key_event.code { - // Exit application on `ESC` or `q`. - KeyCode::Esc | KeyCode::Char('q') => { - app.save(); - app.quit(); - } // Exit application on `Ctrl-C`. KeyCode::Char('c') | KeyCode::Char('C') => { if key_event.modifiers == KeyModifiers::CONTROL { @@ -76,12 +72,22 @@ impl IEventHandlerPrivate for EventHandler { AppState::Error(error) => { >::handle_error_key_event(error, key_event); } + AppState::Critical(critical) => { + >::handle_critical_key_event( + critical, key_event, + ); + } }, } } fn handle_browse_key_event(app: &mut ::BS, key_event: KeyEvent) { match key_event.code { + // Exit application on `ESC` or `q`. + KeyCode::Esc | KeyCode::Char('q') => { + app.save(); + app.quit(); + } // Category change. KeyCode::Left => app.decrement_category(), KeyCode::Right => app.increment_category(), @@ -102,7 +108,9 @@ impl IEventHandlerPrivate for EventHandler { fn handle_info_key_event(app: &mut ::IS, key_event: KeyEvent) { match key_event.code { // Toggle overlay. - KeyCode::Char('m') | KeyCode::Char('M') => app.hide_info_overlay(), + KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('m') | KeyCode::Char('M') => { + app.hide_info_overlay() + } // Othey keys. _ => {} } @@ -114,7 +122,9 @@ impl IEventHandlerPrivate for EventHandler { KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(), KeyCode::Char('d') | KeyCode::Char('D') => app.reload_database(), // Return. - KeyCode::Char('g') | KeyCode::Char('G') => app.hide_reload_menu(), + KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('g') | KeyCode::Char('G') => { + app.hide_reload_menu() + } // Othey keys. _ => {} } @@ -124,5 +134,9 @@ impl IEventHandlerPrivate for EventHandler { // Any key dismisses the error. app.dismiss_error(); } + + fn handle_critical_key_event(_app: &mut ::CS, _key_event: KeyEvent) { + // No action is allowed. + } } // GRCOV_EXCL_STOP diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 1b4b663..b48e650 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -220,7 +220,7 @@ mod tests { handler .expect_handle_next_event() .return_once(|app: &mut App| { - app.quit(); + app.force_quit(); Ok(()) }); handler diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 8d9d952..2aa2d78 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -510,7 +510,7 @@ impl Ui { Self::render_overlay_widget("Reload", reload_text, area, false, frame); } - fn render_error_overlay, B: Backend>(msg: S, frame: &mut Frame<'_, B>) { + fn render_error_overlay, B: Backend>(title: S, msg: S, frame: &mut Frame<'_, B>) { let area = OverlayBuilder::default() .with_height(OverlaySize::Value(4)) .build(frame.size()); @@ -519,7 +519,7 @@ impl Ui { .alignment(Alignment::Center) .wrap(Wrap { trim: true }); - Self::render_overlay_widget("Error", error_text, area, true, frame); + Self::render_overlay_widget(title.as_ref(), error_text, area, true, frame); } } @@ -534,7 +534,8 @@ impl IUi for Ui { match app.state { AppState::Info(_) => Self::render_info_overlay(collection, selection, frame), AppState::Reload(_) => Self::render_reload_overlay(frame), - AppState::Error(ref msg) => Self::render_error_overlay(msg, frame), + AppState::Error(ref msg) => Self::render_error_overlay("Error", msg, frame), + AppState::Critical(ref msg) => Self::render_error_overlay("Critical Error", msg, frame), _ => {} } } @@ -580,6 +581,10 @@ mod tests { let binding = AppState::Error(String::from("get rekt scrub")); app.state = &binding; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); + + let binding = AppState::Critical(String::from("get critically rekt scrub")); + app.state = &binding; + terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } #[test]