Add critical error state (#124)
All checks were successful
Cargo CI / Lint (push) Successful in 42s
Cargo CI / Build and Test (push) Successful in 1m2s

Closes #123

Reviewed-on: #124
This commit is contained in:
Wojciech Kozlowski 2024-02-09 20:07:48 +01:00
parent c2506657c3
commit 87de8d2b4e
4 changed files with 68 additions and 41 deletions

View File

@ -7,14 +7,15 @@ use crate::tui::{
lib::IMusicHoard, lib::IMusicHoard,
}; };
pub enum AppState<BS, IS, RS, ES> { pub enum AppState<BS, IS, RS, ES, CS> {
Browse(BS), Browse(BS),
Info(IS), Info(IS),
Reload(RS), Reload(RS),
Error(ES), Error(ES),
Critical(CS),
} }
impl<BS, IS, RS, ES> AppState<BS, IS, RS, ES> { impl<BS, IS, RS, ES, CS> AppState<BS, IS, RS, ES, CS> {
fn is_browse(&self) -> bool { fn is_browse(&self) -> bool {
matches!(self, AppState::Browse(_)) matches!(self, AppState::Browse(_))
} }
@ -37,17 +38,21 @@ pub trait IAppInteract {
type IS: IAppInteractInfo; type IS: IAppInteractInfo;
type RS: IAppInteractReload; type RS: IAppInteractReload;
type ES: IAppInteractError; type ES: IAppInteractError;
type CS: IAppInteractCritical;
fn is_running(&self) -> bool; fn is_running(&self) -> bool;
fn quit(&mut self);
fn force_quit(&mut self); fn force_quit(&mut self);
fn save(&mut self); #[allow(clippy::type_complexity)]
fn state(
fn state(&mut self) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES>; &mut self,
) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES, &mut Self::CS>;
} }
pub trait IAppInteractBrowse { pub trait IAppInteractBrowse {
fn save(&mut self);
fn quit(&mut self);
fn increment_category(&mut self); fn increment_category(&mut self);
fn decrement_category(&mut self); fn decrement_category(&mut self);
fn increment_selection(&mut self, delta: Delta); fn increment_selection(&mut self, delta: Delta);
@ -72,6 +77,8 @@ pub trait IAppInteractError {
fn dismiss_error(&mut self); fn dismiss_error(&mut self);
} }
pub trait IAppInteractCritical {}
// It would be preferable to have a getter for each field separately. However, the selection field // 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. // 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. // 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 struct AppPublic<'app> {
pub collection: &'app Collection, pub collection: &'app Collection,
pub selection: &'app mut Selection, pub selection: &'app mut Selection,
pub state: &'app AppState<(), (), (), String>, pub state: &'app AppState<(), (), (), String, String>,
} }
pub struct App<MH: IMusicHoard> { pub struct App<MH: IMusicHoard> {
running: bool, running: bool,
music_hoard: MH, music_hoard: MH,
selection: Selection, selection: Selection,
state: AppState<(), (), (), String>, state: AppState<(), (), (), String, String>,
} }
impl<MH: IMusicHoard> App<MH> { impl<MH: IMusicHoard> App<MH> {
pub fn new(mut music_hoard: MH) -> Self { pub fn new(mut music_hoard: MH) -> Self {
let state = match Self::init(&mut music_hoard) { let state = match Self::init(&mut music_hoard) {
Ok(()) => AppState::Browse(()), 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()); let selection = Selection::new(music_hoard.get_collection());
App { App {
@ -120,18 +127,31 @@ impl<MH: IMusicHoard> IAppInteract for App<MH> {
type IS = Self; type IS = Self;
type RS = Self; type RS = Self;
type ES = Self; type ES = Self;
type CS = Self;
fn is_running(&self) -> bool { fn is_running(&self) -> bool {
self.running self.running
} }
fn quit(&mut self) { fn force_quit(&mut self) {
if !self.state.is_error() { self.running = false;
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<MH: IMusicHoard> IAppInteractBrowse for App<MH> {
fn quit(&mut self) {
self.running = false; self.running = false;
} }
@ -141,17 +161,6 @@ impl<MH: IMusicHoard> IAppInteract for App<MH> {
} }
} }
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<MH: IMusicHoard> IAppInteractBrowse for App<MH> {
fn increment_category(&mut self) { fn increment_category(&mut self) {
self.selection.increment_category(); self.selection.increment_category();
} }
@ -232,6 +241,8 @@ impl<MH: IMusicHoard> IAppInteractError for App<MH> {
} }
} }
impl<MH: IMusicHoard> IAppInteractCritical for App<MH> {}
impl<MH: IMusicHoard> IAppAccess for App<MH> { impl<MH: IMusicHoard> IAppAccess for App<MH> {
fn get(&mut self) -> AppPublic { fn get(&mut self) -> AppPublic {
AppPublic { AppPublic {
@ -280,9 +291,6 @@ mod tests {
app.state = AppState::Error(String::from("get rekt")); app.state = AppState::Error(String::from("get rekt"));
app.quit();
assert!(app.is_running());
app.dismiss_error(); app.dismiss_error();
app.quit(); app.quit();
@ -350,10 +358,10 @@ mod tests {
.return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt"))));
music_hoard.expect_get_collection().return_const(vec![]); 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.is_running());
assert!(app.state.is_error()); assert!(matches!(app.state(), AppState::Critical(_)));
} }
#[test] #[test]

View File

@ -25,6 +25,7 @@ trait IEventHandlerPrivate<APP: IAppInteract> {
fn handle_info_key_event(app: &mut <APP as IAppInteract>::IS, key_event: KeyEvent); fn handle_info_key_event(app: &mut <APP as IAppInteract>::IS, key_event: KeyEvent);
fn handle_reload_key_event(app: &mut <APP as IAppInteract>::RS, key_event: KeyEvent); fn handle_reload_key_event(app: &mut <APP as IAppInteract>::RS, key_event: KeyEvent);
fn handle_error_key_event(app: &mut <APP as IAppInteract>::ES, key_event: KeyEvent); fn handle_error_key_event(app: &mut <APP as IAppInteract>::ES, key_event: KeyEvent);
fn handle_critical_key_event(app: &mut <APP as IAppInteract>::CS, key_event: KeyEvent);
} }
pub struct EventHandler { pub struct EventHandler {
@ -52,11 +53,6 @@ impl<APP: IAppInteract> IEventHandler<APP> for EventHandler {
impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler { impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
fn handle_key_event(app: &mut APP, key_event: KeyEvent) { fn handle_key_event(app: &mut APP, key_event: KeyEvent) {
match key_event.code { match key_event.code {
// Exit application on `ESC` or `q`.
KeyCode::Esc | KeyCode::Char('q') => {
app.save();
app.quit();
}
// Exit application on `Ctrl-C`. // Exit application on `Ctrl-C`.
KeyCode::Char('c') | KeyCode::Char('C') => { KeyCode::Char('c') | KeyCode::Char('C') => {
if key_event.modifiers == KeyModifiers::CONTROL { if key_event.modifiers == KeyModifiers::CONTROL {
@ -76,12 +72,22 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
AppState::Error(error) => { AppState::Error(error) => {
<Self as IEventHandlerPrivate<APP>>::handle_error_key_event(error, key_event); <Self as IEventHandlerPrivate<APP>>::handle_error_key_event(error, key_event);
} }
AppState::Critical(critical) => {
<Self as IEventHandlerPrivate<APP>>::handle_critical_key_event(
critical, key_event,
);
}
}, },
} }
} }
fn handle_browse_key_event(app: &mut <APP as IAppInteract>::BS, key_event: KeyEvent) { fn handle_browse_key_event(app: &mut <APP as IAppInteract>::BS, key_event: KeyEvent) {
match key_event.code { match key_event.code {
// Exit application on `ESC` or `q`.
KeyCode::Esc | KeyCode::Char('q') => {
app.save();
app.quit();
}
// Category change. // Category change.
KeyCode::Left => app.decrement_category(), KeyCode::Left => app.decrement_category(),
KeyCode::Right => app.increment_category(), KeyCode::Right => app.increment_category(),
@ -102,7 +108,9 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
fn handle_info_key_event(app: &mut <APP as IAppInteract>::IS, key_event: KeyEvent) { fn handle_info_key_event(app: &mut <APP as IAppInteract>::IS, key_event: KeyEvent) {
match key_event.code { match key_event.code {
// Toggle overlay. // 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. // Othey keys.
_ => {} _ => {}
} }
@ -114,7 +122,9 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(), KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(),
KeyCode::Char('d') | KeyCode::Char('D') => app.reload_database(), KeyCode::Char('d') | KeyCode::Char('D') => app.reload_database(),
// Return. // 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. // Othey keys.
_ => {} _ => {}
} }
@ -124,5 +134,9 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
// Any key dismisses the error. // Any key dismisses the error.
app.dismiss_error(); app.dismiss_error();
} }
fn handle_critical_key_event(_app: &mut <APP as IAppInteract>::CS, _key_event: KeyEvent) {
// No action is allowed.
}
} }
// GRCOV_EXCL_STOP // GRCOV_EXCL_STOP

View File

@ -220,7 +220,7 @@ mod tests {
handler handler
.expect_handle_next_event() .expect_handle_next_event()
.return_once(|app: &mut App<MockIMusicHoard>| { .return_once(|app: &mut App<MockIMusicHoard>| {
app.quit(); app.force_quit();
Ok(()) Ok(())
}); });
handler handler

View File

@ -510,7 +510,7 @@ impl Ui {
Self::render_overlay_widget("Reload", reload_text, area, false, frame); Self::render_overlay_widget("Reload", reload_text, area, false, frame);
} }
fn render_error_overlay<S: AsRef<str>, B: Backend>(msg: S, frame: &mut Frame<'_, B>) { fn render_error_overlay<S: AsRef<str>, B: Backend>(title: S, msg: S, frame: &mut Frame<'_, B>) {
let area = OverlayBuilder::default() let area = OverlayBuilder::default()
.with_height(OverlaySize::Value(4)) .with_height(OverlaySize::Value(4))
.build(frame.size()); .build(frame.size());
@ -519,7 +519,7 @@ impl Ui {
.alignment(Alignment::Center) .alignment(Alignment::Center)
.wrap(Wrap { trim: true }); .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 { match app.state {
AppState::Info(_) => Self::render_info_overlay(collection, selection, frame), AppState::Info(_) => Self::render_info_overlay(collection, selection, frame),
AppState::Reload(_) => Self::render_reload_overlay(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")); let binding = AppState::Error(String::from("get rekt scrub"));
app.state = &binding; app.state = &binding;
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); 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] #[test]