Replace as many Box<dyn Trait> with generics as possible (#32)
Closes #31 Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/32
This commit is contained in:
parent
14a0567fa1
commit
3e6c95d188
@ -56,18 +56,15 @@ pub trait CollectionManager {
|
|||||||
|
|
||||||
/// The collection manager. It is responsible for pulling information from both the library and the
|
/// The collection manager. It is responsible for pulling information from both the library and the
|
||||||
/// database, ensuring its consistent and writing back any changes.
|
/// database, ensuring its consistent and writing back any changes.
|
||||||
pub struct MhCollectionManager {
|
pub struct MhCollectionManager<LIB, DB> {
|
||||||
library: Box<dyn Library + Send + Sync>,
|
library: LIB,
|
||||||
database: Box<dyn Database + Send + Sync>,
|
database: DB,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MhCollectionManager {
|
impl<LIB: Library, DB: Database> MhCollectionManager<LIB, DB> {
|
||||||
/// Create a new [`CollectionManager`] with the provided [`Library`] and [`Database`].
|
/// Create a new [`CollectionManager`] with the provided [`Library`] and [`Database`].
|
||||||
pub fn new(
|
pub fn new(library: LIB, database: DB) -> Self {
|
||||||
library: Box<dyn Library + Send + Sync>,
|
|
||||||
database: Box<dyn Database + Send + Sync>,
|
|
||||||
) -> Self {
|
|
||||||
MhCollectionManager {
|
MhCollectionManager {
|
||||||
library,
|
library,
|
||||||
database,
|
database,
|
||||||
@ -76,7 +73,7 @@ impl MhCollectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CollectionManager for MhCollectionManager {
|
impl<LIB: Library, DB: Database> CollectionManager for MhCollectionManager<LIB, DB> {
|
||||||
fn rescan_library(&mut self) -> Result<(), Error> {
|
fn rescan_library(&mut self) -> Result<(), Error> {
|
||||||
self.collection = self.library.list(&Query::default())?;
|
self.collection = self.library.list(&Query::default())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -97,6 +94,7 @@ mod tests {
|
|||||||
use mockall::predicate;
|
use mockall::predicate;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
collection::Collection,
|
||||||
database::{self, MockDatabase},
|
database::{self, MockDatabase},
|
||||||
library::{self, MockLibrary, Query},
|
library::{self, MockLibrary, Query},
|
||||||
tests::COLLECTION,
|
tests::COLLECTION,
|
||||||
@ -125,10 +123,9 @@ mod tests {
|
|||||||
.expect_write()
|
.expect_write()
|
||||||
.with(predicate::eq(database_input))
|
.with(predicate::eq(database_input))
|
||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| database_result);
|
.return_once(|_: &Collection| database_result);
|
||||||
|
|
||||||
let mut collection_manager =
|
let mut collection_manager = MhCollectionManager::new(library, database);
|
||||||
MhCollectionManager::new(Box::new(library), Box::new(database));
|
|
||||||
|
|
||||||
collection_manager.rescan_library().unwrap();
|
collection_manager.rescan_library().unwrap();
|
||||||
assert_eq!(collection_manager.get_collection(), &*COLLECTION);
|
assert_eq!(collection_manager.get_collection(), &*COLLECTION);
|
||||||
@ -147,8 +144,7 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| library_result);
|
.return_once(|_| library_result);
|
||||||
|
|
||||||
let mut collection_manager =
|
let mut collection_manager = MhCollectionManager::new(library, database);
|
||||||
MhCollectionManager::new(Box::new(library), Box::new(database));
|
|
||||||
|
|
||||||
let actual_err = collection_manager.rescan_library().unwrap_err();
|
let actual_err = collection_manager.rescan_library().unwrap_err();
|
||||||
let expected_err = Error::LibraryError(
|
let expected_err = Error::LibraryError(
|
||||||
@ -169,10 +165,9 @@ mod tests {
|
|||||||
database
|
database
|
||||||
.expect_write()
|
.expect_write()
|
||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| database_result);
|
.return_once(|_: &Collection| database_result);
|
||||||
|
|
||||||
let mut collection_manager =
|
let mut collection_manager = MhCollectionManager::new(library, database);
|
||||||
MhCollectionManager::new(Box::new(library), Box::new(database));
|
|
||||||
|
|
||||||
let actual_err = collection_manager.save_to_database().unwrap_err();
|
let actual_err = collection_manager.save_to_database().unwrap_err();
|
||||||
let expected_err =
|
let expected_err =
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::collection::Collection;
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
@ -27,25 +28,25 @@ pub trait JsonDatabaseBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// JSON database.
|
/// JSON database.
|
||||||
pub struct JsonDatabase {
|
pub struct JsonDatabase<JDB> {
|
||||||
backend: Box<dyn JsonDatabaseBackend + Send + Sync>,
|
backend: JDB,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JsonDatabase {
|
impl<JDB: JsonDatabaseBackend> JsonDatabase<JDB> {
|
||||||
/// Create a new JSON database with the provided backend, e.g. [`JsonDatabaseFileBackend`].
|
/// Create a new JSON database with the provided backend, e.g. [`JsonDatabaseFileBackend`].
|
||||||
pub fn new(backend: Box<dyn JsonDatabaseBackend + Send + Sync>) -> Self {
|
pub fn new(backend: JDB) -> Self {
|
||||||
JsonDatabase { backend }
|
JsonDatabase { backend }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Database for JsonDatabase {
|
impl<JDB: JsonDatabaseBackend> Database for JsonDatabase<JDB> {
|
||||||
fn read(&self, collection: &mut Collection) -> Result<(), Error> {
|
fn read<D: DeserializeOwned>(&self, collection: &mut D) -> Result<(), Error> {
|
||||||
let serialized = self.backend.read()?;
|
let serialized = self.backend.read()?;
|
||||||
*collection = serde_json::from_str(&serialized)?;
|
*collection = serde_json::from_str(&serialized)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write(&mut self, collection: &Collection) -> Result<(), Error> {
|
fn write<S: Serialize>(&mut self, collection: &S) -> Result<(), Error> {
|
||||||
let serialized = serde_json::to_string(&collection)?;
|
let serialized = serde_json::to_string(&collection)?;
|
||||||
self.backend.write(&serialized)?;
|
self.backend.write(&serialized)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -162,9 +163,7 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| Ok(()));
|
.return_once(|_| Ok(()));
|
||||||
|
|
||||||
JsonDatabase::new(Box::new(backend))
|
JsonDatabase::new(backend).write(&write_data).unwrap();
|
||||||
.write(&write_data)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -176,9 +175,7 @@ mod tests {
|
|||||||
backend.expect_read().times(1).return_once(|| result);
|
backend.expect_read().times(1).return_once(|| result);
|
||||||
|
|
||||||
let mut read_data: Vec<Artist> = vec![];
|
let mut read_data: Vec<Artist> = vec![];
|
||||||
JsonDatabase::new(Box::new(backend))
|
JsonDatabase::new(backend).read(&mut read_data).unwrap();
|
||||||
.read(&mut read_data)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(read_data, expected);
|
assert_eq!(read_data, expected);
|
||||||
}
|
}
|
||||||
@ -197,7 +194,7 @@ mod tests {
|
|||||||
.return_once(|_| Ok(()));
|
.return_once(|_| Ok(()));
|
||||||
backend.expect_read().times(1).return_once(|| result);
|
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 write_data = COLLECTION.to_owned();
|
||||||
let mut read_data: Vec<Artist> = vec![];
|
let mut read_data: Vec<Artist> = vec![];
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
use crate::collection::Collection;
|
|
||||||
|
|
||||||
pub mod json;
|
pub mod json;
|
||||||
|
|
||||||
/// Error type for database calls.
|
/// Error type for database calls.
|
||||||
@ -39,8 +39,8 @@ impl From<std::io::Error> for Error {
|
|||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait Database {
|
pub trait Database {
|
||||||
/// Read collection from the database.
|
/// Read collection from the database.
|
||||||
fn read(&self, collection: &mut Collection) -> Result<(), Error>;
|
fn read<D: DeserializeOwned + 'static>(&self, collection: &mut D) -> Result<(), Error>;
|
||||||
|
|
||||||
/// Write collection to the database.
|
/// Write collection to the database.
|
||||||
fn write(&mut self, collection: &Collection) -> Result<(), Error>;
|
fn write<S: Serialize + 'static>(&mut self, collection: &S) -> Result<(), Error>;
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,33 @@ use crate::{Album, AlbumId, Artist, ArtistId, Track, TrackFormat};
|
|||||||
|
|
||||||
use super::{Error, Library, Query, QueryOption};
|
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 {
|
trait QueryOptionArgBeets {
|
||||||
fn to_arg(&self, option_name: &str) -> Option<String>;
|
fn to_arg(&self, option_name: &str) -> Option<String>;
|
||||||
}
|
}
|
||||||
@ -94,29 +121,23 @@ pub trait BeetsLibraryExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Beets library.
|
/// Beets library.
|
||||||
pub struct BeetsLibrary {
|
pub struct BeetsLibrary<BLE> {
|
||||||
executor: Box<dyn BeetsLibraryExecutor + Send + Sync>,
|
executor: BLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
trait LibraryPrivate {
|
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<String>;
|
fn list_cmd_and_args(query: &Query) -> Vec<String>;
|
||||||
fn list_to_artists(list_output: Vec<String>) -> Result<Vec<Artist>, Error>;
|
fn list_to_artists(list_output: Vec<String>) -> Result<Vec<Artist>, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BeetsLibrary {
|
impl<BLE: BeetsLibraryExecutor> BeetsLibrary<BLE> {
|
||||||
/// Create a new beets library with the provided executor, e.g. [`BeetsLibraryCommandExecutor`].
|
/// Create a new beets library with the provided executor, e.g. [`BeetsLibraryCommandExecutor`].
|
||||||
pub fn new(executor: Box<dyn BeetsLibraryExecutor + Send + Sync>) -> BeetsLibrary {
|
pub fn new(executor: BLE) -> Self {
|
||||||
BeetsLibrary { executor }
|
BeetsLibrary { executor }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Library for BeetsLibrary {
|
impl<BLE: BeetsLibraryExecutor> Library for BeetsLibrary<BLE> {
|
||||||
fn list(&mut self, query: &Query) -> Result<Vec<Artist>, Error> {
|
fn list(&mut self, query: &Query) -> Result<Vec<Artist>, Error> {
|
||||||
let cmd = Self::list_cmd_and_args(query);
|
let cmd = Self::list_cmd_and_args(query);
|
||||||
let output = self.executor.exec(&cmd)?;
|
let output = self.executor.exec(&cmd)?;
|
||||||
@ -124,37 +145,10 @@ impl Library for BeetsLibrary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! list_format_separator {
|
impl<BLE: BeetsLibraryExecutor> LibraryPrivate for BeetsLibrary<BLE> {
|
||||||
() => {
|
|
||||||
" -*^- "
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
fn list_cmd_and_args(query: &Query) -> Vec<String> {
|
fn list_cmd_and_args(query: &Query) -> Vec<String> {
|
||||||
let mut cmd: Vec<String> = vec![String::from(Self::CMD_LIST)];
|
let mut cmd: Vec<String> = vec![String::from(CMD_LIST)];
|
||||||
cmd.push(Self::LIST_FORMAT_ARG.to_string());
|
cmd.push(LIST_FORMAT_ARG.to_string());
|
||||||
cmd.append(&mut query.to_args());
|
cmd.append(&mut query.to_args());
|
||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
@ -168,7 +162,7 @@ impl LibraryPrivate for BeetsLibrary {
|
|||||||
continue;
|
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 {
|
if split.len() != 7 {
|
||||||
return Err(Error::InvalidData(line.to_string()));
|
return Err(Error::InvalidData(line.to_string()));
|
||||||
}
|
}
|
||||||
@ -193,8 +187,8 @@ impl LibraryPrivate for BeetsLibrary {
|
|||||||
title: track_title,
|
title: track_title,
|
||||||
artist: track_artist.split("; ").map(|s| s.to_owned()).collect(),
|
artist: track_artist.split("; ").map(|s| s.to_owned()).collect(),
|
||||||
format: match track_format.as_ref() {
|
format: match track_format.as_ref() {
|
||||||
Self::TRACK_FORMAT_FLAC => TrackFormat::Flac,
|
TRACK_FORMAT_FLAC => TrackFormat::Flac,
|
||||||
Self::TRACK_FORMAT_MP3 => TrackFormat::Mp3,
|
TRACK_FORMAT_MP3 => TrackFormat::Mp3,
|
||||||
_ => return Err(Error::InvalidData(track_format)),
|
_ => return Err(Error::InvalidData(track_format)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -309,14 +303,14 @@ mod tests {
|
|||||||
let track_title = &track.title;
|
let track_title = &track.title;
|
||||||
let track_artist = &track.artist.join("; ");
|
let track_artist = &track.artist.join("; ");
|
||||||
let track_format = match track.format {
|
let track_format = match track.format {
|
||||||
TrackFormat::Flac => BeetsLibrary::TRACK_FORMAT_FLAC,
|
TrackFormat::Flac => TRACK_FORMAT_FLAC,
|
||||||
TrackFormat::Mp3 => BeetsLibrary::TRACK_FORMAT_MP3,
|
TrackFormat::Mp3 => TRACK_FORMAT_MP3,
|
||||||
};
|
};
|
||||||
|
|
||||||
strings.push(format!(
|
strings.push(format!(
|
||||||
"{album_artist}{0}{album_year}{0}{album_title}{0}\
|
"{album_artist}{0}{album_year}{0}{album_title}{0}\
|
||||||
{track_number}{0}{track_title}{0}{track_artist}{0}{track_format}",
|
{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]
|
#[test]
|
||||||
fn test_list_empty() {
|
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 result = Ok(vec![]);
|
||||||
|
|
||||||
let mut executor = MockBeetsLibraryExecutor::new();
|
let mut executor = MockBeetsLibraryExecutor::new();
|
||||||
@ -366,7 +360,7 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| result);
|
.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 output = beets.list(&Query::default()).unwrap();
|
||||||
|
|
||||||
let expected: Vec<Artist> = vec![];
|
let expected: Vec<Artist> = vec![];
|
||||||
@ -375,7 +369,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_list_ordered() {
|
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 expected = COLLECTION.to_owned();
|
||||||
let result = Ok(artists_to_beets_string(&expected));
|
let result = Ok(artists_to_beets_string(&expected));
|
||||||
|
|
||||||
@ -386,7 +380,7 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| result);
|
.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 output = beets.list(&Query::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(output, expected);
|
assert_eq!(output, expected);
|
||||||
@ -394,7 +388,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_list_unordered() {
|
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 expected = COLLECTION.to_owned();
|
||||||
let mut output = artists_to_beets_string(&expected);
|
let mut output = artists_to_beets_string(&expected);
|
||||||
let last = output.len() - 1;
|
let last = output.len() - 1;
|
||||||
@ -421,7 +415,7 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| result);
|
.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 output = beets.list(&Query::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(output, expected);
|
assert_eq!(output, expected);
|
||||||
@ -429,7 +423,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_list_album_title_year_clash() {
|
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();
|
let mut expected = COLLECTION.to_owned();
|
||||||
expected[0].albums[0].id.year = expected[1].albums[0].id.year;
|
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();
|
expected[0].albums[0].id.title = expected[1].albums[0].id.title.clone();
|
||||||
@ -443,7 +437,7 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| result);
|
.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 output = beets.list(&Query::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(output, expected);
|
assert_eq!(output, expected);
|
||||||
@ -458,7 +452,7 @@ mod tests {
|
|||||||
|
|
||||||
let arguments = vec![
|
let arguments = vec![
|
||||||
"ls".to_string(),
|
"ls".to_string(),
|
||||||
BeetsLibrary::LIST_FORMAT_ARG.to_string(),
|
LIST_FORMAT_ARG.to_string(),
|
||||||
String::from("^album:some.album"),
|
String::from("^album:some.album"),
|
||||||
String::from("track:5"),
|
String::from("track:5"),
|
||||||
String::from("artist:some.artist"),
|
String::from("artist:some.artist"),
|
||||||
@ -472,7 +466,7 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| result);
|
.return_once(|_| result);
|
||||||
|
|
||||||
let mut beets = BeetsLibrary::new(Box::new(executor));
|
let mut beets = BeetsLibrary::new(executor);
|
||||||
let output = beets.list(&query).unwrap();
|
let output = beets.list(&query).unwrap();
|
||||||
|
|
||||||
let expected: Vec<Artist> = vec![];
|
let expected: Vec<Artist> = vec![];
|
||||||
|
14
src/main.rs
14
src/main.rs
@ -13,7 +13,7 @@ use musichoard::{
|
|||||||
|
|
||||||
mod tui;
|
mod tui;
|
||||||
use tui::{
|
use tui::{
|
||||||
app::App, event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, ui::Ui,
|
app::TuiApp, event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, ui::Ui,
|
||||||
Tui,
|
Tui,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,15 +41,13 @@ fn main() {
|
|||||||
// Create the application.
|
// Create the application.
|
||||||
let opt = Opt::from_args();
|
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()),
|
BeetsLibraryCommandExecutor::default().config(opt.beets_config_file_path.as_deref()),
|
||||||
));
|
);
|
||||||
|
|
||||||
let database = JsonDatabase::new(Box::new(JsonDatabaseFileBackend::new(
|
let database = JsonDatabase::new(JsonDatabaseFileBackend::new(&opt.database_file_path));
|
||||||
&opt.database_file_path,
|
|
||||||
)));
|
|
||||||
|
|
||||||
let collection_manager = MhCollectionManager::new(Box::new(beets), Box::new(database));
|
let collection_manager = MhCollectionManager::new(beets, database);
|
||||||
|
|
||||||
// Initialize the terminal user interface.
|
// Initialize the terminal user interface.
|
||||||
let backend = CrosstermBackend::new(io::stdout());
|
let backend = CrosstermBackend::new(io::stdout());
|
||||||
@ -61,7 +59,7 @@ fn main() {
|
|||||||
|
|
||||||
let ui = Ui::new();
|
let ui = Ui::new();
|
||||||
|
|
||||||
let app = App::new(Box::new(collection_manager)).expect("failed to initialise app");
|
let app = TuiApp::new(collection_manager).expect("failed to initialise app");
|
||||||
|
|
||||||
// Run the TUI application.
|
// Run the TUI application.
|
||||||
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
|
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
|
||||||
|
177
src/tui/app.rs
177
src/tui/app.rs
@ -110,31 +110,74 @@ struct Selection {
|
|||||||
artist: Option<ArtistSelection>,
|
artist: Option<ArtistSelection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub trait App {
|
||||||
collection_manager: Box<dyn CollectionManager>,
|
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<usize>;
|
||||||
|
fn selected_album(&self) -> Option<usize>;
|
||||||
|
fn selected_track(&self) -> Option<usize>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Artist>;
|
||||||
|
fn get_albums(&self) -> Option<&Vec<Album>>;
|
||||||
|
fn get_tracks(&self) -> Option<&Vec<Track>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TuiApp<CM> {
|
||||||
|
collection_manager: CM,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
running: bool,
|
running: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl<CM: CollectionManager> TuiApp<CM> {
|
||||||
pub fn new(mut collection_manager: Box<dyn CollectionManager>) -> Result<Self, Error> {
|
pub fn new(mut collection_manager: CM) -> Result<Self, Error> {
|
||||||
collection_manager.rescan_library()?;
|
collection_manager.rescan_library()?;
|
||||||
let selection = Selection {
|
let selection = Selection {
|
||||||
active: Category::Artist,
|
active: Category::Artist,
|
||||||
artist: ArtistSelection::initialise(collection_manager.get_collection()),
|
artist: ArtistSelection::initialise(collection_manager.get_collection()),
|
||||||
};
|
};
|
||||||
Ok(App {
|
Ok(TuiApp {
|
||||||
collection_manager,
|
collection_manager,
|
||||||
selection,
|
selection,
|
||||||
running: true,
|
running: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_running(&self) -> bool {
|
impl<CM: CollectionManager> App for TuiApp<CM> {
|
||||||
|
fn is_running(&self) -> bool {
|
||||||
self.running
|
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 {
|
self.selection.active = match self.selection.active {
|
||||||
Category::Artist => Category::Album,
|
Category::Artist => Category::Album,
|
||||||
Category::Album => Category::Track,
|
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 {
|
self.selection.active = match self.selection.active {
|
||||||
Category::Artist => Category::Artist,
|
Category::Artist => Category::Artist,
|
||||||
Category::Album => 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 {
|
match self.selection.active {
|
||||||
Category::Artist => self.increment_artist_selection(),
|
Category::Artist => self.increment_artist_selection(),
|
||||||
Category::Album => self.increment_album_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 {
|
match self.selection.active {
|
||||||
Category::Artist => self.decrement_artist_selection(),
|
Category::Artist => self.decrement_artist_selection(),
|
||||||
Category::Album => self.decrement_album_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<usize> {
|
||||||
|
self.selection.artist.as_ref().map(|s| s.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_album(&self) -> Option<usize> {
|
||||||
|
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<usize> {
|
||||||
|
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<CM: CollectionManager> AppPrivate for TuiApp<CM> {
|
||||||
fn increment_artist_selection(&mut self) {
|
fn increment_artist_selection(&mut self) {
|
||||||
if let Some(ref mut artist_selection) = self.selection.artist {
|
if let Some(ref mut artist_selection) = self.selection.artist {
|
||||||
let artists = &self.collection_manager.get_collection();
|
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<Artist> {
|
fn get_artists(&self) -> &Vec<Artist> {
|
||||||
self.collection_manager.get_collection()
|
self.collection_manager.get_collection()
|
||||||
}
|
}
|
||||||
@ -254,55 +344,6 @@ impl App {
|
|||||||
None
|
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<usize> {
|
|
||||||
self.selection.artist.as_ref().map(|s| s.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn selected_album(&self) -> Option<usize> {
|
|
||||||
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<usize> {
|
|
||||||
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)]
|
#[cfg(test)]
|
||||||
@ -516,7 +557,7 @@ mod tests {
|
|||||||
.expect_get_collection()
|
.expect_get_collection()
|
||||||
.return_const(COLLECTION.to_owned());
|
.return_const(COLLECTION.to_owned());
|
||||||
|
|
||||||
let mut app = App::new(Box::new(collection_manager)).unwrap();
|
let mut app = TuiApp::new(collection_manager).unwrap();
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
app.quit();
|
app.quit();
|
||||||
@ -535,7 +576,7 @@ mod tests {
|
|||||||
.expect_get_collection()
|
.expect_get_collection()
|
||||||
.return_const(COLLECTION.to_owned());
|
.return_const(COLLECTION.to_owned());
|
||||||
|
|
||||||
let mut app = App::new(Box::new(collection_manager)).unwrap();
|
let mut app = TuiApp::new(collection_manager).unwrap();
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
assert!(!app.get_artist_ids().is_empty());
|
assert!(!app.get_artist_ids().is_empty());
|
||||||
@ -640,7 +681,7 @@ mod tests {
|
|||||||
.expect_get_collection()
|
.expect_get_collection()
|
||||||
.return_const(collection);
|
.return_const(collection);
|
||||||
|
|
||||||
let mut app = App::new(Box::new(collection_manager)).unwrap();
|
let mut app = TuiApp::new(collection_manager).unwrap();
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
assert!(!app.get_artist_ids().is_empty());
|
assert!(!app.get_artist_ids().is_empty());
|
||||||
@ -682,7 +723,7 @@ mod tests {
|
|||||||
.expect_get_collection()
|
.expect_get_collection()
|
||||||
.return_const(collection);
|
.return_const(collection);
|
||||||
|
|
||||||
let mut app = App::new(Box::new(collection_manager)).unwrap();
|
let mut app = TuiApp::new(collection_manager).unwrap();
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
assert!(!app.get_artist_ids().is_empty());
|
assert!(!app.get_artist_ids().is_empty());
|
||||||
@ -736,7 +777,7 @@ mod tests {
|
|||||||
.expect_get_collection()
|
.expect_get_collection()
|
||||||
.return_const(collection);
|
.return_const(collection);
|
||||||
|
|
||||||
let mut app = App::new(Box::new(collection_manager)).unwrap();
|
let mut app = TuiApp::new(collection_manager).unwrap();
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
assert!(app.get_artist_ids().is_empty());
|
assert!(app.get_artist_ids().is_empty());
|
||||||
|
@ -9,12 +9,12 @@ use super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait EventHandler {
|
pub trait EventHandler<APP> {
|
||||||
fn handle_next_event(&self, app: &mut App) -> Result<(), EventError>;
|
fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
trait EventHandlerPrivate {
|
trait EventHandlerPrivate<APP> {
|
||||||
fn handle_key_event(app: &mut App, key_event: KeyEvent);
|
fn handle_key_event(app: &mut APP, key_event: KeyEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TuiEventHandler {
|
pub struct TuiEventHandler {
|
||||||
@ -28,8 +28,8 @@ impl TuiEventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventHandler for TuiEventHandler {
|
impl<APP: App> EventHandler<APP> for TuiEventHandler {
|
||||||
fn handle_next_event(&self, app: &mut App) -> Result<(), EventError> {
|
fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> {
|
||||||
match self.events.recv()? {
|
match self.events.recv()? {
|
||||||
Event::Key(key_event) => Self::handle_key_event(app, key_event),
|
Event::Key(key_event) => Self::handle_key_event(app, key_event),
|
||||||
Event::Mouse(_) => {}
|
Event::Mouse(_) => {}
|
||||||
@ -39,8 +39,8 @@ impl EventHandler for TuiEventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventHandlerPrivate for TuiEventHandler {
|
impl<APP: App> EventHandlerPrivate<APP> for TuiEventHandler {
|
||||||
fn handle_key_event(app: &mut App, key_event: KeyEvent) {
|
fn handle_key_event(app: &mut APP, key_event: KeyEvent) {
|
||||||
match key_event.code {
|
match key_event.code {
|
||||||
// Exit application on `ESC` or `q`.
|
// Exit application on `ESC` or `q`.
|
||||||
KeyCode::Esc | KeyCode::Char('q') => {
|
KeyCode::Esc | KeyCode::Char('q') => {
|
||||||
|
@ -4,6 +4,7 @@ use musichoard::collection;
|
|||||||
use ratatui::backend::Backend;
|
use ratatui::backend::Backend;
|
||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
@ -43,11 +44,12 @@ impl From<EventError> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Tui<B: Backend> {
|
pub struct Tui<B: Backend, APP> {
|
||||||
terminal: Terminal<B>,
|
terminal: Terminal<B>,
|
||||||
|
_phantom: PhantomData<APP>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: Backend> Tui<B> {
|
impl<B: Backend, APP: App> Tui<B, APP> {
|
||||||
fn init(&mut self) -> Result<(), Error> {
|
fn init(&mut self) -> Result<(), Error> {
|
||||||
self.terminal.hide_cursor()?;
|
self.terminal.hide_cursor()?;
|
||||||
self.terminal.clear()?;
|
self.terminal.clear()?;
|
||||||
@ -64,7 +66,12 @@ impl<B: Backend> Tui<B> {
|
|||||||
self.exit();
|
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<APP>,
|
||||||
|
handler: impl EventHandler<APP>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
while app.is_running() {
|
while app.is_running() {
|
||||||
self.terminal.draw(|frame| ui.render(&app, frame))?;
|
self.terminal.draw(|frame| ui.render(&app, frame))?;
|
||||||
handler.handle_next_event(&mut app)?;
|
handler.handle_next_event(&mut app)?;
|
||||||
@ -75,12 +82,15 @@ impl<B: Backend> Tui<B> {
|
|||||||
|
|
||||||
fn main(
|
fn main(
|
||||||
term: Terminal<B>,
|
term: Terminal<B>,
|
||||||
app: App,
|
app: APP,
|
||||||
ui: Ui,
|
ui: Ui<APP>,
|
||||||
handler: impl EventHandler,
|
handler: impl EventHandler<APP>,
|
||||||
listener: impl EventListener,
|
listener: impl EventListener,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut tui = Tui { terminal: term };
|
let mut tui = Tui {
|
||||||
|
terminal: term,
|
||||||
|
_phantom: PhantomData,
|
||||||
|
};
|
||||||
|
|
||||||
tui.init()?;
|
tui.init()?;
|
||||||
|
|
||||||
@ -132,9 +142,9 @@ impl<B: Backend> Tui<B> {
|
|||||||
|
|
||||||
pub fn run(
|
pub fn run(
|
||||||
term: Terminal<B>,
|
term: Terminal<B>,
|
||||||
app: App,
|
app: APP,
|
||||||
ui: Ui,
|
ui: Ui<APP>,
|
||||||
handler: impl EventHandler,
|
handler: impl EventHandler<APP>,
|
||||||
listener: impl EventListener,
|
listener: impl EventListener,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
Self::enable()?;
|
Self::enable()?;
|
||||||
@ -165,8 +175,12 @@ mod tests {
|
|||||||
use crate::tests::{MockCollectionManager, COLLECTION};
|
use crate::tests::{MockCollectionManager, COLLECTION};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
app::App, event::EventError, handler::MockEventHandler, listener::MockEventListener,
|
app::{App, TuiApp},
|
||||||
ui::Ui, Error, Tui,
|
event::EventError,
|
||||||
|
handler::MockEventHandler,
|
||||||
|
listener::MockEventListener,
|
||||||
|
ui::Ui,
|
||||||
|
Error, Tui,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn terminal() -> Terminal<TestBackend> {
|
pub fn terminal() -> Terminal<TestBackend> {
|
||||||
@ -174,7 +188,7 @@ mod tests {
|
|||||||
Terminal::new(backend).unwrap()
|
Terminal::new(backend).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app(collection: Collection) -> App {
|
pub fn app(collection: Collection) -> TuiApp<MockCollectionManager> {
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
let mut collection_manager = MockCollectionManager::new();
|
||||||
|
|
||||||
collection_manager
|
collection_manager
|
||||||
@ -184,7 +198,7 @@ mod tests {
|
|||||||
.expect_get_collection()
|
.expect_get_collection()
|
||||||
.return_const(collection);
|
.return_const(collection);
|
||||||
|
|
||||||
App::new(Box::new(collection_manager)).unwrap()
|
TuiApp::new(collection_manager).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn listener() -> MockEventListener {
|
fn listener() -> MockEventListener {
|
||||||
@ -198,12 +212,14 @@ mod tests {
|
|||||||
listener
|
listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handler() -> MockEventHandler {
|
fn handler() -> MockEventHandler<TuiApp<MockCollectionManager>> {
|
||||||
let mut handler = MockEventHandler::new();
|
let mut handler = MockEventHandler::new();
|
||||||
handler.expect_handle_next_event().return_once(|app| {
|
handler.expect_handle_next_event().return_once(
|
||||||
|
|app: &mut TuiApp<MockCollectionManager>| {
|
||||||
app.quit();
|
app.quit();
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
},
|
||||||
|
);
|
||||||
handler
|
handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use musichoard::TrackFormat;
|
use musichoard::TrackFormat;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
@ -57,11 +59,15 @@ struct AppState<'a> {
|
|||||||
tracks: TrackState<'a>,
|
tracks: TrackState<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Ui {}
|
pub struct Ui<APP> {
|
||||||
|
_phantom: PhantomData<APP>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Ui {
|
impl<APP: App> Ui<APP> {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Ui {}
|
Ui {
|
||||||
|
_phantom: PhantomData,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_areas(frame: Rect) -> FrameAreas {
|
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 artists = app.get_artist_ids();
|
||||||
let list = List::new(
|
let list = List::new(
|
||||||
artists
|
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 albums = app.get_album_ids();
|
||||||
let list = List::new(
|
let list = List::new(
|
||||||
albums
|
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 tracks = app.get_track_ids();
|
||||||
let list = List::new(
|
let list = List::new(
|
||||||
tracks
|
tracks
|
||||||
@ -220,7 +226,7 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_app_state(app: &App) -> AppState {
|
fn construct_app_state(app: &APP) -> AppState {
|
||||||
AppState {
|
AppState {
|
||||||
artists: Self::construct_artist_list(app),
|
artists: Self::construct_artist_list(app),
|
||||||
albums: Self::construct_album_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);
|
Self::render_info_widget("Track info", state.info, state.active, area.info, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render<B: Backend>(&self, app: &App, frame: &mut Frame<'_, B>) {
|
pub fn render<B: Backend>(&self, app: &APP, frame: &mut Frame<'_, B>) {
|
||||||
let areas = Self::construct_areas(frame.size());
|
let areas = Self::construct_areas(frame.size());
|
||||||
let app_state = Self::construct_app_state(app);
|
let app_state = Self::construct_app_state(app);
|
||||||
|
|
||||||
@ -328,7 +334,10 @@ mod tests {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
tests::COLLECTION,
|
tests::COLLECTION,
|
||||||
tui::tests::{app, terminal},
|
tui::{
|
||||||
|
app::App,
|
||||||
|
tests::{app, terminal},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Ui;
|
use super::Ui;
|
||||||
|
@ -20,7 +20,7 @@ fn write() {
|
|||||||
let file = NamedTempFile::new().unwrap();
|
let file = NamedTempFile::new().unwrap();
|
||||||
|
|
||||||
let backend = JsonDatabaseFileBackend::new(file.path());
|
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();
|
let write_data = COLLECTION.to_owned();
|
||||||
database.write(&write_data).unwrap();
|
database.write(&write_data).unwrap();
|
||||||
@ -34,7 +34,7 @@ fn write() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn read() {
|
fn read() {
|
||||||
let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE);
|
let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE);
|
||||||
let database = JsonDatabase::new(Box::new(backend));
|
let database = JsonDatabase::new(backend);
|
||||||
|
|
||||||
let mut read_data: Vec<Artist> = vec![];
|
let mut read_data: Vec<Artist> = vec![];
|
||||||
database.read(&mut read_data).unwrap();
|
database.read(&mut read_data).unwrap();
|
||||||
@ -48,7 +48,7 @@ fn reverse() {
|
|||||||
let file = NamedTempFile::new().unwrap();
|
let file = NamedTempFile::new().unwrap();
|
||||||
|
|
||||||
let backend = JsonDatabaseFileBackend::new(file.path());
|
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();
|
let write_data = COLLECTION.to_owned();
|
||||||
database.write(&write_data).unwrap();
|
database.write(&write_data).unwrap();
|
||||||
|
@ -15,18 +15,20 @@ use musichoard::{
|
|||||||
|
|
||||||
use crate::COLLECTION;
|
use crate::COLLECTION;
|
||||||
|
|
||||||
static BEETS_EMPTY_CONFIG: Lazy<Arc<Mutex<BeetsLibrary>>> = Lazy::new(|| {
|
static BEETS_EMPTY_CONFIG: Lazy<Arc<Mutex<BeetsLibrary<BeetsLibraryCommandExecutor>>>> =
|
||||||
Arc::new(Mutex::new(BeetsLibrary::new(Box::new(
|
Lazy::new(|| {
|
||||||
|
Arc::new(Mutex::new(BeetsLibrary::new(
|
||||||
BeetsLibraryCommandExecutor::default(),
|
BeetsLibraryCommandExecutor::default(),
|
||||||
))))
|
)))
|
||||||
});
|
});
|
||||||
|
|
||||||
static BEETS_TEST_CONFIG: Lazy<Arc<Mutex<BeetsLibrary>>> = Lazy::new(|| {
|
static BEETS_TEST_CONFIG: Lazy<Arc<Mutex<BeetsLibrary<BeetsLibraryCommandExecutor>>>> =
|
||||||
Arc::new(Mutex::new(BeetsLibrary::new(Box::new(
|
Lazy::new(|| {
|
||||||
|
Arc::new(Mutex::new(BeetsLibrary::new(
|
||||||
BeetsLibraryCommandExecutor::default().config(Some(
|
BeetsLibraryCommandExecutor::default().config(Some(
|
||||||
&fs::canonicalize("./tests/files/library/config.yml").unwrap(),
|
&fs::canonicalize("./tests/files/library/config.yml").unwrap(),
|
||||||
)),
|
)),
|
||||||
))))
|
)))
|
||||||
});
|
});
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
Loading…
Reference in New Issue
Block a user