From 3f1845ed23956c31427912621145c51819f23774 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Mon, 2 Sep 2024 21:07:42 +0200 Subject: [PATCH] Proof of principle working --- src/main.rs | 11 +++--- src/tui/app/machine/error.rs | 4 +++ src/tui/app/machine/fetch.rs | 69 ++++++++++++++++++++++++++---------- src/tui/app/machine/mod.rs | 31 ++++++++++------ src/tui/app/mod.rs | 10 +++++- src/tui/event.rs | 2 ++ src/tui/handler.rs | 27 +++++++++++--- src/tui/mod.rs | 7 +++- 8 files changed, 120 insertions(+), 41 deletions(-) diff --git a/src/main.rs b/src/main.rs index 73233f3..dde33f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,18 +79,19 @@ fn with( 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"); } diff --git a/src/tui/app/machine/error.rs b/src/tui/app/machine/error.rs index 1f9b90f..f7cee44 100644 --- a/src/tui/app/machine/error.rs +++ b/src/tui/app/machine/error.rs @@ -39,6 +39,10 @@ impl IAppInteractError for AppMachine { fn dismiss_error(self) -> Self::APP { AppMachine::browse(self.inner).into() } + + fn no_op(self) -> Self::APP { + self.into() + } } #[cfg(test)] diff --git a/src/tui/app/machine/fetch.rs b/src/tui/app/machine/fetch.rs index a113c5d..9938bf2 100644 --- a/src/tui/app/machine/fetch.rs +++ b/src/tui/app/machine/fetch.rs @@ -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 { + 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 { 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 { } 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 { } } +impl IAppEventFetch for AppMachine { + 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; type FetchSender = mpsc::Sender; @@ -113,11 +132,13 @@ trait IAppInteractFetchPrivate { fn fetch_artist( musicbrainz: Arc>, fetch_tx: FetchSender, + events: EventSender, artist: ArtistMeta, ); fn fetch_albums( musicbrainz: Arc>, fetch_tx: FetchSender, + events: EventSender, arid: Mbid, albums: Vec, ); @@ -127,16 +148,22 @@ impl IAppInteractFetchPrivate for AppMachine { fn fetch_artist( musicbrainz: Arc>, 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>, fetch_tx: FetchSender, + events: EventSender, arid: Mbid, albums: Vec, ) { @@ -154,6 +181,12 @@ impl IAppInteractFetchPrivate for AppMachine { 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)); } diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index 5b140d0..24f4711 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -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, musicbrainz: Arc>, selection: Selection, + events: EventSender, } impl App { pub fn new( 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( 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(); } diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 4c14370..b931842 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -24,7 +24,7 @@ pub trait IAppInteract { type IS: IAppInteractInfo; type RS: IAppInteractReload; type SS: IAppInteractSearch; - type FS: IAppInteractFetch; + type FS: IAppInteractFetch + IAppEventFetch; type MS: IAppInteractMatches; type ES: IAppInteractError; type CS: IAppInteractCritical; @@ -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 { diff --git a/src/tui/event.rs b/src/tui/event.rs index 8143c29..6f34116 100644 --- a/src/tui/event.rs +++ b/src/tui/event.rs @@ -36,6 +36,7 @@ impl From 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, } +#[derive(Clone)] pub struct EventSender { sender: mpsc::Sender, } diff --git a/src/tui/handler.rs b/src/tui/handler.rs index bb2e09c..e934870 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -12,6 +12,8 @@ use crate::tui::{ event::{Event, EventError, EventReceiver}, }; +use super::app::IAppEventFetch; + #[cfg_attr(test, automock)] pub trait IEventHandler { fn handle_next_event(&self, app: APP) -> Result; @@ -27,6 +29,8 @@ trait IEventHandlerPrivate { fn handle_matches_key_event(app: ::MS, key_event: KeyEvent) -> APP; fn handle_error_key_event(app: ::ES, key_event: KeyEvent) -> APP; fn handle_critical_key_event(app: ::CS, key_event: KeyEvent) -> APP; + + fn handle_fetch_result_ready_event(app: APP) -> APP; } pub struct EventHandler { @@ -41,11 +45,11 @@ impl EventHandler { } impl IEventHandler for EventHandler { - fn handle_next_event(&self, mut app: APP) -> Result { - 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 { + 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 IEventHandlerPrivate 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: ::BS, key_event: KeyEvent) -> APP { match key_event.code { // Exit application on `ESC` or `q`. diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 131ebb7..d1ce805 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -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 {