From 5edcceeae56beda434de5f2b6e9bc8fca938464c Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 13 Apr 2023 14:30:11 +0200 Subject: [PATCH 1/7] Generic collection --- src/main.rs | 2 +- src/tui/app.rs | 18 +++++++++--------- src/tui/handler.rs | 17 +++++++++-------- src/tui/mod.rs | 38 ++++++++++++++++++++++++-------------- src/tui/ui.rs | 24 +++++++++++++++--------- 5 files changed, 58 insertions(+), 41 deletions(-) diff --git a/src/main.rs b/src/main.rs index 827dbda..04ab395 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,7 +61,7 @@ fn main() { let ui = Ui::new(); - let app = App::new(Box::new(collection_manager)).expect("failed to initialise app"); + let app = App::new(collection_manager).expect("failed to initialise app"); // Run the TUI application. Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui"); diff --git a/src/tui/app.rs b/src/tui/app.rs index 3dccf0c..f3163c5 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -110,14 +110,14 @@ struct Selection { artist: Option, } -pub struct App { - collection_manager: Box, +pub struct App { + collection_manager: CM, selection: Selection, running: bool, } -impl App { - pub fn new(mut collection_manager: Box) -> Result { +impl App { + pub fn new(mut collection_manager: CM) -> Result { collection_manager.rescan_library()?; let selection = Selection { active: Category::Artist, @@ -516,7 +516,7 @@ mod tests { .expect_get_collection() .return_const(COLLECTION.to_owned()); - let mut app = App::new(Box::new(collection_manager)).unwrap(); + let mut app = App::new(collection_manager).unwrap(); assert!(app.is_running()); app.quit(); @@ -535,7 +535,7 @@ mod tests { .expect_get_collection() .return_const(COLLECTION.to_owned()); - let mut app = App::new(Box::new(collection_manager)).unwrap(); + let mut app = App::new(collection_manager).unwrap(); assert!(app.is_running()); assert!(!app.get_artist_ids().is_empty()); @@ -640,7 +640,7 @@ mod tests { .expect_get_collection() .return_const(collection); - let mut app = App::new(Box::new(collection_manager)).unwrap(); + let mut app = App::new(collection_manager).unwrap(); assert!(app.is_running()); assert!(!app.get_artist_ids().is_empty()); @@ -682,7 +682,7 @@ mod tests { .expect_get_collection() .return_const(collection); - let mut app = App::new(Box::new(collection_manager)).unwrap(); + let mut app = App::new(collection_manager).unwrap(); assert!(app.is_running()); assert!(!app.get_artist_ids().is_empty()); @@ -736,7 +736,7 @@ mod tests { .expect_get_collection() .return_const(collection); - let mut app = App::new(Box::new(collection_manager)).unwrap(); + let mut app = App::new(collection_manager).unwrap(); assert!(app.is_running()); assert!(app.get_artist_ids().is_empty()); diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 8de4cb7..01db45c 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -2,6 +2,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; #[cfg(test)] use mockall::automock; +use musichoard::collection::CollectionManager; use super::{ app::App, @@ -9,12 +10,12 @@ use super::{ }; #[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, app: &mut App) -> Result<(), EventError>; } -trait EventHandlerPrivate { - fn handle_key_event(app: &mut App, key_event: KeyEvent); +trait EventHandlerPrivate { + fn handle_key_event(app: &mut App, key_event: KeyEvent); } pub struct TuiEventHandler { @@ -28,8 +29,8 @@ 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, app: &mut App) -> Result<(), EventError> { match self.events.recv()? { Event::Key(key_event) => Self::handle_key_event(app, key_event), Event::Mouse(_) => {} @@ -39,8 +40,8 @@ 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(app: &mut App, key_event: KeyEvent) { match key_event.code { // Exit application on `ESC` or `q`. KeyCode::Esc | KeyCode::Char('q') => { diff --git a/src/tui/mod.rs b/src/tui/mod.rs index b3c3313..ce2b5d7 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -1,9 +1,10 @@ use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; -use musichoard::collection; +use musichoard::collection::{self, CollectionManager}; use ratatui::backend::Backend; use ratatui::Terminal; use std::io; +use std::marker::PhantomData; pub mod app; pub mod event; @@ -43,11 +44,12 @@ impl From for Error { } } -pub struct Tui { +pub struct Tui { terminal: Terminal, + _phantom: PhantomData, } -impl Tui { +impl Tui { fn init(&mut self) -> Result<(), Error> { self.terminal.hide_cursor()?; self.terminal.clear()?; @@ -64,7 +66,12 @@ impl Tui { self.exit(); } - fn main_loop(&mut self, mut app: App, ui: Ui, handler: impl EventHandler) -> Result<(), Error> { + 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)?; @@ -75,12 +82,15 @@ impl Tui { fn main( term: Terminal, - app: App, - ui: Ui, - handler: impl EventHandler, + app: App, + ui: Ui, + handler: impl EventHandler, listener: impl EventListener, ) -> Result<(), Error> { - let mut tui = Tui { terminal: term }; + let mut tui = Tui { + terminal: term, + _phantom: PhantomData, + }; tui.init()?; @@ -132,9 +142,9 @@ impl Tui { pub fn run( term: Terminal, - app: App, - ui: Ui, - handler: impl EventHandler, + app: App, + ui: Ui, + handler: impl EventHandler, listener: impl EventListener, ) -> Result<(), Error> { Self::enable()?; @@ -174,7 +184,7 @@ mod tests { Terminal::new(backend).unwrap() } - pub fn app(collection: Collection) -> App { + pub fn app(collection: Collection) -> App { let mut collection_manager = MockCollectionManager::new(); collection_manager @@ -184,7 +194,7 @@ mod tests { .expect_get_collection() .return_const(collection); - App::new(Box::new(collection_manager)).unwrap() + App::new(collection_manager).unwrap() } fn listener() -> MockEventListener { @@ -198,7 +208,7 @@ mod tests { listener } - fn handler() -> MockEventHandler { + fn handler() -> MockEventHandler { let mut handler = MockEventHandler::new(); handler.expect_handle_next_event().return_once(|app| { app.quit(); diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 448c900..1e36c26 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -1,4 +1,6 @@ -use musichoard::TrackFormat; +use std::marker::PhantomData; + +use musichoard::{collection::CollectionManager, TrackFormat}; use ratatui::{ backend::Backend, layout::{Alignment, Rect}, @@ -57,11 +59,15 @@ struct AppState<'a> { tracks: TrackState<'a>, } -pub struct Ui {} +pub struct Ui { + _phantom: PhantomData, +} -impl Ui { +impl Ui { pub fn new() -> Self { - Ui {} + Ui { + _phantom: PhantomData, + } } fn construct_areas(frame: Rect) -> FrameAreas { @@ -121,7 +127,7 @@ impl Ui { } } - fn construct_artist_list(app: &App) -> ArtistState { + fn construct_artist_list(app: &App) -> ArtistState { let artists = app.get_artist_ids(); let list = List::new( artists @@ -143,7 +149,7 @@ impl Ui { } } - fn construct_album_list(app: &App) -> AlbumState { + fn construct_album_list(app: &App) -> AlbumState { let albums = app.get_album_ids(); let list = List::new( albums @@ -176,7 +182,7 @@ impl Ui { } } - fn construct_track_list(app: &App) -> TrackState { + fn construct_track_list(app: &App) -> TrackState { let tracks = app.get_track_ids(); let list = List::new( tracks @@ -220,7 +226,7 @@ impl Ui { } } - fn construct_app_state(app: &App) -> AppState { + fn construct_app_state(app: &App) -> AppState { AppState { artists: Self::construct_artist_list(app), albums: Self::construct_album_list(app), @@ -312,7 +318,7 @@ impl Ui { Self::render_info_widget("Track info", state.info, state.active, area.info, frame); } - pub fn render(&self, app: &App, frame: &mut Frame<'_, B>) { + 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); -- 2.45.2 From 73b4cc8b2861b393f0e8066abe3af1b049d79aca Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 13 Apr 2023 14:44:30 +0200 Subject: [PATCH 2/7] Generic beets executor --- src/library/beets.rs | 108 +++++++++++++++++++---------------------- src/main.rs | 4 +- tests/library/beets.rs | 26 +++++----- 3 files changed, 67 insertions(+), 71 deletions(-) diff --git a/src/library/beets.rs b/src/library/beets.rs index e9c1f2e..b042e6f 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -16,6 +16,33 @@ use crate::{Album, AlbumId, Artist, ArtistId, Track, TrackFormat}; use super::{Error, Library, Query, QueryOption}; +macro_rules! list_format_separator { + () => { + " -*^- " + }; +} + +const CMD_LIST: &str = "ls"; +const LIST_FORMAT_SEPARATOR: &str = list_format_separator!(); +const LIST_FORMAT_ARG: &str = concat!( + "--format=", + "$albumartist", + list_format_separator!(), + "$year", + list_format_separator!(), + "$album", + list_format_separator!(), + "$track", + list_format_separator!(), + "$title", + list_format_separator!(), + "$artist", + list_format_separator!(), + "$format" +); +const TRACK_FORMAT_FLAC: &str = "FLAC"; +const TRACK_FORMAT_MP3: &str = "MP3"; + trait QueryOptionArgBeets { fn to_arg(&self, option_name: &str) -> Option; } @@ -94,29 +121,23 @@ pub trait BeetsLibraryExecutor { } /// Beets library. -pub struct BeetsLibrary { - executor: Box, +pub struct BeetsLibrary { + executor: BLE, } trait LibraryPrivate { - const CMD_LIST: &'static str; - const LIST_FORMAT_SEPARATOR: &'static str; - const LIST_FORMAT_ARG: &'static str; - const TRACK_FORMAT_FLAC: &'static str; - const TRACK_FORMAT_MP3: &'static str; - fn list_cmd_and_args(query: &Query) -> Vec; fn list_to_artists(list_output: Vec) -> Result, Error>; } -impl BeetsLibrary { +impl BeetsLibrary { /// Create a new beets library with the provided executor, e.g. [`BeetsLibraryCommandExecutor`]. - pub fn new(executor: Box) -> BeetsLibrary { + pub fn new(executor: BLE) -> Self { BeetsLibrary { executor } } } -impl Library for BeetsLibrary { +impl Library for BeetsLibrary { fn list(&mut self, query: &Query) -> Result, Error> { let cmd = Self::list_cmd_and_args(query); let output = self.executor.exec(&cmd)?; @@ -124,37 +145,10 @@ impl Library for BeetsLibrary { } } -macro_rules! list_format_separator { - () => { - " -*^- " - }; -} - -impl LibraryPrivate for BeetsLibrary { - const CMD_LIST: &'static str = "ls"; - const LIST_FORMAT_SEPARATOR: &'static str = list_format_separator!(); - const LIST_FORMAT_ARG: &'static str = concat!( - "--format=", - "$albumartist", - list_format_separator!(), - "$year", - list_format_separator!(), - "$album", - list_format_separator!(), - "$track", - list_format_separator!(), - "$title", - list_format_separator!(), - "$artist", - list_format_separator!(), - "$format" - ); - const TRACK_FORMAT_FLAC: &'static str = "FLAC"; - const TRACK_FORMAT_MP3: &'static str = "MP3"; - +impl LibraryPrivate for BeetsLibrary { fn list_cmd_and_args(query: &Query) -> Vec { - let mut cmd: Vec = vec![String::from(Self::CMD_LIST)]; - cmd.push(Self::LIST_FORMAT_ARG.to_string()); + let mut cmd: Vec = vec![String::from(CMD_LIST)]; + cmd.push(LIST_FORMAT_ARG.to_string()); cmd.append(&mut query.to_args()); cmd } @@ -168,7 +162,7 @@ impl LibraryPrivate for BeetsLibrary { continue; } - let split: Vec<&str> = line.split(Self::LIST_FORMAT_SEPARATOR).collect(); + let split: Vec<&str> = line.split(LIST_FORMAT_SEPARATOR).collect(); if split.len() != 7 { return Err(Error::InvalidData(line.to_string())); } @@ -193,8 +187,8 @@ impl LibraryPrivate for BeetsLibrary { title: track_title, artist: track_artist.split("; ").map(|s| s.to_owned()).collect(), format: match track_format.as_ref() { - Self::TRACK_FORMAT_FLAC => TrackFormat::Flac, - Self::TRACK_FORMAT_MP3 => TrackFormat::Mp3, + TRACK_FORMAT_FLAC => TrackFormat::Flac, + TRACK_FORMAT_MP3 => TrackFormat::Mp3, _ => return Err(Error::InvalidData(track_format)), }, }; @@ -309,14 +303,14 @@ mod tests { let track_title = &track.title; let track_artist = &track.artist.join("; "); let track_format = match track.format { - TrackFormat::Flac => BeetsLibrary::TRACK_FORMAT_FLAC, - TrackFormat::Mp3 => BeetsLibrary::TRACK_FORMAT_MP3, + TrackFormat::Flac => TRACK_FORMAT_FLAC, + TrackFormat::Mp3 => TRACK_FORMAT_MP3, }; strings.push(format!( "{album_artist}{0}{album_year}{0}{album_title}{0}\ {track_number}{0}{track_title}{0}{track_artist}{0}{track_format}", - BeetsLibrary::LIST_FORMAT_SEPARATOR, + LIST_FORMAT_SEPARATOR, )); } } @@ -356,7 +350,7 @@ mod tests { #[test] fn test_list_empty() { - let arguments = vec!["ls".to_string(), BeetsLibrary::LIST_FORMAT_ARG.to_string()]; + let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()]; let result = Ok(vec![]); let mut executor = MockBeetsLibraryExecutor::new(); @@ -366,7 +360,7 @@ mod tests { .times(1) .return_once(|_| result); - let mut beets = BeetsLibrary::new(Box::new(executor)); + let mut beets = BeetsLibrary::new(executor); let output = beets.list(&Query::default()).unwrap(); let expected: Vec = vec![]; @@ -375,7 +369,7 @@ mod tests { #[test] fn test_list_ordered() { - let arguments = vec!["ls".to_string(), BeetsLibrary::LIST_FORMAT_ARG.to_string()]; + let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()]; let expected = COLLECTION.to_owned(); let result = Ok(artists_to_beets_string(&expected)); @@ -386,7 +380,7 @@ mod tests { .times(1) .return_once(|_| result); - let mut beets = BeetsLibrary::new(Box::new(executor)); + let mut beets = BeetsLibrary::new(executor); let output = beets.list(&Query::default()).unwrap(); assert_eq!(output, expected); @@ -394,7 +388,7 @@ mod tests { #[test] fn test_list_unordered() { - let arguments = vec!["ls".to_string(), BeetsLibrary::LIST_FORMAT_ARG.to_string()]; + let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()]; let mut expected = COLLECTION.to_owned(); let mut output = artists_to_beets_string(&expected); let last = output.len() - 1; @@ -421,7 +415,7 @@ mod tests { .times(1) .return_once(|_| result); - let mut beets = BeetsLibrary::new(Box::new(executor)); + let mut beets = BeetsLibrary::new(executor); let output = beets.list(&Query::default()).unwrap(); assert_eq!(output, expected); @@ -429,7 +423,7 @@ mod tests { #[test] fn test_list_album_title_year_clash() { - let arguments = vec!["ls".to_string(), BeetsLibrary::LIST_FORMAT_ARG.to_string()]; + let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()]; let mut expected = COLLECTION.to_owned(); expected[0].albums[0].id.year = expected[1].albums[0].id.year; expected[0].albums[0].id.title = expected[1].albums[0].id.title.clone(); @@ -443,7 +437,7 @@ mod tests { .times(1) .return_once(|_| result); - let mut beets = BeetsLibrary::new(Box::new(executor)); + let mut beets = BeetsLibrary::new(executor); let output = beets.list(&Query::default()).unwrap(); assert_eq!(output, expected); @@ -458,7 +452,7 @@ mod tests { let arguments = vec![ "ls".to_string(), - BeetsLibrary::LIST_FORMAT_ARG.to_string(), + LIST_FORMAT_ARG.to_string(), String::from("^album:some.album"), String::from("track:5"), String::from("artist:some.artist"), @@ -472,7 +466,7 @@ mod tests { .times(1) .return_once(|_| result); - let mut beets = BeetsLibrary::new(Box::new(executor)); + let mut beets = BeetsLibrary::new(executor); let output = beets.list(&query).unwrap(); let expected: Vec = vec![]; diff --git a/src/main.rs b/src/main.rs index 04ab395..81a8fab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,9 +41,9 @@ fn main() { // Create the application. let opt = Opt::from_args(); - let beets = BeetsLibrary::new(Box::new( + let beets = BeetsLibrary::new( BeetsLibraryCommandExecutor::default().config(opt.beets_config_file_path.as_deref()), - )); + ); let database = JsonDatabase::new(Box::new(JsonDatabaseFileBackend::new( &opt.database_file_path, diff --git a/tests/library/beets.rs b/tests/library/beets.rs index efdecbf..bea16d0 100644 --- a/tests/library/beets.rs +++ b/tests/library/beets.rs @@ -15,19 +15,21 @@ use musichoard::{ use crate::COLLECTION; -static BEETS_EMPTY_CONFIG: Lazy>> = Lazy::new(|| { - Arc::new(Mutex::new(BeetsLibrary::new(Box::new( - BeetsLibraryCommandExecutor::default(), - )))) -}); +static BEETS_EMPTY_CONFIG: Lazy>>> = + Lazy::new(|| { + Arc::new(Mutex::new(BeetsLibrary::new( + BeetsLibraryCommandExecutor::default(), + ))) + }); -static BEETS_TEST_CONFIG: Lazy>> = Lazy::new(|| { - Arc::new(Mutex::new(BeetsLibrary::new(Box::new( - BeetsLibraryCommandExecutor::default().config(Some( - &fs::canonicalize("./tests/files/library/config.yml").unwrap(), - )), - )))) -}); +static BEETS_TEST_CONFIG: Lazy>>> = + Lazy::new(|| { + Arc::new(Mutex::new(BeetsLibrary::new( + BeetsLibraryCommandExecutor::default().config(Some( + &fs::canonicalize("./tests/files/library/config.yml").unwrap(), + )), + ))) + }); #[test] fn test_no_config_list() { -- 2.45.2 From 45e65073e949eb8c76d1efc190f7e1fc76def353 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 13 Apr 2023 14:47:31 +0200 Subject: [PATCH 3/7] Generic beets library --- src/collection/mod.rs | 22 ++++++++-------------- src/main.rs | 2 +- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/collection/mod.rs b/src/collection/mod.rs index 62352ad..e21039a 100644 --- a/src/collection/mod.rs +++ b/src/collection/mod.rs @@ -56,18 +56,15 @@ pub trait CollectionManager { /// The collection manager. It is responsible for pulling information from both the library and the /// database, ensuring its consistent and writing back any changes. -pub struct MhCollectionManager { - library: Box, +pub struct MhCollectionManager { + library: L, database: Box, collection: Collection, } -impl MhCollectionManager { +impl MhCollectionManager { /// Create a new [`CollectionManager`] with the provided [`Library`] and [`Database`]. - pub fn new( - library: Box, - database: Box, - ) -> Self { + pub fn new(library: L, database: Box) -> Self { MhCollectionManager { library, database, @@ -76,7 +73,7 @@ impl MhCollectionManager { } } -impl CollectionManager for MhCollectionManager { +impl CollectionManager for MhCollectionManager { fn rescan_library(&mut self) -> Result<(), Error> { self.collection = self.library.list(&Query::default())?; Ok(()) @@ -127,8 +124,7 @@ mod tests { .times(1) .return_once(|_| database_result); - let mut collection_manager = - MhCollectionManager::new(Box::new(library), Box::new(database)); + let mut collection_manager = MhCollectionManager::new(library, Box::new(database)); collection_manager.rescan_library().unwrap(); assert_eq!(collection_manager.get_collection(), &*COLLECTION); @@ -147,8 +143,7 @@ mod tests { .times(1) .return_once(|_| library_result); - let mut collection_manager = - MhCollectionManager::new(Box::new(library), Box::new(database)); + let mut collection_manager = MhCollectionManager::new(library, Box::new(database)); let actual_err = collection_manager.rescan_library().unwrap_err(); let expected_err = Error::LibraryError( @@ -171,8 +166,7 @@ mod tests { .times(1) .return_once(|_| database_result); - let mut collection_manager = - MhCollectionManager::new(Box::new(library), Box::new(database)); + let mut collection_manager = MhCollectionManager::new(library, Box::new(database)); let actual_err = collection_manager.save_to_database().unwrap_err(); let expected_err = diff --git a/src/main.rs b/src/main.rs index 81a8fab..545e453 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,7 +49,7 @@ fn main() { &opt.database_file_path, ))); - let collection_manager = MhCollectionManager::new(Box::new(beets), Box::new(database)); + let collection_manager = MhCollectionManager::new(beets, Box::new(database)); // Initialize the terminal user interface. let backend = CrosstermBackend::new(io::stdout()); -- 2.45.2 From ea736cebed679fe0e9a2d1131a2e3bb27e2527e1 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 13 Apr 2023 14:49:44 +0200 Subject: [PATCH 4/7] Generic database --- src/collection/mod.rs | 16 ++++++++-------- src/main.rs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/collection/mod.rs b/src/collection/mod.rs index e21039a..a745f0f 100644 --- a/src/collection/mod.rs +++ b/src/collection/mod.rs @@ -56,15 +56,15 @@ pub trait CollectionManager { /// The collection manager. It is responsible for pulling information from both the library and the /// database, ensuring its consistent and writing back any changes. -pub struct MhCollectionManager { +pub struct MhCollectionManager { library: L, - database: Box, + database: D, collection: Collection, } -impl MhCollectionManager { +impl MhCollectionManager { /// Create a new [`CollectionManager`] with the provided [`Library`] and [`Database`]. - pub fn new(library: L, database: Box) -> Self { + pub fn new(library: L, database: D) -> Self { MhCollectionManager { library, database, @@ -73,7 +73,7 @@ impl MhCollectionManager { } } -impl CollectionManager for MhCollectionManager { +impl CollectionManager for MhCollectionManager { fn rescan_library(&mut self) -> Result<(), Error> { self.collection = self.library.list(&Query::default())?; Ok(()) @@ -124,7 +124,7 @@ mod tests { .times(1) .return_once(|_| database_result); - let mut collection_manager = MhCollectionManager::new(library, Box::new(database)); + let mut collection_manager = MhCollectionManager::new(library, database); collection_manager.rescan_library().unwrap(); assert_eq!(collection_manager.get_collection(), &*COLLECTION); @@ -143,7 +143,7 @@ mod tests { .times(1) .return_once(|_| library_result); - let mut collection_manager = MhCollectionManager::new(library, Box::new(database)); + let mut collection_manager = MhCollectionManager::new(library, database); let actual_err = collection_manager.rescan_library().unwrap_err(); let expected_err = Error::LibraryError( @@ -166,7 +166,7 @@ mod tests { .times(1) .return_once(|_| database_result); - let mut collection_manager = MhCollectionManager::new(library, Box::new(database)); + let mut collection_manager = MhCollectionManager::new(library, database); let actual_err = collection_manager.save_to_database().unwrap_err(); let expected_err = diff --git a/src/main.rs b/src/main.rs index 545e453..b927ddd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,7 +49,7 @@ fn main() { &opt.database_file_path, ))); - let collection_manager = MhCollectionManager::new(beets, Box::new(database)); + let collection_manager = MhCollectionManager::new(beets, database); // Initialize the terminal user interface. let backend = CrosstermBackend::new(io::stdout()); -- 2.45.2 From 23d4aba09f6fd7830a2345f148f8dade8a09a94f Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 13 Apr 2023 15:04:28 +0200 Subject: [PATCH 5/7] Generic json database backend --- src/collection/mod.rs | 5 +++-- src/database/json.rs | 27 ++++++++++++--------------- src/database/mod.rs | 8 ++++---- src/main.rs | 4 +--- tests/database/json.rs | 6 +++--- 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/collection/mod.rs b/src/collection/mod.rs index a745f0f..cbc3fe9 100644 --- a/src/collection/mod.rs +++ b/src/collection/mod.rs @@ -94,6 +94,7 @@ mod tests { use mockall::predicate; use crate::{ + collection::Collection, database::{self, MockDatabase}, library::{self, MockLibrary, Query}, tests::COLLECTION, @@ -122,7 +123,7 @@ mod tests { .expect_write() .with(predicate::eq(database_input)) .times(1) - .return_once(|_| database_result); + .return_once(|_: &Collection| database_result); let mut collection_manager = MhCollectionManager::new(library, database); @@ -164,7 +165,7 @@ mod tests { database .expect_write() .times(1) - .return_once(|_| database_result); + .return_once(|_: &Collection| database_result); let mut collection_manager = MhCollectionManager::new(library, database); diff --git a/src/database/json.rs b/src/database/json.rs index 52955f8..54fbdf6 100644 --- a/src/database/json.rs +++ b/src/database/json.rs @@ -3,7 +3,8 @@ use std::fs; use std::path::{Path, PathBuf}; -use crate::collection::Collection; +use serde::de::DeserializeOwned; +use serde::Serialize; #[cfg(test)] use mockall::automock; @@ -27,25 +28,25 @@ pub trait JsonDatabaseBackend { } /// JSON database. -pub struct JsonDatabase { - backend: Box, +pub struct JsonDatabase { + backend: JDB, } -impl JsonDatabase { +impl JsonDatabase { /// Create a new JSON database with the provided backend, e.g. [`JsonDatabaseFileBackend`]. - pub fn new(backend: Box) -> Self { + pub fn new(backend: JDB) -> Self { JsonDatabase { backend } } } -impl Database for JsonDatabase { - fn read(&self, collection: &mut Collection) -> Result<(), Error> { +impl Database for JsonDatabase { + fn read(&self, collection: &mut D) -> Result<(), Error> { let serialized = self.backend.read()?; *collection = serde_json::from_str(&serialized)?; Ok(()) } - fn write(&mut self, collection: &Collection) -> Result<(), Error> { + fn write(&mut self, collection: &S) -> Result<(), Error> { let serialized = serde_json::to_string(&collection)?; self.backend.write(&serialized)?; Ok(()) @@ -162,9 +163,7 @@ mod tests { .times(1) .return_once(|_| Ok(())); - JsonDatabase::new(Box::new(backend)) - .write(&write_data) - .unwrap(); + JsonDatabase::new(backend).write(&write_data).unwrap(); } #[test] @@ -176,9 +175,7 @@ mod tests { backend.expect_read().times(1).return_once(|| result); let mut read_data: Vec = vec![]; - JsonDatabase::new(Box::new(backend)) - .read(&mut read_data) - .unwrap(); + JsonDatabase::new(backend).read(&mut read_data).unwrap(); assert_eq!(read_data, expected); } @@ -197,7 +194,7 @@ mod tests { .return_once(|_| Ok(())); backend.expect_read().times(1).return_once(|| result); - let mut database = JsonDatabase::new(Box::new(backend)); + let mut database = JsonDatabase::new(backend); let write_data = COLLECTION.to_owned(); let mut read_data: Vec = vec![]; diff --git a/src/database/mod.rs b/src/database/mod.rs index 28aff71..db77a9a 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -2,11 +2,11 @@ use std::fmt; +use serde::{de::DeserializeOwned, Serialize}; + #[cfg(test)] use mockall::automock; -use crate::collection::Collection; - pub mod json; /// Error type for database calls. @@ -39,8 +39,8 @@ impl From for Error { #[cfg_attr(test, automock)] pub trait Database { /// Read collection from the database. - fn read(&self, collection: &mut Collection) -> Result<(), Error>; + fn read(&self, collection: &mut D) -> Result<(), Error>; /// Write collection to the database. - fn write(&mut self, collection: &Collection) -> Result<(), Error>; + fn write(&mut self, collection: &S) -> Result<(), Error>; } diff --git a/src/main.rs b/src/main.rs index b927ddd..88a8d8c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,9 +45,7 @@ fn main() { BeetsLibraryCommandExecutor::default().config(opt.beets_config_file_path.as_deref()), ); - let database = JsonDatabase::new(Box::new(JsonDatabaseFileBackend::new( - &opt.database_file_path, - ))); + let database = JsonDatabase::new(JsonDatabaseFileBackend::new(&opt.database_file_path)); let collection_manager = MhCollectionManager::new(beets, database); diff --git a/tests/database/json.rs b/tests/database/json.rs index e28d870..d39cb7e 100644 --- a/tests/database/json.rs +++ b/tests/database/json.rs @@ -20,7 +20,7 @@ fn write() { let file = NamedTempFile::new().unwrap(); let backend = JsonDatabaseFileBackend::new(file.path()); - let mut database = JsonDatabase::new(Box::new(backend)); + let mut database = JsonDatabase::new(backend); let write_data = COLLECTION.to_owned(); database.write(&write_data).unwrap(); @@ -34,7 +34,7 @@ fn write() { #[test] fn read() { let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE); - let database = JsonDatabase::new(Box::new(backend)); + let database = JsonDatabase::new(backend); let mut read_data: Vec = vec![]; database.read(&mut read_data).unwrap(); @@ -48,7 +48,7 @@ fn reverse() { let file = NamedTempFile::new().unwrap(); let backend = JsonDatabaseFileBackend::new(file.path()); - let mut database = JsonDatabase::new(Box::new(backend)); + let mut database = JsonDatabase::new(backend); let write_data = COLLECTION.to_owned(); database.write(&write_data).unwrap(); -- 2.45.2 From f58ffb036a25a93a37fc5d5b082116a4f0ee3e29 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 13 Apr 2023 15:08:20 +0200 Subject: [PATCH 6/7] fix --- src/collection/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/collection/mod.rs b/src/collection/mod.rs index cbc3fe9..14e6240 100644 --- a/src/collection/mod.rs +++ b/src/collection/mod.rs @@ -56,15 +56,15 @@ pub trait CollectionManager { /// The collection manager. It is responsible for pulling information from both the library and the /// database, ensuring its consistent and writing back any changes. -pub struct MhCollectionManager { - library: L, - database: D, +pub struct MhCollectionManager { + library: LIB, + database: DB, collection: Collection, } -impl MhCollectionManager { +impl MhCollectionManager { /// Create a new [`CollectionManager`] with the provided [`Library`] and [`Database`]. - pub fn new(library: L, database: D) -> Self { + pub fn new(library: LIB, database: DB) -> Self { MhCollectionManager { library, database, @@ -73,7 +73,7 @@ impl MhCollectionManager { } } -impl CollectionManager for MhCollectionManager { +impl CollectionManager for MhCollectionManager { fn rescan_library(&mut self) -> Result<(), Error> { self.collection = self.library.list(&Query::default())?; Ok(()) -- 2.45.2 From d8f1db4e4254d760ef2bbd06f31b3f2018027074 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 13 Apr 2023 15:27:00 +0200 Subject: [PATCH 7/7] Improve generics in TUI --- src/main.rs | 4 +- src/tui/app.rs | 173 ++++++++++++++++++++++++++++----------------- src/tui/handler.rs | 17 +++-- src/tui/mod.rs | 50 +++++++------ src/tui/ui.rs | 23 +++--- 5 files changed, 158 insertions(+), 109 deletions(-) diff --git a/src/main.rs b/src/main.rs index 88a8d8c..cc241d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use musichoard::{ mod tui; use tui::{ - app::App, event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, ui::Ui, + app::TuiApp, event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, ui::Ui, Tui, }; @@ -59,7 +59,7 @@ fn main() { let ui = Ui::new(); - let app = App::new(collection_manager).expect("failed to initialise app"); + let app = TuiApp::new(collection_manager).expect("failed to initialise app"); // Run the TUI application. Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui"); diff --git a/src/tui/app.rs b/src/tui/app.rs index f3163c5..2609c9e 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -110,31 +110,74 @@ struct Selection { artist: Option, } -pub struct App { +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 App { +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(App { + Ok(TuiApp { collection_manager, selection, running: true, }) } +} - pub fn is_running(&self) -> bool { +impl App for TuiApp { + fn is_running(&self) -> bool { self.running } - pub fn increment_category(&mut self) { + 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, @@ -142,7 +185,7 @@ impl App { }; } - pub fn decrement_category(&mut self) { + fn decrement_category(&mut self) { self.selection.active = match self.selection.active { Category::Artist => Category::Artist, Category::Album => Category::Artist, @@ -150,7 +193,7 @@ impl App { }; } - pub fn increment_selection(&mut self) { + fn increment_selection(&mut self) { match self.selection.active { Category::Artist => self.increment_artist_selection(), Category::Album => self.increment_album_selection(), @@ -158,7 +201,7 @@ impl App { } } - pub fn decrement_selection(&mut self) { + fn decrement_selection(&mut self) { match self.selection.active { Category::Artist => self.decrement_artist_selection(), Category::Album => self.decrement_album_selection(), @@ -166,6 +209,57 @@ impl App { } } + 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(); @@ -226,10 +320,6 @@ impl App { } } - pub fn get_active_category(&self) -> Category { - self.selection.active - } - fn get_artists(&self) -> &Vec { self.collection_manager.get_collection() } @@ -254,55 +344,6 @@ impl App { None } } - - pub fn get_artist_ids(&self) -> Vec<&ArtistId> { - let artists = self.get_artists(); - artists.iter().map(|a| &a.id).collect() - } - - pub fn get_album_ids(&self) -> Vec<&AlbumId> { - if let Some(albums) = self.get_albums() { - albums.iter().map(|a| &a.id).collect() - } else { - vec![] - } - } - - pub fn get_track_ids(&self) -> Vec<&Track> { - if let Some(tracks) = self.get_tracks() { - tracks.iter().collect() - } else { - vec![] - } - } - - pub fn selected_artist(&self) -> Option { - self.selection.artist.as_ref().map(|s| s.index) - } - - pub 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 - } - } - - pub 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 - } - } - - pub fn quit(&mut self) { - self.running = false; - } } #[cfg(test)] @@ -516,7 +557,7 @@ mod tests { .expect_get_collection() .return_const(COLLECTION.to_owned()); - let mut app = App::new(collection_manager).unwrap(); + let mut app = TuiApp::new(collection_manager).unwrap(); assert!(app.is_running()); app.quit(); @@ -535,7 +576,7 @@ mod tests { .expect_get_collection() .return_const(COLLECTION.to_owned()); - let mut app = App::new(collection_manager).unwrap(); + let mut app = TuiApp::new(collection_manager).unwrap(); assert!(app.is_running()); assert!(!app.get_artist_ids().is_empty()); @@ -640,7 +681,7 @@ mod tests { .expect_get_collection() .return_const(collection); - let mut app = App::new(collection_manager).unwrap(); + let mut app = TuiApp::new(collection_manager).unwrap(); assert!(app.is_running()); assert!(!app.get_artist_ids().is_empty()); @@ -682,7 +723,7 @@ mod tests { .expect_get_collection() .return_const(collection); - let mut app = App::new(collection_manager).unwrap(); + let mut app = TuiApp::new(collection_manager).unwrap(); assert!(app.is_running()); assert!(!app.get_artist_ids().is_empty()); @@ -736,7 +777,7 @@ mod tests { .expect_get_collection() .return_const(collection); - let mut app = App::new(collection_manager).unwrap(); + let mut app = TuiApp::new(collection_manager).unwrap(); assert!(app.is_running()); assert!(app.get_artist_ids().is_empty()); diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 01db45c..fecc501 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -2,7 +2,6 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; #[cfg(test)] use mockall::automock; -use musichoard::collection::CollectionManager; use super::{ app::App, @@ -10,12 +9,12 @@ use super::{ }; #[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, app: &mut APP) -> Result<(), EventError>; } -trait EventHandlerPrivate { - fn handle_key_event(app: &mut App, key_event: KeyEvent); +trait EventHandlerPrivate { + fn handle_key_event(app: &mut APP, key_event: KeyEvent); } pub struct TuiEventHandler { @@ -29,8 +28,8 @@ 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, app: &mut APP) -> Result<(), EventError> { match self.events.recv()? { Event::Key(key_event) => Self::handle_key_event(app, key_event), Event::Mouse(_) => {} @@ -40,8 +39,8 @@ 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(app: &mut APP, key_event: KeyEvent) { match key_event.code { // Exit application on `ESC` or `q`. KeyCode::Esc | KeyCode::Char('q') => { diff --git a/src/tui/mod.rs b/src/tui/mod.rs index ce2b5d7..da87d7b 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -1,6 +1,6 @@ use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; -use musichoard::collection::{self, CollectionManager}; +use musichoard::collection; use ratatui::backend::Backend; use ratatui::Terminal; use std::io; @@ -44,12 +44,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()?; @@ -68,9 +68,9 @@ impl Tui { fn main_loop( &mut self, - mut app: App, - ui: Ui, - handler: impl EventHandler, + mut app: APP, + ui: Ui, + handler: impl EventHandler, ) -> Result<(), Error> { while app.is_running() { self.terminal.draw(|frame| ui.render(&app, frame))?; @@ -82,9 +82,9 @@ impl Tui { fn main( term: Terminal, - app: App, - ui: Ui, - handler: impl EventHandler, + app: APP, + ui: Ui, + handler: impl EventHandler, listener: impl EventListener, ) -> Result<(), Error> { let mut tui = Tui { @@ -142,9 +142,9 @@ impl Tui { pub fn run( term: Terminal, - app: App, - ui: Ui, - handler: impl EventHandler, + app: APP, + ui: Ui, + handler: impl EventHandler, listener: impl EventListener, ) -> Result<(), Error> { Self::enable()?; @@ -175,8 +175,12 @@ mod tests { use crate::tests::{MockCollectionManager, COLLECTION}; use super::{ - app::App, event::EventError, handler::MockEventHandler, listener::MockEventListener, - ui::Ui, Error, Tui, + app::{App, TuiApp}, + event::EventError, + handler::MockEventHandler, + listener::MockEventListener, + ui::Ui, + Error, Tui, }; pub fn terminal() -> Terminal { @@ -184,7 +188,7 @@ mod tests { Terminal::new(backend).unwrap() } - pub fn app(collection: Collection) -> App { + pub fn app(collection: Collection) -> TuiApp { let mut collection_manager = MockCollectionManager::new(); collection_manager @@ -194,7 +198,7 @@ mod tests { .expect_get_collection() .return_const(collection); - App::new(collection_manager).unwrap() + TuiApp::new(collection_manager).unwrap() } fn listener() -> MockEventListener { @@ -208,12 +212,14 @@ mod tests { listener } - fn handler() -> MockEventHandler { + fn handler() -> MockEventHandler> { let mut handler = MockEventHandler::new(); - handler.expect_handle_next_event().return_once(|app| { - app.quit(); - Ok(()) - }); + handler.expect_handle_next_event().return_once( + |app: &mut TuiApp| { + app.quit(); + Ok(()) + }, + ); handler } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 1e36c26..47288e3 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use musichoard::{collection::CollectionManager, TrackFormat}; +use musichoard::TrackFormat; use ratatui::{ backend::Backend, layout::{Alignment, Rect}, @@ -59,11 +59,11 @@ struct AppState<'a> { tracks: TrackState<'a>, } -pub struct Ui { - _phantom: PhantomData, +pub struct Ui { + _phantom: PhantomData, } -impl Ui { +impl Ui { pub fn new() -> Self { Ui { _phantom: PhantomData, @@ -127,7 +127,7 @@ impl Ui { } } - fn construct_artist_list(app: &App) -> ArtistState { + fn construct_artist_list(app: &APP) -> ArtistState { let artists = app.get_artist_ids(); let list = List::new( artists @@ -149,7 +149,7 @@ impl Ui { } } - fn construct_album_list(app: &App) -> AlbumState { + fn construct_album_list(app: &APP) -> AlbumState { let albums = app.get_album_ids(); let list = List::new( albums @@ -182,7 +182,7 @@ impl Ui { } } - fn construct_track_list(app: &App) -> TrackState { + fn construct_track_list(app: &APP) -> TrackState { let tracks = app.get_track_ids(); let list = List::new( tracks @@ -226,7 +226,7 @@ impl Ui { } } - fn construct_app_state(app: &App) -> AppState { + fn construct_app_state(app: &APP) -> AppState { AppState { artists: Self::construct_artist_list(app), albums: Self::construct_album_list(app), @@ -318,7 +318,7 @@ impl Ui { Self::render_info_widget("Track info", state.info, state.active, area.info, frame); } - pub fn render(&self, app: &App, frame: &mut Frame<'_, B>) { + 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); @@ -334,7 +334,10 @@ mod tests { use crate::{ tests::COLLECTION, - tui::tests::{app, terminal}, + tui::{ + app::App, + tests::{app, terminal}, + }, }; use super::Ui; -- 2.45.2