Add critical error state #124

Merged
wojtek merged 1 commits from 123---a-failed-initial-read-of-the-database-does-not-prevent-saving into main 2024-02-09 20:07:48 +01:00
4 changed files with 68 additions and 41 deletions

View File

@ -7,14 +7,15 @@ use crate::tui::{
lib::IMusicHoard,
};
pub enum AppState<BS, IS, RS, ES> {
pub enum AppState<BS, IS, RS, ES, CS> {
Browse(BS),
Info(IS),
Reload(RS),
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 {
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<MH: IMusicHoard> {
running: bool,
music_hoard: MH,
selection: Selection,
state: AppState<(), (), (), String>,
state: AppState<(), (), (), String, String>,
}
impl<MH: IMusicHoard> App<MH> {
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<MH: IMusicHoard> IAppInteract for App<MH> {
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<MH: IMusicHoard> IAppInteractBrowse for App<MH> {
fn quit(&mut self) {
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) {
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> {
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]

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_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_critical_key_event(app: &mut <APP as IAppInteract>::CS, key_event: KeyEvent);
}
pub struct EventHandler {
@ -52,11 +53,6 @@ impl<APP: IAppInteract> IEventHandler<APP> for EventHandler {
impl<APP: IAppInteract> IEventHandlerPrivate<APP> 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<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
AppState::Error(error) => {
<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) {
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<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
fn handle_info_key_event(app: &mut <APP as IAppInteract>::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<APP: IAppInteract> IEventHandlerPrivate<APP> 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<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
// Any key dismisses the 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

View File

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

View File

@ -510,7 +510,7 @@ impl Ui {
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()
.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]