diff --git a/src/database/json/mod.rs b/src/database/json/mod.rs index 5caeefa..0398292 100644 --- a/src/database/json/mod.rs +++ b/src/database/json/mod.rs @@ -32,7 +32,8 @@ pub struct JsonDatabase { } impl JsonDatabase { - /// Create a new JSON database with the provided backend, e.g. [`JsonDatabaseFileBackend`]. + /// Create a new JSON database with the provided backend, e.g. + /// [`backend::JsonDatabaseFileBackend`]. pub fn new(backend: JDB) -> Self { JsonDatabase { backend } } diff --git a/src/library/beets/mod.rs b/src/library/beets/mod.rs index 7ce5214..d066ea0 100644 --- a/src/library/beets/mod.rs +++ b/src/library/beets/mod.rs @@ -94,7 +94,8 @@ trait LibraryPrivate { } impl BeetsLibrary { - /// Create a new beets library with the provided executor, e.g. [`BeetsLibraryProcessExecutor`]. + /// Create a new beets library with the provided executor, e.g. + /// [`executor::BeetsLibraryProcessExecutor`]. pub fn new(executor: BLE) -> Self { BeetsLibrary { executor } } diff --git a/src/main.rs b/src/main.rs index cc74a41..c4c140d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,10 +20,8 @@ use musichoard::{ }; mod tui; -use tui::{ - app::TuiApp, event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, ui::Ui, - Tui, -}; +use tui::ui::MhUi; +use tui::{event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, Tui}; #[derive(StructOpt)] struct Opt { @@ -52,12 +50,10 @@ fn with(lib: LIB, db: DB) { let listener = TuiEventListener::new(channel.sender()); let handler = TuiEventHandler::new(channel.receiver()); - let ui = Ui::new(); - - let app = TuiApp::new(collection_manager).expect("failed to initialise app"); + let ui = MhUi::new(collection_manager).expect("failed to initialise ui"); // Run the TUI application. - Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui"); + Tui::run(terminal, ui, handler, listener).expect("failed to run tui"); } fn main() { diff --git a/src/tui/app.rs b/src/tui/app.rs deleted file mode 100644 index e6bc869..0000000 --- a/src/tui/app.rs +++ /dev/null @@ -1,832 +0,0 @@ -use musichoard::{collection::CollectionManager, Album, AlbumId, Artist, ArtistId, Track}; - -use super::Error; - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum Category { - Artist, - Album, - Track, -} - -struct TrackSelection { - index: usize, -} - -impl TrackSelection { - fn initialise(tracks: &[Track]) -> Option { - if !tracks.is_empty() { - Some(TrackSelection { index: 0 }) - } else { - None - } - } - - fn increment(&mut self, tracks: &[Track]) { - if let Some(result) = self.index.checked_add(1) { - if result < tracks.len() { - self.index = result; - } - } - } - - fn decrement(&mut self, _tracks: &[Track]) { - if let Some(result) = self.index.checked_sub(1) { - self.index = result; - } - } -} - -struct AlbumSelection { - index: usize, - track: Option, -} - -impl AlbumSelection { - fn initialise(albums: &[Album]) -> Option { - if !albums.is_empty() { - Some(AlbumSelection { - index: 0, - track: TrackSelection::initialise(&albums[0].tracks), - }) - } else { - None - } - } - - fn increment(&mut self, albums: &[Album]) { - if let Some(result) = self.index.checked_add(1) { - if result < albums.len() { - self.index = result; - self.track = TrackSelection::initialise(&albums[self.index].tracks); - } - } - } - - fn decrement(&mut self, albums: &[Album]) { - if let Some(result) = self.index.checked_sub(1) { - self.index = result; - self.track = TrackSelection::initialise(&albums[self.index].tracks); - } - } -} - -struct ArtistSelection { - index: usize, - album: Option, -} - -impl ArtistSelection { - fn initialise(artists: &[Artist]) -> Option { - if !artists.is_empty() { - Some(ArtistSelection { - index: 0, - album: AlbumSelection::initialise(&artists[0].albums), - }) - } else { - None - } - } - - fn increment(&mut self, artists: &[Artist]) { - if let Some(result) = self.index.checked_add(1) { - if result < artists.len() { - self.index = result; - self.album = AlbumSelection::initialise(&artists[self.index].albums); - } - } - } - - fn decrement(&mut self, artists: &[Artist]) { - if let Some(result) = self.index.checked_sub(1) { - self.index = result; - self.album = AlbumSelection::initialise(&artists[self.index].albums); - } - } -} - -struct Selection { - active: Category, - artist: Option, -} - -pub trait App { - fn is_running(&self) -> bool; - fn quit(&mut self); - - fn increment_category(&mut self); - fn decrement_category(&mut self); - - fn increment_selection(&mut self); - fn decrement_selection(&mut self); - - fn get_active_category(&self) -> Category; - - fn get_artist_ids(&self) -> Vec<&ArtistId>; - fn get_album_ids(&self) -> Vec<&AlbumId>; - fn get_track_ids(&self) -> Vec<&Track>; - - fn selected_artist(&self) -> Option; - fn selected_album(&self) -> Option; - fn selected_track(&self) -> Option; -} - -trait AppPrivate { - fn increment_artist_selection(&mut self); - fn decrement_artist_selection(&mut self); - - fn increment_album_selection(&mut self); - fn decrement_album_selection(&mut self); - - fn increment_track_selection(&mut self); - - fn decrement_track_selection(&mut self); - - fn get_artists(&self) -> &Vec; - fn get_albums(&self) -> Option<&Vec>; - fn get_tracks(&self) -> Option<&Vec>; -} - -pub struct TuiApp { - collection_manager: CM, - selection: Selection, - running: bool, -} - -impl TuiApp { - pub fn new(mut collection_manager: CM) -> Result { - collection_manager.rescan_library()?; - let selection = Selection { - active: Category::Artist, - artist: ArtistSelection::initialise(collection_manager.get_collection()), - }; - Ok(TuiApp { - collection_manager, - selection, - running: true, - }) - } -} - -impl App for TuiApp { - fn is_running(&self) -> bool { - self.running - } - - fn quit(&mut self) { - self.running = false; - } - - fn increment_category(&mut self) { - self.selection.active = match self.selection.active { - Category::Artist => Category::Album, - Category::Album => Category::Track, - Category::Track => Category::Track, - }; - } - - fn decrement_category(&mut self) { - self.selection.active = match self.selection.active { - Category::Artist => Category::Artist, - Category::Album => Category::Artist, - Category::Track => Category::Album, - }; - } - - fn increment_selection(&mut self) { - match self.selection.active { - Category::Artist => self.increment_artist_selection(), - Category::Album => self.increment_album_selection(), - Category::Track => self.increment_track_selection(), - } - } - - fn decrement_selection(&mut self) { - match self.selection.active { - Category::Artist => self.decrement_artist_selection(), - Category::Album => self.decrement_album_selection(), - Category::Track => self.decrement_track_selection(), - } - } - - fn get_active_category(&self) -> Category { - self.selection.active - } - - fn get_artist_ids(&self) -> Vec<&ArtistId> { - let artists = self.get_artists(); - artists.iter().map(|a| &a.id).collect() - } - - fn get_album_ids(&self) -> Vec<&AlbumId> { - if let Some(albums) = self.get_albums() { - albums.iter().map(|a| &a.id).collect() - } else { - vec![] - } - } - - fn get_track_ids(&self) -> Vec<&Track> { - if let Some(tracks) = self.get_tracks() { - tracks.iter().collect() - } else { - vec![] - } - } - - fn selected_artist(&self) -> Option { - self.selection.artist.as_ref().map(|s| s.index) - } - - fn selected_album(&self) -> Option { - if let Some(ref artist_selection) = self.selection.artist { - artist_selection.album.as_ref().map(|s| s.index) - } else { - None - } - } - - fn selected_track(&self) -> Option { - if let Some(ref artist_selection) = self.selection.artist { - if let Some(ref album_selection) = artist_selection.album { - album_selection.track.as_ref().map(|s| s.index) - } else { - None - } - } else { - None - } - } -} - -impl AppPrivate for TuiApp { - fn increment_artist_selection(&mut self) { - if let Some(ref mut artist_selection) = self.selection.artist { - let artists = &self.collection_manager.get_collection(); - artist_selection.increment(artists); - } - } - - fn decrement_artist_selection(&mut self) { - if let Some(ref mut artist_selection) = self.selection.artist { - let artists = &self.collection_manager.get_collection(); - artist_selection.decrement(artists); - } - } - - fn increment_album_selection(&mut self) { - if let Some(ref mut artist_selection) = self.selection.artist { - if let Some(ref mut album_selection) = artist_selection.album { - let artists = &self.collection_manager.get_collection(); - let albums = &artists[artist_selection.index].albums; - album_selection.increment(albums); - } - } - } - - fn decrement_album_selection(&mut self) { - if let Some(ref mut artist_selection) = self.selection.artist { - if let Some(ref mut album_selection) = artist_selection.album { - let artists = &self.collection_manager.get_collection(); - let albums = &artists[artist_selection.index].albums; - album_selection.decrement(albums); - } - } - } - - fn increment_track_selection(&mut self) { - if let Some(ref mut artist_selection) = self.selection.artist { - if let Some(ref mut album_selection) = artist_selection.album { - if let Some(ref mut track_selection) = album_selection.track { - let artists = &self.collection_manager.get_collection(); - let albums = &artists[artist_selection.index].albums; - let tracks = &albums[album_selection.index].tracks; - track_selection.increment(tracks); - } - } - } - } - - fn decrement_track_selection(&mut self) { - if let Some(ref mut artist_selection) = self.selection.artist { - if let Some(ref mut album_selection) = artist_selection.album { - if let Some(ref mut track_selection) = album_selection.track { - let artists = &self.collection_manager.get_collection(); - let albums = &artists[artist_selection.index].albums; - let tracks = &albums[album_selection.index].tracks; - track_selection.decrement(tracks); - } - } - } - } - - fn get_artists(&self) -> &Vec { - self.collection_manager.get_collection() - } - - fn get_albums(&self) -> Option<&Vec> { - let artists = self.get_artists(); - if let Some(artist_index) = self.selected_artist() { - Some(&artists[artist_index].albums) - } else { - None - } - } - - fn get_tracks(&self) -> Option<&Vec> { - if let Some(albums) = self.get_albums() { - if let Some(album_index) = self.selected_album() { - Some(&albums[album_index].tracks) - } else { - None - } - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use crate::tests::{MockCollectionManager, COLLECTION}; - - use super::*; - - #[test] - fn test_track_selection() { - let tracks = &COLLECTION[0].albums[0].tracks; - assert!(tracks.len() > 1); - - let empty = TrackSelection::initialise(&vec![]); - assert!(empty.is_none()); - - let sel = TrackSelection::initialise(tracks); - assert!(sel.is_some()); - - let mut sel = sel.unwrap(); - assert_eq!(sel.index, 0); - - sel.decrement(tracks); - assert_eq!(sel.index, 0); - - sel.increment(tracks); - assert_eq!(sel.index, 1); - - sel.decrement(tracks); - assert_eq!(sel.index, 0); - - for _ in 0..(tracks.len() + 5) { - sel.increment(tracks); - } - assert_eq!(sel.index, tracks.len() - 1); - - // Artifical test case to verify upper limit. - let mut sel = TrackSelection { - index: std::usize::MAX, - }; - assert_eq!(sel.index, std::usize::MAX); - - sel.increment(&vec![]); - assert_eq!(sel.index, std::usize::MAX); - } - - #[test] - fn test_album_selection() { - let albums = &COLLECTION[0].albums; - assert!(albums.len() > 1); - - let empty = AlbumSelection::initialise(&vec![]); - assert!(empty.is_none()); - - let sel = AlbumSelection::initialise(albums); - assert!(sel.is_some()); - - let mut sel = sel.unwrap(); - assert_eq!(sel.index, 0); - assert!(sel.track.is_some()); - assert_eq!(sel.track.as_ref().unwrap().index, 0); - - sel.track - .as_mut() - .unwrap() - .increment(&albums[sel.index].tracks); - assert_eq!(sel.index, 0); - assert!(sel.track.is_some()); - assert_eq!(sel.track.as_ref().unwrap().index, 1); - - // Verify that decrement that doesn't change index does not reset track. - sel.decrement(albums); - assert_eq!(sel.index, 0); - assert!(sel.track.is_some()); - assert_eq!(sel.track.as_ref().unwrap().index, 1); - - sel.increment(albums); - assert_eq!(sel.index, 1); - assert!(sel.track.is_some()); - assert_eq!(sel.track.as_ref().unwrap().index, 0); - - sel.decrement(albums); - assert_eq!(sel.index, 0); - assert!(sel.track.is_some()); - assert_eq!(sel.track.as_ref().unwrap().index, 0); - - for _ in 0..(albums.len() + 5) { - sel.increment(albums); - } - assert_eq!(sel.index, albums.len() - 1); - assert!(sel.track.is_some()); - assert_eq!(sel.track.as_ref().unwrap().index, 0); - - sel.track - .as_mut() - .unwrap() - .increment(&albums[sel.index].tracks); - assert_eq!(sel.index, albums.len() - 1); - assert!(sel.track.is_some()); - assert_eq!(sel.track.as_ref().unwrap().index, 1); - - // Verify that increment that doesn't change index does not reset track. - sel.increment(albums); - assert_eq!(sel.index, albums.len() - 1); - assert!(sel.track.is_some()); - assert_eq!(sel.track.as_ref().unwrap().index, 1); - - // Artifical test case to verify upper limit. - let mut sel = AlbumSelection { - index: std::usize::MAX, - track: Some(TrackSelection { index: 1 }), - }; - assert_eq!(sel.index, std::usize::MAX); - assert!(sel.track.is_some()); - assert_eq!(sel.track.as_ref().unwrap().index, 1); - - sel.increment(&vec![]); - assert_eq!(sel.index, std::usize::MAX); - assert!(sel.track.is_some()); - assert_eq!(sel.track.as_ref().unwrap().index, 1); - } - - #[test] - fn test_artist_selection() { - let artists = &COLLECTION; - assert!(artists.len() > 1); - - let empty = ArtistSelection::initialise(&vec![]); - assert!(empty.is_none()); - - let sel = ArtistSelection::initialise(artists); - assert!(sel.is_some()); - - let mut sel = sel.unwrap(); - assert_eq!(sel.index, 0); - assert!(sel.album.is_some()); - assert_eq!(sel.album.as_ref().unwrap().index, 0); - - sel.album - .as_mut() - .unwrap() - .increment(&artists[sel.index].albums); - assert_eq!(sel.index, 0); - assert!(sel.album.is_some()); - assert_eq!(sel.album.as_ref().unwrap().index, 1); - - // Verify that decrement that doesn't change index does not reset album. - sel.decrement(artists); - assert_eq!(sel.index, 0); - assert!(sel.album.is_some()); - assert_eq!(sel.album.as_ref().unwrap().index, 1); - - sel.increment(artists); - assert_eq!(sel.index, 1); - assert!(sel.album.is_some()); - assert_eq!(sel.album.as_ref().unwrap().index, 0); - - sel.decrement(artists); - assert_eq!(sel.index, 0); - assert!(sel.album.is_some()); - assert_eq!(sel.album.as_ref().unwrap().index, 0); - - for _ in 0..(artists.len() + 5) { - sel.increment(artists); - } - assert_eq!(sel.index, artists.len() - 1); - assert!(sel.album.is_some()); - assert_eq!(sel.album.as_ref().unwrap().index, 0); - - sel.album - .as_mut() - .unwrap() - .increment(&artists[sel.index].albums); - assert_eq!(sel.index, artists.len() - 1); - assert!(sel.album.is_some()); - assert_eq!(sel.album.as_ref().unwrap().index, 1); - - // Verify that increment that doesn't change index does not reset album. - sel.increment(artists); - assert_eq!(sel.index, artists.len() - 1); - assert!(sel.album.is_some()); - assert_eq!(sel.album.as_ref().unwrap().index, 1); - - // Artifical test case to verify upper limit. - let mut sel = ArtistSelection { - index: std::usize::MAX, - album: Some(AlbumSelection { - index: 1, - track: None, - }), - }; - assert_eq!(sel.index, std::usize::MAX); - assert!(sel.album.is_some()); - assert_eq!(sel.album.as_ref().unwrap().index, 1); - - sel.increment(&vec![]); - assert_eq!(sel.index, std::usize::MAX); - assert!(sel.album.is_some()); - assert_eq!(sel.album.as_ref().unwrap().index, 1); - } - - #[test] - fn app_running() { - let mut collection_manager = MockCollectionManager::new(); - - collection_manager - .expect_rescan_library() - .times(1) - .return_once(|| Ok(())); - collection_manager - .expect_get_collection() - .return_const(COLLECTION.to_owned()); - - let mut app = TuiApp::new(collection_manager).unwrap(); - assert!(app.is_running()); - - app.quit(); - assert!(!app.is_running()); - } - - #[test] - fn app_modifiers() { - let mut collection_manager = MockCollectionManager::new(); - - collection_manager - .expect_rescan_library() - .times(1) - .return_once(|| Ok(())); - collection_manager - .expect_get_collection() - .return_const(COLLECTION.to_owned()); - - let mut app = TuiApp::new(collection_manager).unwrap(); - assert!(app.is_running()); - - assert!(!app.get_artist_ids().is_empty()); - assert!(!app.get_album_ids().is_empty()); - assert!(!app.get_track_ids().is_empty()); - - assert_eq!(app.get_active_category(), Category::Artist); - assert_eq!(app.selected_artist(), Some(0)); - assert_eq!(app.selected_album(), Some(0)); - assert_eq!(app.selected_track(), Some(0)); - - app.increment_selection(); - assert_eq!(app.get_active_category(), Category::Artist); - assert_eq!(app.selected_artist(), Some(1)); - assert_eq!(app.selected_album(), Some(0)); - assert_eq!(app.selected_track(), Some(0)); - - app.increment_category(); - assert_eq!(app.get_active_category(), Category::Album); - assert_eq!(app.selected_artist(), Some(1)); - assert_eq!(app.selected_album(), Some(0)); - assert_eq!(app.selected_track(), Some(0)); - - app.increment_selection(); - assert_eq!(app.get_active_category(), Category::Album); - assert_eq!(app.selected_artist(), Some(1)); - assert_eq!(app.selected_album(), Some(1)); - assert_eq!(app.selected_track(), Some(0)); - - app.increment_category(); - assert_eq!(app.get_active_category(), Category::Track); - assert_eq!(app.selected_artist(), Some(1)); - assert_eq!(app.selected_album(), Some(1)); - assert_eq!(app.selected_track(), Some(0)); - - app.increment_selection(); - assert_eq!(app.get_active_category(), Category::Track); - assert_eq!(app.selected_artist(), Some(1)); - assert_eq!(app.selected_album(), Some(1)); - assert_eq!(app.selected_track(), Some(1)); - - app.increment_category(); - assert_eq!(app.get_active_category(), Category::Track); - assert_eq!(app.selected_artist(), Some(1)); - assert_eq!(app.selected_album(), Some(1)); - assert_eq!(app.selected_track(), Some(1)); - - app.decrement_selection(); - assert_eq!(app.get_active_category(), Category::Track); - assert_eq!(app.selected_artist(), Some(1)); - assert_eq!(app.selected_album(), Some(1)); - assert_eq!(app.selected_track(), Some(0)); - - app.increment_selection(); - app.decrement_category(); - assert_eq!(app.get_active_category(), Category::Album); - assert_eq!(app.selected_artist(), Some(1)); - assert_eq!(app.selected_album(), Some(1)); - assert_eq!(app.selected_track(), Some(1)); - - app.decrement_selection(); - assert_eq!(app.get_active_category(), Category::Album); - assert_eq!(app.selected_artist(), Some(1)); - assert_eq!(app.selected_album(), Some(0)); - assert_eq!(app.selected_track(), Some(0)); - - app.increment_selection(); - app.decrement_category(); - assert_eq!(app.get_active_category(), Category::Artist); - assert_eq!(app.selected_artist(), Some(1)); - assert_eq!(app.selected_album(), Some(1)); - assert_eq!(app.selected_track(), Some(0)); - - app.decrement_selection(); - assert_eq!(app.get_active_category(), Category::Artist); - assert_eq!(app.selected_artist(), Some(0)); - assert_eq!(app.selected_album(), Some(0)); - assert_eq!(app.selected_track(), Some(0)); - - app.increment_category(); - app.increment_selection(); - app.decrement_category(); - app.decrement_selection(); - app.decrement_category(); - assert_eq!(app.get_active_category(), Category::Artist); - assert_eq!(app.selected_artist(), Some(0)); - assert_eq!(app.selected_album(), Some(1)); - assert_eq!(app.selected_track(), Some(0)); - } - - #[test] - fn app_no_tracks() { - let mut collection_manager = MockCollectionManager::new(); - let mut collection = COLLECTION.to_owned(); - collection[0].albums[0].tracks = vec![]; - - collection_manager - .expect_rescan_library() - .times(1) - .return_once(|| Ok(())); - collection_manager - .expect_get_collection() - .return_const(collection); - - let mut app = TuiApp::new(collection_manager).unwrap(); - assert!(app.is_running()); - - assert!(!app.get_artist_ids().is_empty()); - assert!(!app.get_album_ids().is_empty()); - assert!(app.get_track_ids().is_empty()); - - assert_eq!(app.get_active_category(), Category::Artist); - assert_eq!(app.selected_artist(), Some(0)); - assert_eq!(app.selected_album(), Some(0)); - assert_eq!(app.selected_track(), None); - - app.increment_category(); - app.increment_category(); - - app.increment_selection(); - assert_eq!(app.get_active_category(), Category::Track); - assert_eq!(app.selected_artist(), Some(0)); - assert_eq!(app.selected_album(), Some(0)); - assert_eq!(app.selected_track(), None); - - app.decrement_selection(); - assert_eq!(app.get_active_category(), Category::Track); - assert_eq!(app.selected_artist(), Some(0)); - assert_eq!(app.selected_album(), Some(0)); - assert_eq!(app.selected_track(), None); - } - - #[test] - fn app_no_albums() { - let mut collection_manager = MockCollectionManager::new(); - let mut collection = COLLECTION.to_owned(); - collection[0].albums = vec![]; - - collection_manager - .expect_rescan_library() - .times(1) - .return_once(|| Ok(())); - collection_manager - .expect_get_collection() - .return_const(collection); - - let mut app = TuiApp::new(collection_manager).unwrap(); - assert!(app.is_running()); - - assert!(!app.get_artist_ids().is_empty()); - assert!(app.get_album_ids().is_empty()); - assert!(app.get_track_ids().is_empty()); - - assert_eq!(app.get_active_category(), Category::Artist); - assert_eq!(app.selected_artist(), Some(0)); - assert_eq!(app.selected_album(), None); - assert_eq!(app.selected_track(), None); - - app.increment_category(); - - app.increment_selection(); - assert_eq!(app.get_active_category(), Category::Album); - assert_eq!(app.selected_artist(), Some(0)); - assert_eq!(app.selected_album(), None); - assert_eq!(app.selected_track(), None); - - app.decrement_selection(); - assert_eq!(app.get_active_category(), Category::Album); - assert_eq!(app.selected_artist(), Some(0)); - assert_eq!(app.selected_album(), None); - assert_eq!(app.selected_track(), None); - - app.increment_category(); - - app.increment_selection(); - assert_eq!(app.get_active_category(), Category::Track); - assert_eq!(app.selected_artist(), Some(0)); - assert_eq!(app.selected_album(), None); - assert_eq!(app.selected_track(), None); - - app.decrement_selection(); - assert_eq!(app.get_active_category(), Category::Track); - assert_eq!(app.selected_artist(), Some(0)); - assert_eq!(app.selected_album(), None); - assert_eq!(app.selected_track(), None); - } - - #[test] - fn app_no_artists() { - let mut collection_manager = MockCollectionManager::new(); - let collection = vec![]; - - collection_manager - .expect_rescan_library() - .times(1) - .return_once(|| Ok(())); - collection_manager - .expect_get_collection() - .return_const(collection); - - let mut app = TuiApp::new(collection_manager).unwrap(); - assert!(app.is_running()); - - assert!(app.get_artist_ids().is_empty()); - assert!(app.get_album_ids().is_empty()); - assert!(app.get_track_ids().is_empty()); - - assert_eq!(app.get_active_category(), Category::Artist); - assert_eq!(app.selected_artist(), None); - assert_eq!(app.selected_album(), None); - assert_eq!(app.selected_track(), None); - - app.increment_selection(); - assert_eq!(app.get_active_category(), Category::Artist); - assert_eq!(app.selected_artist(), None); - assert_eq!(app.selected_album(), None); - assert_eq!(app.selected_track(), None); - - app.decrement_selection(); - assert_eq!(app.get_active_category(), Category::Artist); - assert_eq!(app.selected_artist(), None); - assert_eq!(app.selected_album(), None); - assert_eq!(app.selected_track(), None); - - app.increment_category(); - - app.increment_selection(); - assert_eq!(app.get_active_category(), Category::Album); - assert_eq!(app.selected_artist(), None); - assert_eq!(app.selected_album(), None); - assert_eq!(app.selected_track(), None); - - app.decrement_selection(); - assert_eq!(app.get_active_category(), Category::Album); - assert_eq!(app.selected_artist(), None); - assert_eq!(app.selected_album(), None); - assert_eq!(app.selected_track(), None); - - app.increment_category(); - - app.increment_selection(); - assert_eq!(app.get_active_category(), Category::Track); - assert_eq!(app.selected_artist(), None); - assert_eq!(app.selected_album(), None); - assert_eq!(app.selected_track(), None); - - app.decrement_selection(); - assert_eq!(app.get_active_category(), Category::Track); - assert_eq!(app.selected_artist(), None); - assert_eq!(app.selected_album(), None); - assert_eq!(app.selected_track(), None); - } -} diff --git a/src/tui/handler.rs b/src/tui/handler.rs index fecc501..b9346f6 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -4,17 +4,17 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use mockall::automock; use super::{ - app::App, event::{Event, EventError, EventReceiver}, + ui::Ui, }; #[cfg_attr(test, automock)] -pub trait EventHandler { - fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError>; +pub trait EventHandler { + fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError>; } -trait EventHandlerPrivate { - fn handle_key_event(app: &mut APP, key_event: KeyEvent); +trait EventHandlerPrivate { + fn handle_key_event(ui: &mut UI, key_event: KeyEvent); } pub struct TuiEventHandler { @@ -28,10 +28,10 @@ impl TuiEventHandler { } } -impl EventHandler for TuiEventHandler { - fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> { +impl EventHandler for TuiEventHandler { + fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError> { match self.events.recv()? { - Event::Key(key_event) => Self::handle_key_event(app, key_event), + Event::Key(key_event) => Self::handle_key_event(ui, key_event), Event::Mouse(_) => {} Event::Resize(_, _) => {} }; @@ -39,32 +39,32 @@ impl EventHandler for TuiEventHandler { } } -impl EventHandlerPrivate for TuiEventHandler { - fn handle_key_event(app: &mut APP, key_event: KeyEvent) { +impl EventHandlerPrivate for TuiEventHandler { + fn handle_key_event(ui: &mut UI, key_event: KeyEvent) { match key_event.code { // Exit application on `ESC` or `q`. KeyCode::Esc | KeyCode::Char('q') => { - app.quit(); + ui.quit(); } // Exit application on `Ctrl-C`. KeyCode::Char('c') | KeyCode::Char('C') => { if key_event.modifiers == KeyModifiers::CONTROL { - app.quit(); + ui.quit(); } } // Category change. KeyCode::Left => { - app.decrement_category(); + ui.decrement_category(); } KeyCode::Right => { - app.increment_category(); + ui.increment_category(); } // Selection change. KeyCode::Up => { - app.decrement_selection(); + ui.decrement_selection(); } KeyCode::Down => { - app.increment_selection(); + ui.increment_selection(); } // Other keys. _ => {} diff --git a/src/tui/mod.rs b/src/tui/mod.rs index da87d7b..50478b6 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -6,13 +6,11 @@ use ratatui::Terminal; use std::io; use std::marker::PhantomData; -pub mod app; pub mod event; pub mod handler; pub mod listener; pub mod ui; -use self::app::App; use self::event::EventError; use self::handler::EventHandler; use self::listener::EventListener; @@ -44,12 +42,12 @@ impl From for Error { } } -pub struct Tui { +pub struct Tui { terminal: Terminal, - _phantom: PhantomData, + _phantom: PhantomData, } -impl Tui { +impl Tui { fn init(&mut self) -> Result<(), Error> { self.terminal.hide_cursor()?; self.terminal.clear()?; @@ -66,15 +64,10 @@ impl Tui { self.exit(); } - fn main_loop( - &mut self, - mut app: APP, - ui: Ui, - handler: impl EventHandler, - ) -> Result<(), Error> { - while app.is_running() { - self.terminal.draw(|frame| ui.render(&app, frame))?; - handler.handle_next_event(&mut app)?; + fn main_loop(&mut self, mut ui: UI, handler: impl EventHandler) -> Result<(), Error> { + while ui.is_running() { + self.terminal.draw(|frame| ui.render(frame))?; + handler.handle_next_event(&mut ui)?; } Ok(()) @@ -82,9 +75,8 @@ impl Tui { fn main( term: Terminal, - app: APP, - ui: Ui, - handler: impl EventHandler, + ui: UI, + handler: impl EventHandler, listener: impl EventListener, ) -> Result<(), Error> { let mut tui = Tui { @@ -95,7 +87,7 @@ impl Tui { tui.init()?; let listener_handle = listener.spawn(); - let result = tui.main_loop(app, ui, handler); + let result = tui.main_loop(ui, handler); match result { Ok(_) => { @@ -142,13 +134,12 @@ impl Tui { pub fn run( term: Terminal, - app: APP, - ui: Ui, - handler: impl EventHandler, + ui: UI, + handler: impl EventHandler, listener: impl EventListener, ) -> Result<(), Error> { Self::enable()?; - let result = Self::main(term, app, ui, handler, listener); + let result = Self::main(term, ui, handler, listener); match result { Ok(_) => { Self::disable()?; @@ -175,11 +166,10 @@ mod tests { use crate::tests::{MockCollectionManager, COLLECTION}; use super::{ - app::{App, TuiApp}, event::EventError, handler::MockEventHandler, listener::MockEventListener, - ui::Ui, + ui::{MhUi, Ui}, Error, Tui, }; @@ -188,7 +178,7 @@ mod tests { Terminal::new(backend).unwrap() } - pub fn app(collection: Collection) -> TuiApp { + pub fn ui(collection: Collection) -> MhUi { let mut collection_manager = MockCollectionManager::new(); collection_manager @@ -198,7 +188,7 @@ mod tests { .expect_get_collection() .return_const(collection); - TuiApp::new(collection_manager).unwrap() + MhUi::new(collection_manager).unwrap() } fn listener() -> MockEventListener { @@ -212,35 +202,33 @@ mod tests { listener } - fn handler() -> MockEventHandler> { + fn handler() -> MockEventHandler> { let mut handler = MockEventHandler::new(); - handler.expect_handle_next_event().return_once( - |app: &mut TuiApp| { - app.quit(); + handler + .expect_handle_next_event() + .return_once(|ui: &mut MhUi| { + ui.quit(); Ok(()) - }, - ); + }); handler } #[test] fn run() { let terminal = terminal(); - let app = app(COLLECTION.to_owned()); - let ui = Ui::new(); + let ui = ui(COLLECTION.to_owned()); let listener = listener(); let handler = handler(); - let result = Tui::main(terminal, app, ui, handler, listener); + let result = Tui::main(terminal, ui, handler, listener); assert!(result.is_ok()); } #[test] fn event_error() { let terminal = terminal(); - let app = app(COLLECTION.to_owned()); - let ui = Ui::new(); + let ui = ui(COLLECTION.to_owned()); let listener = listener(); @@ -249,7 +237,7 @@ mod tests { .expect_handle_next_event() .return_once(|_| Err(EventError::Recv)); - let result = Tui::main(terminal, app, ui, handler, listener); + let result = Tui::main(terminal, ui, handler, listener); assert!(result.is_err()); assert_eq!( result.unwrap_err(), @@ -260,8 +248,7 @@ mod tests { #[test] fn listener_error() { let terminal = terminal(); - let app = app(COLLECTION.to_owned()); - let ui = Ui::new(); + let ui = ui(COLLECTION.to_owned()); let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error")); let listener_handle: thread::JoinHandle = thread::spawn(|| error); @@ -275,7 +262,7 @@ mod tests { .expect_handle_next_event() .return_once(|_| Err(EventError::Recv)); - let result = Tui::main(terminal, app, ui, handler, listener); + let result = Tui::main(terminal, ui, handler, listener); assert!(result.is_err()); let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error")); @@ -285,8 +272,7 @@ mod tests { #[test] fn listener_panic() { let terminal = terminal(); - let app = app(COLLECTION.to_owned()); - let ui = Ui::new(); + let ui = ui(COLLECTION.to_owned()); let listener_handle: thread::JoinHandle = thread::spawn(|| panic!()); while !listener_handle.is_finished() {} @@ -299,7 +285,7 @@ mod tests { .expect_handle_next_event() .return_once(|_| Err(EventError::Recv)); - let result = Tui::main(terminal, app, ui, handler, listener); + let result = Tui::main(terminal, ui, handler, listener); assert!(result.is_err()); assert_eq!(result.unwrap_err(), Error::ListenerPanic); } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 47288e3..cb13a98 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -1,6 +1,7 @@ -use std::marker::PhantomData; - -use musichoard::TrackFormat; +use musichoard::{ + collection::{Collection, CollectionManager}, + Album, Artist, Track, TrackFormat, +}; use ratatui::{ backend::Backend, layout::{Alignment, Rect}, @@ -9,7 +10,240 @@ use ratatui::{ Frame, }; -use super::app::{App, Category}; +use super::Error; + +struct TrackSelection { + state: ListState, +} + +struct AlbumSelection { + state: ListState, + track: TrackSelection, +} + +struct ArtistSelection { + state: ListState, + album: AlbumSelection, +} + +impl TrackSelection { + fn initialise(tracks: Option<&[Track]>) -> Self { + let mut state = ListState::default(); + if let Some(tracks) = tracks { + state.select(if !tracks.is_empty() { Some(0) } else { None }); + } else { + state.select(None); + }; + TrackSelection { state } + } + + fn increment(&mut self, tracks: &[Track]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_add(1) { + if result < tracks.len() { + self.state.select(Some(result)); + } + } + } + } + + fn decrement(&mut self, _tracks: &[Track]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_sub(1) { + self.state.select(Some(result)); + } + } + } +} + +impl AlbumSelection { + fn initialise(albums: Option<&[Album]>) -> Self { + let mut state = ListState::default(); + let track: TrackSelection; + if let Some(albums) = albums { + state.select(if !albums.is_empty() { Some(0) } else { None }); + track = TrackSelection::initialise(albums.get(0).map(|a| a.tracks.as_slice())); + } else { + state.select(None); + track = TrackSelection::initialise(None); + } + AlbumSelection { state, track } + } + + fn increment(&mut self, albums: &[Album]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_add(1) { + if result < albums.len() { + self.state.select(Some(result)); + self.track = TrackSelection::initialise(Some(&albums[result].tracks)); + } + } + } + } + + fn increment_track(&mut self, albums: &[Album]) { + if let Some(index) = self.state.selected() { + self.track.increment(&albums[index].tracks); + } + } + + fn decrement(&mut self, albums: &[Album]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_sub(1) { + self.state.select(Some(result)); + self.track = TrackSelection::initialise(Some(&albums[result].tracks)); + } + } + } + + fn decrement_track(&mut self, albums: &[Album]) { + if let Some(index) = self.state.selected() { + self.track.decrement(&albums[index].tracks); + } + } +} + +impl ArtistSelection { + fn initialise(artists: Option<&[Artist]>) -> Self { + let mut state = ListState::default(); + let album: AlbumSelection; + if let Some(artists) = artists { + state.select(if !artists.is_empty() { Some(0) } else { None }); + album = AlbumSelection::initialise(artists.get(0).map(|a| a.albums.as_slice())); + } else { + state.select(None); + album = AlbumSelection::initialise(None); + } + ArtistSelection { state, album } + } + + fn increment(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_add(1) { + if result < artists.len() { + self.state.select(Some(result)); + self.album = AlbumSelection::initialise(Some(&artists[result].albums)); + } + } + } + } + + fn increment_album(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.selected() { + self.album.increment(&artists[index].albums); + } + } + + fn increment_track(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.selected() { + self.album.increment_track(&artists[index].albums); + } + } + + fn decrement(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_sub(1) { + self.state.select(Some(result)); + self.album = AlbumSelection::initialise(Some(&artists[result].albums)); + } + } + } + + fn decrement_album(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.selected() { + self.album.decrement(&artists[index].albums); + } + } + + fn decrement_track(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.selected() { + self.album.decrement_track(&artists[index].albums); + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Category { + Artist, + Album, + Track, +} + +struct Selection { + active: Category, + artist: ArtistSelection, +} + +impl Selection { + fn new(artists: Option<&[Artist]>) -> Self { + Selection { + active: Category::Artist, + artist: ArtistSelection::initialise(artists), + } + } + + fn increment_category(&mut self) { + self.active = match self.active { + Category::Artist => Category::Album, + Category::Album => Category::Track, + Category::Track => Category::Track, + }; + } + + fn decrement_category(&mut self) { + self.active = match self.active { + Category::Artist => Category::Artist, + Category::Album => Category::Artist, + Category::Track => Category::Album, + }; + } + + fn increment_selection(&mut self, collection: &Collection) { + match self.active { + Category::Artist => self.increment_artist(collection), + Category::Album => self.increment_album(collection), + Category::Track => self.increment_track(collection), + } + } + + fn decrement_selection(&mut self, collection: &Collection) { + match self.active { + Category::Artist => self.decrement_artist(collection), + Category::Album => self.decrement_album(collection), + Category::Track => self.decrement_track(collection), + } + } + + fn increment_artist(&mut self, artists: &[Artist]) { + self.artist.increment(artists); + } + + fn decrement_artist(&mut self, artists: &[Artist]) { + self.artist.decrement(artists); + } + + fn increment_album(&mut self, artists: &[Artist]) { + self.artist.increment_album(artists); + } + + fn decrement_album(&mut self, artists: &[Artist]) { + self.artist.decrement_album(artists); + } + + fn increment_track(&mut self, artists: &[Artist]) { + self.artist.increment_track(artists); + } + + fn decrement_track(&mut self, artists: &[Artist]) { + self.artist.decrement_track(artists); + } +} + +pub struct MhUi { + collection_manager: CM, + selection: Selection, + running: bool, +} struct ArtistArea { list: Rect, @@ -25,52 +259,14 @@ struct TrackArea { info: Rect, } -struct FrameAreas { - artists: ArtistArea, - albums: AlbumArea, - tracks: TrackArea, +struct FrameArea { + artist: ArtistArea, + album: AlbumArea, + track: TrackArea, } -struct SelectionList<'a> { - list: List<'a>, - state: ListState, -} - -struct ArtistState<'a> { - list: SelectionList<'a>, - active: bool, -} - -struct AlbumState<'a> { - list: SelectionList<'a>, - info: Paragraph<'a>, - active: bool, -} - -struct TrackState<'a> { - list: SelectionList<'a>, - info: Paragraph<'a>, - active: bool, -} - -struct AppState<'a> { - artists: ArtistState<'a>, - albums: AlbumState<'a>, - tracks: TrackState<'a>, -} - -pub struct Ui { - _phantom: PhantomData, -} - -impl Ui { - pub fn new() -> Self { - Ui { - _phantom: PhantomData, - } - } - - fn construct_areas(frame: Rect) -> FrameAreas { +impl FrameArea { + fn new(frame: Rect) -> FrameArea { let width_one_third = frame.width / 3; let height_one_third = frame.height / 3; @@ -114,76 +310,87 @@ impl Ui { height: panel_height_bottom, }; - FrameAreas { - artists: ArtistArea { list: artist_list }, - albums: AlbumArea { + FrameArea { + artist: ArtistArea { list: artist_list }, + album: AlbumArea { list: album_list, info: album_info, }, - tracks: TrackArea { + track: TrackArea { list: track_list, info: track_info, }, } } +} - fn construct_artist_list(app: &APP) -> ArtistState { - let artists = app.get_artist_ids(); +struct ArtistState<'a, 'b> { + active: bool, + list: List<'a>, + state: &'b mut ListState, +} + +impl<'a, 'b> ArtistState<'a, 'b> { + fn new(active: bool, artists: &'a [Artist], state: &'b mut ListState) -> ArtistState<'a, 'b> { let list = List::new( artists .iter() - .map(|id| ListItem::new(id.name.as_str())) + .map(|a| ListItem::new(a.id.name.as_str())) .collect::>(), ); - let selected_artist = app.selected_artist(); - - let mut state = ListState::default(); - state.select(selected_artist); - - let active = app.get_active_category() == Category::Artist; - ArtistState { - list: SelectionList { list, state }, active, + list, + state, } } +} - fn construct_album_list(app: &APP) -> AlbumState { - let albums = app.get_album_ids(); +struct AlbumState<'a, 'b> { + active: bool, + list: List<'a>, + state: &'b mut ListState, + info: Paragraph<'a>, +} + +impl<'a, 'b> AlbumState<'a, 'b> { + fn new(active: bool, albums: &'a [Album], state: &'b mut ListState) -> AlbumState<'a, 'b> { let list = List::new( albums .iter() - .map(|id| ListItem::new(id.title.as_str())) + .map(|a| ListItem::new(a.id.title.as_str())) .collect::>(), ); - let selected_album = app.selected_album(); - - let mut state = ListState::default(); - state.select(selected_album); - - let active = app.get_active_category() == Category::Album; - - let album = selected_album.map(|i| albums[i]); + let album = state.selected().map(|i| &albums[i]); let info = Paragraph::new(format!( "Title: {}\n\ Year: {}", - album.map(|a| a.title.as_str()).unwrap_or(""), + album.map(|a| a.id.title.as_str()).unwrap_or(""), album - .map(|a| a.year.to_string()) + .map(|a| a.id.year.to_string()) .unwrap_or_else(|| "".to_string()), )); AlbumState { - list: SelectionList { list, state }, - info, active, + list, + state, + info, } } +} - fn construct_track_list(app: &APP) -> TrackState { - let tracks = app.get_track_ids(); +struct TrackState<'a, 'b> { + active: bool, + list: List<'a>, + state: &'b mut ListState, + info: Paragraph<'a>, +} + +impl<'a, 'b> TrackState<'a, 'b> { + fn new(active: bool, tracks: &'a [Track], state: &'b mut ListState) -> TrackState<'a, 'b> { let list = List::new( tracks .iter() @@ -191,14 +398,7 @@ impl Ui { .collect::>(), ); - let selected_track = app.selected_track(); - - let mut state = ListState::default(); - state.select(selected_track); - - let active = app.get_active_category() == Category::Track; - - let track = selected_track.map(|i| tracks[i]); + let track = state.selected().map(|i| &tracks[i]); let info = Paragraph::new(format!( "Track: {}\n\ Title: {}\n\ @@ -220,18 +420,23 @@ impl Ui { )); TrackState { - list: SelectionList { list, state }, - info, active, + list, + state, + info, } } +} - fn construct_app_state(app: &APP) -> AppState { - AppState { - artists: Self::construct_artist_list(app), - albums: Self::construct_album_list(app), - tracks: Self::construct_track_list(app), - } +impl MhUi { + pub fn new(mut collection_manager: CM) -> Result { + collection_manager.rescan_library()?; + let selection = Selection::new(Some(collection_manager.get_collection())); + Ok(MhUi { + collection_manager, + selection, + running: true, + }) } fn style(_active: bool) -> Style { @@ -261,19 +466,19 @@ impl Ui { fn render_list_widget( title: &str, - mut list: SelectionList, + list: List, + list_state: &mut ListState, active: bool, area: Rect, frame: &mut Frame<'_, B>, ) { frame.render_stateful_widget( - list.list - .highlight_style(Self::highlight_style(active)) + list.highlight_style(Self::highlight_style(active)) .highlight_symbol(">> ") .style(Self::style(active)) .block(Self::block(title, active)), area, - &mut list.state, + list_state, ); } @@ -292,78 +497,557 @@ impl Ui { ); } - fn render_artist_column( - state: ArtistState, - area: ArtistArea, - frame: &mut Frame<'_, B>, - ) { - Self::render_list_widget("Artists", state.list, state.active, area.list, frame); + fn render_artist_column(st: ArtistState, ar: ArtistArea, fr: &mut Frame<'_, B>) { + Self::render_list_widget("Artists", st.list, st.state, st.active, ar.list, fr); } - fn render_album_column( - state: AlbumState, - area: AlbumArea, - frame: &mut Frame<'_, B>, - ) { - Self::render_list_widget("Albums", state.list, state.active, area.list, frame); - Self::render_info_widget("Album info", state.info, state.active, area.info, frame); + fn render_album_column(st: AlbumState, ar: AlbumArea, fr: &mut Frame<'_, B>) { + Self::render_list_widget("Albums", st.list, st.state, st.active, ar.list, fr); + Self::render_info_widget("Album info", st.info, st.active, ar.info, fr); } - fn render_track_column( - state: TrackState, - area: TrackArea, - frame: &mut Frame<'_, B>, - ) { - Self::render_list_widget("Tracks", state.list, state.active, area.list, frame); - Self::render_info_widget("Track info", state.info, state.active, area.info, frame); + fn render_track_column(st: TrackState, ar: TrackArea, fr: &mut Frame<'_, B>) { + Self::render_list_widget("Tracks", st.list, st.state, st.active, ar.list, fr); + Self::render_info_widget("Track info", st.info, st.active, ar.info, fr); + } +} + +pub trait Ui { + fn is_running(&self) -> bool; + fn quit(&mut self); + + fn increment_category(&mut self); + fn decrement_category(&mut self); + + fn increment_selection(&mut self); + fn decrement_selection(&mut self); + + fn render(&mut self, frame: &mut Frame<'_, B>); +} + +impl Ui for MhUi { + fn is_running(&self) -> bool { + self.running } - pub fn render(&self, app: &APP, frame: &mut Frame<'_, B>) { - let areas = Self::construct_areas(frame.size()); - let app_state = Self::construct_app_state(app); + fn quit(&mut self) { + self.running = false; + } - Self::render_artist_column(app_state.artists, areas.artists, frame); - Self::render_album_column(app_state.albums, areas.albums, frame); - Self::render_track_column(app_state.tracks, areas.tracks, frame); + fn increment_category(&mut self) { + self.selection.increment_category(); + } + + fn decrement_category(&mut self) { + self.selection.decrement_category(); + } + + fn increment_selection(&mut self) { + self.selection + .increment_selection(self.collection_manager.get_collection()); + } + + fn decrement_selection(&mut self) { + self.selection + .decrement_selection(self.collection_manager.get_collection()); + } + + fn render(&mut self, frame: &mut Frame<'_, B>) { + let active = self.selection.active; + let areas = FrameArea::new(frame.size()); + + let artists = self.collection_manager.get_collection(); + let artist_selection = &mut self.selection.artist; + let artist_state = ArtistState::new( + active == Category::Artist, + artists, + &mut artist_selection.state, + ); + + Self::render_artist_column(artist_state, areas.artist, frame); + + let no_albums: Vec = vec![]; + let albums = artist_selection + .state + .selected() + .map(|i| &artists[i].albums) + .unwrap_or_else(|| &no_albums); + let album_selection = &mut artist_selection.album; + let album_state = AlbumState::new( + active == Category::Album, + albums, + &mut album_selection.state, + ); + + Self::render_album_column(album_state, areas.album, frame); + + let no_tracks: Vec = vec![]; + let tracks = album_selection + .state + .selected() + .map(|i| &albums[i].tracks) + .unwrap_or_else(|| &no_tracks); + let track_selection = &mut album_selection.track; + let track_state = TrackState::new( + active == Category::Track, + tracks, + &mut track_selection.state, + ); + + Self::render_track_column(track_state, areas.track, frame); } } #[cfg(test)] mod tests { + use crate::tests::{MockCollectionManager, COLLECTION}; + use crate::tui::tests::{terminal, ui}; + + use super::*; + + #[test] + fn test_track_selection() { + let tracks = &COLLECTION[0].albums[0].tracks; + assert!(tracks.len() > 1); + + let empty = TrackSelection::initialise(None); + assert_eq!(empty.state.selected(), None); + + let empty = TrackSelection::initialise(Some(&vec![])); + assert_eq!(empty.state.selected(), None); + + let mut sel = TrackSelection::initialise(Some(tracks)); + assert_eq!(sel.state.selected(), Some(0)); + + sel.decrement(tracks); + assert_eq!(sel.state.selected(), Some(0)); + + sel.increment(tracks); + assert_eq!(sel.state.selected(), Some(1)); + + sel.decrement(tracks); + assert_eq!(sel.state.selected(), Some(0)); + + for _ in 0..(tracks.len() + 5) { + sel.increment(tracks); + } + assert_eq!(sel.state.selected(), Some(tracks.len() - 1)); + + // Artifical test case to verify upper limit. + sel.state.select(Some(std::usize::MAX)); + assert_eq!(sel.state.selected(), Some(std::usize::MAX)); + + sel.increment(&vec![]); + assert_eq!(sel.state.selected(), Some(std::usize::MAX)); + } + + #[test] + fn test_album_selection() { + let albums = &COLLECTION[0].albums; + assert!(albums.len() > 1); + + let empty = AlbumSelection::initialise(None); + assert_eq!(empty.state.selected(), None); + + let empty = AlbumSelection::initialise(Some(&vec![])); + assert_eq!(empty.state.selected(), None); + + let mut sel = AlbumSelection::initialise(Some(albums)); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.track.state.selected(), Some(0)); + + sel.increment_track(albums); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.track.state.selected(), Some(1)); + + // Verify that decrement that doesn't change index does not reset track. + sel.decrement(albums); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.track.state.selected(), Some(1)); + + sel.increment(albums); + assert_eq!(sel.state.selected(), Some(1)); + assert_eq!(sel.track.state.selected(), Some(0)); + + sel.decrement(albums); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.track.state.selected(), Some(0)); + + for _ in 0..(albums.len() + 5) { + sel.increment(albums); + } + assert_eq!(sel.state.selected(), Some(albums.len() - 1)); + assert_eq!(sel.track.state.selected(), Some(0)); + + sel.increment_track(albums); + assert_eq!(sel.state.selected(), Some(albums.len() - 1)); + assert_eq!(sel.track.state.selected(), Some(1)); + + // Verify that increment that doesn't change index does not reset track. + sel.increment(albums); + assert_eq!(sel.state.selected(), Some(albums.len() - 1)); + assert_eq!(sel.track.state.selected(), Some(1)); + + // Artifical test case to verify upper limit. + sel.state.select(Some(std::usize::MAX)); + sel.track.state.select(Some(1)); + assert_eq!(sel.state.selected(), Some(std::usize::MAX)); + assert_eq!(sel.track.state.selected(), Some(1)); + + sel.increment(&vec![]); + assert_eq!(sel.state.selected(), Some(std::usize::MAX)); + assert_eq!(sel.track.state.selected(), Some(1)); + } + + #[test] + fn test_artist_selection() { + let artists = &COLLECTION; + assert!(artists.len() > 1); + + let empty = ArtistSelection::initialise(None); + assert_eq!(empty.state.selected(), None); + + let empty = ArtistSelection::initialise(Some(&vec![])); + assert_eq!(empty.state.selected(), None); + + let mut sel = ArtistSelection::initialise(Some(artists)); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.album.state.selected(), Some(0)); + + sel.increment_album(artists); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.album.state.selected(), Some(1)); + + // Verify that decrement that doesn't change index does not reset album. + sel.decrement(artists); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.album.state.selected(), Some(1)); + + sel.increment(artists); + assert_eq!(sel.state.selected(), Some(1)); + assert_eq!(sel.album.state.selected(), Some(0)); + + sel.decrement(artists); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.album.state.selected(), Some(0)); + + for _ in 0..(artists.len() + 5) { + sel.increment(artists); + } + assert_eq!(sel.state.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.state.selected(), Some(0)); + + sel.increment_album(artists); + assert_eq!(sel.state.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.state.selected(), Some(1)); + + // Verify that increment that doesn't change index does not reset album. + sel.increment(artists); + assert_eq!(sel.state.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.state.selected(), Some(1)); + + // Artifical test case to verify upper limit. + sel.state.select(Some(std::usize::MAX)); + sel.album.state.select(Some(1)); + assert_eq!(sel.state.selected(), Some(std::usize::MAX)); + assert_eq!(sel.album.state.selected(), Some(1)); + + sel.increment(&vec![]); + assert_eq!(sel.state.selected(), Some(std::usize::MAX)); + assert_eq!(sel.album.state.selected(), Some(1)); + } + + #[test] + fn ui_running() { + let mut collection_manager = MockCollectionManager::new(); + + collection_manager + .expect_rescan_library() + .times(1) + .return_once(|| Ok(())); + collection_manager + .expect_get_collection() + .return_const(COLLECTION.to_owned()); + + let mut ui = MhUi::new(collection_manager).unwrap(); + assert!(ui.is_running()); + + ui.quit(); + assert!(!ui.is_running()); + } + + #[test] + fn ui_modifiers() { + let mut collection_manager = MockCollectionManager::new(); + + collection_manager + .expect_rescan_library() + .times(1) + .return_once(|| Ok(())); + collection_manager + .expect_get_collection() + .return_const(COLLECTION.to_owned()); + + let mut ui = MhUi::new(collection_manager).unwrap(); + assert!(ui.is_running()); + + assert_eq!(ui.selection.active, Category::Artist); + assert_eq!(ui.selection.artist.state.selected(), Some(0)); + assert_eq!(ui.selection.artist.album.state.selected(), Some(0)); + assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + + ui.increment_selection(); + assert_eq!(ui.selection.active, Category::Artist); + assert_eq!(ui.selection.artist.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.state.selected(), Some(0)); + assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + + ui.increment_category(); + assert_eq!(ui.selection.active, Category::Album); + assert_eq!(ui.selection.artist.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.state.selected(), Some(0)); + assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + + ui.increment_selection(); + assert_eq!(ui.selection.active, Category::Album); + assert_eq!(ui.selection.artist.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + + ui.increment_category(); + assert_eq!(ui.selection.active, Category::Track); + assert_eq!(ui.selection.artist.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + + ui.increment_selection(); + assert_eq!(ui.selection.active, Category::Track); + assert_eq!(ui.selection.artist.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.track.state.selected(), Some(1)); + + ui.increment_category(); + assert_eq!(ui.selection.active, Category::Track); + assert_eq!(ui.selection.artist.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.track.state.selected(), Some(1)); + + ui.decrement_selection(); + assert_eq!(ui.selection.active, Category::Track); + assert_eq!(ui.selection.artist.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + + ui.increment_selection(); + ui.decrement_category(); + assert_eq!(ui.selection.active, Category::Album); + assert_eq!(ui.selection.artist.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.track.state.selected(), Some(1)); + + ui.decrement_selection(); + assert_eq!(ui.selection.active, Category::Album); + assert_eq!(ui.selection.artist.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.state.selected(), Some(0)); + assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + + ui.increment_selection(); + ui.decrement_category(); + assert_eq!(ui.selection.active, Category::Artist); + assert_eq!(ui.selection.artist.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + + ui.decrement_selection(); + assert_eq!(ui.selection.active, Category::Artist); + assert_eq!(ui.selection.artist.state.selected(), Some(0)); + assert_eq!(ui.selection.artist.album.state.selected(), Some(0)); + assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + + ui.increment_category(); + ui.increment_selection(); + ui.decrement_category(); + ui.decrement_selection(); + ui.decrement_category(); + assert_eq!(ui.selection.active, Category::Artist); + assert_eq!(ui.selection.artist.state.selected(), Some(0)); + assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); + assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + } + + #[test] + fn app_no_tracks() { + let mut collection_manager = MockCollectionManager::new(); + let mut collection = COLLECTION.to_owned(); + collection[0].albums[0].tracks = vec![]; + + collection_manager + .expect_rescan_library() + .times(1) + .return_once(|| Ok(())); + collection_manager + .expect_get_collection() + .return_const(collection); + + let mut app = MhUi::new(collection_manager).unwrap(); + assert!(app.is_running()); + + assert_eq!(app.selection.active, Category::Artist); + assert_eq!(app.selection.artist.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + + app.increment_category(); + app.increment_category(); + + app.increment_selection(); + assert_eq!(app.selection.active, Category::Track); + assert_eq!(app.selection.artist.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + + app.decrement_selection(); + assert_eq!(app.selection.active, Category::Track); + assert_eq!(app.selection.artist.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + } + + #[test] + fn app_no_albums() { + let mut collection_manager = MockCollectionManager::new(); + let mut collection = COLLECTION.to_owned(); + collection[0].albums = vec![]; + + collection_manager + .expect_rescan_library() + .times(1) + .return_once(|| Ok(())); + collection_manager + .expect_get_collection() + .return_const(collection); + + let mut app = MhUi::new(collection_manager).unwrap(); + assert!(app.is_running()); + + assert_eq!(app.selection.active, Category::Artist); + assert_eq!(app.selection.artist.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.state.selected(), None); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + + app.increment_category(); + + app.increment_selection(); + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.state.selected(), None); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + + app.decrement_selection(); + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.state.selected(), None); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + + app.increment_category(); + + app.increment_selection(); + assert_eq!(app.selection.active, Category::Track); + assert_eq!(app.selection.artist.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.state.selected(), None); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + + app.decrement_selection(); + assert_eq!(app.selection.active, Category::Track); + assert_eq!(app.selection.artist.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.state.selected(), None); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + } + + #[test] + fn app_no_artists() { + let mut collection_manager = MockCollectionManager::new(); + let collection = vec![]; + + collection_manager + .expect_rescan_library() + .times(1) + .return_once(|| Ok(())); + collection_manager + .expect_get_collection() + .return_const(collection); + + let mut app = MhUi::new(collection_manager).unwrap(); + assert!(app.is_running()); + + assert_eq!(app.selection.active, Category::Artist); + assert_eq!(app.selection.artist.state.selected(), None); + assert_eq!(app.selection.artist.album.state.selected(), None); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + + app.increment_selection(); + assert_eq!(app.selection.active, Category::Artist); + assert_eq!(app.selection.artist.state.selected(), None); + assert_eq!(app.selection.artist.album.state.selected(), None); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + + app.decrement_selection(); + assert_eq!(app.selection.active, Category::Artist); + assert_eq!(app.selection.artist.state.selected(), None); + assert_eq!(app.selection.artist.album.state.selected(), None); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + + app.increment_category(); + + app.increment_selection(); + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.selected(), None); + assert_eq!(app.selection.artist.album.state.selected(), None); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + + app.decrement_selection(); + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.selected(), None); + assert_eq!(app.selection.artist.album.state.selected(), None); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + + app.increment_category(); + + app.increment_selection(); + assert_eq!(app.selection.active, Category::Track); + assert_eq!(app.selection.artist.state.selected(), None); + assert_eq!(app.selection.artist.album.state.selected(), None); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + + app.decrement_selection(); + assert_eq!(app.selection.active, Category::Track); + assert_eq!(app.selection.artist.state.selected(), None); + assert_eq!(app.selection.artist.album.state.selected(), None); + assert_eq!(app.selection.artist.album.track.state.selected(), None); + } + // This is UI so the only sensible unit test is to run the code through various app states. - use crate::{ - tests::COLLECTION, - tui::{ - app::App, - tests::{app, terminal}, - }, - }; - - use super::Ui; - #[test] fn empty() { let mut terminal = terminal(); - let app = app(vec![]); - let ui = Ui::new(); + let mut ui = ui(vec![]); - terminal.draw(|frame| ui.render(&app, frame)).unwrap(); + terminal.draw(|frame| ui.render(frame)).unwrap(); } #[test] fn collection() { let mut terminal = terminal(); - let mut app = app(COLLECTION.to_owned()); - let ui = Ui::new(); + let mut ui = ui(COLLECTION.to_owned()); - terminal.draw(|frame| ui.render(&app, frame)).unwrap(); + terminal.draw(|frame| ui.render(frame)).unwrap(); // Change the track (which has a different track format). - app.increment_category(); - app.increment_category(); - app.increment_selection(); + ui.increment_category(); + ui.increment_category(); + ui.increment_selection(); - terminal.draw(|frame| ui.render(&app, frame)).unwrap(); + terminal.draw(|frame| ui.render(frame)).unwrap(); } }