Add shortcut to reload database and/or library #116

Merged
wojtek merged 14 commits from 105---add-shortcut-to-reload-database-and/or-library into main 2024-02-03 14:32:13 +01:00
5 changed files with 131 additions and 150 deletions
Showing only changes of commit 805b62e241 - Show all commits

View File

@ -67,7 +67,7 @@ fn with<LIB: ILibrary, DB: IDatabase>(builder: MusicHoardBuilder<LIB, DB>) {
let listener = EventListener::new(channel.sender()); let listener = EventListener::new(channel.sender());
let handler = EventHandler::new(channel.receiver()); let handler = EventHandler::new(channel.receiver());
let app = App::new(music_hoard).expect("failed to initialise application"); let app = App::new(music_hoard);
let ui = Ui; let ui = Ui;
// Run the TUI application. // Run the TUI application.

View File

@ -1,28 +1,7 @@
use std::fmt;
use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection}; use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection};
use ratatui::widgets::ListState; use ratatui::widgets::ListState;
use crate::tui::{lib::IMusicHoard, Error}; use crate::tui::lib::IMusicHoard;
#[derive(Debug)]
pub enum AppError {
Lib(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::Lib(ref s) => write!(f, "the musichoard library returned an error: {s}"),
}
}
}
impl From<musichoard::Error> for AppError {
fn from(err: musichoard::Error) -> AppError {
AppError::Lib(err.to_string())
}
}
pub enum AppState<BS, IS, RS, ES> { pub enum AppState<BS, IS, RS, ES> {
Browse(BS), Browse(BS),
@ -57,8 +36,9 @@ pub trait IAppInteract {
fn is_running(&self) -> bool; fn is_running(&self) -> bool;
fn quit(&mut self); fn quit(&mut self);
fn force_quit(&mut self);
fn save(&mut self) -> Result<(), AppError>; fn save(&mut self);
fn state(&mut self) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES>; fn state(&mut self) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES>;
} }
@ -337,17 +317,24 @@ pub struct App<MH: IMusicHoard> {
} }
impl<MH: IMusicHoard> App<MH> { impl<MH: IMusicHoard> App<MH> {
pub fn new(mut music_hoard: MH) -> Result<Self, Error> { pub fn new(mut music_hoard: MH) -> Self {
// FIXME: if either returns an error start in an error state let state = match Self::init(&mut music_hoard) {
music_hoard.load_from_database()?; Ok(()) => AppState::Browse(()),
music_hoard.rescan_library()?; Err(err) => AppState::Error(err.to_string()),
};
let selection = Selection::new(Some(music_hoard.get_collection())); let selection = Selection::new(Some(music_hoard.get_collection()));
Ok(App { App {
running: true, running: true,
music_hoard, music_hoard,
selection, selection,
state: AppState::Browse(()), state,
}) }
}
fn init(music_hoard: &mut MH) -> Result<(), musichoard::Error> {
music_hoard.load_from_database()?;
music_hoard.rescan_library()?;
Ok(())
} }
} }
@ -362,11 +349,19 @@ impl<MH: IMusicHoard> IAppInteract for App<MH> {
} }
fn quit(&mut self) { fn quit(&mut self) {
if !self.state.is_error() {
self.running = false;
}
}
fn force_quit(&mut self) {
self.running = false; self.running = false;
} }
fn save(&mut self) -> Result<(), AppError> { fn save(&mut self) {
Ok(self.music_hoard.save_to_database()?) if let Err(err) = self.music_hoard.save_to_database() {
self.state = AppState::Error(err.to_string());
}
} }
fn state(&mut self) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES> { fn state(&mut self) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES> {
@ -642,14 +637,50 @@ mod tests {
} }
#[test] #[test]
fn running() { fn running_quit() {
let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap(); let mut app = App::new(music_hoard(COLLECTION.to_owned()));
assert!(app.is_running()); assert!(app.is_running());
app.quit(); app.quit();
assert!(!app.is_running()); assert!(!app.is_running());
} }
#[test]
fn error_quit() {
let mut app = App::new(music_hoard(COLLECTION.to_owned()));
assert!(app.is_running());
app.state = AppState::Error(String::from("get rekt"));
app.quit();
assert!(app.is_running());
app.dismiss_error();
app.quit();
assert!(!app.is_running());
}
#[test]
fn running_force_quit() {
let mut app = App::new(music_hoard(COLLECTION.to_owned()));
assert!(app.is_running());
app.force_quit();
assert!(!app.is_running());
}
#[test]
fn error_force_quit() {
let mut app = App::new(music_hoard(COLLECTION.to_owned()));
assert!(app.is_running());
app.state = AppState::Error(String::from("get rekt"));
app.force_quit();
assert!(!app.is_running());
}
#[test] #[test]
fn save() { fn save() {
let mut music_hoard = music_hoard(COLLECTION.to_owned()); let mut music_hoard = music_hoard(COLLECTION.to_owned());
@ -659,15 +690,47 @@ mod tests {
.times(1) .times(1)
.return_once(|| Ok(())); .return_once(|| Ok(()));
let mut app = App::new(music_hoard).unwrap(); let mut app = App::new(music_hoard);
let result = app.save(); app.save();
assert!(result.is_ok()); assert!(app.state.is_browse());
}
#[test]
fn save_error() {
let mut music_hoard = music_hoard(COLLECTION.to_owned());
music_hoard
.expect_save_to_database()
.times(1)
.return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt"))));
let mut app = App::new(music_hoard);
app.save();
assert!(app.state.is_error());
}
#[test]
fn init_error() {
let mut music_hoard = MockIMusicHoard::new();
music_hoard
.expect_load_from_database()
.times(1)
.return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt"))));
music_hoard.expect_get_collection().return_const(vec![]);
let app = App::new(music_hoard);
assert!(app.is_running());
assert!(app.state.is_error());
} }
#[test] #[test]
fn modifiers() { fn modifiers() {
let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap(); let mut app = App::new(music_hoard(COLLECTION.to_owned()));
assert!(app.is_running()); assert!(app.is_running());
assert_eq!(app.selection.active, Category::Artist); assert_eq!(app.selection.active, Category::Artist);
@ -759,7 +822,7 @@ mod tests {
let mut collection = COLLECTION.to_owned(); let mut collection = COLLECTION.to_owned();
collection[0].albums[0].tracks = vec![]; collection[0].albums[0].tracks = vec![];
let mut app = App::new(music_hoard(collection)).unwrap(); let mut app = App::new(music_hoard(collection));
assert!(app.is_running()); assert!(app.is_running());
assert_eq!(app.selection.active, Category::Artist); assert_eq!(app.selection.active, Category::Artist);
@ -788,7 +851,7 @@ mod tests {
let mut collection = COLLECTION.to_owned(); let mut collection = COLLECTION.to_owned();
collection[0].albums = vec![]; collection[0].albums = vec![];
let mut app = App::new(music_hoard(collection)).unwrap(); let mut app = App::new(music_hoard(collection));
assert!(app.is_running()); assert!(app.is_running());
assert_eq!(app.selection.active, Category::Artist); assert_eq!(app.selection.active, Category::Artist);
@ -827,7 +890,7 @@ mod tests {
#[test] #[test]
fn no_artists() { fn no_artists() {
let mut app = App::new(music_hoard(vec![])).unwrap(); let mut app = App::new(music_hoard(vec![]));
assert!(app.is_running()); assert!(app.is_running());
assert_eq!(app.selection.active, Category::Artist); assert_eq!(app.selection.active, Category::Artist);
@ -880,7 +943,7 @@ mod tests {
#[test] #[test]
fn info_overlay() { fn info_overlay() {
let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap(); let mut app = App::new(music_hoard(COLLECTION.to_owned()));
assert!(app.state().is_browse()); assert!(app.state().is_browse());
app.show_info_overlay(); app.show_info_overlay();
@ -892,7 +955,7 @@ mod tests {
#[test] #[test]
fn reload_go_back() { fn reload_go_back() {
let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap(); let mut app = App::new(music_hoard(COLLECTION.to_owned()));
assert!(app.state().is_browse()); assert!(app.state().is_browse());
app.show_reload_menu(); app.show_reload_menu();
@ -911,7 +974,7 @@ mod tests {
.times(1) .times(1)
.return_once(|| Ok(())); .return_once(|| Ok(()));
let mut app = App::new(music_hoard).unwrap(); let mut app = App::new(music_hoard);
assert!(app.state().is_browse()); assert!(app.state().is_browse());
app.show_reload_menu(); app.show_reload_menu();
@ -930,7 +993,7 @@ mod tests {
.times(1) .times(1)
.return_once(|| Ok(())); .return_once(|| Ok(()));
let mut app = App::new(music_hoard).unwrap(); let mut app = App::new(music_hoard);
assert!(app.state().is_browse()); assert!(app.state().is_browse());
app.show_reload_menu(); app.show_reload_menu();
@ -949,7 +1012,7 @@ mod tests {
.times(1) .times(1)
.return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt"))));
let mut app = App::new(music_hoard).unwrap(); let mut app = App::new(music_hoard);
assert!(app.state().is_browse()); assert!(app.state().is_browse());
app.show_reload_menu(); app.show_reload_menu();
@ -961,13 +1024,4 @@ mod tests {
app.dismiss_error(); app.dismiss_error();
assert!(app.state().is_browse()); assert!(app.state().is_browse());
} }
#[test]
fn errors() {
let app_err: AppError = musichoard::Error::DatabaseError(String::from("get rekt")).into();
assert!(!app_err.to_string().is_empty());
assert!(!format!("{:?}", app_err).is_empty());
}
} }

View File

@ -2,14 +2,11 @@ use crossterm::event::{KeyEvent, MouseEvent};
use std::fmt; use std::fmt;
use std::sync::mpsc; use std::sync::mpsc;
use crate::tui::app::AppError;
#[derive(Debug)] #[derive(Debug)]
pub enum EventError { pub enum EventError {
Send(Event), Send(Event),
Recv, Recv,
Io(std::io::Error), Io(std::io::Error),
App(String),
} }
impl fmt::Display for EventError { impl fmt::Display for EventError {
@ -20,12 +17,6 @@ impl fmt::Display for EventError {
Self::Io(ref e) => { Self::Io(ref e) => {
write!(f, "an I/O error was triggered during event handling: {e}") write!(f, "an I/O error was triggered during event handling: {e}")
} }
Self::App(ref s) => {
write!(
f,
"the application returned an error during event handling: {s}"
)
}
} }
} }
} }
@ -42,12 +33,6 @@ impl From<mpsc::RecvError> for EventError {
} }
} }
impl From<AppError> for EventError {
fn from(err: AppError) -> EventError {
EventError::App(err.to_string())
}
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum Event { pub enum Event {
Key(KeyEvent), Key(KeyEvent),
@ -148,16 +133,13 @@ mod tests {
})); }));
let recv_err = EventError::Recv; let recv_err = EventError::Recv;
let io_err = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "interrupted")); let io_err = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "interrupted"));
let app_err: EventError = AppError::Lib(String::from("lib error")).into();
assert!(!send_err.to_string().is_empty()); assert!(!send_err.to_string().is_empty());
assert!(!recv_err.to_string().is_empty()); assert!(!recv_err.to_string().is_empty());
assert!(!io_err.to_string().is_empty()); assert!(!io_err.to_string().is_empty());
assert!(!app_err.to_string().is_empty());
assert!(!format!("{:?}", send_err).is_empty()); assert!(!format!("{:?}", send_err).is_empty());
assert!(!format!("{:?}", recv_err).is_empty()); assert!(!format!("{:?}", recv_err).is_empty());
assert!(!format!("{:?}", io_err).is_empty()); assert!(!format!("{:?}", io_err).is_empty());
assert!(!format!("{:?}", app_err).is_empty());
} }
} }

