Compare commits

...

2 Commits

Author SHA1 Message Date
6ecf9aa04d Rename for clarity
All checks were successful
Cargo CI / Build and Test (pull_request) Successful in 2m27s
Cargo CI / Lint (pull_request) Successful in 1m2s
2024-09-12 23:07:16 +02:00
1404666e12 Verbosity 2024-09-12 21:09:55 +02:00
21 changed files with 429 additions and 404 deletions

View File

@ -4,25 +4,25 @@ use crate::tui::app::{
AppPublic, AppState, IAppInteractBrowse, AppPublic, AppState, IAppInteractBrowse,
}; };
pub struct AppBrowse; pub struct BrowseState;
impl AppMachine<AppBrowse> { impl AppMachine<BrowseState> {
pub fn browse(inner: AppInner) -> Self { pub fn browse_state(inner: AppInner) -> Self {
AppMachine { AppMachine {
inner, inner,
state: AppBrowse, state: BrowseState,
} }
} }
} }
impl From<AppMachine<AppBrowse>> for App { impl From<AppMachine<BrowseState>> for App {
fn from(machine: AppMachine<AppBrowse>) -> Self { fn from(machine: AppMachine<BrowseState>) -> Self {
AppState::Browse(machine) AppState::Browse(machine)
} }
} }
impl<'a> From<&'a mut AppMachine<AppBrowse>> for AppPublic<'a> { impl<'a> From<&'a mut AppMachine<BrowseState>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppBrowse>) -> Self { fn from(machine: &'a mut AppMachine<BrowseState>) -> Self {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Browse(()), state: AppState::Browse(()),
@ -30,7 +30,7 @@ impl<'a> From<&'a mut AppMachine<AppBrowse>> for AppPublic<'a> {
} }
} }
impl IAppInteractBrowse for AppMachine<AppBrowse> { impl IAppInteractBrowse for AppMachine<BrowseState> {
type APP = App; type APP = App;
fn quit(mut self) -> Self::APP { fn quit(mut self) -> Self::APP {
@ -63,11 +63,11 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
} }
fn show_info_overlay(self) -> Self::APP { fn show_info_overlay(self) -> Self::APP {
AppMachine::info(self.inner).into() AppMachine::info_state(self.inner).into()
} }
fn show_reload_menu(self) -> Self::APP { fn show_reload_menu(self) -> Self::APP {
AppMachine::reload(self.inner).into() AppMachine::reload_state(self.inner).into()
} }
fn begin_search(mut self) -> Self::APP { fn begin_search(mut self) -> Self::APP {
@ -75,7 +75,7 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
self.inner self.inner
.selection .selection
.reset(self.inner.music_hoard.get_collection()); .reset(self.inner.music_hoard.get_collection());
AppMachine::search(self.inner, orig).into() AppMachine::search_state(self.inner, orig).into()
} }
fn fetch_musicbrainz(self) -> Self::APP { fn fetch_musicbrainz(self) -> Self::APP {
@ -88,7 +88,7 @@ mod tests {
use crate::tui::{ use crate::tui::{
app::{ app::{
machine::tests::{inner, inner_with_mb, music_hoard}, machine::tests::{inner, inner_with_mb, music_hoard},
Category, IAppAccess, IAppInteract, Category, IApp, IAppAccess,
}, },
lib::interface::musicbrainz::MockIMusicBrainz, lib::interface::musicbrainz::MockIMusicBrainz,
testmod::COLLECTION, testmod::COLLECTION,
@ -100,7 +100,7 @@ mod tests {
fn quit() { fn quit() {
let music_hoard = music_hoard(vec![]); let music_hoard = music_hoard(vec![]);
let browse = AppMachine::browse(inner(music_hoard)); let browse = AppMachine::browse_state(inner(music_hoard));
let app = browse.quit(); let app = browse.quit();
assert!(!app.is_running()); assert!(!app.is_running());
@ -109,7 +109,7 @@ mod tests {
#[test] #[test]
fn increment_decrement() { fn increment_decrement() {
let mut browse = AppMachine::browse(inner(music_hoard(COLLECTION.to_owned()))); let mut browse = AppMachine::browse_state(inner(music_hoard(COLLECTION.to_owned())));
let sel = &browse.inner.selection; let sel = &browse.inner.selection;
assert_eq!(sel.category(), Category::Artist); assert_eq!(sel.category(), Category::Artist);
assert_eq!(sel.selected(), Some(0)); assert_eq!(sel.selected(), Some(0));
@ -147,21 +147,21 @@ mod tests {
#[test] #[test]
fn show_info_overlay() { fn show_info_overlay() {
let browse = AppMachine::browse(inner(music_hoard(vec![]))); let browse = AppMachine::browse_state(inner(music_hoard(vec![])));
let app = browse.show_info_overlay(); let app = browse.show_info_overlay();
app.unwrap_info(); app.unwrap_info();
} }
#[test] #[test]
fn show_reload_menu() { fn show_reload_menu() {
let browse = AppMachine::browse(inner(music_hoard(vec![]))); let browse = AppMachine::browse_state(inner(music_hoard(vec![])));
let app = browse.show_reload_menu(); let app = browse.show_reload_menu();
app.unwrap_reload(); app.unwrap_reload();
} }
#[test] #[test]
fn begin_search() { fn begin_search() {
let browse = AppMachine::browse(inner(music_hoard(vec![]))); let browse = AppMachine::browse_state(inner(music_hoard(vec![])));
let app = browse.begin_search(); let app = browse.begin_search();
app.unwrap_search(); app.unwrap_search();
} }
@ -169,7 +169,8 @@ mod tests {
#[test] #[test]
fn fetch_musicbrainz() { fn fetch_musicbrainz() {
let mb_api = MockIMusicBrainz::new(); let mb_api = MockIMusicBrainz::new();
let browse = AppMachine::browse(inner_with_mb(music_hoard(COLLECTION.to_owned()), mb_api)); let browse =
AppMachine::browse_state(inner_with_mb(music_hoard(COLLECTION.to_owned()), mb_api));
// Use the second artist for this test. // Use the second artist for this test.
let browse = browse.increment_selection(Delta::Line).unwrap_browse(); let browse = browse.increment_selection(Delta::Line).unwrap_browse();
@ -179,7 +180,7 @@ mod tests {
// Because of fetch's threaded behaviour, this unit test cannot expect one or the other. // Because of fetch's threaded behaviour, this unit test cannot expect one or the other.
assert!( assert!(
matches!(public.state, AppState::Matches(_)) matches!(public.state, AppState::Match(_))
|| matches!(public.state, AppState::Fetch(_)) || matches!(public.state, AppState::Fetch(_))
); );
} }

View File

@ -3,29 +3,29 @@ use crate::tui::app::{
AppPublic, AppState, AppPublic, AppState,
}; };
pub struct AppCritical { pub struct CriticalState {
string: String, string: String,
} }
impl AppMachine<AppCritical> { impl AppMachine<CriticalState> {
pub fn critical<S: Into<String>>(inner: AppInner, string: S) -> Self { pub fn critical_state<S: Into<String>>(inner: AppInner, string: S) -> Self {
AppMachine { AppMachine {
inner, inner,
state: AppCritical { state: CriticalState {
string: string.into(), string: string.into(),
}, },
} }
} }
} }
impl From<AppMachine<AppCritical>> for App { impl From<AppMachine<CriticalState>> for App {
fn from(machine: AppMachine<AppCritical>) -> Self { fn from(machine: AppMachine<CriticalState>) -> Self {
AppState::Critical(machine) AppState::Critical(machine)
} }
} }
impl<'a> From<&'a mut AppMachine<AppCritical>> for AppPublic<'a> { impl<'a> From<&'a mut AppMachine<CriticalState>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppCritical>) -> Self { fn from(machine: &'a mut AppMachine<CriticalState>) -> Self {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Critical(&machine.state.string), state: AppState::Critical(&machine.state.string),

View File

@ -3,29 +3,29 @@ use crate::tui::app::{
AppPublic, AppState, IAppInteractError, AppPublic, AppState, IAppInteractError,
}; };
pub struct AppError { pub struct ErrorState {
string: String, string: String,
} }
impl AppMachine<AppError> { impl AppMachine<ErrorState> {
pub fn error<S: Into<String>>(inner: AppInner, string: S) -> Self { pub fn error_state<S: Into<String>>(inner: AppInner, string: S) -> Self {
AppMachine { AppMachine {
inner, inner,
state: AppError { state: ErrorState {
string: string.into(), string: string.into(),
}, },
} }
} }
} }
impl From<AppMachine<AppError>> for App { impl From<AppMachine<ErrorState>> for App {
fn from(machine: AppMachine<AppError>) -> Self { fn from(machine: AppMachine<ErrorState>) -> Self {
AppState::Error(machine) AppState::Error(machine)
} }
} }
impl<'a> From<&'a mut AppMachine<AppError>> for AppPublic<'a> { impl<'a> From<&'a mut AppMachine<ErrorState>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppError>) -> Self { fn from(machine: &'a mut AppMachine<ErrorState>) -> Self {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Error(&machine.state.string), state: AppState::Error(&machine.state.string),
@ -33,11 +33,11 @@ impl<'a> From<&'a mut AppMachine<AppError>> for AppPublic<'a> {
} }
} }
impl IAppInteractError for AppMachine<AppError> { impl IAppInteractError for AppMachine<ErrorState> {
type APP = App; type APP = App;
fn dismiss_error(self) -> Self::APP { fn dismiss_error(self) -> Self::APP {
AppMachine::browse(self.inner).into() AppMachine::browse_state(self.inner).into()
} }
} }
@ -49,7 +49,7 @@ mod tests {
#[test] #[test]
fn dismiss_error() { fn dismiss_error() {
let error = AppMachine::error(inner(music_hoard(vec![])), "get rekt"); let error = AppMachine::error_state(inner(music_hoard(vec![])), "get rekt");
let app = error.dismiss_error(); let app = error.dismiss_error();
app.unwrap_browse(); app.unwrap_browse();
} }

View File

@ -15,31 +15,31 @@ use musichoard::collection::{
use crate::tui::{ use crate::tui::{
app::{ app::{
machine::{App, AppInner, AppMachine}, machine::{App, AppInner, AppMachine},
AppMatchesInfo, AppPublic, AppState, IAppEventFetch, IAppInteractFetch, AppPublic, AppState, IAppEventFetch, IAppInteractFetch, MatchStateInfo,
}, },
event::{Event, EventSender}, event::{Event, EventSender},
lib::interface::musicbrainz::{self, Error as MbError, IMusicBrainz}, lib::interface::musicbrainz::{self, Error as MbError, IMusicBrainz},
}; };
use super::matches::AppMatches; use super::match_state::MatchState;
pub struct AppFetch { pub struct FetchState {
fetch_rx: FetchReceiver, fetch_rx: FetchReceiver,
} }
impl AppFetch { impl FetchState {
pub fn new(fetch_rx: FetchReceiver) -> Self { pub fn new(fetch_rx: FetchReceiver) -> Self {
AppFetch { fetch_rx } FetchState { fetch_rx }
} }
} }
pub type FetchError = MbError; pub type FetchError = MbError;
pub type FetchResult = Result<AppMatchesInfo, FetchError>; pub type FetchResult = Result<MatchStateInfo, FetchError>;
pub type FetchSender = mpsc::Sender<FetchResult>; pub type FetchSender = mpsc::Sender<FetchResult>;
pub type FetchReceiver = mpsc::Receiver<FetchResult>; pub type FetchReceiver = mpsc::Receiver<FetchResult>;
impl AppMachine<AppFetch> { impl AppMachine<FetchState> {
fn fetch(inner: AppInner, state: AppFetch) -> Self { fn fetch_state(inner: AppInner, state: FetchState) -> Self {
AppMachine { inner, state } AppMachine { inner, state }
} }
@ -47,38 +47,40 @@ impl AppMachine<AppFetch> {
let coll = inner.music_hoard.get_collection(); let coll = inner.music_hoard.get_collection();
let artist = match inner.selection.state_artist(coll) { let artist = match inner.selection.state_artist(coll) {
Some(artist_state) => &coll[artist_state.index], Some(artist_state) => &coll[artist_state.index],
None => return AppMachine::error(inner, "cannot fetch: no artist selected").into(), None => {
return AppMachine::error_state(inner, "cannot fetch: no artist selected").into()
}
}; };
let (fetch_tx, fetch_rx) = mpsc::channel::<FetchResult>(); let (fetch_tx, fetch_rx) = mpsc::channel::<FetchResult>();
Self::spawn_fetch_thread(&inner, artist, fetch_tx); Self::spawn_fetch_thread(&inner, artist, fetch_tx);
let fetch = AppFetch::new(fetch_rx); let fetch = FetchState::new(fetch_rx);
AppMachine::app_fetch(inner, fetch, true) AppMachine::app_fetch(inner, fetch, true)
} }
pub fn app_fetch_next(inner: AppInner, fetch: AppFetch) -> App { pub fn app_fetch_next(inner: AppInner, fetch: FetchState) -> App {
Self::app_fetch(inner, fetch, false) Self::app_fetch(inner, fetch, false)
} }
fn app_fetch(inner: AppInner, fetch: AppFetch, first: bool) -> App { fn app_fetch(inner: AppInner, fetch: FetchState, first: bool) -> App {
match fetch.fetch_rx.try_recv() { match fetch.fetch_rx.try_recv() {
Ok(fetch_result) => match fetch_result { Ok(fetch_result) => match fetch_result {
Ok(next_match) => { Ok(next_match) => {
let current = Some(next_match); let current = Some(next_match);
AppMachine::matches(inner, AppMatches::new(current, fetch)).into() AppMachine::match_state(inner, MatchState::new(current, fetch)).into()
} }
Err(fetch_err) => { Err(fetch_err) => {
AppMachine::error(inner, format!("fetch failed: {fetch_err}")).into() AppMachine::error_state(inner, format!("fetch failed: {fetch_err}")).into()
} }
}, },
Err(recv_err) => match recv_err { Err(recv_err) => match recv_err {
TryRecvError::Empty => AppMachine::fetch(inner, fetch).into(), TryRecvError::Empty => AppMachine::fetch_state(inner, fetch).into(),
TryRecvError::Disconnected => { TryRecvError::Disconnected => {
if first { if first {
AppMachine::matches(inner, AppMatches::new(None, fetch)).into() AppMachine::match_state(inner, MatchState::new(None, fetch)).into()
} else { } else {
AppMachine::browse(inner).into() AppMachine::browse_state(inner).into()
} }
} }
}, },
@ -114,7 +116,7 @@ impl AppMachine<AppFetch> {
artist: ArtistMeta, artist: ArtistMeta,
) { ) {
let result = musicbrainz.lock().unwrap().search_artist(&artist); let result = musicbrainz.lock().unwrap().search_artist(&artist);
let result = result.map(|list| AppMatchesInfo::artist(artist, list)); let result = result.map(|list| MatchStateInfo::artist(artist, list));
Self::send_fetch_result(&fetch_tx, &events, result).ok(); Self::send_fetch_result(&fetch_tx, &events, result).ok();
} }
@ -133,7 +135,7 @@ impl AppMachine<AppFetch> {
} }
let result = musicbrainz.search_release_group(&arid, &album); let result = musicbrainz.search_release_group(&arid, &album);
let result = result.map(|list| AppMatchesInfo::album(album, list)); let result = result.map(|list| MatchStateInfo::album(album, list));
if Self::send_fetch_result(&fetch_tx, &events, result).is_err() { if Self::send_fetch_result(&fetch_tx, &events, result).is_err() {
return; return;
}; };
@ -147,7 +149,7 @@ impl AppMachine<AppFetch> {
fn send_fetch_result( fn send_fetch_result(
fetch_tx: &FetchSender, fetch_tx: &FetchSender,
events: &EventSender, events: &EventSender,
result: Result<AppMatchesInfo, musicbrainz::Error>, result: Result<MatchStateInfo, musicbrainz::Error>,
) -> Result<(), ()> { ) -> Result<(), ()> {
// If receiver disconnects just drop the rest. // If receiver disconnects just drop the rest.
fetch_tx.send(result).map_err(|_| ())?; fetch_tx.send(result).map_err(|_| ())?;
@ -160,14 +162,14 @@ impl AppMachine<AppFetch> {
} }
} }
impl From<AppMachine<AppFetch>> for App { impl From<AppMachine<FetchState>> for App {
fn from(machine: AppMachine<AppFetch>) -> Self { fn from(machine: AppMachine<FetchState>) -> Self {
AppState::Fetch(machine) AppState::Fetch(machine)
} }
} }
impl<'a> From<&'a mut AppMachine<AppFetch>> for AppPublic<'a> { impl<'a> From<&'a mut AppMachine<FetchState>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppFetch>) -> Self { fn from(machine: &'a mut AppMachine<FetchState>) -> Self {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Fetch(()), state: AppState::Fetch(()),
@ -175,15 +177,15 @@ impl<'a> From<&'a mut AppMachine<AppFetch>> for AppPublic<'a> {
} }
} }
impl IAppInteractFetch for AppMachine<AppFetch> { impl IAppInteractFetch for AppMachine<FetchState> {
type APP = App; type APP = App;
fn abort(self) -> Self::APP { fn abort(self) -> Self::APP {
AppMachine::browse(self.inner).into() AppMachine::browse_state(self.inner).into()
} }
} }
impl IAppEventFetch for AppMachine<AppFetch> { impl IAppEventFetch for AppMachine<FetchState> {
type APP = App; type APP = App;
fn fetch_result_ready(self) -> Self::APP { fn fetch_result_ready(self) -> Self::APP {
@ -199,7 +201,7 @@ mod tests {
use crate::tui::{ use crate::tui::{
app::{ app::{
machine::tests::{inner, music_hoard}, machine::tests::{inner, music_hoard},
AppAlbumMatches, AppArtistMatches, IAppInteract, AlbumMatches, ArtistMatches, IApp,
}, },
event::EventReceiver, event::EventReceiver,
lib::interface::musicbrainz::{self, Match, MockIMusicBrainz}, lib::interface::musicbrainz::{self, Match, MockIMusicBrainz},
@ -285,7 +287,7 @@ mod tests {
assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady); assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady);
let result = fetch_rx.try_recv().unwrap(); let result = fetch_rx.try_recv().unwrap();
let expected = Ok(AppMatchesInfo::Album(AppAlbumMatches { let expected = Ok(MatchStateInfo::Album(AlbumMatches {
matching: album_1.clone(), matching: album_1.clone(),
list: matches_1.iter().cloned().map(Into::into).collect(), list: matches_1.iter().cloned().map(Into::into).collect(),
})); }));
@ -293,7 +295,7 @@ mod tests {
assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady); assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady);
let result = fetch_rx.try_recv().unwrap(); let result = fetch_rx.try_recv().unwrap();
let expected = Ok(AppMatchesInfo::Album(AppAlbumMatches { let expected = Ok(MatchStateInfo::Album(AlbumMatches {
matching: album_4.clone(), matching: album_4.clone(),
list: matches_4.iter().cloned().map(Into::into).collect(), list: matches_4.iter().cloned().map(Into::into).collect(),
})); }));
@ -343,7 +345,7 @@ mod tests {
assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady); assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady);
let result = fetch_rx.try_recv().unwrap(); let result = fetch_rx.try_recv().unwrap();
let expected = Ok(AppMatchesInfo::Artist(AppArtistMatches { let expected = Ok(MatchStateInfo::Artist(ArtistMatches {
matching: artist.clone(), matching: artist.clone(),
list: matches.iter().cloned().map(Into::into).collect(), list: matches.iter().cloned().map(Into::into).collect(),
})); }));
@ -391,7 +393,7 @@ mod tests {
handle.join().unwrap(); handle.join().unwrap();
let result = fetch_rx.try_recv().unwrap(); let result = fetch_rx.try_recv().unwrap();
let expected = Ok(AppMatchesInfo::Album(AppAlbumMatches { let expected = Ok(MatchStateInfo::Album(AlbumMatches {
matching: album_1.clone(), matching: album_1.clone(),
list: matches_1.iter().cloned().map(Into::into).collect(), list: matches_1.iter().cloned().map(Into::into).collect(),
})); }));
@ -405,13 +407,13 @@ mod tests {
let (tx, rx) = mpsc::channel::<FetchResult>(); let (tx, rx) = mpsc::channel::<FetchResult>();
let artist = COLLECTION[3].meta.clone(); let artist = COLLECTION[3].meta.clone();
let fetch_result = Ok(AppMatchesInfo::artist::<Match<ArtistMeta>>(artist, vec![])); let fetch_result = Ok(MatchStateInfo::artist::<Match<ArtistMeta>>(artist, vec![]));
tx.send(fetch_result).unwrap(); tx.send(fetch_result).unwrap();
let inner = inner(music_hoard(COLLECTION.clone())); let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = AppFetch::new(rx); let fetch = FetchState::new(rx);
let app = AppMachine::app_fetch(inner, fetch, true); let app = AppMachine::app_fetch(inner, fetch, true);
assert!(matches!(app, AppState::Matches(_))); assert!(matches!(app, AppState::Match(_)));
} }
#[test] #[test]
@ -422,7 +424,7 @@ mod tests {
tx.send(fetch_result).unwrap(); tx.send(fetch_result).unwrap();
let inner = inner(music_hoard(COLLECTION.clone())); let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = AppFetch::new(rx); let fetch = FetchState::new(rx);
let app = AppMachine::app_fetch(inner, fetch, true); let app = AppMachine::app_fetch(inner, fetch, true);
assert!(matches!(app, AppState::Error(_))); assert!(matches!(app, AppState::Error(_)));
} }
@ -432,7 +434,7 @@ mod tests {
let (_tx, rx) = mpsc::channel::<FetchResult>(); let (_tx, rx) = mpsc::channel::<FetchResult>();
let inner = inner(music_hoard(COLLECTION.clone())); let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = AppFetch::new(rx); let fetch = FetchState::new(rx);
let app = AppMachine::app_fetch(inner, fetch, true); let app = AppMachine::app_fetch(inner, fetch, true);
assert!(matches!(app, AppState::Fetch(_))); assert!(matches!(app, AppState::Fetch(_)));
} }
@ -442,16 +444,16 @@ mod tests {
let (_, rx) = mpsc::channel::<FetchResult>(); let (_, rx) = mpsc::channel::<FetchResult>();
let inner = inner(music_hoard(COLLECTION.clone())); let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = AppFetch::new(rx); let fetch = FetchState::new(rx);
let app = AppMachine::app_fetch(inner, fetch, true); let app = AppMachine::app_fetch(inner, fetch, true);
assert!(matches!(app, AppState::Matches(_))); assert!(matches!(app, AppState::Match(_)));
} }
#[test] #[test]
fn recv_err_disconnected_next() { fn recv_err_disconnected_next() {
let (_, rx) = mpsc::channel::<FetchResult>(); let (_, rx) = mpsc::channel::<FetchResult>();
let fetch = AppFetch::new(rx); let fetch = FetchState::new(rx);
let app = AppMachine::app_fetch_next(inner(music_hoard(COLLECTION.clone())), fetch); let app = AppMachine::app_fetch_next(inner(music_hoard(COLLECTION.clone())), fetch);
assert!(matches!(app, AppState::Browse(_))); assert!(matches!(app, AppState::Browse(_)));
} }
@ -461,24 +463,24 @@ mod tests {
let (tx, rx) = mpsc::channel::<FetchResult>(); let (tx, rx) = mpsc::channel::<FetchResult>();
let inner = inner(music_hoard(COLLECTION.clone())); let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = AppFetch::new(rx); let fetch = FetchState::new(rx);
let app = AppMachine::app_fetch(inner, fetch, true); let app = AppMachine::app_fetch(inner, fetch, true);
assert!(matches!(app, AppState::Fetch(_))); assert!(matches!(app, AppState::Fetch(_)));
let artist = COLLECTION[3].meta.clone(); let artist = COLLECTION[3].meta.clone();
let fetch_result = Ok(AppMatchesInfo::artist::<Match<ArtistMeta>>(artist, vec![])); let fetch_result = Ok(MatchStateInfo::artist::<Match<ArtistMeta>>(artist, vec![]));
tx.send(fetch_result).unwrap(); tx.send(fetch_result).unwrap();
let app = app.unwrap_fetch().fetch_result_ready(); let app = app.unwrap_fetch().fetch_result_ready();
assert!(matches!(app, AppState::Matches(_))); assert!(matches!(app, AppState::Match(_)));
} }
#[test] #[test]
fn abort() { fn abort() {
let (_, rx) = mpsc::channel::<FetchResult>(); let (_, rx) = mpsc::channel::<FetchResult>();
let fetch = AppFetch::new(rx); let fetch = FetchState::new(rx);
let app = AppMachine::fetch(inner(music_hoard(COLLECTION.clone())), fetch); let app = AppMachine::fetch_state(inner(music_hoard(COLLECTION.clone())), fetch);
let app = app.abort(); let app = app.abort();
assert!(matches!(app, AppState::Browse(_))); assert!(matches!(app, AppState::Browse(_)));

View File

@ -3,25 +3,25 @@ use crate::tui::app::{
AppPublic, AppState, IAppInteractInfo, AppPublic, AppState, IAppInteractInfo,
}; };
pub struct AppInfo; pub struct InfoState;
impl AppMachine<AppInfo> { impl AppMachine<InfoState> {
pub fn info(inner: AppInner) -> Self { pub fn info_state(inner: AppInner) -> Self {
AppMachine { AppMachine {
inner, inner,
state: AppInfo, state: InfoState,
} }
} }
} }
impl From<AppMachine<AppInfo>> for App { impl From<AppMachine<InfoState>> for App {
fn from(machine: AppMachine<AppInfo>) -> Self { fn from(machine: AppMachine<InfoState>) -> Self {
AppState::Info(machine) AppState::Info(machine)
} }
} }
impl<'a> From<&'a mut AppMachine<AppInfo>> for AppPublic<'a> { impl<'a> From<&'a mut AppMachine<InfoState>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppInfo>) -> Self { fn from(machine: &'a mut AppMachine<InfoState>) -> Self {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Info(()), state: AppState::Info(()),
@ -29,11 +29,11 @@ impl<'a> From<&'a mut AppMachine<AppInfo>> for AppPublic<'a> {
} }
} }
impl IAppInteractInfo for AppMachine<AppInfo> { impl IAppInteractInfo for AppMachine<InfoState> {
type APP = App; type APP = App;
fn hide_info_overlay(self) -> Self::APP { fn hide_info_overlay(self) -> Self::APP {
AppMachine::browse(self.inner).into() AppMachine::browse_state(self.inner).into()
} }
} }
@ -45,7 +45,7 @@ mod tests {
#[test] #[test]
fn hide_info_overlay() { fn hide_info_overlay() {
let info = AppMachine::info(inner(music_hoard(vec![]))); let info = AppMachine::info_state(inner(music_hoard(vec![])));
let app = info.hide_info_overlay(); let app = info.hide_info_overlay();
app.unwrap_browse(); app.unwrap_browse();
} }

View File

@ -2,13 +2,13 @@ use std::cmp;
use crate::tui::app::{ use crate::tui::app::{
machine::{App, AppInner, AppMachine}, machine::{App, AppInner, AppMachine},
AppAlbumMatches, AppArtistMatches, AppMatchesInfo, AppPublic, AppPublicMatches, AppState, AlbumMatches, AppPublic, AppState, ArtistMatches, IAppInteractMatch, MatchOption,
IAppInteractMatches, MatchOption, WidgetState, MatchStateInfo, MatchStatePublic, WidgetState,
}; };
use super::fetch::AppFetch; use super::fetch_state::FetchState;
impl AppArtistMatches { impl ArtistMatches {
fn len(&self) -> usize { fn len(&self) -> usize {
self.list.len() self.list.len()
} }
@ -18,7 +18,7 @@ impl AppArtistMatches {
} }
} }
impl AppAlbumMatches { impl AlbumMatches {
fn len(&self) -> usize { fn len(&self) -> usize {
self.list.len() self.list.len()
} }
@ -28,7 +28,7 @@ impl AppAlbumMatches {
} }
} }
impl AppMatchesInfo { impl MatchStateInfo {
fn len(&self) -> usize { fn len(&self) -> usize {
match self { match self {
Self::Artist(a) => a.len(), Self::Artist(a) => a.len(),
@ -44,20 +44,20 @@ impl AppMatchesInfo {
} }
} }
pub struct AppMatches { pub struct MatchState {
current: Option<AppMatchesInfo>, current: Option<MatchStateInfo>,
state: WidgetState, state: WidgetState,
fetch: AppFetch, fetch: FetchState,
} }
impl AppMatches { impl MatchState {
pub fn new(mut current: Option<AppMatchesInfo>, fetch: AppFetch) -> Self { pub fn new(mut current: Option<MatchStateInfo>, fetch: FetchState) -> Self {
let mut state = WidgetState::default(); let mut state = WidgetState::default();
if let Some(ref mut current) = current { if let Some(ref mut current) = current {
state.list.select(Some(0)); state.list.select(Some(0));
current.push_cannot_have_mbid(); current.push_cannot_have_mbid();
} }
AppMatches { MatchState {
current, current,
state, state,
fetch, fetch,
@ -65,31 +65,31 @@ impl AppMatches {
} }
} }
impl AppMachine<AppMatches> { impl AppMachine<MatchState> {
pub fn matches(inner: AppInner, state: AppMatches) -> Self { pub fn match_state(inner: AppInner, state: MatchState) -> Self {
AppMachine { inner, state } AppMachine { inner, state }
} }
} }
impl From<AppMachine<AppMatches>> for App { impl From<AppMachine<MatchState>> for App {
fn from(machine: AppMachine<AppMatches>) -> Self { fn from(machine: AppMachine<MatchState>) -> Self {
AppState::Matches(machine) AppState::Match(machine)
} }
} }
impl<'a> From<&'a mut AppMachine<AppMatches>> for AppPublic<'a> { impl<'a> From<&'a mut AppMachine<MatchState>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppMatches>) -> Self { fn from(machine: &'a mut AppMachine<MatchState>) -> Self {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Matches(AppPublicMatches { state: AppState::Match(MatchStatePublic {
matches: machine.state.current.as_ref().map(Into::into), info: machine.state.current.as_ref().map(Into::into),
state: &mut machine.state.state, state: &mut machine.state.state,
}), }),
} }
} }
} }
impl IAppInteractMatches for AppMachine<AppMatches> { impl IAppInteractMatch for AppMachine<MatchState> {
type APP = App; type APP = App;
fn prev_match(mut self) -> Self::APP { fn prev_match(mut self) -> Self::APP {
@ -120,7 +120,7 @@ impl IAppInteractMatches for AppMachine<AppMatches> {
} }
fn abort(self) -> Self::APP { fn abort(self) -> Self::APP {
AppMachine::browse(self.inner).into() AppMachine::browse_state(self.inner).into()
} }
} }
@ -153,7 +153,7 @@ mod tests {
} }
} }
fn artist_match() -> AppMatchesInfo { fn artist_match() -> MatchStateInfo {
let artist = ArtistMeta::new(ArtistId::new("Artist")); let artist = ArtistMeta::new(ArtistId::new("Artist"));
let artist_1 = artist.clone(); let artist_1 = artist.clone();
@ -164,10 +164,10 @@ mod tests {
artist_match_2.disambiguation = Some(String::from("some disambiguation")); artist_match_2.disambiguation = Some(String::from("some disambiguation"));
let list = vec![artist_match_1.clone(), artist_match_2.clone()]; let list = vec![artist_match_1.clone(), artist_match_2.clone()];
AppMatchesInfo::artist(artist, list) MatchStateInfo::artist(artist, list)
} }
fn album_match() -> AppMatchesInfo { fn album_match() -> MatchStateInfo {
let album = AlbumMeta::new( let album = AlbumMeta::new(
AlbumId::new("Album"), AlbumId::new("Album"),
AlbumDate::new(Some(1990), Some(5), None), AlbumDate::new(Some(1990), Some(5), None),
@ -184,21 +184,21 @@ mod tests {
let album_match_2 = Match::new(100, album_2); let album_match_2 = Match::new(100, album_2);
let list = vec![album_match_1.clone(), album_match_2.clone()]; let list = vec![album_match_1.clone(), album_match_2.clone()];
AppMatchesInfo::album(album, list) MatchStateInfo::album(album, list)
} }
fn fetch() -> AppFetch { fn fetch_state() -> FetchState {
let (_, rx) = mpsc::channel(); let (_, rx) = mpsc::channel();
AppFetch::new(rx) FetchState::new(rx)
} }
fn matches(matches_info: Option<AppMatchesInfo>) -> AppMatches { fn match_state(matches_info: Option<MatchStateInfo>) -> MatchState {
AppMatches::new(matches_info, fetch()) MatchState::new(matches_info, fetch_state())
} }
#[test] #[test]
fn create_empty() { fn create_empty() {
let matches = AppMachine::matches(inner(music_hoard(vec![])), matches(None)); let matches = AppMachine::match_state(inner(music_hoard(vec![])), match_state(None));
let widget_state = WidgetState::default(); let widget_state = WidgetState::default();
@ -207,18 +207,18 @@ mod tests {
let mut app: App = matches.into(); let mut app: App = matches.into();
let public = app.get(); let public = app.get();
let public_matches = public.state.unwrap_matches(); let public_matches = public.state.unwrap_match();
assert_eq!(public_matches.matches, None); assert_eq!(public_matches.info, None);
assert_eq!(public_matches.state, &widget_state); assert_eq!(public_matches.state, &widget_state);
} }
#[test] #[test]
fn create_nonempty() { fn create_nonempty() {
let mut album_match = album_match(); let mut album_match = album_match();
let matches = AppMachine::matches( let matches = AppMachine::match_state(
inner(music_hoard(vec![])), inner(music_hoard(vec![])),
matches(Some(album_match.clone())), match_state(Some(album_match.clone())),
); );
album_match.push_cannot_have_mbid(); album_match.push_cannot_have_mbid();
@ -230,19 +230,19 @@ mod tests {
let mut app: App = matches.into(); let mut app: App = matches.into();
let public = app.get(); let public = app.get();
let public_matches = public.state.unwrap_matches(); let public_matches = public.state.unwrap_match();
assert_eq!(public_matches.matches, Some(&album_match)); assert_eq!(public_matches.info, Some(&album_match));
assert_eq!(public_matches.state, &widget_state); assert_eq!(public_matches.state, &widget_state);
} }
fn matches_flow(mut matches_info: AppMatchesInfo) { fn match_state_flow(mut matches_info: MatchStateInfo) {
// tx must exist for rx to return Empty rather than Disconnected. // tx must exist for rx to return Empty rather than Disconnected.
#[allow(unused_variables)] #[allow(unused_variables)]
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let app_matches = AppMatches::new(Some(matches_info.clone()), AppFetch::new(rx)); let app_matches = MatchState::new(Some(matches_info.clone()), FetchState::new(rx));
let matches = AppMachine::matches(inner(music_hoard(vec![])), app_matches); let matches = AppMachine::match_state(inner(music_hoard(vec![])), app_matches);
matches_info.push_cannot_have_mbid(); matches_info.push_cannot_have_mbid();
let mut widget_state = WidgetState::default(); let mut widget_state = WidgetState::default();
@ -251,23 +251,23 @@ mod tests {
assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
assert_eq!(matches.state.state, widget_state); assert_eq!(matches.state.state, widget_state);
let matches = matches.prev_match().unwrap_matches(); let matches = matches.prev_match().unwrap_match();
assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
assert_eq!(matches.state.state.list.selected(), Some(0)); assert_eq!(matches.state.state.list.selected(), Some(0));
let matches = matches.next_match().unwrap_matches(); let matches = matches.next_match().unwrap_match();
assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
assert_eq!(matches.state.state.list.selected(), Some(1)); assert_eq!(matches.state.state.list.selected(), Some(1));
// Next is CannotHaveMBID // Next is CannotHaveMBID
let matches = matches.next_match().unwrap_matches(); let matches = matches.next_match().unwrap_match();
assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
assert_eq!(matches.state.state.list.selected(), Some(2)); assert_eq!(matches.state.state.list.selected(), Some(2));
let matches = matches.next_match().unwrap_matches(); let matches = matches.next_match().unwrap_match();
assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
assert_eq!(matches.state.state.list.selected(), Some(2)); assert_eq!(matches.state.state.list.selected(), Some(2));
@ -278,20 +278,20 @@ mod tests {
#[test] #[test]
fn artist_matches_flow() { fn artist_matches_flow() {
matches_flow(artist_match()); match_state_flow(artist_match());
} }
#[test] #[test]
fn album_matches_flow() { fn album_matches_flow() {
matches_flow(album_match()); match_state_flow(album_match());
} }
#[test] #[test]
fn matches_abort() { fn abort() {
let mut album_match = album_match(); let mut album_match = album_match();
let matches = AppMachine::matches( let matches = AppMachine::match_state(
inner(music_hoard(vec![])), inner(music_hoard(vec![])),
matches(Some(album_match.clone())), match_state(Some(album_match.clone())),
); );
album_match.push_cannot_have_mbid(); album_match.push_cannot_have_mbid();
@ -305,10 +305,10 @@ mod tests {
} }
#[test] #[test]
fn matches_select_empty() { fn select_empty() {
// Note that what really matters in this test is actually that the transmit channel has // Note that what really matters in this test is actually that the transmit channel has
// disconnected and so the receive within AppFetch concludes there are no more matches. // disconnected and so the receive within FetchState concludes there are no more matches.
let matches = AppMachine::matches(inner(music_hoard(vec![])), matches(None)); let matches = AppMachine::match_state(inner(music_hoard(vec![])), match_state(None));
matches.select().unwrap_browse(); matches.select().unwrap_browse();
} }
} }

View File

@ -1,40 +1,40 @@
mod browse; mod browse_state;
mod critical; mod critical_state;
mod error; mod error_state;
mod fetch; mod fetch_state;
mod info; mod info_state;
mod matches; mod match_state;
mod reload; mod reload_state;
mod search; mod search_state;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use crate::tui::{ use crate::tui::{
app::{selection::Selection, AppPublic, AppPublicInner, AppState, IAppAccess, IAppInteract}, app::{selection::Selection, AppPublic, AppPublicInner, AppState, IApp, IAppAccess},
event::EventSender, event::EventSender,
lib::{interface::musicbrainz::IMusicBrainz, IMusicHoard}, lib::{interface::musicbrainz::IMusicBrainz, IMusicHoard},
}; };
use browse::AppBrowse; use browse_state::BrowseState;
use critical::AppCritical; use critical_state::CriticalState;
use error::AppError; use error_state::ErrorState;
use fetch::AppFetch; use fetch_state::FetchState;
use info::AppInfo; use info_state::InfoState;
use matches::AppMatches; use match_state::MatchState;
use reload::AppReload; use reload_state::ReloadState;
use search::AppSearch; use search_state::SearchState;
use super::IAppBase; use super::IAppBase;
pub type App = AppState< pub type App = AppState<
AppMachine<AppBrowse>, AppMachine<BrowseState>,
AppMachine<AppInfo>, AppMachine<InfoState>,
AppMachine<AppReload>, AppMachine<ReloadState>,
AppMachine<AppSearch>, AppMachine<SearchState>,
AppMachine<AppFetch>, AppMachine<FetchState>,
AppMachine<AppMatches>, AppMachine<MatchState>,
AppMachine<AppError>, AppMachine<ErrorState>,
AppMachine<AppCritical>, AppMachine<CriticalState>,
>; >;
pub struct AppMachine<STATE> { pub struct AppMachine<STATE> {
@ -59,8 +59,8 @@ impl App {
let init_result = Self::init(&mut music_hoard); let init_result = Self::init(&mut music_hoard);
let inner = AppInner::new(music_hoard, musicbrainz, events); let inner = AppInner::new(music_hoard, musicbrainz, events);
match init_result { match init_result {
Ok(()) => AppMachine::browse(inner).into(), Ok(()) => AppMachine::browse_state(inner).into(),
Err(err) => AppMachine::critical(inner, err.to_string()).into(), Err(err) => AppMachine::critical_state(inner, err.to_string()).into(),
} }
} }
@ -71,40 +71,40 @@ impl App {
fn inner_ref(&self) -> &AppInner { fn inner_ref(&self) -> &AppInner {
match self { match self {
AppState::Browse(browse) => &browse.inner, AppState::Browse(browse_state) => &browse_state.inner,
AppState::Info(info) => &info.inner, AppState::Info(info_state) => &info_state.inner,
AppState::Reload(reload) => &reload.inner, AppState::Reload(reload_state) => &reload_state.inner,
AppState::Search(search) => &search.inner, AppState::Search(search_state) => &search_state.inner,
AppState::Fetch(fetch) => &fetch.inner, AppState::Fetch(fetch_state) => &fetch_state.inner,
AppState::Matches(matches) => &matches.inner, AppState::Match(match_state) => &match_state.inner,
AppState::Error(error) => &error.inner, AppState::Error(error_state) => &error_state.inner,
AppState::Critical(critical) => &critical.inner, AppState::Critical(critical_state) => &critical_state.inner,
} }
} }
fn inner_mut(&mut self) -> &mut AppInner { fn inner_mut(&mut self) -> &mut AppInner {
match self { match self {
AppState::Browse(browse) => &mut browse.inner, AppState::Browse(browse_state) => &mut browse_state.inner,
AppState::Info(info) => &mut info.inner, AppState::Info(info_state) => &mut info_state.inner,
AppState::Reload(reload) => &mut reload.inner, AppState::Reload(reload_state) => &mut reload_state.inner,
AppState::Search(search) => &mut search.inner, AppState::Search(search_state) => &mut search_state.inner,
AppState::Fetch(fetch) => &mut fetch.inner, AppState::Fetch(fetch_state) => &mut fetch_state.inner,
AppState::Matches(matches) => &mut matches.inner, AppState::Match(match_state) => &mut match_state.inner,
AppState::Error(error) => &mut error.inner, AppState::Error(error_state) => &mut error_state.inner,
AppState::Critical(critical) => &mut critical.inner, AppState::Critical(critical_state) => &mut critical_state.inner,
} }
} }
} }
impl IAppInteract for App { impl IApp for App {
type BS = AppMachine<AppBrowse>; type BrowseState = AppMachine<BrowseState>;
type IS = AppMachine<AppInfo>; type InfoState = AppMachine<InfoState>;
type RS = AppMachine<AppReload>; type ReloadState = AppMachine<ReloadState>;
type SS = AppMachine<AppSearch>; type SearchState = AppMachine<SearchState>;
type FS = AppMachine<AppFetch>; type FetchState = AppMachine<FetchState>;
type MS = AppMachine<AppMatches>; type MatchState = AppMachine<MatchState>;
type ES = AppMachine<AppError>; type ErrorState = AppMachine<ErrorState>;
type CS = AppMachine<AppCritical>; type CriticalState = AppMachine<CriticalState>;
fn is_running(&self) -> bool { fn is_running(&self) -> bool {
self.inner_ref().running self.inner_ref().running
@ -117,8 +117,16 @@ impl IAppInteract for App {
fn state( fn state(
self, self,
) -> AppState<Self::BS, Self::IS, Self::RS, Self::SS, Self::FS, Self::MS, Self::ES, Self::CS> ) -> AppState<
{ Self::BrowseState,
Self::InfoState,
Self::ReloadState,
Self::SearchState,
Self::FetchState,
Self::MatchState,
Self::ErrorState,
Self::CriticalState,
> {
self self
} }
} }
@ -139,7 +147,7 @@ impl IAppAccess for App {
AppState::Reload(reload) => reload.into(), AppState::Reload(reload) => reload.into(),
AppState::Search(search) => search.into(), AppState::Search(search) => search.into(),
AppState::Fetch(fetch) => fetch.into(), AppState::Fetch(fetch) => fetch.into(),
AppState::Matches(matches) => matches.into(), AppState::Match(matches) => matches.into(),
AppState::Error(error) => error.into(), AppState::Error(error) => error.into(),
AppState::Critical(critical) => critical.into(), AppState::Critical(critical) => critical.into(),
} }
@ -179,7 +187,7 @@ mod tests {
use musichoard::collection::Collection; use musichoard::collection::Collection;
use crate::tui::{ use crate::tui::{
app::{AppState, IAppInteract, IAppInteractBrowse}, app::{AppState, IApp, IAppInteractBrowse},
lib::{interface::musicbrainz::MockIMusicBrainz, MockIMusicHoard}, lib::{interface::musicbrainz::MockIMusicBrainz, MockIMusicHoard},
EventChannel, EventChannel,
}; };
@ -222,9 +230,9 @@ mod tests {
} }
} }
pub fn unwrap_matches(self) -> MS { pub fn unwrap_match(self) -> MS {
match self { match self {
AppState::Matches(matches) => matches, AppState::Match(matches) => matches,
_ => panic!(), _ => panic!(),
} }
} }
@ -375,7 +383,7 @@ mod tests {
let (_, rx) = mpsc::channel(); let (_, rx) = mpsc::channel();
let inner = app.unwrap_browse().inner; let inner = app.unwrap_browse().inner;
let state = AppFetch::new(rx); let state = FetchState::new(rx);
app = AppMachine { inner, state }.into(); app = AppMachine { inner, state }.into();
let state = app.state(); let state = app.state();
@ -400,20 +408,21 @@ mod tests {
assert!(app.is_running()); assert!(app.is_running());
let (_, rx) = mpsc::channel(); let (_, rx) = mpsc::channel();
let fetch = AppFetch::new(rx); let fetch = FetchState::new(rx);
app = AppMachine::matches(app.unwrap_browse().inner, AppMatches::new(None, fetch)).into(); app =
AppMachine::match_state(app.unwrap_browse().inner, MatchState::new(None, fetch)).into();
let state = app.state(); let state = app.state();
assert!(matches!(state, AppState::Matches(_))); assert!(matches!(state, AppState::Match(_)));
app = state; app = state;
app = app.no_op(); app = app.no_op();
let state = app.state(); let state = app.state();
assert!(matches!(state, AppState::Matches(_))); assert!(matches!(state, AppState::Match(_)));
app = state; app = state;
let public = app.get(); let public = app.get();
assert!(matches!(public.state, AppState::Matches(_))); assert!(matches!(public.state, AppState::Match(_)));
let app = app.force_quit(); let app = app.force_quit();
assert!(!app.is_running()); assert!(!app.is_running());
@ -424,7 +433,7 @@ mod tests {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
assert!(app.is_running()); assert!(app.is_running());
app = AppMachine::error(app.unwrap_browse().inner, "get rekt").into(); app = AppMachine::error_state(app.unwrap_browse().inner, "get rekt").into();
let state = app.state(); let state = app.state();
assert!(matches!(state, AppState::Error(_))); assert!(matches!(state, AppState::Error(_)));
@ -447,7 +456,7 @@ mod tests {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
assert!(app.is_running()); assert!(app.is_running());
app = AppMachine::critical(app.unwrap_browse().inner, "get rekt").into(); app = AppMachine::critical_state(app.unwrap_browse().inner, "get rekt").into();
let state = app.state(); let state = app.state();
assert!(matches!(state, AppState::Critical(_))); assert!(matches!(state, AppState::Critical(_)));

View File

@ -4,24 +4,24 @@ use crate::tui::app::{
AppPublic, AppState, IAppInteractReload, AppPublic, AppState, IAppInteractReload,
}; };
pub struct AppReload; pub struct ReloadState;
impl AppMachine<AppReload> { impl AppMachine<ReloadState> {
pub fn reload(inner: AppInner) -> Self { pub fn reload_state(inner: AppInner) -> Self {
AppMachine { AppMachine {
inner, inner,
state: AppReload, state: ReloadState,
} }
} }
} }
impl From<AppMachine<AppReload>> for App { impl From<AppMachine<ReloadState>> for App {
fn from(machine: AppMachine<AppReload>) -> Self { fn from(machine: AppMachine<ReloadState>) -> Self {
AppState::Reload(machine) AppState::Reload(machine)
} }
} }
impl<'a> From<&'a mut AppMachine<AppReload>> for AppPublic<'a> { impl<'a> From<&'a mut AppMachine<ReloadState>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppReload>) -> Self { fn from(machine: &'a mut AppMachine<ReloadState>) -> Self {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Reload(()), state: AppState::Reload(()),
@ -29,7 +29,7 @@ impl<'a> From<&'a mut AppMachine<AppReload>> for AppPublic<'a> {
} }
} }
impl IAppInteractReload for AppMachine<AppReload> { impl IAppInteractReload for AppMachine<ReloadState> {
type APP = App; type APP = App;
fn reload_library(mut self) -> Self::APP { fn reload_library(mut self) -> Self::APP {
@ -51,7 +51,7 @@ impl IAppInteractReload for AppMachine<AppReload> {
} }
fn hide_reload_menu(self) -> Self::APP { fn hide_reload_menu(self) -> Self::APP {
AppMachine::browse(self.inner).into() AppMachine::browse_state(self.inner).into()
} }
} }
@ -59,16 +59,16 @@ trait IAppInteractReloadPrivate {
fn refresh(self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App; fn refresh(self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App;
} }
impl IAppInteractReloadPrivate for AppMachine<AppReload> { impl IAppInteractReloadPrivate for AppMachine<ReloadState> {
fn refresh(mut self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App { fn refresh(mut self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App {
match result { match result {
Ok(()) => { Ok(()) => {
self.inner self.inner
.selection .selection
.select_by_id(self.inner.music_hoard.get_collection(), previous); .select_by_id(self.inner.music_hoard.get_collection(), previous);
AppMachine::browse(self.inner).into() AppMachine::browse_state(self.inner).into()
} }
Err(err) => AppMachine::error(self.inner, err.to_string()).into(), Err(err) => AppMachine::error_state(self.inner, err.to_string()).into(),
} }
} }
} }
@ -81,7 +81,7 @@ mod tests {
#[test] #[test]
fn hide_reload_menu() { fn hide_reload_menu() {
let reload = AppMachine::reload(inner(music_hoard(vec![]))); let reload = AppMachine::reload_state(inner(music_hoard(vec![])));
let app = reload.hide_reload_menu(); let app = reload.hide_reload_menu();
app.unwrap_browse(); app.unwrap_browse();
} }
@ -95,7 +95,7 @@ mod tests {
.times(1) .times(1)
.return_once(|| Ok(())); .return_once(|| Ok(()));
let reload = AppMachine::reload(inner(music_hoard)); let reload = AppMachine::reload_state(inner(music_hoard));
let app = reload.reload_database(); let app = reload.reload_database();
app.unwrap_browse(); app.unwrap_browse();
} }
@ -109,7 +109,7 @@ mod tests {
.times(1) .times(1)
.return_once(|| Ok(())); .return_once(|| Ok(()));
let reload = AppMachine::reload(inner(music_hoard)); let reload = AppMachine::reload_state(inner(music_hoard));
let app = reload.reload_library(); let app = reload.reload_library();
app.unwrap_browse(); app.unwrap_browse();
} }
@ -123,7 +123,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 reload = AppMachine::reload(inner(music_hoard)); let reload = AppMachine::reload_state(inner(music_hoard));
let app = reload.reload_database(); let app = reload.reload_database();
app.unwrap_error(); app.unwrap_error();
} }

View File

@ -20,22 +20,22 @@ const REPLACE: [&str; 11] = ["-", "-", "-", "-", "-", "'", "'", "\"", "\"", "...
static AC: Lazy<AhoCorasick> = static AC: Lazy<AhoCorasick> =
Lazy::new(|| AhoCorasick::new(SPECIAL.map(|ch| ch.to_string())).unwrap()); Lazy::new(|| AhoCorasick::new(SPECIAL.map(|ch| ch.to_string())).unwrap());
pub struct AppSearch { pub struct SearchState {
string: String, string: String,
orig: ListSelection, orig: ListSelection,
memo: Vec<AppSearchMemo>, memo: Vec<SearchStateMemo>,
} }
struct AppSearchMemo { struct SearchStateMemo {
index: Option<usize>, index: Option<usize>,
char: bool, char: bool,
} }
impl AppMachine<AppSearch> { impl AppMachine<SearchState> {
pub fn search(inner: AppInner, orig: ListSelection) -> Self { pub fn search_state(inner: AppInner, orig: ListSelection) -> Self {
AppMachine { AppMachine {
inner, inner,
state: AppSearch { state: SearchState {
string: String::new(), string: String::new(),
orig, orig,
memo: vec![], memo: vec![],
@ -44,14 +44,14 @@ impl AppMachine<AppSearch> {
} }
} }
impl From<AppMachine<AppSearch>> for App { impl From<AppMachine<SearchState>> for App {
fn from(machine: AppMachine<AppSearch>) -> Self { fn from(machine: AppMachine<SearchState>) -> Self {
AppState::Search(machine) AppState::Search(machine)
} }
} }
impl<'a> From<&'a mut AppMachine<AppSearch>> for AppPublic<'a> { impl<'a> From<&'a mut AppMachine<SearchState>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<AppSearch>) -> Self { fn from(machine: &'a mut AppMachine<SearchState>) -> Self {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Search(&machine.state.string), state: AppState::Search(&machine.state.string),
@ -59,13 +59,13 @@ impl<'a> From<&'a mut AppMachine<AppSearch>> for AppPublic<'a> {
} }
} }
impl IAppInteractSearch for AppMachine<AppSearch> { impl IAppInteractSearch for AppMachine<SearchState> {
type APP = App; type APP = App;
fn append_character(mut self, ch: char) -> Self::APP { fn append_character(mut self, ch: char) -> Self::APP {
self.state.string.push(ch); self.state.string.push(ch);
let index = self.inner.selection.selected(); let index = self.inner.selection.selected();
self.state.memo.push(AppSearchMemo { index, char: true }); self.state.memo.push(SearchStateMemo { index, char: true });
self.incremental_search(false); self.incremental_search(false);
self.into() self.into()
} }
@ -73,7 +73,7 @@ impl IAppInteractSearch for AppMachine<AppSearch> {
fn search_next(mut self) -> Self::APP { fn search_next(mut self) -> Self::APP {
if !self.state.string.is_empty() { if !self.state.string.is_empty() {
let index = self.inner.selection.selected(); let index = self.inner.selection.selected();
self.state.memo.push(AppSearchMemo { index, char: false }); self.state.memo.push(SearchStateMemo { index, char: false });
self.incremental_search(true); self.incremental_search(true);
} }
self.into() self.into()
@ -91,12 +91,12 @@ impl IAppInteractSearch for AppMachine<AppSearch> {
} }
fn finish_search(self) -> Self::APP { fn finish_search(self) -> Self::APP {
AppMachine::browse(self.inner).into() AppMachine::browse_state(self.inner).into()
} }
fn cancel_search(mut self) -> Self::APP { fn cancel_search(mut self) -> Self::APP {
self.inner.selection.select_by_list(self.state.orig); self.inner.selection.select_by_list(self.state.orig);
AppMachine::browse(self.inner).into() AppMachine::browse_state(self.inner).into()
} }
} }
@ -120,7 +120,7 @@ trait IAppInteractSearchPrivate {
fn normalize_search(search: &str, lowercase: bool, asciify: bool) -> String; fn normalize_search(search: &str, lowercase: bool, asciify: bool) -> String;
} }
impl IAppInteractSearchPrivate for AppMachine<AppSearch> { impl IAppInteractSearchPrivate for AppMachine<SearchState> {
fn incremental_search(&mut self, next: bool) { fn incremental_search(&mut self, next: bool) {
let collection = self.inner.music_hoard.get_collection(); let collection = self.inner.music_hoard.get_collection();
let search = &self.state.string; let search = &self.state.string;
@ -254,7 +254,7 @@ mod tests {
#[test] #[test]
fn artist_incremental_search() { fn artist_incremental_search() {
// Empty collection. // Empty collection.
let mut search = AppMachine::search(inner(music_hoard(vec![])), orig(None)); let mut search = AppMachine::search_state(inner(music_hoard(vec![])), orig(None));
assert_eq!(search.inner.selection.selected(), None); assert_eq!(search.inner.selection.selected(), None);
search.state.string = String::from("album_artist 'a'"); search.state.string = String::from("album_artist 'a'");
@ -263,7 +263,7 @@ mod tests {
// Basic test, first element. // Basic test, first element.
let mut search = let mut search =
AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from(""); search.state.string = String::from("");
@ -280,7 +280,7 @@ mod tests {
// Basic test, non-first element. // Basic test, non-first element.
let mut search = let mut search =
AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("album_artist "); search.state.string = String::from("album_artist ");
@ -293,7 +293,7 @@ mod tests {
// Non-lowercase. // Non-lowercase.
let mut search = let mut search =
AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("Album_Artist "); search.state.string = String::from("Album_Artist ");
@ -306,7 +306,7 @@ mod tests {
// Non-ascii. // Non-ascii.
let mut search = let mut search =
AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("album_artist "); search.state.string = String::from("album_artist ");
@ -319,7 +319,7 @@ mod tests {
// Non-lowercase, non-ascii. // Non-lowercase, non-ascii.
let mut search = let mut search =
AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("Album_Artist "); search.state.string = String::from("Album_Artist ");
@ -332,7 +332,7 @@ mod tests {
// Stop at name, not sort name. // Stop at name, not sort name.
let mut search = let mut search =
AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("the "); search.state.string = String::from("the ");
@ -345,7 +345,7 @@ mod tests {
// Search next with common prefix. // Search next with common prefix.
let mut search = let mut search =
AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("album_artist"); search.state.string = String::from("album_artist");
@ -368,7 +368,7 @@ mod tests {
#[test] #[test]
fn album_incremental_search() { fn album_incremental_search() {
let mut search = let mut search =
AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
search.inner.selection.increment_category(); search.inner.selection.increment_category();
assert_eq!(search.inner.selection.category(), Category::Album); assert_eq!(search.inner.selection.category(), Category::Album);
@ -392,7 +392,7 @@ mod tests {
#[test] #[test]
fn track_incremental_search() { fn track_incremental_search() {
let mut search = let mut search =
AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
search.inner.selection.increment_category(); search.inner.selection.increment_category();
search.inner.selection.increment_category(); search.inner.selection.increment_category();
assert_eq!(search.inner.selection.category(), Category::Track); assert_eq!(search.inner.selection.category(), Category::Track);
@ -418,7 +418,8 @@ mod tests {
#[test] #[test]
fn search() { fn search() {
let search = AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2))); let search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2)));
assert_eq!(search.inner.selection.selected(), Some(0)); assert_eq!(search.inner.selection.selected(), Some(0));
let search = search.append_character('a').unwrap_search(); let search = search.append_character('a').unwrap_search();
@ -458,7 +459,8 @@ mod tests {
#[test] #[test]
fn search_next_step_back() { fn search_next_step_back() {
let search = AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2))); let search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2)));
assert_eq!(search.inner.selection.selected(), Some(0)); assert_eq!(search.inner.selection.selected(), Some(0));
let search = search.step_back().unwrap_search(); let search = search.step_back().unwrap_search();
@ -495,7 +497,8 @@ mod tests {
#[test] #[test]
fn cancel_search() { fn cancel_search() {
let search = AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2))); let search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2)));
assert_eq!(search.inner.selection.selected(), Some(0)); assert_eq!(search.inner.selection.selected(), Some(0));
let search = search.append_character('a').unwrap_search(); let search = search.append_character('a').unwrap_search();
@ -522,7 +525,7 @@ mod tests {
#[test] #[test]
fn empty_search() { fn empty_search() {
let search = AppMachine::search(inner(music_hoard(vec![])), orig(None)); let search = AppMachine::search_state(inner(music_hoard(vec![])), orig(None));
assert_eq!(search.inner.selection.selected(), None); assert_eq!(search.inner.selection.selected(), None);
let search = search.append_character('a').unwrap_search(); let search = search.append_character('a').unwrap_search();
@ -552,7 +555,7 @@ mod benches {
use super::*; use super::*;
type Search = AppMachine<MockIMusicHoard, AppSearch>; type Search = AppMachine<MockIMusicHoard, SearchState>;
#[bench] #[bench]
fn is_char_sensitive(b: &mut Bencher) { fn is_char_sensitive(b: &mut Bencher) {

View File

@ -8,26 +8,37 @@ use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection};
use crate::tui::lib::interface::musicbrainz::Match; use crate::tui::lib::interface::musicbrainz::Match;
pub enum AppState<BS, IS, RS, SS, FS, MS, ES, CS> { pub enum AppState<
Browse(BS), BrowseState,
Info(IS), InfoState,
Reload(RS), ReloadState,
Search(SS), SearchState,
Fetch(FS), FetchState,
Matches(MS), MatchState,
Error(ES), ErrorState,
Critical(CS), CriticalState,
> {
Browse(BrowseState),
Info(InfoState),
Reload(ReloadState),
Search(SearchState),
Fetch(FetchState),
Match(MatchState),
Error(ErrorState),
Critical(CriticalState),
} }
pub trait IAppInteract { pub trait IApp {
type BS: IAppBase<APP = Self> + IAppInteractBrowse<APP = Self>; type BrowseState: IAppBase<APP = Self> + IAppInteractBrowse<APP = Self>;
type IS: IAppBase<APP = Self> + IAppInteractInfo<APP = Self>; type InfoState: IAppBase<APP = Self> + IAppInteractInfo<APP = Self>;
type RS: IAppBase<APP = Self> + IAppInteractReload<APP = Self>; type ReloadState: IAppBase<APP = Self> + IAppInteractReload<APP = Self>;
type SS: IAppBase<APP = Self> + IAppInteractSearch<APP = Self>; type SearchState: IAppBase<APP = Self> + IAppInteractSearch<APP = Self>;
type FS: IAppBase<APP = Self> + IAppInteractFetch<APP = Self> + IAppEventFetch<APP = Self>; type FetchState: IAppBase<APP = Self>
type MS: IAppBase<APP = Self> + IAppInteractMatches<APP = Self>; + IAppInteractFetch<APP = Self>
type ES: IAppBase<APP = Self> + IAppInteractError<APP = Self>; + IAppEventFetch<APP = Self>;
type CS: IAppBase<APP = Self>; type MatchState: IAppBase<APP = Self> + IAppInteractMatch<APP = Self>;
type ErrorState: IAppBase<APP = Self> + IAppInteractError<APP = Self>;
type CriticalState: IAppBase<APP = Self>;
fn is_running(&self) -> bool; fn is_running(&self) -> bool;
fn force_quit(self) -> Self; fn force_quit(self) -> Self;
@ -35,17 +46,26 @@ pub trait IAppInteract {
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn state( fn state(
self, self,
) -> AppState<Self::BS, Self::IS, Self::RS, Self::SS, Self::FS, Self::MS, Self::ES, Self::CS>; ) -> AppState<
Self::BrowseState,
Self::InfoState,
Self::ReloadState,
Self::SearchState,
Self::FetchState,
Self::MatchState,
Self::ErrorState,
Self::CriticalState,
>;
} }
pub trait IAppBase { pub trait IAppBase {
type APP: IAppInteract; type APP: IApp;
fn no_op(self) -> Self::APP; fn no_op(self) -> Self::APP;
} }
pub trait IAppInteractBrowse { pub trait IAppInteractBrowse {
type APP: IAppInteract; type APP: IApp;
fn quit(self) -> Self::APP; fn quit(self) -> Self::APP;
@ -64,13 +84,13 @@ pub trait IAppInteractBrowse {
} }
pub trait IAppInteractInfo { pub trait IAppInteractInfo {
type APP: IAppInteract; type APP: IApp;
fn hide_info_overlay(self) -> Self::APP; fn hide_info_overlay(self) -> Self::APP;
} }
pub trait IAppInteractReload { pub trait IAppInteractReload {
type APP: IAppInteract; type APP: IApp;
fn reload_library(self) -> Self::APP; fn reload_library(self) -> Self::APP;
fn reload_database(self) -> Self::APP; fn reload_database(self) -> Self::APP;
@ -78,7 +98,7 @@ pub trait IAppInteractReload {
} }
pub trait IAppInteractSearch { pub trait IAppInteractSearch {
type APP: IAppInteract; type APP: IApp;
fn append_character(self, ch: char) -> Self::APP; fn append_character(self, ch: char) -> Self::APP;
fn search_next(self) -> Self::APP; fn search_next(self) -> Self::APP;
@ -88,19 +108,19 @@ pub trait IAppInteractSearch {
} }
pub trait IAppInteractFetch { pub trait IAppInteractFetch {
type APP: IAppInteract; type APP: IApp;
fn abort(self) -> Self::APP; fn abort(self) -> Self::APP;
} }
pub trait IAppEventFetch { pub trait IAppEventFetch {
type APP: IAppInteract; type APP: IApp;
fn fetch_result_ready(self) -> Self::APP; fn fetch_result_ready(self) -> Self::APP;
} }
pub trait IAppInteractMatches { pub trait IAppInteractMatch {
type APP: IAppInteract; type APP: IApp;
fn prev_match(self) -> Self::APP; fn prev_match(self) -> Self::APP;
fn next_match(self) -> Self::APP; fn next_match(self) -> Self::APP;
@ -110,7 +130,7 @@ pub trait IAppInteractMatches {
} }
pub trait IAppInteractError { pub trait IAppInteractError {
type APP: IAppInteract; type APP: IApp;
fn dismiss_error(self) -> Self::APP; fn dismiss_error(self) -> Self::APP;
} }
@ -146,42 +166,42 @@ impl<T> From<Match<T>> for MatchOption<T> {
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct AppArtistMatches { pub struct ArtistMatches {
pub matching: ArtistMeta, pub matching: ArtistMeta,
pub list: Vec<MatchOption<ArtistMeta>>, pub list: Vec<MatchOption<ArtistMeta>>,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct AppAlbumMatches { pub struct AlbumMatches {
pub matching: AlbumMeta, pub matching: AlbumMeta,
pub list: Vec<MatchOption<AlbumMeta>>, pub list: Vec<MatchOption<AlbumMeta>>,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum AppMatchesInfo { pub enum MatchStateInfo {
Artist(AppArtistMatches), Artist(ArtistMatches),
Album(AppAlbumMatches), Album(AlbumMatches),
} }
impl AppMatchesInfo { impl MatchStateInfo {
pub fn artist<M: Into<MatchOption<ArtistMeta>>>(matching: ArtistMeta, list: Vec<M>) -> Self { pub fn artist<M: Into<MatchOption<ArtistMeta>>>(matching: ArtistMeta, list: Vec<M>) -> Self {
let list: Vec<MatchOption<ArtistMeta>> = list.into_iter().map(Into::into).collect(); let list: Vec<MatchOption<ArtistMeta>> = list.into_iter().map(Into::into).collect();
AppMatchesInfo::Artist(AppArtistMatches { matching, list }) MatchStateInfo::Artist(ArtistMatches { matching, list })
} }
pub fn album<M: Into<MatchOption<AlbumMeta>>>(matching: AlbumMeta, list: Vec<M>) -> Self { pub fn album<M: Into<MatchOption<AlbumMeta>>>(matching: AlbumMeta, list: Vec<M>) -> Self {
let list: Vec<MatchOption<AlbumMeta>> = list.into_iter().map(Into::into).collect(); let list: Vec<MatchOption<AlbumMeta>> = list.into_iter().map(Into::into).collect();
AppMatchesInfo::Album(AppAlbumMatches { matching, list }) MatchStateInfo::Album(AlbumMatches { matching, list })
} }
} }
pub struct AppPublicMatches<'app> { pub struct MatchStatePublic<'app> {
pub matches: Option<&'app AppMatchesInfo>, pub info: Option<&'app MatchStateInfo>,
pub state: &'app mut WidgetState, pub state: &'app mut WidgetState,
} }
pub type AppPublicState<'app> = pub type AppPublicState<'app> =
AppState<(), (), (), &'app str, (), AppPublicMatches<'app>, &'app str, &'app str>; AppState<(), (), (), &'app str, (), MatchStatePublic<'app>, &'app str, &'app str>;
impl<BS, IS, RS, SS, FS, MS, ES, CS> AppState<BS, IS, RS, SS, FS, MS, ES, CS> { impl<BS, IS, RS, SS, FS, MS, ES, CS> AppState<BS, IS, RS, SS, FS, MS, ES, CS> {
pub fn is_search(&self) -> bool { pub fn is_search(&self) -> bool {

View File

@ -5,8 +5,8 @@ use mockall::automock;
use crate::tui::{ use crate::tui::{
app::{ app::{
AppState, Delta, IAppInteract, IAppInteractBrowse, IAppInteractError, IAppInteractFetch, AppState, Delta, IApp, IAppInteractBrowse, IAppInteractError, IAppInteractFetch,
IAppInteractInfo, IAppInteractMatches, IAppInteractReload, IAppInteractSearch, IAppInteractInfo, IAppInteractMatch, IAppInteractReload, IAppInteractSearch,
}, },
event::{Event, EventError, EventReceiver}, event::{Event, EventError, EventReceiver},
}; };
@ -14,20 +14,20 @@ use crate::tui::{
use super::app::{IAppBase, IAppEventFetch}; use super::app::{IAppBase, IAppEventFetch};
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IEventHandler<APP: IAppInteract> { pub trait IEventHandler<APP: IApp> {
fn handle_next_event(&self, app: APP) -> Result<APP, EventError>; fn handle_next_event(&self, app: APP) -> Result<APP, EventError>;
} }
trait IEventHandlerPrivate<APP: IAppInteract> { trait IEventHandlerPrivate<APP: IApp> {
fn handle_key_event(app: APP, key_event: KeyEvent) -> APP; fn handle_key_event(app: APP, key_event: KeyEvent) -> APP;
fn handle_browse_key_event(app: <APP as IAppInteract>::BS, key_event: KeyEvent) -> APP; fn handle_browse_key_event(app: <APP as IApp>::BrowseState, key_event: KeyEvent) -> APP;
fn handle_info_key_event(app: <APP as IAppInteract>::IS, key_event: KeyEvent) -> APP; fn handle_info_key_event(app: <APP as IApp>::InfoState, key_event: KeyEvent) -> APP;
fn handle_reload_key_event(app: <APP as IAppInteract>::RS, key_event: KeyEvent) -> APP; fn handle_reload_key_event(app: <APP as IApp>::ReloadState, key_event: KeyEvent) -> APP;
fn handle_search_key_event(app: <APP as IAppInteract>::SS, key_event: KeyEvent) -> APP; fn handle_search_key_event(app: <APP as IApp>::SearchState, key_event: KeyEvent) -> APP;
fn handle_fetch_key_event(app: <APP as IAppInteract>::FS, key_event: KeyEvent) -> APP; fn handle_fetch_key_event(app: <APP as IApp>::FetchState, key_event: KeyEvent) -> APP;
fn handle_matches_key_event(app: <APP as IAppInteract>::MS, key_event: KeyEvent) -> APP; fn handle_match_key_event(app: <APP as IApp>::MatchState, key_event: KeyEvent) -> APP;
fn handle_error_key_event(app: <APP as IAppInteract>::ES, key_event: KeyEvent) -> APP; fn handle_error_key_event(app: <APP as IApp>::ErrorState, key_event: KeyEvent) -> APP;
fn handle_critical_key_event(app: <APP as IAppInteract>::CS, key_event: KeyEvent) -> APP; fn handle_critical_key_event(app: <APP as IApp>::CriticalState, key_event: KeyEvent) -> APP;
fn handle_fetch_result_ready_event(app: APP) -> APP; fn handle_fetch_result_ready_event(app: APP) -> APP;
} }
@ -43,7 +43,7 @@ impl EventHandler {
} }
} }
impl<APP: IAppInteract> IEventHandler<APP> for EventHandler { impl<APP: IApp> IEventHandler<APP> for EventHandler {
fn handle_next_event(&self, app: APP) -> Result<APP, EventError> { fn handle_next_event(&self, app: APP) -> Result<APP, EventError> {
Ok(match self.events.recv()? { Ok(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),
@ -52,7 +52,7 @@ impl<APP: IAppInteract> IEventHandler<APP> for EventHandler {
} }
} }
impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler { impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
fn handle_key_event(app: APP, key_event: KeyEvent) -> APP { fn handle_key_event(app: APP, key_event: KeyEvent) -> APP {
if key_event.modifiers == KeyModifiers::CONTROL { if key_event.modifiers == KeyModifiers::CONTROL {
match key_event.code { match key_event.code {
@ -63,47 +63,39 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
} }
match app.state() { match app.state() {
AppState::Browse(browse) => { AppState::Browse(browse_state) => {
<Self as IEventHandlerPrivate<APP>>::handle_browse_key_event(browse, key_event) Self::handle_browse_key_event(browse_state, key_event)
} }
AppState::Info(info) => { AppState::Info(info_state) => Self::handle_info_key_event(info_state, key_event),
<Self as IEventHandlerPrivate<APP>>::handle_info_key_event(info, key_event) AppState::Reload(reload_state) => {
Self::handle_reload_key_event(reload_state, key_event)
} }
AppState::Reload(reload) => { AppState::Search(search_state) => {
<Self as IEventHandlerPrivate<APP>>::handle_reload_key_event(reload, key_event) Self::handle_search_key_event(search_state, key_event)
} }
AppState::Search(search) => { AppState::Fetch(fetch_state) => Self::handle_fetch_key_event(fetch_state, key_event),
<Self as IEventHandlerPrivate<APP>>::handle_search_key_event(search, key_event) AppState::Match(match_state) => Self::handle_match_key_event(match_state, key_event),
} AppState::Error(error_state) => Self::handle_error_key_event(error_state, key_event),
AppState::Fetch(fetch) => { AppState::Critical(critical_state) => {
<Self as IEventHandlerPrivate<APP>>::handle_fetch_key_event(fetch, key_event) Self::handle_critical_key_event(critical_state, key_event)
}
AppState::Matches(matches) => {
<Self as IEventHandlerPrivate<APP>>::handle_matches_key_event(matches, key_event)
}
AppState::Error(error) => {
<Self as IEventHandlerPrivate<APP>>::handle_error_key_event(error, key_event)
}
AppState::Critical(critical) => {
<Self as IEventHandlerPrivate<APP>>::handle_critical_key_event(critical, key_event)
} }
} }
} }
fn handle_fetch_result_ready_event(app: APP) -> APP { fn handle_fetch_result_ready_event(app: APP) -> APP {
match app.state() { match app.state() {
AppState::Browse(browse) => browse.no_op(), AppState::Browse(browse_state) => browse_state.no_op(),
AppState::Info(info) => info.no_op(), AppState::Info(info_state) => info_state.no_op(),
AppState::Reload(reload) => reload.no_op(), AppState::Reload(reload_state) => reload_state.no_op(),
AppState::Search(search) => search.no_op(), AppState::Search(search_state) => search_state.no_op(),
AppState::Fetch(fetch) => fetch.fetch_result_ready(), AppState::Fetch(fetch_state) => fetch_state.fetch_result_ready(),
AppState::Matches(matches) => matches.no_op(), AppState::Match(match_state) => match_state.no_op(),
AppState::Error(error) => error.no_op(), AppState::Error(error_state) => error_state.no_op(),
AppState::Critical(critical) => critical.no_op(), AppState::Critical(critical_state) => critical_state.no_op(),
} }
} }
fn handle_browse_key_event(app: <APP as IAppInteract>::BS, key_event: KeyEvent) -> APP { fn handle_browse_key_event(app: <APP as IApp>::BrowseState, key_event: KeyEvent) -> APP {
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::Char('Q') => app.quit(), KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.quit(),
@ -133,7 +125,7 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
} }
} }
fn handle_info_key_event(app: <APP as IAppInteract>::IS, key_event: KeyEvent) -> APP { fn handle_info_key_event(app: <APP as IApp>::InfoState, key_event: KeyEvent) -> APP {
match key_event.code { match key_event.code {
// Toggle overlay. // Toggle overlay.
KeyCode::Esc KeyCode::Esc
@ -146,7 +138,7 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
} }
} }
fn handle_reload_key_event(app: <APP as IAppInteract>::RS, key_event: KeyEvent) -> APP { fn handle_reload_key_event(app: <APP as IApp>::ReloadState, key_event: KeyEvent) -> APP {
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(),
@ -162,7 +154,7 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
} }
} }
fn handle_search_key_event(app: <APP as IAppInteract>::SS, key_event: KeyEvent) -> APP { fn handle_search_key_event(app: <APP as IApp>::SearchState, key_event: KeyEvent) -> APP {
if key_event.modifiers == KeyModifiers::CONTROL { if key_event.modifiers == KeyModifiers::CONTROL {
return match key_event.code { return match key_event.code {
KeyCode::Char('s') | KeyCode::Char('S') => app.search_next(), KeyCode::Char('s') | KeyCode::Char('S') => app.search_next(),
@ -182,7 +174,7 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
} }
} }
fn handle_fetch_key_event(app: <APP as IAppInteract>::FS, key_event: KeyEvent) -> APP { fn handle_fetch_key_event(app: <APP as IApp>::FetchState, key_event: KeyEvent) -> APP {
match key_event.code { match key_event.code {
// Abort. // Abort.
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(), KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(),
@ -191,7 +183,7 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
} }
} }
fn handle_matches_key_event(app: <APP as IAppInteract>::MS, key_event: KeyEvent) -> APP { fn handle_match_key_event(app: <APP as IApp>::MatchState, key_event: KeyEvent) -> APP {
match key_event.code { match key_event.code {
// Abort. // Abort.
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(), KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(),
@ -204,12 +196,12 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
} }
} }
fn handle_error_key_event(app: <APP as IAppInteract>::ES, _key_event: KeyEvent) -> APP { fn handle_error_key_event(app: <APP as IApp>::ErrorState, _key_event: KeyEvent) -> APP {
// Any key dismisses the error. // Any key dismisses the error.
app.dismiss_error() app.dismiss_error()
} }
fn handle_critical_key_event(app: <APP as IAppInteract>::CS, _key_event: KeyEvent) -> APP { fn handle_critical_key_event(app: <APP as IApp>::CriticalState, _key_event: KeyEvent) -> APP {
// No action is allowed. // No action is allowed.
app.no_op() app.no_op()
} }

View File

@ -20,7 +20,7 @@ use ratatui::{backend::Backend, Terminal};
use std::{io, marker::PhantomData}; use std::{io, marker::PhantomData};
use crate::tui::{ use crate::tui::{
app::{IAppAccess, IAppInteract}, app::{IApp, IAppAccess},
event::EventError, event::EventError,
handler::IEventHandler, handler::IEventHandler,
listener::IEventListener, listener::IEventListener,
@ -46,12 +46,12 @@ impl From<EventError> for Error {
} }
} }
pub struct Tui<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> { pub struct Tui<B: Backend, UI: IUi, APP: IApp + IAppAccess> {
terminal: Terminal<B>, terminal: Terminal<B>,
_phantom: PhantomData<(UI, APP)>, _phantom: PhantomData<(UI, APP)>,
} }
impl<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> Tui<B, UI, APP> { impl<B: Backend, UI: IUi, APP: IApp + IAppAccess> Tui<B, UI, APP> {
fn init(&mut self) -> Result<(), Error> { fn init(&mut self) -> Result<(), Error> {
self.terminal.hide_cursor()?; self.terminal.hide_cursor()?;
self.terminal.clear()?; self.terminal.clear()?;

View File

@ -4,7 +4,7 @@ use musichoard::collection::{
track::{TrackFormat, TrackQuality}, track::{TrackFormat, TrackQuality},
}; };
use crate::tui::app::{AppMatchesInfo, MatchOption}; use crate::tui::app::{MatchOption, MatchStateInfo};
pub struct UiDisplay; pub struct UiDisplay;
@ -114,11 +114,11 @@ impl UiDisplay {
"Matching nothing" "Matching nothing"
} }
pub fn display_matching_info(matches: Option<&AppMatchesInfo>) -> String { pub fn display_matching_info(info: Option<&MatchStateInfo>) -> String {
match matches.as_ref() { match info.as_ref() {
Some(kind) => match kind { Some(kind) => match kind {
AppMatchesInfo::Artist(m) => UiDisplay::display_artist_matching(&m.matching), MatchStateInfo::Artist(m) => UiDisplay::display_artist_matching(&m.matching),
AppMatchesInfo::Album(m) => UiDisplay::display_album_matching(&m.matching), MatchStateInfo::Album(m) => UiDisplay::display_album_matching(&m.matching),
}, },
None => UiDisplay::display_nothing_matching().to_string(), None => UiDisplay::display_nothing_matching().to_string(),
} }

View File

@ -2,29 +2,29 @@ use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta};
use ratatui::widgets::{List, ListItem}; use ratatui::widgets::{List, ListItem};
use crate::tui::{ use crate::tui::{
app::{AppMatchesInfo, MatchOption, WidgetState}, app::{MatchOption, MatchStateInfo, WidgetState},
ui::display::UiDisplay, ui::display::UiDisplay,
}; };
pub struct MatchesState<'a, 'b> { pub struct MatchOverlay<'a, 'b> {
pub matching: String, pub matching: String,
pub list: List<'a>, pub list: List<'a>,
pub state: &'b mut WidgetState, pub state: &'b mut WidgetState,
} }
impl<'a, 'b> MatchesState<'a, 'b> { impl<'a, 'b> MatchOverlay<'a, 'b> {
pub fn new(matches: Option<&AppMatchesInfo>, state: &'b mut WidgetState) -> Self { pub fn new(info: Option<&MatchStateInfo>, state: &'b mut WidgetState) -> Self {
match matches { match info {
Some(info) => match info { Some(info) => match info {
AppMatchesInfo::Artist(m) => Self::artists(&m.matching, &m.list, state), MatchStateInfo::Artist(m) => Self::artists(&m.matching, &m.list, state),
AppMatchesInfo::Album(m) => Self::albums(&m.matching, &m.list, state), MatchStateInfo::Album(m) => Self::albums(&m.matching, &m.list, state),
}, },
None => Self::empty(state), None => Self::empty(state),
} }
} }
fn empty(state: &'b mut WidgetState) -> Self { fn empty(state: &'b mut WidgetState) -> Self {
MatchesState { MatchOverlay {
matching: UiDisplay::display_nothing_matching().to_string(), matching: UiDisplay::display_nothing_matching().to_string(),
list: List::default(), list: List::default(),
state, state,
@ -46,7 +46,7 @@ impl<'a, 'b> MatchesState<'a, 'b> {
.collect::<Vec<ListItem>>(), .collect::<Vec<ListItem>>(),
); );
MatchesState { MatchOverlay {
matching, matching,
list, list,
state, state,
@ -68,7 +68,7 @@ impl<'a, 'b> MatchesState<'a, 'b> {
.collect::<Vec<ListItem>>(), .collect::<Vec<ListItem>>(),
); );
MatchesState { MatchOverlay {
matching, matching,
list, list,
state, state,

View File

@ -60,9 +60,9 @@ impl Minibuffer<'_> {
paragraphs: vec![Paragraph::new("fetching..."), Paragraph::new("q: abort")], paragraphs: vec![Paragraph::new("fetching..."), Paragraph::new("q: abort")],
columns: 2, columns: 2,
}, },
AppState::Matches(public) => Minibuffer { AppState::Match(public) => Minibuffer {
paragraphs: vec![ paragraphs: vec![
Paragraph::new(UiDisplay::display_matching_info(public.matches)), Paragraph::new(UiDisplay::display_matching_info(public.info)),
Paragraph::new("q: abort"), Paragraph::new("q: abort"),
], ],
columns: 2, columns: 2,

View File

@ -1,33 +1,33 @@
mod browse; mod browse_state;
mod display; mod display;
mod error; mod error_state;
mod fetch; mod fetch_state;
mod info; mod info_state;
mod matches; mod match_state;
mod minibuffer; mod minibuffer;
mod overlay; mod overlay;
mod reload; mod reload_state;
mod style; mod style;
mod widgets; mod widgets;
use fetch::FetchOverlay;
use ratatui::{layout::Rect, widgets::Paragraph, Frame}; use ratatui::{layout::Rect, widgets::Paragraph, Frame};
use musichoard::collection::{album::Album, Collection}; use musichoard::collection::{album::Album, Collection};
use crate::tui::{ use crate::tui::{
app::{AppMatchesInfo, AppPublicState, AppState, Category, IAppAccess, Selection, WidgetState}, app::{AppPublicState, AppState, Category, IAppAccess, MatchStateInfo, Selection, WidgetState},
ui::{ ui::{
browse::{ browse_state::{
AlbumArea, AlbumState, ArtistArea, ArtistState, FrameArea, TrackArea, TrackState, AlbumArea, AlbumState, ArtistArea, ArtistState, FrameArea, TrackArea, TrackState,
}, },
display::UiDisplay, display::UiDisplay,
error::ErrorOverlay, error_state::ErrorOverlay,
info::{AlbumOverlay, ArtistOverlay}, fetch_state::FetchOverlay,
matches::MatchesState, info_state::{AlbumOverlay, ArtistOverlay},
match_state::MatchOverlay,
minibuffer::Minibuffer, minibuffer::Minibuffer,
overlay::{OverlayBuilder, OverlaySize}, overlay::{OverlayBuilder, OverlaySize},
reload::ReloadOverlay, reload_state::ReloadOverlay,
widgets::UiWidget, widgets::UiWidget,
}, },
}; };
@ -137,13 +137,13 @@ impl Ui {
UiWidget::render_overlay_widget("Fetching", fetch_text, area, false, frame) UiWidget::render_overlay_widget("Fetching", fetch_text, area, false, frame)
} }
fn render_matches_overlay( fn render_match_overlay(
matches: Option<&AppMatchesInfo>, info: Option<&MatchStateInfo>,
state: &mut WidgetState, state: &mut WidgetState,
frame: &mut Frame, frame: &mut Frame,
) { ) {
let area = OverlayBuilder::default().build(frame.size()); let area = OverlayBuilder::default().build(frame.size());
let st = MatchesState::new(matches, state); let st = MatchOverlay::new(info, state);
UiWidget::render_overlay_list_widget(&st.matching, st.list, st.state, true, area, frame) UiWidget::render_overlay_list_widget(&st.matching, st.list, st.state, true, area, frame)
} }
@ -169,9 +169,7 @@ impl IUi for Ui {
AppState::Info(()) => Self::render_info_overlay(collection, selection, frame), AppState::Info(()) => Self::render_info_overlay(collection, selection, frame),
AppState::Reload(()) => Self::render_reload_overlay(frame), AppState::Reload(()) => Self::render_reload_overlay(frame),
AppState::Fetch(()) => Self::render_fetch_overlay(frame), AppState::Fetch(()) => Self::render_fetch_overlay(frame),
AppState::Matches(public) => { AppState::Match(public) => Self::render_match_overlay(public.info, public.state, frame),
Self::render_matches_overlay(public.matches, public.state, frame)
}
AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame), AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame),
AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame), AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame),
_ => {} _ => {}
@ -187,7 +185,7 @@ mod tests {
}; };
use crate::tui::{ use crate::tui::{
app::{AppPublic, AppPublicInner, AppPublicMatches, Delta, MatchOption}, app::{AppPublic, AppPublicInner, Delta, MatchOption, MatchStatePublic},
lib::interface::musicbrainz::Match, lib::interface::musicbrainz::Match,
testmod::COLLECTION, testmod::COLLECTION,
tests::terminal, tests::terminal,
@ -209,8 +207,8 @@ mod tests {
AppState::Reload(()) => AppState::Reload(()), AppState::Reload(()) => AppState::Reload(()),
AppState::Search(s) => AppState::Search(s), AppState::Search(s) => AppState::Search(s),
AppState::Fetch(()) => AppState::Fetch(()), AppState::Fetch(()) => AppState::Fetch(()),
AppState::Matches(ref mut m) => AppState::Matches(AppPublicMatches { AppState::Match(ref mut m) => AppState::Match(MatchStatePublic {
matches: m.matches, info: m.info,
state: m.state, state: m.state,
}), }),
AppState::Error(s) => AppState::Error(s), AppState::Error(s) => AppState::Error(s),
@ -230,16 +228,16 @@ mod tests {
} }
} }
fn artist_matches(matching: ArtistMeta, list: Vec<Match<ArtistMeta>>) -> AppMatchesInfo { fn artist_matches(matching: ArtistMeta, list: Vec<Match<ArtistMeta>>) -> MatchStateInfo {
let mut list: Vec<MatchOption<ArtistMeta>> = list.into_iter().map(Into::into).collect(); let mut list: Vec<MatchOption<ArtistMeta>> = list.into_iter().map(Into::into).collect();
list.push(MatchOption::CannotHaveMbid); list.push(MatchOption::CannotHaveMbid);
AppMatchesInfo::artist(matching, list) MatchStateInfo::artist(matching, list)
} }
fn album_matches(matching: AlbumMeta, list: Vec<Match<AlbumMeta>>) -> AppMatchesInfo { fn album_matches(matching: AlbumMeta, list: Vec<Match<AlbumMeta>>) -> MatchStateInfo {
let mut list: Vec<MatchOption<AlbumMeta>> = list.into_iter().map(Into::into).collect(); let mut list: Vec<MatchOption<AlbumMeta>> = list.into_iter().map(Into::into).collect();
list.push(MatchOption::CannotHaveMbid); list.push(MatchOption::CannotHaveMbid);
AppMatchesInfo::album(matching, list) MatchStateInfo::album(matching, list)
} }
fn draw_test_suite(collection: &Collection, selection: &mut Selection) { fn draw_test_suite(collection: &Collection, selection: &mut Selection) {
@ -322,8 +320,8 @@ mod tests {
let mut app = AppPublic { let mut app = AppPublic {
inner: public_inner(collection, &mut selection), inner: public_inner(collection, &mut selection),
state: AppState::Matches(AppPublicMatches { state: AppState::Match(MatchStatePublic {
matches: None, info: None,
state: &mut widget_state, state: &mut widget_state,
}), }),
}; };
@ -351,8 +349,8 @@ mod tests {
let mut app = AppPublic { let mut app = AppPublic {
inner: public_inner(collection, &mut selection), inner: public_inner(collection, &mut selection),
state: AppState::Matches(AppPublicMatches { state: AppState::Match(MatchStatePublic {
matches: Some(&artist_matches), info: Some(&artist_matches),
state: &mut widget_state, state: &mut widget_state,
}), }),
}; };
@ -385,8 +383,8 @@ mod tests {
let mut app = AppPublic { let mut app = AppPublic {
inner: public_inner(collection, &mut selection), inner: public_inner(collection, &mut selection),
state: AppState::Matches(AppPublicMatches { state: AppState::Match(MatchStatePublic {
matches: Some(&album_matches), info: Some(&album_matches),
state: &mut widget_state, state: &mut widget_state,
}), }),
}; };