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 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;
// Run the TUI application.

View File

@ -1,28 +1,7 @@
use std::fmt;
use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection};
use ratatui::widgets::ListState;
use crate::tui::{lib::IMusicHoard, Error};
#[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())
}
}
use crate::tui::lib::IMusicHoard;
pub enum AppState<BS, IS, RS, ES> {
Browse(BS),
@ -57,8 +36,9 @@ pub trait IAppInteract {
fn is_running(&self) -> bool;
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>;
}
@ -337,17 +317,24 @@ pub struct App<MH: IMusicHoard> {
}
impl<MH: IMusicHoard> App<MH> {
pub fn new(mut music_hoard: MH) -> Result<Self, Error> {
// FIXME: if either returns an error start in an error state
music_hoard.load_from_database()?;
music_hoard.rescan_library()?;
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()),
};
let selection = Selection::new(Some(music_hoard.get_collection()));
Ok(App {
App {
running: true,
music_hoard,
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) {
if !self.state.is_error() {
self.running = false;
}
}
fn force_quit(&mut self) {
self.running = false;
}
fn save(&mut self) -> Result<(), AppError> {
Ok(self.music_hoard.save_to_database()?)
fn save(&mut self) {
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> {
@ -642,14 +637,50 @@ mod tests {
}
#[test]
fn running() {
let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap();
fn running_quit() {
let mut app = App::new(music_hoard(COLLECTION.to_owned()));
assert!(app.is_running());
app.quit();
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]
fn save() {
let mut music_hoard = music_hoard(COLLECTION.to_owned());
@ -659,15 +690,47 @@ mod tests {
.times(1)
.return_once(|| Ok(()));
let mut app = App::new(music_hoard).unwrap();
let mut app = App::new(music_hoard);
let result = app.save();
assert!(result.is_ok());
app.save();
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]
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_eq!(app.selection.active, Category::Artist);
@ -759,7 +822,7 @@ mod tests {
let mut collection = COLLECTION.to_owned();
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_eq!(app.selection.active, Category::Artist);
@ -788,7 +851,7 @@ mod tests {
let mut collection = COLLECTION.to_owned();
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_eq!(app.selection.active, Category::Artist);
@ -827,7 +890,7 @@ mod tests {
#[test]
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_eq!(app.selection.active, Category::Artist);
@ -880,7 +943,7 @@ mod tests {
#[test]
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());
app.show_info_overlay();
@ -892,7 +955,7 @@ mod tests {
#[test]
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());
app.show_reload_menu();
@ -911,7 +974,7 @@ mod tests {
.times(1)
.return_once(|| Ok(()));
let mut app = App::new(music_hoard).unwrap();
let mut app = App::new(music_hoard);
assert!(app.state().is_browse());
app.show_reload_menu();
@ -930,7 +993,7 @@ mod tests {
.times(1)
.return_once(|| Ok(()));
let mut app = App::new(music_hoard).unwrap();
let mut app = App::new(music_hoard);
assert!(app.state().is_browse());
app.show_reload_menu();
@ -949,7 +1012,7 @@ mod tests {
.times(1)
.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());
app.show_reload_menu();
@ -961,13 +1024,4 @@ mod tests {
app.dismiss_error();
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::sync::mpsc;
use crate::tui::app::AppError;
#[derive(Debug)]
pub enum EventError {
Send(Event),
Recv,
Io(std::io::Error),
App(String),
}
impl fmt::Display for EventError {
@ -20,12 +17,6 @@ impl fmt::Display for EventError {
Self::Io(ref 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)]
pub enum Event {
Key(KeyEvent),
@ -148,16 +133,13 @@ mod tests {
}));
let recv_err = EventError::Recv;
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!(!recv_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!("{:?}", recv_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};
#[cfg(test)]
@ -18,24 +17,11 @@ pub trait IEventHandler<APP: IAppInteract> {
}
trait IEventHandlerPrivate<APP: IAppInteract> {
fn handle_key_event(app: &mut APP, key_event: KeyEvent) -> Result<(), EventError>;
fn handle_browse_key_event(
app: &mut <APP as IAppInteract>::BS,
key_event: KeyEvent,
) -> Result<(), EventError>;
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>;
fn handle_key_event(app: &mut APP, key_event: KeyEvent);
fn handle_browse_key_event(app: &mut <APP as IAppInteract>::BS, 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_error_key_event(app: &mut <APP as IAppInteract>::ES, key_event: KeyEvent);
}
pub struct EventHandler {
@ -52,7 +38,7 @@ impl EventHandler {
impl<APP: IAppInteract> IEventHandler<APP> for EventHandler {
fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> {
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::Resize(_, _) => {}
};
@ -61,44 +47,37 @@ impl<APP: IAppInteract> IEventHandler<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 {
// Exit application on `ESC` or `q`.
KeyCode::Esc | KeyCode::Char('q') => {
Self::quit(app)?;
app.save();
app.quit();
}
// Exit application on `Ctrl-C`.
KeyCode::Char('c') | KeyCode::Char('C') => {
if key_event.modifiers == KeyModifiers::CONTROL {
Self::quit(app)?;
app.force_quit();
}
}
_ => match app.state() {
AppState::Browse(browse) => {
<Self as IEventHandlerPrivate<APP>>::handle_browse_key_event(
browse, key_event,
)?;
<Self as IEventHandlerPrivate<APP>>::handle_browse_key_event(browse, key_event);
}
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) => {
<Self as IEventHandlerPrivate<APP>>::handle_reload_key_event(
reload, key_event,
)?;
<Self as IEventHandlerPrivate<APP>>::handle_reload_key_event(reload, key_event);
}
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(
app: &mut <APP as IAppInteract>::BS,
key_event: KeyEvent,
) -> Result<(), EventError> {
fn handle_browse_key_event(app: &mut <APP as IAppInteract>::BS, key_event: KeyEvent) {
match key_event.code {
// Category change.
KeyCode::Left => app.decrement_category(),
@ -113,28 +92,18 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
// Othey keys.
_ => {}
}
Ok(())
}
fn handle_info_key_event(
app: &mut <APP as IAppInteract>::IS,
key_event: KeyEvent,
) -> Result<(), EventError> {
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(),
// Othey keys.
_ => {}
}
Ok(())
}
fn handle_reload_key_event(
app: &mut <APP as IAppInteract>::RS,
key_event: KeyEvent,
) -> Result<(), EventError> {
fn handle_reload_key_event(app: &mut <APP as IAppInteract>::RS, key_event: KeyEvent) {
match key_event.code {
// Reload keys.
KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(),
@ -144,23 +113,11 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
// Othey keys.
_ => {}
}
Ok(())
}
fn handle_error_key_event(
app: &mut <APP as IAppInteract>::ES,
_key_event: KeyEvent,
) -> Result<(), EventError> {
fn handle_error_key_event(app: &mut <APP as IAppInteract>::ES, _key_event: KeyEvent) {
// Any key dismisses the error.
app.dismiss_error();
Ok(())
}
fn quit(app: &mut APP) -> Result<(), EventError> {
app.quit();
app.save()?;
Ok(())
}
}
// GRCOV_EXCL_STOP

View File

@ -28,18 +28,11 @@ use crate::tui::{
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
Lib(String),
Io(String),
Event(String),
ListenerPanic,
}
impl From<musichoard::Error> for Error {
fn from(err: musichoard::Error) -> Error {
Error::Lib(err.to_string())
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
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>,
) -> Result<(), Error> {
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))?;
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()),
// 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
// 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),
}
}
@ -211,7 +201,7 @@ mod tests {
}
fn app(collection: Collection) -> App<MockIMusicHoard> {
App::new(music_hoard(collection)).unwrap()
App::new(music_hoard(collection))
}
fn listener() -> MockIEventListener {
@ -319,12 +309,10 @@ mod tests {
#[test]
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 event_err: Error = EventError::Recv.into();
let listener_err = Error::ListenerPanic;
assert!(!format!("{:?}", lib_err).is_empty());
assert!(!format!("{:?}", io_err).is_empty());
assert!(!format!("{:?}", event_err).is_empty());
assert!(!format!("{:?}", listener_err).is_empty());