View File

@ -1,4 +1,3 @@
// FIXME: Can code here be less verbose
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[cfg(test)] #[cfg(test)]
@ -18,24 +17,11 @@ pub trait IEventHandler<APP: IAppInteract> {
} }
trait IEventHandlerPrivate<APP: IAppInteract> { trait IEventHandlerPrivate<APP: IAppInteract> {
fn handle_key_event(app: &mut APP, key_event: KeyEvent) -> Result<(), EventError>; fn handle_key_event(app: &mut APP, key_event: KeyEvent);
fn handle_browse_key_event( fn handle_browse_key_event(app: &mut <APP as IAppInteract>::BS, key_event: KeyEvent);
app: &mut <APP as IAppInteract>::BS, fn handle_info_key_event(app: &mut <APP as IAppInteract>::IS, key_event: KeyEvent);
key_event: KeyEvent, fn handle_reload_key_event(app: &mut <APP as IAppInteract>::RS, key_event: KeyEvent);
) -> Result<(), EventError>; fn handle_error_key_event(app: &mut <APP as IAppInteract>::ES, key_event: KeyEvent);
fn handle_info_key_event(
app: &mut <APP as IAppInteract>::IS,
key_event: KeyEvent,
) -> Result<(), EventError>;
fn handle_reload_key_event(
app: &mut <APP as IAppInteract>::RS,
key_event: KeyEvent,
) -> Result<(), EventError>;
fn handle_error_key_event(
app: &mut <APP as IAppInteract>::ES,
key_event: KeyEvent,
) -> Result<(), EventError>;
fn quit(app: &mut APP) -> Result<(), EventError>;
} }
pub struct EventHandler { pub struct EventHandler {
@ -52,7 +38,7 @@ impl EventHandler {
impl<APP: IAppInteract> IEventHandler<APP> for EventHandler { impl<APP: IAppInteract> IEventHandler<APP> for EventHandler {
fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> { fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> {
match self.events.recv()? { match self.events.recv()? {
Event::Key(key_event) => Self::handle_key_event(app, key_event)?, Event::Key(key_event) => Self::handle_key_event(app, key_event),
Event::Mouse(_) => {} Event::Mouse(_) => {}
Event::Resize(_, _) => {} Event::Resize(_, _) => {}
}; };
@ -61,44 +47,37 @@ 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) -> Result<(), EventError> { fn handle_key_event(app: &mut APP, key_event: KeyEvent) {
match key_event.code { match key_event.code {
// Exit application on `ESC` or `q`. // Exit application on `ESC` or `q`.
KeyCode::Esc | KeyCode::Char('q') => { KeyCode::Esc | KeyCode::Char('q') => {
Self::quit(app)?; 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 {
Self::quit(app)?; app.force_quit();
} }
} }
_ => match app.state() { _ => match app.state() {
AppState::Browse(browse) => { AppState::Browse(browse) => {
<Self as IEventHandlerPrivate<APP>>::handle_browse_key_event( <Self as IEventHandlerPrivate<APP>>::handle_browse_key_event(browse, key_event);
browse, key_event,
)?;
} }
AppState::Info(info) => { AppState::Info(info) => {
<Self as IEventHandlerPrivate<APP>>::handle_info_key_event(info, key_event)?; <Self as IEventHandlerPrivate<APP>>::handle_info_key_event(info, key_event);
} }
AppState::Reload(reload) => { AppState::Reload(reload) => {
<Self as IEventHandlerPrivate<APP>>::handle_reload_key_event( <Self as IEventHandlerPrivate<APP>>::handle_reload_key_event(reload, key_event);
reload, key_event,
)?;
} }
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);
} }
}, },
}; }
Ok(())
} }
fn handle_browse_key_event( fn handle_browse_key_event(app: &mut <APP as IAppInteract>::BS, key_event: KeyEvent) {
app: &mut <APP as IAppInteract>::BS,
key_event: KeyEvent,
) -> Result<(), EventError> {
match key_event.code { match key_event.code {
// Category change. // Category change.
KeyCode::Left => app.decrement_category(), KeyCode::Left => app.decrement_category(),
@ -113,28 +92,18 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
// Othey keys. // Othey keys.
_ => {} _ => {}
} }
Ok(())
} }
fn handle_info_key_event( fn handle_info_key_event(app: &mut <APP as IAppInteract>::IS, key_event: KeyEvent) {
app: &mut <APP as IAppInteract>::IS,
key_event: KeyEvent,
) -> Result<(), EventError> {
match key_event.code { match key_event.code {
// Toggle overlay. // Toggle overlay.
KeyCode::Char('m') | KeyCode::Char('M') => app.hide_info_overlay(), KeyCode::Char('m') | KeyCode::Char('M') => app.hide_info_overlay(),
// Othey keys. // Othey keys.
_ => {} _ => {}
} }
Ok(())
} }
fn handle_reload_key_event( fn handle_reload_key_event(app: &mut <APP as IAppInteract>::RS, key_event: KeyEvent) {
app: &mut <APP as IAppInteract>::RS,
key_event: KeyEvent,
) -> Result<(), EventError> {
match key_event.code { match key_event.code {
// Reload keys. // Reload keys.
KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(), KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(),
@ -144,23 +113,11 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
// Othey keys. // Othey keys.
_ => {} _ => {}
} }
Ok(())
} }
fn handle_error_key_event( fn handle_error_key_event(app: &mut <APP as IAppInteract>::ES, _key_event: KeyEvent) {
app: &mut <APP as IAppInteract>::ES,
_key_event: KeyEvent,
) -> Result<(), EventError> {
// Any key dismisses the error. // Any key dismisses the error.
app.dismiss_error(); app.dismiss_error();
Ok(())
}
fn quit(app: &mut APP) -> Result<(), EventError> {
app.quit();
app.save()?;
Ok(())
} }
} }
// GRCOV_EXCL_STOP // GRCOV_EXCL_STOP

