Proof of principle working
This commit is contained in:
parent
7925d8ce91
commit
3f1845ed23
11
src/main.rs
11
src/main.rs
@ -79,18 +79,19 @@ fn with<Database: IDatabase + 'static, Library: ILibrary + 'static>(
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
let terminal = Terminal::new(backend).expect("failed to initialise terminal");
|
||||
|
||||
let channel = EventChannel::new();
|
||||
let listener = EventListener::new(channel.sender());
|
||||
let handler = EventHandler::new(channel.receiver());
|
||||
|
||||
let http =
|
||||
MusicBrainzHttp::new(MUSICHOARD_HTTP_USER_AGENT).expect("failed to initialise HTTP client");
|
||||
let client = MusicBrainzClient::new(http);
|
||||
let musicbrainz = MusicBrainz::new(client);
|
||||
|
||||
let app = App::new(music_hoard, musicbrainz);
|
||||
let channel = EventChannel::new();
|
||||
let listener = EventListener::new(channel.sender());
|
||||
|
||||
let app = App::new(music_hoard, musicbrainz, channel.sender());
|
||||
let ui = Ui;
|
||||
|
||||
let handler = EventHandler::new(channel.receiver());
|
||||
|
||||
// Run the TUI application.
|
||||
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
|
||||
}
|
||||
|
@ -39,6 +39,10 @@ impl IAppInteractError for AppMachine<AppError> {
|
||||
fn dismiss_error(self) -> Self::APP {
|
||||
AppMachine::browse(self.inner).into()
|
||||
}
|
||||
|
||||
fn no_op(self) -> Self::APP {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -1,5 +1,8 @@
|
||||
use std::{
|
||||
sync::{mpsc, Arc, Mutex},
|
||||
sync::{
|
||||
mpsc::{self, TryRecvError},
|
||||
Arc, Mutex,
|
||||
},
|
||||
thread, time,
|
||||
};
|
||||
|
||||
@ -11,10 +14,8 @@ use musichoard::collection::{
|
||||
|
||||
use crate::tui::{
|
||||
app::{
|
||||
machine::{App, AppInner, AppMachine},
|
||||
AppMatchesInfo, AppPublic, AppState, IAppInteractFetch,
|
||||
},
|
||||
lib::interface::musicbrainz::{Error as MbError, IMusicBrainz},
|
||||
machine::{App, AppInner, AppMachine}, AppMatchesInfo, AppPublic, AppState, IAppEventFetch, IAppInteractFetch
|
||||
}, event::{Event, EventSender}, lib::interface::musicbrainz::{Error as MbError, IMusicBrainz}
|
||||
};
|
||||
|
||||
use super::matches::AppMatches;
|
||||
@ -24,6 +25,10 @@ pub struct AppFetch {
|
||||
}
|
||||
|
||||
impl AppMachine<AppFetch> {
|
||||
fn fetch(inner: AppInner, state: AppFetch) -> Self {
|
||||
AppMachine { inner, state }
|
||||
}
|
||||
|
||||
pub fn app_fetch_new(inner: AppInner) -> App {
|
||||
let coll = inner.music_hoard.get_collection();
|
||||
let artist = match inner.selection.state_artist(coll) {
|
||||
@ -36,14 +41,16 @@ impl AppMachine<AppFetch> {
|
||||
match artist.meta.musicbrainz {
|
||||
Some(ref arid) => {
|
||||
let musicbrainz = Arc::clone(&inner.musicbrainz);
|
||||
let events = inner.events.clone();
|
||||
let arid = arid.mbid().clone();
|
||||
let albums = artist.albums.iter().map(|a| &a.meta).cloned().collect();
|
||||
thread::spawn(|| Self::fetch_albums(musicbrainz, fetch_tx, arid, albums));
|
||||
thread::spawn(|| Self::fetch_albums(musicbrainz, fetch_tx, events, arid, albums));
|
||||
}
|
||||
None => {
|
||||
let musicbrainz = Arc::clone(&inner.musicbrainz);
|
||||
let events = inner.events.clone();
|
||||
let artist = artist.meta.clone();
|
||||
thread::spawn(|| Self::fetch_artist(musicbrainz, fetch_tx, artist));
|
||||
thread::spawn(|| Self::fetch_artist(musicbrainz, fetch_tx, events, artist));
|
||||
}
|
||||
};
|
||||
|
||||
@ -52,23 +59,27 @@ impl AppMachine<AppFetch> {
|
||||
}
|
||||
|
||||
pub fn app_fetch_next(inner: AppInner, fetch: AppFetch, first: bool) -> App {
|
||||
match fetch.fetch_rx.recv() {
|
||||
match fetch.fetch_rx.try_recv() {
|
||||
Ok(fetch_result) => match fetch_result {
|
||||
Ok(mut next_match) => {
|
||||
next_match.push_cannot_have_mbid();
|
||||
let current = Some(next_match);
|
||||
AppMachine::matches(inner, AppMatches::new(current, fetch)).into()
|
||||
}
|
||||
Err(err) => AppMachine::error(inner, format!("fetch failed: {err}")).into(),
|
||||
},
|
||||
// only happens when the sender disconnects which means it finished its job
|
||||
Err(_) => {
|
||||
if first {
|
||||
AppMachine::matches(inner, AppMatches::empty(fetch)).into()
|
||||
} else {
|
||||
AppMachine::browse(inner).into()
|
||||
Err(fetch_err) => {
|
||||
AppMachine::error(inner, format!("fetch failed: {fetch_err}")).into()
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(try_recv_err) => match try_recv_err {
|
||||
TryRecvError::Disconnected => {
|
||||
if first {
|
||||
AppMachine::matches(inner, AppMatches::empty(fetch)).into()
|
||||
} else {
|
||||
AppMachine::browse(inner).into()
|
||||
}
|
||||
}
|
||||
TryRecvError::Empty => AppMachine::fetch(inner, fetch).into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -104,6 +115,14 @@ impl IAppInteractFetch for AppMachine<AppFetch> {
|
||||
}
|
||||
}
|
||||
|
||||
impl IAppEventFetch for AppMachine<AppFetch> {
|
||||
type APP = App;
|
||||
|
||||
fn fetch_result_ready(self) -> Self::APP {
|
||||
Self::app_fetch_next(self.inner, self.state, false)
|
||||
}
|
||||
}
|
||||
|
||||
type FetchError = MbError;
|
||||
type FetchResult = Result<AppMatchesInfo, FetchError>;
|
||||
type FetchSender = mpsc::Sender<FetchResult>;
|
||||
@ -113,11 +132,13 @@ trait IAppInteractFetchPrivate {
|
||||
fn fetch_artist(
|
||||
musicbrainz: Arc<Mutex<dyn IMusicBrainz + Send>>,
|
||||
fetch_tx: FetchSender,
|
||||
events: EventSender,
|
||||
artist: ArtistMeta,
|
||||
);
|
||||
fn fetch_albums(
|
||||
musicbrainz: Arc<Mutex<dyn IMusicBrainz + Send>>,
|
||||
fetch_tx: FetchSender,
|
||||
events: EventSender,
|
||||
arid: Mbid,
|
||||
albums: Vec<AlbumMeta>,
|
||||
);
|
||||
@ -127,16 +148,22 @@ impl IAppInteractFetchPrivate for AppMachine<AppFetch> {
|
||||
fn fetch_artist(
|
||||
musicbrainz: Arc<Mutex<dyn IMusicBrainz + Send>>,
|
||||
fetch_tx: FetchSender,
|
||||
events: EventSender,
|
||||
artist: ArtistMeta,
|
||||
) {
|
||||
let result = musicbrainz.lock().unwrap().search_artist(&artist);
|
||||
let result = result.map(|list| AppMatchesInfo::artist(artist, list));
|
||||
fetch_tx.send(result).ok();
|
||||
if fetch_tx.send(result).is_ok() {
|
||||
// If this send fails the event listener is dead. Don't panic as this function runs
|
||||
// in a detached thread so this might be happening during normal shut down.
|
||||
events.send(Event::FetchResultReady).ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_albums(
|
||||
musicbrainz: Arc<Mutex<dyn IMusicBrainz + Send>>,
|
||||
fetch_tx: FetchSender,
|
||||
events: EventSender,
|
||||
arid: Mbid,
|
||||
albums: Vec<AlbumMeta>,
|
||||
) {
|
||||
@ -154,6 +181,12 @@ impl IAppInteractFetchPrivate for AppMachine<AppFetch> {
|
||||
return;
|
||||
}
|
||||
|
||||
if events.send(Event::FetchResultReady).is_err() {
|
||||
// If this send fails the event listener is dead. Don't panic as this function runs
|
||||
// in a detached thread so this might be happening during normal shut down.
|
||||
return;
|
||||
}
|
||||
|
||||
if album_iter.peek().is_some() {
|
||||
thread::sleep(time::Duration::from_secs(1));
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::tui::{
|
||||
app::{selection::Selection, AppPublic, AppPublicInner, AppState, IAppAccess, IAppInteract},
|
||||
event::EventSender,
|
||||
lib::{interface::musicbrainz::IMusicBrainz, IMusicHoard},
|
||||
};
|
||||
|
||||
@ -44,15 +45,17 @@ pub struct AppInner {
|
||||
music_hoard: Box<dyn IMusicHoard>,
|
||||
musicbrainz: Arc<Mutex<dyn IMusicBrainz + Send>>,
|
||||
selection: Selection,
|
||||
events: EventSender,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new<MH: IMusicHoard + 'static, MB: IMusicBrainz + Send + 'static>(
|
||||
mut music_hoard: MH,
|
||||
musicbrainz: MB,
|
||||
events: EventSender,
|
||||
) -> Self {
|
||||
let init_result = Self::init(&mut music_hoard);
|
||||
let inner = AppInner::new(music_hoard, musicbrainz);
|
||||
let inner = AppInner::new(music_hoard, musicbrainz, events);
|
||||
match init_result {
|
||||
Ok(()) => AppMachine::browse(inner).into(),
|
||||
Err(err) => AppMachine::critical(inner, err.to_string()).into(),
|
||||
@ -137,6 +140,7 @@ impl AppInner {
|
||||
pub fn new<MH: IMusicHoard + 'static, MB: IMusicBrainz + Send + 'static>(
|
||||
music_hoard: MH,
|
||||
musicbrainz: MB,
|
||||
events: EventSender,
|
||||
) -> Self {
|
||||
let selection = Selection::new(music_hoard.get_collection());
|
||||
AppInner {
|
||||
@ -144,6 +148,7 @@ impl AppInner {
|
||||
music_hoard: Box::new(music_hoard),
|
||||
musicbrainz: Arc::new(Mutex::new(musicbrainz)),
|
||||
selection,
|
||||
events,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -165,7 +170,7 @@ mod tests {
|
||||
|
||||
use crate::tui::{
|
||||
app::{AppState, IAppInteract, IAppInteractBrowse},
|
||||
lib::{interface::musicbrainz::MockIMusicBrainz, MockIMusicHoard},
|
||||
lib::{interface::musicbrainz::MockIMusicBrainz, MockIMusicHoard}, EventChannel,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@ -250,17 +255,21 @@ mod tests {
|
||||
MockIMusicBrainz::new()
|
||||
}
|
||||
|
||||
fn events() -> EventSender {
|
||||
EventChannel::new().sender()
|
||||
}
|
||||
|
||||
pub fn inner(music_hoard: MockIMusicHoard) -> AppInner {
|
||||
AppInner::new(music_hoard, mb_api())
|
||||
AppInner::new(music_hoard, mb_api(), events())
|
||||
}
|
||||
|
||||
pub fn inner_with_mb(music_hoard: MockIMusicHoard, mb_api: MockIMusicBrainz) -> AppInner {
|
||||
AppInner::new(music_hoard, mb_api)
|
||||
AppInner::new(music_hoard, mb_api, events())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_browse() {
|
||||
let mut app = App::new(music_hoard_init(vec![]), mb_api());
|
||||
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
|
||||
assert!(app.is_running());
|
||||
|
||||
let state = app.state();
|
||||
@ -276,7 +285,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn state_info() {
|
||||
let mut app = App::new(music_hoard_init(vec![]), mb_api());
|
||||
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
|
||||
assert!(app.is_running());
|
||||
|
||||
app = app.unwrap_browse().show_info_overlay();
|
||||
@ -294,7 +303,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn state_reload() {
|
||||
let mut app = App::new(music_hoard_init(vec![]), mb_api());
|
||||
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
|
||||
assert!(app.is_running());
|
||||
|
||||
app = app.unwrap_browse().show_reload_menu();
|
||||
@ -312,7 +321,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn state_search() {
|
||||
let mut app = App::new(music_hoard_init(vec![]), mb_api());
|
||||
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
|
||||
assert!(app.is_running());
|
||||
|
||||
app = app.unwrap_browse().begin_search();
|
||||
@ -349,7 +358,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn state_error() {
|
||||
let mut app = App::new(music_hoard_init(vec![]), mb_api());
|
||||
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
|
||||
assert!(app.is_running());
|
||||
|
||||
app = AppMachine::error(app.unwrap_browse().inner, "get rekt").into();
|
||||
@ -367,7 +376,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn state_critical() {
|
||||
let mut app = App::new(music_hoard_init(vec![]), mb_api());
|
||||
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
|
||||
assert!(app.is_running());
|
||||
|
||||
app = AppMachine::critical(app.unwrap_browse().inner, "get rekt").into();
|
||||
@ -393,7 +402,7 @@ mod tests {
|
||||
.return_once(|| Err(musichoard::Error::LibraryError(String::from("get rekt"))));
|
||||
music_hoard.expect_get_collection().return_const(vec![]);
|
||||
|
||||
let app = App::new(music_hoard, mb_api());
|
||||
let app = App::new(music_hoard, mb_api(), events());
|
||||
assert!(app.is_running());
|
||||
app.unwrap_critical();
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ pub trait IAppInteract {
|
||||
type IS: IAppInteractInfo<APP = Self>;
|
||||
type RS: IAppInteractReload<APP = Self>;
|
||||
type SS: IAppInteractSearch<APP = Self>;
|
||||
type FS: IAppInteractFetch<APP = Self>;
|
||||
type FS: IAppInteractFetch<APP = Self> + IAppEventFetch<APP = Self>;
|
||||
type MS: IAppInteractMatches<APP = Self>;
|
||||
type ES: IAppInteractError<APP = Self>;
|
||||
type CS: IAppInteractCritical<APP = Self>;
|
||||
@ -98,6 +98,12 @@ pub trait IAppInteractFetch {
|
||||
fn no_op(self) -> Self::APP;
|
||||
}
|
||||
|
||||
pub trait IAppEventFetch {
|
||||
type APP: IAppInteract;
|
||||
|
||||
fn fetch_result_ready(self) -> Self::APP;
|
||||
}
|
||||
|
||||
pub trait IAppInteractMatches {
|
||||
type APP: IAppInteract;
|
||||
|
||||
@ -114,6 +120,8 @@ pub trait IAppInteractError {
|
||||
type APP: IAppInteract;
|
||||
|
||||
fn dismiss_error(self) -> Self::APP;
|
||||
|
||||
fn no_op(self) -> Self::APP;
|
||||
}
|
||||
|
||||
pub trait IAppInteractCritical {
|
||||
|
@ -36,6 +36,7 @@ impl From<mpsc::RecvError> for EventError {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Event {
|
||||
Key(KeyEvent),
|
||||
FetchResultReady,
|
||||
}
|
||||
|
||||
pub struct EventChannel {
|
||||
@ -43,6 +44,7 @@ pub struct EventChannel {
|
||||
receiver: mpsc::Receiver<Event>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EventSender {
|
||||
sender: mpsc::Sender<Event>,
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ use crate::tui::{
|
||||
event::{Event, EventError, EventReceiver},
|
||||
};
|
||||
|
||||
use super::app::IAppEventFetch;
|
||||
|
||||
#[cfg_attr(test, automock)]
|
||||
pub trait IEventHandler<APP: IAppInteract> {
|
||||
fn handle_next_event(&self, app: APP) -> Result<APP, EventError>;
|
||||
@ -27,6 +29,8 @@ trait IEventHandlerPrivate<APP: IAppInteract> {
|
||||
fn handle_matches_key_event(app: <APP as IAppInteract>::MS, key_event: KeyEvent) -> APP;
|
||||
fn handle_error_key_event(app: <APP as IAppInteract>::ES, key_event: KeyEvent) -> APP;
|
||||
fn handle_critical_key_event(app: <APP as IAppInteract>::CS, key_event: KeyEvent) -> APP;
|
||||
|
||||
fn handle_fetch_result_ready_event(app: APP) -> APP;
|
||||
}
|
||||
|
||||
pub struct EventHandler {
|
||||
@ -41,11 +45,11 @@ impl EventHandler {
|
||||
}
|
||||
|
||||
impl<APP: IAppInteract> IEventHandler<APP> for EventHandler {
|
||||
fn handle_next_event(&self, mut app: APP) -> Result<APP, EventError> {
|
||||
match self.events.recv()? {
|
||||
Event::Key(key_event) => app = Self::handle_key_event(app, key_event),
|
||||
};
|
||||
Ok(app)
|
||||
fn handle_next_event(&self, app: APP) -> Result<APP, EventError> {
|
||||
Ok(match self.events.recv()? {
|
||||
Event::Key(key_event) => Self::handle_key_event(app, key_event),
|
||||
Event::FetchResultReady => Self::handle_fetch_result_ready_event(app),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +91,19 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_fetch_result_ready_event(app: APP) -> APP {
|
||||
match app.state() {
|
||||
AppState::Browse(browse) => browse.no_op(),
|
||||
AppState::Info(info) => info.no_op(),
|
||||
AppState::Reload(reload) => reload.no_op(),
|
||||
AppState::Search(search) => search.no_op(),
|
||||
AppState::Fetch(fetch) => fetch.fetch_result_ready(),
|
||||
AppState::Matches(matches) => matches.no_op(),
|
||||
AppState::Error(error) => error.no_op(),
|
||||
AppState::Critical(critical) => critical.no_op(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_browse_key_event(app: <APP as IAppInteract>::BS, key_event: KeyEvent) -> APP {
|
||||
match key_event.code {
|
||||
// Exit application on `ESC` or `q`.
|
||||
|
@ -174,6 +174,7 @@ mod testmod;
|
||||
mod tests {
|
||||
use std::{io, thread};
|
||||
|
||||
use event::EventSender;
|
||||
use lib::interface::musicbrainz::MockIMusicBrainz;
|
||||
use ratatui::{backend::TestBackend, Terminal};
|
||||
|
||||
@ -202,8 +203,12 @@ mod tests {
|
||||
music_hoard
|
||||
}
|
||||
|
||||
fn events() -> EventSender {
|
||||
EventChannel::new().sender()
|
||||
}
|
||||
|
||||
fn app(collection: Collection) -> App {
|
||||
App::new(music_hoard(collection), MockIMusicBrainz::new())
|
||||
App::new(music_hoard(collection), MockIMusicBrainz::new(), events())
|
||||
}
|
||||
|
||||
fn listener() -> MockIEventListener {
|
||||
|
Loading…
Reference in New Issue
Block a user