parent
c2506657c3
commit
87de8d2b4e
@ -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]
|
||||
|
@ -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
|
||||
|
@ -220,7 +220,7 @@ mod tests {
|
||||
handler
|
||||
.expect_handle_next_event()
|
||||
.return_once(|app: &mut App<MockIMusicHoard>| {
|
||||
app.quit();
|
||||
app.force_quit();
|
||||
Ok(())
|
||||
});
|
||||
handler
|
||||
|
@ -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]
|
||||
|
Loading…
Reference in New Issue
Block a user