From b74eafc9de674bf8f3e7fc0c525e1c9db666dc82 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 16 Feb 2024 19:20:46 +0100 Subject: [PATCH] State machine solution works --- src/tui/app/app.rs | 1348 +++++++++++++++++++++++++------------------- src/tui/handler.rs | 92 ++- src/tui/mod.rs | 7 +- src/tui/ui.rs | 21 +- 4 files changed, 823 insertions(+), 645 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index ef7d842..c955dc1 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -7,6 +7,7 @@ use crate::tui::{ lib::IMusicHoard, }; +#[derive(Copy, Clone)] pub enum AppState { Browse(BS), Info(IS), @@ -17,108 +18,86 @@ pub enum AppState { } impl AppState { - pub fn is_browse(&self) -> bool { - matches!(self, AppState::Browse(_)) - } - - pub fn is_info(&self) -> bool { - matches!(self, AppState::Info(_)) - } - - pub fn is_reload(&self) -> bool { - matches!(self, AppState::Reload(_)) - } - pub fn is_search(&self) -> bool { matches!(self, AppState::Search(_)) } - - pub fn unwrap_search(self) -> SS { - match self { - AppState::Search(ss) => ss, - _ => panic!(), - } - } - - pub fn is_error(&self) -> bool { - matches!(self, AppState::Error(_)) - } - - pub fn as_mut(&mut self) -> AppState<&mut BS, &mut IS, &mut RS, &mut SS, &mut ES, &mut CS> { - match self { - AppState::Browse(ref mut bs) => AppState::Browse(bs), - AppState::Info(ref mut is) => AppState::Info(is), - AppState::Reload(ref mut rs) => AppState::Reload(rs), - AppState::Search(ref mut ss) => AppState::Search(ss), - AppState::Error(ref mut es) => AppState::Error(es), - AppState::Critical(ref mut cs) => AppState::Critical(cs), - } - } } pub trait IAppInteract { - type BS: IAppInteractBrowse; - type IS: IAppInteractInfo; - type RS: IAppInteractReload; - type SS: IAppInteractSearch; - type ES: IAppInteractError; - type CS: IAppInteractCritical; + type BS: IAppInteractBrowse; + type IS: IAppInteractInfo; + type RS: IAppInteractReload; + type SS: IAppInteractSearch; + type ES: IAppInteractError; + type CS: IAppInteractCritical; fn is_running(&self) -> bool; - fn force_quit(&mut self); + fn force_quit(self) -> Self; #[allow(clippy::type_complexity)] - fn state( - &mut self, - ) -> AppState< - &mut Self::BS, - &mut Self::IS, - &mut Self::RS, - &mut Self::SS, - &mut Self::ES, - &mut Self::CS, - >; + fn state(self) -> AppState; } pub trait IAppInteractBrowse { - fn save(&mut self); - fn quit(&mut self); + type APP: IAppInteract; - fn increment_category(&mut self); - fn decrement_category(&mut self); - fn increment_selection(&mut self, delta: Delta); - fn decrement_selection(&mut self, delta: Delta); + fn save_and_quit(self) -> Self::APP; - fn show_info_overlay(&mut self); + fn increment_category(self) -> Self::APP; + fn decrement_category(self) -> Self::APP; + fn increment_selection(self, delta: Delta) -> Self::APP; + fn decrement_selection(self, delta: Delta) -> Self::APP; - fn show_reload_menu(&mut self); + fn show_info_overlay(self) -> Self::APP; - fn begin_search(&mut self); + fn show_reload_menu(self) -> Self::APP; + + fn begin_search(self) -> Self::APP; + + fn no_op(self) -> Self::APP; } pub trait IAppInteractInfo { - fn hide_info_overlay(&mut self); + type APP: IAppInteract; + + fn hide_info_overlay(self) -> Self::APP; + + fn no_op(self) -> Self::APP; } pub trait IAppInteractReload { - fn reload_library(&mut self); - fn reload_database(&mut self); - fn hide_reload_menu(&mut self); + type APP: IAppInteract; + + fn reload_library(self) -> Self::APP; + fn reload_database(self) -> Self::APP; + fn hide_reload_menu(self) -> Self::APP; + + fn no_op(self) -> Self::APP; } pub trait IAppInteractSearch { - fn append_character(&mut self, ch: char); - fn search_next(&mut self); - fn step_back(&mut self); - fn finish_search(&mut self); - fn cancel_search(&mut self); + type APP: IAppInteract; + + fn append_character(self, ch: char) -> Self::APP; + fn search_next(self) -> Self::APP; + fn step_back(self) -> Self::APP; + fn finish_search(self) -> Self::APP; + fn cancel_search(self) -> Self::APP; + + fn no_op(self) -> Self::APP; } pub trait IAppInteractError { - fn dismiss_error(&mut self); + type APP: IAppInteract; + + fn dismiss_error(self) -> Self::APP; } -pub trait IAppInteractCritical {} +pub trait IAppInteractCritical { + type APP: IAppInteract; + + fn no_op(self) -> Self::APP; +} // 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. @@ -128,22 +107,35 @@ pub trait IAppAccess { fn get(&mut self) -> AppPublic; } -pub type AppPublicState = AppState<(), (), (), String, String, String>; +pub type AppPublicState<'app> = AppState<(), (), (), &'app str, &'app str, &'app str>; pub struct AppPublic<'app> { pub collection: &'app Collection, pub selection: &'app mut Selection, - pub state: &'app AppPublicState, + pub state: AppPublicState<'app>, } -pub struct App { +struct AppInner { running: bool, music_hoard: MH, selection: Selection, - state: AppState<(), (), (), String, String, String>, - // FIXME: is it possible to use a wrapper struct? - when state() is called return a wrapper - // around App which will contain App. - orig: Option, +} + +pub type App = AppState< + AppGenericState, + AppGenericState, + AppGenericState, + AppSearchState, + AppErrorState, + AppErrorState, +>; + +pub struct AppGenericState(AppInner); + +pub struct AppSearchState { + inner: AppInner, + search: String, + orig: ListSelection, memo: Vec, } @@ -152,20 +144,26 @@ struct AppSearchMemo { char: bool, } +pub struct AppErrorState { + inner: AppInner, + msg: String, +} + impl App { pub fn new(mut music_hoard: MH) -> Self { - let state = match Self::init(&mut music_hoard) { - Ok(()) => AppState::Browse(()), - Err(err) => AppState::Critical(err.to_string()), - }; + let init_result = Self::init(&mut music_hoard); let selection = Selection::new(music_hoard.get_collection()); - App { + let inner = AppInner { running: true, music_hoard, selection, - state, - orig: None, - memo: vec![], + }; + match init_result { + Ok(()) => Self::Browse(AppGenericState(inner)), + Err(err) => Self::Critical(AppErrorState { + inner, + msg: err.to_string(), + }), } } @@ -174,198 +172,279 @@ impl App { music_hoard.rescan_library()?; Ok(()) } + + // fn take_inner(self) -> AppInner { + // match self { + // AppState::Browse(inner) | AppState::Info(inner) | AppState::Reload(inner) => inner.0, + // AppState::Search(search) => search.inner, + // AppState::Error(error) | AppState::Critical(error) => error.inner, + // } + // } + + fn inner_ref(&self) -> &AppInner { + match self { + AppState::Browse(inner) | AppState::Info(inner) | AppState::Reload(inner) => &inner.0, + AppState::Search(search) => &search.inner, + AppState::Error(error) | AppState::Critical(error) => &error.inner, + } + } + + fn inner_mut(&mut self) -> &mut AppInner { + match self { + AppState::Browse(inner) | AppState::Info(inner) | AppState::Reload(inner) => { + &mut inner.0 + } + AppState::Search(search) => &mut search.inner, + AppState::Error(error) | AppState::Critical(error) => &mut error.inner, + } + } } impl IAppInteract for App { - type BS = Self; - type IS = Self; - type RS = Self; - type SS = Self; - type ES = Self; - type CS = Self; + type BS = AppGenericState; + type IS = AppGenericState; + type RS = AppGenericState; + type SS = AppSearchState; + type ES = AppErrorState; + type CS = AppErrorState; fn is_running(&self) -> bool { - self.running + self.inner_ref().running } - fn force_quit(&mut self) { - self.running = false; + fn force_quit(mut self) -> Self { + self.inner_mut().running = false; + self } - fn state( - &mut self, - ) -> AppState< - &mut Self::BS, - &mut Self::IS, - &mut Self::RS, - &mut Self::SS, - &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::Search(_) => AppState::Search(self), - AppState::Error(_) => AppState::Error(self), - AppState::Critical(_) => AppState::Critical(self), - } + fn state(self) -> AppState { + self } } -impl IAppInteractBrowse for App { - fn quit(&mut self) { - self.running = false; - } +impl IAppInteractBrowse for AppGenericState { + type APP = App; - fn save(&mut self) { - if let Err(err) = self.music_hoard.save_to_database() { - self.state = AppState::Error(err.to_string()); + fn save_and_quit(mut self) -> Self::APP { + match self.0.music_hoard.save_to_database() { + Ok(_) => { + self.0.running = false; + AppState::Browse(self) + } + Err(err) => AppState::Error(AppErrorState { + inner: self.0, + msg: err.to_string(), + }), } } - fn increment_category(&mut self) { - self.selection.increment_category(); + fn increment_category(mut self) -> Self::APP { + self.0.selection.increment_category(); + AppState::Browse(self) } - fn decrement_category(&mut self) { - self.selection.decrement_category(); + fn decrement_category(mut self) -> Self::APP { + self.0.selection.decrement_category(); + AppState::Browse(self) } - fn increment_selection(&mut self, delta: Delta) { - self.selection - .increment_selection(self.music_hoard.get_collection(), delta); + fn increment_selection(mut self, delta: Delta) -> Self::APP { + self.0 + .selection + .increment_selection(self.0.music_hoard.get_collection(), delta); + AppState::Browse(self) } - fn decrement_selection(&mut self, delta: Delta) { - self.selection - .decrement_selection(self.music_hoard.get_collection(), delta); + fn decrement_selection(mut self, delta: Delta) -> Self::APP { + self.0 + .selection + .decrement_selection(self.0.music_hoard.get_collection(), delta); + AppState::Browse(self) } - fn show_info_overlay(&mut self) { - assert!(self.state.is_browse()); - self.state = AppState::Info(()); + fn show_info_overlay(self) -> Self::APP { + AppState::Info(self) } - fn show_reload_menu(&mut self) { - assert!(self.state.is_browse()); - self.state = AppState::Reload(()); + fn show_reload_menu(self) -> Self::APP { + AppState::Reload(self) } - fn begin_search(&mut self) { - assert!(self.state.is_browse()); - self.state = AppState::Search(String::new()); - self.orig = Some(ListSelection::get(&self.selection)); - self.memo = vec![]; - self.selection - .reset_artist(self.music_hoard.get_collection()); + fn begin_search(mut self) -> Self::APP { + let orig = ListSelection::get(&self.0.selection); + self.0 + .selection + .reset_artist(self.0.music_hoard.get_collection()); + AppState::Search(AppSearchState { + inner: self.0, + search: String::new(), + orig, + memo: vec![], + }) + } + + fn no_op(self) -> Self::APP { + AppState::Browse(self) } } -impl IAppInteractInfo for App { - fn hide_info_overlay(&mut self) { - assert!(self.state.is_info()); - self.state = AppState::Browse(()); +impl IAppInteractInfo for AppGenericState { + type APP = App; + + fn hide_info_overlay(self) -> Self::APP { + AppState::Browse(AppGenericState(self.0)) + } + + fn no_op(self) -> Self::APP { + AppState::Info(self) } } -impl IAppInteractReload for App { - fn reload_library(&mut self) { - let previous = IdSelection::get(self.music_hoard.get_collection(), &self.selection); - let result = self.music_hoard.rescan_library(); - self.refresh(previous, result); +impl IAppInteractReload for AppGenericState { + type APP = App; + + fn reload_library(mut self) -> Self::APP { + let previous = IdSelection::get(self.0.music_hoard.get_collection(), &self.0.selection); + let result = self.0.music_hoard.rescan_library(); + self.refresh(previous, result) } - fn reload_database(&mut self) { - let previous = IdSelection::get(self.music_hoard.get_collection(), &self.selection); - let result = self.music_hoard.load_from_database(); - self.refresh(previous, result); + fn reload_database(mut self) -> Self::APP { + let previous = IdSelection::get(self.0.music_hoard.get_collection(), &self.0.selection); + let result = self.0.music_hoard.load_from_database(); + self.refresh(previous, result) } - fn hide_reload_menu(&mut self) { - assert!(self.state.is_reload()); - self.state = AppState::Browse(()); + fn hide_reload_menu(self) -> Self::APP { + AppState::Browse(AppGenericState(self.0)) + } + + fn no_op(self) -> Self::APP { + AppState::Reload(self) } } -trait IAppInteractReloadPrivate { - fn refresh(&mut self, previous: IdSelection, result: Result<(), musichoard::Error>); +trait IAppInteractReloadPrivate { + fn refresh(self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App; } -impl IAppInteractReloadPrivate for App { - fn refresh(&mut self, previous: IdSelection, result: Result<(), musichoard::Error>) { - assert!(self.state.is_reload()); +impl IAppInteractReloadPrivate for AppGenericState { + fn refresh(mut self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App { match result { Ok(()) => { - self.selection - .select_by_id(self.music_hoard.get_collection(), previous); - self.state = AppState::Browse(()) + self.0 + .selection + .select_by_id(self.0.music_hoard.get_collection(), previous); + AppState::Browse(AppGenericState(self.0)) } - Err(err) => self.state = AppState::Error(err.to_string()), + Err(err) => AppState::Error(AppErrorState { + inner: self.0, + msg: err.to_string(), + }), } } } -impl IAppInteractSearch for App { - fn append_character(&mut self, ch: char) { - let collection = self.music_hoard.get_collection(); - let search = self.state.as_mut().unwrap_search(); - search.push(ch); +impl IAppInteractSearch for AppSearchState { + type APP = App; + + fn append_character(mut self, ch: char) -> Self::APP { + let collection = self.inner.music_hoard.get_collection(); + self.search.push(ch); let index = self + .inner .selection - .incremental_artist_search(collection, search, false); + .incremental_artist_search(collection, &self.search, false); self.memo.push(AppSearchMemo { index, char: true }); + AppState::Search(self) } - fn search_next(&mut self) { - let collection = self.music_hoard.get_collection(); - let search = self.state.as_mut().unwrap_search(); - if !search.is_empty() { - let index = self - .selection - .incremental_artist_search(collection, search, true); + fn search_next(mut self) -> Self::APP { + let collection = self.inner.music_hoard.get_collection(); + if !self.search.is_empty() { + let index = + self.inner + .selection + .incremental_artist_search(collection, &self.search, true); self.memo.push(AppSearchMemo { index, char: false }); } + AppState::Search(self) } - fn step_back(&mut self) { - let collection = self.music_hoard.get_collection(); + fn step_back(mut self) -> Self::APP { + let collection = self.inner.music_hoard.get_collection(); if let Some(memo) = self.memo.pop() { if memo.char { - let search = self.state.as_mut().unwrap_search(); - search.pop(); + self.search.pop(); } - self.selection.select_artist(collection, memo.index); + self.inner.selection.select_artist(collection, memo.index); } + AppState::Search(self) } - fn finish_search(&mut self) { - assert!(self.state.is_search()); - self.state = AppState::Browse(()); + fn finish_search(self) -> Self::APP { + AppState::Browse(AppGenericState(self.inner)) } - fn cancel_search(&mut self) { - assert!(self.state.is_search()); - self.selection.select_by_list(self.orig.take().unwrap()); - self.state = AppState::Browse(()); + fn cancel_search(mut self) -> Self::APP { + self.inner.selection.select_by_list(self.orig); + AppState::Browse(AppGenericState(self.inner)) + } + + fn no_op(self) -> Self::APP { + AppState::Search(self) } } -impl IAppInteractError for App { - fn dismiss_error(&mut self) { - assert!(self.state.is_error()); - self.state = AppState::Browse(()); +impl IAppInteractError for AppErrorState { + type APP = App; + + fn dismiss_error(self) -> Self::APP { + AppState::Browse(AppGenericState(self.inner)) } } -impl IAppInteractCritical for App {} +impl IAppInteractCritical for AppErrorState { + type APP = App; + + fn no_op(self) -> Self::APP { + AppState::Critical(self) + } +} impl IAppAccess for App { fn get(&mut self) -> AppPublic { - AppPublic { - collection: self.music_hoard.get_collection(), - selection: &mut self.selection, - state: &self.state, + match self { + AppState::Browse(generic) => AppPublic { + collection: generic.0.music_hoard.get_collection(), + selection: &mut generic.0.selection, + state: AppState::Browse(()), + }, + AppState::Info(generic) => AppPublic { + collection: generic.0.music_hoard.get_collection(), + selection: &mut generic.0.selection, + state: AppState::Info(()), + }, + AppState::Reload(generic) => AppPublic { + collection: generic.0.music_hoard.get_collection(), + selection: &mut generic.0.selection, + state: AppState::Reload(()), + }, + AppState::Search(search) => AppPublic { + collection: search.inner.music_hoard.get_collection(), + selection: &mut search.inner.selection, + state: AppState::Search(&search.search), + }, + AppState::Error(error) => AppPublic { + collection: error.inner.music_hoard.get_collection(), + selection: &mut error.inner.selection, + state: AppState::Error(&error.msg), + }, + AppState::Critical(critical) => AppPublic { + collection: critical.inner.music_hoard.get_collection(), + selection: &mut critical.inner.selection, + state: AppState::Error(&critical.msg), + }, } } } @@ -376,6 +455,50 @@ mod tests { use super::*; + impl AppState { + fn unwrap_browse(self) -> BS { + match self { + AppState::Browse(browse) => browse, + _ => panic!(), + } + } + + fn unwrap_info(self) -> IS { + match self { + AppState::Info(info) => info, + _ => panic!(), + } + } + + fn unwrap_reload(self) -> RS { + match self { + AppState::Reload(reload) => reload, + _ => panic!(), + } + } + + fn unwrap_search(self) -> SS { + match self { + AppState::Search(search) => search, + _ => panic!(), + } + } + + fn unwrap_error(self) -> ES { + match self { + AppState::Error(error) => error, + _ => panic!(), + } + } + + fn unwrap_critical(self) -> CS { + match self { + AppState::Critical(critical) => critical, + _ => panic!(), + } + } + } + fn music_hoard(collection: Collection) -> MockIMusicHoard { let mut music_hoard = MockIMusicHoard::new(); @@ -393,80 +516,74 @@ mod tests { } #[test] - fn app_state() { - let mut state = AppPublicState::Browse(()); - assert!(state.is_browse()); - assert!(state.as_mut().is_browse()); - - let mut state = AppPublicState::Info(()); - assert!(state.is_info()); - assert!(state.as_mut().is_info()); - - let mut state = AppPublicState::Reload(()); - assert!(state.is_reload()); - assert!(state.as_mut().is_reload()); - - let mut state = AppPublicState::Search(String::from("get rekt")); + fn app_is_state() { + let state = AppPublicState::Search("get rekt"); assert!(state.is_search()); - assert!(state.as_mut().is_search()); - assert_eq!(state.unwrap_search().as_str(), "get rekt"); - - let mut state = AppPublicState::Error(String::new()); - assert!(state.is_error()); - assert!(state.as_mut().is_error()); - - let mut state = AppPublicState::Critical(String::new()); - assert!(matches!(state, AppState::Critical(_))); - assert!(matches!(state.as_mut(), AppState::Critical(_))); - } - - #[test] - #[should_panic] - fn app_state_unwrap_search_panic() { - let state = AppPublicState::Browse(()); - assert!(state.is_browse()); - state.unwrap_search(); } #[test] fn running_quit() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); + let mut music_hoard = music_hoard(COLLECTION.to_owned()); + + music_hoard + .expect_save_to_database() + .times(1) + .return_once(|| Ok(())); + + let app = App::new(music_hoard); assert!(app.is_running()); - app.quit(); + let browse = app.unwrap_browse(); + + let app = browse.save_and_quit(); assert!(!app.is_running()); } #[test] fn error_quit() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); + let mut music_hoard = music_hoard(COLLECTION.to_owned()); + + music_hoard + .expect_save_to_database() + .times(1) + .return_once(|| Ok(())); + + let app = App::new(music_hoard); assert!(app.is_running()); - app.state = AppState::Error(String::from("get rekt")); + let app = App::Error(AppErrorState { + inner: app.unwrap_browse().0, + msg: String::from("get rekt"), + }); - app.dismiss_error(); + let error = app.unwrap_error(); - app.quit(); + let browse = error.dismiss_error().unwrap_browse(); + + let app = browse.save_and_quit(); assert!(!app.is_running()); } #[test] fn running_force_quit() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); + let app = App::new(music_hoard(COLLECTION.to_owned())); assert!(app.is_running()); - app.force_quit(); + let app = app.force_quit(); assert!(!app.is_running()); } #[test] fn error_force_quit() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); + let app = App::new(music_hoard(COLLECTION.to_owned())); assert!(app.is_running()); - app.state = AppState::Error(String::from("get rekt")); + let app = App::Error(AppErrorState { + inner: app.unwrap_browse().0, + msg: String::from("get rekt"), + }); - app.force_quit(); + let app = app.force_quit(); assert!(!app.is_running()); } @@ -479,10 +596,9 @@ mod tests { .times(1) .return_once(|| Ok(())); - let mut app = App::new(music_hoard); + let browse = App::new(music_hoard).unwrap_browse(); - app.save(); - assert!(app.state.is_browse()); + browse.save_and_quit().unwrap_browse(); } #[test] @@ -494,11 +610,9 @@ mod tests { .times(1) .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); - let mut app = App::new(music_hoard); + let browse = App::new(music_hoard).unwrap_browse(); - app.save(); - - assert!(app.state.is_error()); + browse.save_and_quit().unwrap_error(); } #[test] @@ -511,136 +625,177 @@ mod tests { .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); music_hoard.expect_get_collection().return_const(vec![]); - let mut app = App::new(music_hoard); + let app = App::new(music_hoard); assert!(app.is_running()); - assert!(matches!(app.state(), AppState::Critical(_))); + app.unwrap_critical(); } #[test] fn modifiers() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); + let app = App::new(music_hoard(COLLECTION.to_owned())); assert!(app.is_running()); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); + let browse = app.unwrap_browse(); + + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_category(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); + let browse = browse.increment_category().unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_category(); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_category().unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(1) ); - app.increment_category(); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_category().unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(1) ); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_selection(Delta::Line); - app.decrement_category(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.decrement_category().unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(1) ); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_selection(Delta::Line); - app.decrement_category(); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.decrement_category().unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_category(); - app.increment_selection(Delta::Line); - app.decrement_category(); - app.decrement_selection(Delta::Line); - app.decrement_category(); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_category().unwrap_browse(); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.decrement_category().unwrap_browse(); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let browse = browse.decrement_category().unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); } @@ -650,28 +805,48 @@ mod tests { let mut collection = COLLECTION.to_owned(); collection[0].albums[0].tracks = vec![]; - let mut app = App::new(music_hoard(collection)); + let app = App::new(music_hoard(collection)); assert!(app.is_running()); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = app.unwrap_browse(); - app.increment_category(); - app.increment_category(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!( + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_category().unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!( + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!( + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); } #[test] @@ -679,116 +854,150 @@ mod tests { let mut collection = COLLECTION.to_owned(); collection[0].albums = vec![]; - let mut app = App::new(music_hoard(collection)); + let app = App::new(music_hoard(collection)); assert!(app.is_running()); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = app.unwrap_browse(); - app.increment_category(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_category().unwrap_browse(); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_category(); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_category().unwrap_browse(); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); } #[test] fn no_artists() { - let mut app = App::new(music_hoard(vec![])); + let app = App::new(music_hoard(vec![])); assert!(app.is_running()); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = app.unwrap_browse(); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_category(); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_category().unwrap_browse(); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_category(); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_category().unwrap_browse(); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); } #[test] fn info_overlay() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - app.show_info_overlay(); - assert!(app.state().is_info()); + let info = browse.show_info_overlay().unwrap_info(); - app.hide_info_overlay(); - assert!(app.state().is_browse()); + info.hide_info_overlay().unwrap_browse(); } #[test] fn reload_hide_menu() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - app.show_reload_menu(); - assert!(app.state().is_reload()); + let reload = browse.show_reload_menu().unwrap_reload(); - app.hide_reload_menu(); - assert!(app.state().is_browse()); + reload.hide_reload_menu().unwrap_browse(); } #[test] @@ -800,14 +1009,11 @@ mod tests { .times(1) .return_once(|| Ok(())); - let mut app = App::new(music_hoard); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard).unwrap_browse(); - app.show_reload_menu(); - assert!(app.state().is_reload()); + let reload = browse.show_reload_menu().unwrap_reload(); - app.reload_database(); - assert!(app.state().is_browse()); + reload.reload_database().unwrap_browse(); } #[test] @@ -819,14 +1025,11 @@ mod tests { .times(1) .return_once(|| Ok(())); - let mut app = App::new(music_hoard); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard).unwrap_browse(); - app.show_reload_menu(); - assert!(app.state().is_reload()); + let reload = browse.show_reload_menu().unwrap_reload(); - app.reload_library(); - assert!(app.state().is_browse()); + reload.reload_library().unwrap_browse(); } #[test] @@ -838,234 +1041,221 @@ mod tests { .times(1) .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); - let mut app = App::new(music_hoard); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard).unwrap_browse(); - app.show_reload_menu(); - assert!(app.state().is_reload()); + let reload = browse.show_reload_menu().unwrap_reload(); - app.reload_database(); - assert!(app.state().is_error()); + let error = reload.reload_database().unwrap_error(); - app.dismiss_error(); - assert!(app.state().is_browse()); + error.dismiss_error().unwrap_browse(); } #[test] fn search() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - app.increment_selection(Delta::Line); - app.increment_category(); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - app.begin_search(); - assert!(app.state().is_search()); + let search = browse.begin_search().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.append_character('a'); - app.append_character('l'); - app.append_character('b'); - app.append_character('u'); - app.append_character('m'); - app.append_character('_'); - app.append_character('a'); - app.append_character('r'); - app.append_character('t'); - app.append_character('i'); - app.append_character('s'); - app.append_character('t'); - app.append_character(' '); + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('l').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + let search = search.append_character('u').unwrap_search(); + let search = search.append_character('m').unwrap_search(); + let search = search.append_character('_').unwrap_search(); + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('r').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character('i').unwrap_search(); + let search = search.append_character('s').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character(' ').unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.append_character('\''); - app.append_character('c'); - app.append_character('\''); + let search = search.append_character('\'').unwrap_search(); + let search = search.append_character('c').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(2)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - app.step_back(); - app.step_back(); - app.step_back(); + let search = search.step_back().unwrap_search(); + let search = search.step_back().unwrap_search(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.append_character('\''); - app.append_character('b'); - app.append_character('\''); + let search = search.append_character('\'').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); - app.finish_search(); - assert!(app.state().is_browse()); + let browse = search.finish_search().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); } #[test] fn search_next() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - app.increment_selection(Delta::Line); - app.increment_category(); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - app.begin_search(); - assert!(app.state().is_search()); + let search = browse.begin_search().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.append_character('a'); + let search = search.append_character('a').unwrap_search(); - app.search_next(); + let search = search.search_next().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); - app.search_next(); + let search = search.search_next().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(2)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - app.search_next(); + let search = search.search_next().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(3)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); - app.search_next(); + let search = search.search_next().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(3)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(3)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(2)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); } #[test] fn cancel_search() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - app.increment_selection(Delta::Line); - app.increment_category(); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - app.begin_search(); - assert!(app.state().is_search()); + let search = browse.begin_search().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.append_character('a'); - app.append_character('l'); - app.append_character('b'); - app.append_character('u'); - app.append_character('m'); - app.append_character('_'); - app.append_character('a'); - app.append_character('r'); - app.append_character('t'); - app.append_character('i'); - app.append_character('s'); - app.append_character('t'); - app.append_character(' '); - app.append_character('\''); - app.append_character('c'); - app.append_character('\''); + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('l').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + let search = search.append_character('u').unwrap_search(); + let search = search.append_character('m').unwrap_search(); + let search = search.append_character('_').unwrap_search(); + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('r').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character('i').unwrap_search(); + let search = search.append_character('s').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character(' ').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); + let search = search.append_character('c').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(2)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - app.cancel_search(); + let browse = search.cancel_search().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); } #[test] fn empty_search() { - let mut app = App::new(music_hoard(vec![])); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard(vec![])).unwrap_browse(); - app.increment_selection(Delta::Line); - app.increment_category(); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); - app.begin_search(); - assert!(app.state().is_search()); + let search = browse.begin_search().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); - app.append_character('a'); + let search = search.append_character('a').unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); - app.search_next(); + let search = search.search_next().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); - app.cancel_search(); + let browse = search.cancel_search().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); } } diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 672c4d6..dfd3d8b 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -14,19 +14,21 @@ use crate::tui::{ event::{Event, EventError, EventReceiver}, }; +use super::app::app::IAppInteractCritical; + #[cfg_attr(test, automock)] pub trait IEventHandler { - fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError>; + fn handle_next_event(&self, app: APP) -> Result; } trait IEventHandlerPrivate { - fn handle_key_event(app: &mut APP, key_event: KeyEvent); - fn handle_browse_key_event(app: &mut ::BS, key_event: KeyEvent); - fn handle_info_key_event(app: &mut ::IS, key_event: KeyEvent); - fn handle_reload_key_event(app: &mut ::RS, key_event: KeyEvent); - fn handle_search_key_event(app: &mut ::SS, key_event: KeyEvent); - fn handle_error_key_event(app: &mut ::ES, key_event: KeyEvent); - fn handle_critical_key_event(app: &mut ::CS, key_event: KeyEvent); + fn handle_key_event(app: APP, key_event: KeyEvent) -> APP; + fn handle_browse_key_event(app: ::BS, key_event: KeyEvent) -> APP; + fn handle_info_key_event(app: ::IS, key_event: KeyEvent) -> APP; + fn handle_reload_key_event(app: ::RS, key_event: KeyEvent) -> APP; + fn handle_search_key_event(app: ::SS, key_event: KeyEvent) -> APP; + fn handle_error_key_event(app: ::ES, key_event: KeyEvent) -> APP; + fn handle_critical_key_event(app: ::CS, key_event: KeyEvent) -> APP; } pub struct EventHandler { @@ -41,58 +43,52 @@ impl EventHandler { } impl IEventHandler for EventHandler { - fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> { + fn handle_next_event(&self, mut app: APP) -> Result { match self.events.recv()? { - Event::Key(key_event) => Self::handle_key_event(app, key_event), + Event::Key(key_event) => app = Self::handle_key_event(app, key_event), Event::Mouse(_) => {} Event::Resize(_, _) => {} }; - Ok(()) + Ok(app) } } impl IEventHandlerPrivate for EventHandler { - fn handle_key_event(app: &mut APP, key_event: KeyEvent) { + fn handle_key_event(app: APP, key_event: KeyEvent) -> APP { if key_event.modifiers == KeyModifiers::CONTROL { match key_event.code { // Exit application on `Ctrl-C`. - KeyCode::Char('c') | KeyCode::Char('C') => { - app.force_quit(); - return; - } + KeyCode::Char('c') | KeyCode::Char('C') => return app.force_quit(), _ => {} - } + }; } match app.state() { AppState::Browse(browse) => { - >::handle_browse_key_event(browse, key_event); + >::handle_browse_key_event(browse, key_event) } AppState::Info(info) => { - >::handle_info_key_event(info, key_event); + >::handle_info_key_event(info, key_event) } AppState::Reload(reload) => { - >::handle_reload_key_event(reload, key_event); + >::handle_reload_key_event(reload, key_event) } AppState::Search(search) => { - >::handle_search_key_event(search, key_event); + >::handle_search_key_event(search, key_event) } AppState::Error(error) => { - >::handle_error_key_event(error, key_event); + >::handle_error_key_event(error, key_event) } AppState::Critical(critical) => { - >::handle_critical_key_event(critical, key_event); + >::handle_critical_key_event(critical, key_event) } } } - fn handle_browse_key_event(app: &mut ::BS, key_event: KeyEvent) { + fn handle_browse_key_event(app: ::BS, key_event: KeyEvent) -> APP { match key_event.code { // Exit application on `ESC` or `q`. - KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => { - app.save(); - app.quit(); - } + KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.save_and_quit(), // Category change. KeyCode::Left => app.decrement_category(), KeyCode::Right => app.increment_category(), @@ -108,15 +104,17 @@ impl IEventHandlerPrivate for EventHandler { // Toggle search. KeyCode::Char('s') | KeyCode::Char('S') => { if key_event.modifiers == KeyModifiers::CONTROL { - app.begin_search(); + app.begin_search() + } else { + app.no_op() } } // Othey keys. - _ => {} + _ => app.no_op(), } } - fn handle_info_key_event(app: &mut ::IS, key_event: KeyEvent) { + fn handle_info_key_event(app: ::IS, key_event: KeyEvent) -> APP { match key_event.code { // Toggle overlay. KeyCode::Esc @@ -125,11 +123,11 @@ impl IEventHandlerPrivate for EventHandler { | KeyCode::Char('m') | KeyCode::Char('M') => app.hide_info_overlay(), // Othey keys. - _ => {} + _ => app.no_op(), } } - fn handle_reload_key_event(app: &mut ::RS, key_event: KeyEvent) { + fn handle_reload_key_event(app: ::RS, key_event: KeyEvent) -> APP { match key_event.code { // Reload keys. KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(), @@ -141,22 +139,17 @@ impl IEventHandlerPrivate for EventHandler { | KeyCode::Char('g') | KeyCode::Char('G') => app.hide_reload_menu(), // Othey keys. - _ => {} + _ => app.no_op(), } } - fn handle_search_key_event(app: &mut ::SS, key_event: KeyEvent) { + fn handle_search_key_event(app: ::SS, key_event: KeyEvent) -> APP { if key_event.modifiers == KeyModifiers::CONTROL { - match key_event.code { - KeyCode::Char('s') | KeyCode::Char('S') => { - app.search_next(); - } - KeyCode::Char('g') | KeyCode::Char('G') => { - app.cancel_search(); - } - _ => {} - } - return; + return match key_event.code { + KeyCode::Char('s') | KeyCode::Char('S') => app.search_next(), + KeyCode::Char('g') | KeyCode::Char('G') => app.cancel_search(), + _ => app.no_op(), + }; } match key_event.code { @@ -166,17 +159,18 @@ impl IEventHandlerPrivate for EventHandler { // Return. KeyCode::Esc | KeyCode::Enter => app.finish_search(), // Othey keys. - _ => {} + _ => app.no_op(), } } - fn handle_error_key_event(app: &mut ::ES, _key_event: KeyEvent) { + fn handle_error_key_event(app: ::ES, _key_event: KeyEvent) -> APP { // Any key dismisses the error. - app.dismiss_error(); + app.dismiss_error() } - fn handle_critical_key_event(_app: &mut ::CS, _key_event: KeyEvent) { + fn handle_critical_key_event(app: ::CS, _key_event: KeyEvent) -> APP { // No action is allowed. + app.no_op() } } // GRCOV_EXCL_STOP diff --git a/src/tui/mod.rs b/src/tui/mod.rs index b48e650..fd1f2c8 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -75,7 +75,7 @@ impl Tui { ) -> Result<(), Error> { while app.is_running() { self.terminal.draw(|frame| UI::render(&mut app, frame))?; - handler.handle_next_event(&mut app)?; + app = handler.handle_next_event(app)?; } Ok(()) @@ -219,10 +219,7 @@ mod tests { let mut handler = MockIEventHandler::new(); handler .expect_handle_next_event() - .return_once(|app: &mut App| { - app.force_quit(); - Ok(()) - }); + .return_once(|app: App| Ok(app.force_quit())); handler } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 7833d13..378d31c 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -681,12 +681,12 @@ impl IUi for Ui { let selection = app.selection; let state = app.state; - Self::render_main_frame(collection, selection, state, frame); + Self::render_main_frame(collection, selection, &state, frame); match 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("Error", msg, frame), - AppState::Critical(ref msg) => Self::render_error_overlay("Critical Error", msg, frame), + AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame), + AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame), _ => {} } } @@ -719,26 +719,23 @@ mod tests { let mut app = AppPublic { collection, selection, - state: &AppState::Browse(()), + state: AppState::Browse(()), }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); - app.state = &AppState::Info(()); + app.state = AppState::Info(()); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); - app.state = &AppState::Reload(()); + app.state = AppState::Reload(()); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); - let binding = AppState::Search(String::new()); - app.state = &binding; + app.state = AppState::Search(""); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); - let binding = AppState::Error(String::from("get rekt scrub")); - app.state = &binding; + app.state = AppState::Error("get rekt scrub"); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); - let binding = AppState::Critical(String::from("get critically rekt scrub")); - app.state = &binding; + app.state = AppState::Critical("get critically rekt scrub"); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); }