View File

@ -28,18 +28,11 @@ use crate::tui::{
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Error { pub enum Error {
Lib(String),
Io(String), Io(String),
Event(String), Event(String),
ListenerPanic, ListenerPanic,
} }
impl From<musichoard::Error> for Error {
fn from(err: musichoard::Error) -> Error {
Error::Lib(err.to_string())
}
}
impl From<io::Error> for Error { impl From<io::Error> for Error {
fn from(err: io::Error) -> Error { fn from(err: io::Error) -> Error {
Error::Io(err.to_string()) Error::Io(err.to_string())
@ -81,9 +74,6 @@ impl<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> Tui<B, UI, APP> {
handler: impl IEventHandler<APP>, handler: impl IEventHandler<APP>,
) -> Result<(), Error> { ) -> Result<(), Error> {
while app.is_running() { while app.is_running() {
// FIXME: rendering logic should be in render.rs - reverse Renderer/Ui relationship so
// that TAPP calls are renderer.render(ui, frame); Then rename ui -> app and render -> ui
// as it used to be originally.
self.terminal.draw(|frame| UI::render(&mut app, frame))?; self.terminal.draw(|frame| UI::render(&mut app, frame))?;
handler.handle_next_event(&mut app)?; handler.handle_next_event(&mut app)?;
} }
@ -123,7 +113,7 @@ impl<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> Tui<B, UI, APP> {
Ok(err) => return Err(err.into()), Ok(err) => return Err(err.into()),
// Calling std::panic::resume_unwind(err) as recommended by the Rust docs // Calling std::panic::resume_unwind(err) as recommended by the Rust docs
// will not produce an error message. The panic error message is printed at // will not produce an error message. The panic error message is printed at
// the location of the panic which at the time is hidden by the TAPP. // the location of the panic which at the time is hidden by the TUI.
Err(_) => return Err(Error::ListenerPanic), Err(_) => return Err(Error::ListenerPanic),
} }
} }
@ -211,7 +201,7 @@ mod tests {
} }
fn app(collection: Collection) -> App<MockIMusicHoard> { fn app(collection: Collection) -> App<MockIMusicHoard> {
App::new(music_hoard(collection)).unwrap() App::new(music_hoard(collection))
} }
fn listener() -> MockIEventListener { fn listener() -> MockIEventListener {
@ -319,12 +309,10 @@ mod tests {
#[test] #[test]
fn errors() { fn errors() {
let lib_err: Error = musichoard::Error::DatabaseError(String::from("")).into();
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into(); let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into();
let event_err: Error = EventError::Recv.into(); let event_err: Error = EventError::Recv.into();
let listener_err = Error::ListenerPanic; let listener_err = Error::ListenerPanic;
assert!(!format!("{:?}", lib_err).is_empty());
assert!(!format!("{:?}", io_err).is_empty()); assert!(!format!("{:?}", io_err).is_empty());
assert!(!format!("{:?}", event_err).is_empty()); assert!(!format!("{:?}", event_err).is_empty());
assert!(!format!("{:?}", listener_err).is_empty()); assert!(!format!("{:?}", listener_err).is_empty());