parent
c2506657c3
commit
87de8d2b4e
@ -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]
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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]
|
||||||
|
Loading…
Reference in New Issue
Block a user