From cba1f2dff64e0eced83a44165d5502b9d664a909 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 14 Sep 2024 13:04:39 +0200 Subject: [PATCH] Skeleton of new manual input state --- src/tui/app/machine/input_state.rs | 68 ++++++++++++++++++++++++++++ src/tui/app/machine/match_state.rs | 41 ++++++++++++++++- src/tui/app/machine/mod.rs | 71 +++++++++++++++++++++++------- src/tui/app/mod.rs | 40 ++++++++++++++++- src/tui/handler.rs | 38 ++++++++++++---- src/tui/ui/display.rs | 6 +++ src/tui/ui/minibuffer.rs | 7 +++ src/tui/ui/mod.rs | 1 + 8 files changed, 243 insertions(+), 29 deletions(-) create mode 100644 src/tui/app/machine/input_state.rs diff --git a/src/tui/app/machine/input_state.rs b/src/tui/app/machine/input_state.rs new file mode 100644 index 0000000..1f1abb4 --- /dev/null +++ b/src/tui/app/machine/input_state.rs @@ -0,0 +1,68 @@ +use crate::tui::app::{ + machine::{App, AppInner, AppMachine}, + AppPublic, AppState, IAppInteractInput, +}; + +use super::match_state::MatchState; + +pub struct InputState { + string: String, + client: InputClient, +} + +pub enum InputClient { + Match(MatchState), +} + +impl AppMachine { + pub fn input_state(inner: AppInner, client: InputClient) -> Self { + AppMachine { + inner, + state: InputState { + string: String::new(), + client, + }, + } + } +} + +impl From> for App { + fn from(machine: AppMachine) -> Self { + AppState::Input(machine) + } +} + +impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: AppState::Input(&machine.state.string), + } + } +} + +impl IAppInteractInput for AppMachine { + type APP = App; + + fn append_character(mut self, ch: char) -> Self::APP { + self.state.string.push(ch); + self.into() + } + + fn delete_character(mut self) -> Self::APP { + self.state.string.pop(); + self.into() + } + + fn confirm(self) -> Self::APP { + match self.state.client { + InputClient::Match(state) => AppMachine::match_state(self.inner, state).into() + } + } + + fn cancel(self) -> Self::APP { + match self.state.client { + InputClient::Match(state) => AppMachine::match_state(self.inner, state).into() + } + } +} diff --git a/src/tui/app/machine/match_state.rs b/src/tui/app/machine/match_state.rs index d695c39..ee8c593 100644 --- a/src/tui/app/machine/match_state.rs +++ b/src/tui/app/machine/match_state.rs @@ -6,7 +6,7 @@ use crate::tui::app::{ MatchStateInfo, MatchStatePublic, WidgetState, }; -use super::fetch_state::FetchState; +use super::{fetch_state::FetchState, input_state::InputClient}; impl ArtistMatches { fn len(&self) -> usize { @@ -16,6 +16,14 @@ impl ArtistMatches { fn push_cannot_have_mbid(&mut self) { self.list.push(MatchOption::CannotHaveMbid) } + + fn push_manual_input_mbid(&mut self) { + self.list.push(MatchOption::ManualInputMbid) + } + + fn is_manual_input_mbid(&self, index: usize) -> bool { + self.list.get(index) == Some(&MatchOption::ManualInputMbid) + } } impl AlbumMatches { @@ -26,6 +34,14 @@ impl AlbumMatches { fn push_cannot_have_mbid(&mut self) { self.list.push(MatchOption::CannotHaveMbid) } + + fn push_manual_input_mbid(&mut self) { + self.list.push(MatchOption::ManualInputMbid) + } + + fn is_manual_input_mbid(&self, index: usize) -> bool { + self.list.get(index) == Some(&MatchOption::ManualInputMbid) + } } impl MatchStateInfo { @@ -36,12 +52,26 @@ impl MatchStateInfo { } } - pub fn push_cannot_have_mbid(&mut self) { + fn push_cannot_have_mbid(&mut self) { match self { Self::Artist(a) => a.push_cannot_have_mbid(), Self::Album(a) => a.push_cannot_have_mbid(), } } + + fn push_manual_input_mbid(&mut self) { + match self { + Self::Artist(a) => a.push_manual_input_mbid(), + Self::Album(a) => a.push_manual_input_mbid(), + } + } + + fn is_manual_input_mbid(&self, index: usize) -> bool { + match self { + Self::Artist(a) => a.is_manual_input_mbid(index), + Self::Album(a) => a.is_manual_input_mbid(index), + } + } } pub struct MatchState { @@ -56,6 +86,7 @@ impl MatchState { if let Some(ref mut current) = current { state.list.select(Some(0)); current.push_cannot_have_mbid(); + current.push_manual_input_mbid(); } MatchState { current, @@ -116,6 +147,12 @@ impl IAppInteractMatch for AppMachine { } fn select(self) -> Self::APP { + if let Some(index) = self.state.state.list.selected() { + // selected() implies current exists + if self.state.current.as_ref().unwrap().is_manual_input_mbid(index) { + return AppMachine::input_state(self.inner, InputClient::Match(self.state)).into() + } + } AppMachine::app_fetch_next(self.inner, self.state.fetch) } diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index 525832e..e56ec0b 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -3,6 +3,7 @@ mod critical_state; mod error_state; mod fetch_state; mod info_state; +mod input_state; mod match_state; mod reload_state; mod search_state; @@ -20,6 +21,7 @@ use critical_state::CriticalState; use error_state::ErrorState; use fetch_state::FetchState; use info_state::InfoState; +use input_state::InputState; use match_state::MatchState; use reload_state::ReloadState; use search_state::SearchState; @@ -33,6 +35,7 @@ pub type App = AppState< AppMachine, AppMachine, AppMachine, + AppMachine, AppMachine, AppMachine, >; @@ -77,6 +80,7 @@ impl App { AppState::Search(search_state) => &search_state.inner, AppState::Fetch(fetch_state) => &fetch_state.inner, AppState::Match(match_state) => &match_state.inner, + AppState::Input(input_state) => &input_state.inner, AppState::Error(error_state) => &error_state.inner, AppState::Critical(critical_state) => &critical_state.inner, } @@ -90,6 +94,7 @@ impl App { AppState::Search(search_state) => &mut search_state.inner, AppState::Fetch(fetch_state) => &mut fetch_state.inner, AppState::Match(match_state) => &mut match_state.inner, + AppState::Input(input_state) => &mut input_state.inner, AppState::Error(error_state) => &mut error_state.inner, AppState::Critical(critical_state) => &mut critical_state.inner, } @@ -103,6 +108,7 @@ impl IApp for App { type SearchState = AppMachine; type FetchState = AppMachine; type MatchState = AppMachine; + type InputState = AppMachine; type ErrorState = AppMachine; type CriticalState = AppMachine; @@ -124,6 +130,7 @@ impl IApp for App { Self::SearchState, Self::FetchState, Self::MatchState, + Self::InputState, Self::ErrorState, Self::CriticalState, > { @@ -142,14 +149,15 @@ impl> IAppBase for T { impl IAppAccess for App { fn get(&mut self) -> AppPublic { match self { - AppState::Browse(browse) => browse.into(), - AppState::Info(info) => info.into(), - AppState::Reload(reload) => reload.into(), - AppState::Search(search) => search.into(), - AppState::Fetch(fetch) => fetch.into(), - AppState::Match(matches) => matches.into(), - AppState::Error(error) => error.into(), - AppState::Critical(critical) => critical.into(), + AppState::Browse(state) => state.into(), + AppState::Info(state) => state.into(), + AppState::Reload(state) => state.into(), + AppState::Search(state) => state.into(), + AppState::Fetch(state) => state.into(), + AppState::Match(state) => state.into(), + AppState::Input(state) => state.into(), + AppState::Error(state) => state.into(), + AppState::Critical(state) => state.into(), } } } @@ -194,57 +202,86 @@ mod tests { use super::*; - impl AppState { - pub fn unwrap_browse(self) -> BS { + impl< + BrowseState, + InfoState, + ReloadState, + SearchState, + FetchState, + MatchState, + InputState, + ErrorState, + CriticalState, + > + AppState< + BrowseState, + InfoState, + ReloadState, + SearchState, + FetchState, + MatchState, + InputState, + ErrorState, + CriticalState, + > + { + pub fn unwrap_browse(self) -> BrowseState { match self { AppState::Browse(browse) => browse, _ => panic!(), } } - pub fn unwrap_info(self) -> IS { + pub fn unwrap_info(self) -> InfoState { match self { AppState::Info(info) => info, _ => panic!(), } } - pub fn unwrap_reload(self) -> RS { + pub fn unwrap_reload(self) -> ReloadState { match self { AppState::Reload(reload) => reload, _ => panic!(), } } - pub fn unwrap_search(self) -> SS { + pub fn unwrap_search(self) -> SearchState { match self { AppState::Search(search) => search, _ => panic!(), } } - pub fn unwrap_fetch(self) -> FS { + pub fn unwrap_fetch(self) -> FetchState { match self { AppState::Fetch(fetch) => fetch, _ => panic!(), } } - pub fn unwrap_match(self) -> MS { + pub fn unwrap_match(self) -> MatchState { match self { AppState::Match(matches) => matches, _ => panic!(), } } - pub fn unwrap_error(self) -> ES { + pub fn unwrap_input(self) -> InputState { + match self { + AppState::Input(input) => input, + _ => panic!(), + } + } + + pub fn unwrap_error(self) -> ErrorState { match self { AppState::Error(error) => error, _ => panic!(), } } - pub fn unwrap_critical(self) -> CS { + pub fn unwrap_critical(self) -> CriticalState { match self { AppState::Critical(critical) => critical, _ => panic!(), diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 999baec..ae99314 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -15,6 +15,7 @@ pub enum AppState< SearchState, FetchState, MatchState, + InputState, ErrorState, CriticalState, > { @@ -24,6 +25,7 @@ pub enum AppState< Search(SearchState), Fetch(FetchState), Match(MatchState), + Input(InputState), Error(ErrorState), Critical(CriticalState), } @@ -37,6 +39,7 @@ pub trait IApp { + IAppInteractFetch + IAppEventFetch; type MatchState: IAppBase + IAppInteractMatch; + type InputState: IAppBase + IAppInteractInput; type ErrorState: IAppBase + IAppInteractError; type CriticalState: IAppBase; @@ -53,6 +56,7 @@ pub trait IApp { Self::SearchState, Self::FetchState, Self::MatchState, + Self::InputState, Self::ErrorState, Self::CriticalState, >; @@ -129,6 +133,15 @@ pub trait IAppInteractMatch { fn abort(self) -> Self::APP; } +pub trait IAppInteractInput { + type APP: IApp; + + fn append_character(self, ch: char) -> Self::APP; + fn delete_character(self) -> Self::APP; + fn confirm(self) -> Self::APP; + fn cancel(self) -> Self::APP; +} + pub trait IAppInteractError { type APP: IApp; @@ -157,6 +170,7 @@ pub struct AppPublicInner<'app> { pub enum MatchOption { Match(Match), CannotHaveMbid, + ManualInputMbid, } impl From> for MatchOption { @@ -201,9 +215,31 @@ pub struct MatchStatePublic<'app> { } pub type AppPublicState<'app> = - AppState<(), (), (), &'app str, (), MatchStatePublic<'app>, &'app str, &'app str>; + AppState<(), (), (), &'app str, (), MatchStatePublic<'app>, &'app str, &'app str, &'app str>; -impl AppState { +impl< + BrowseState, + InfoState, + ReloadState, + SearchState, + FetchState, + MatchState, + InputState, + ErrorState, + CriticalState, + > + AppState< + BrowseState, + InfoState, + ReloadState, + SearchState, + FetchState, + MatchState, + InputState, + ErrorState, + CriticalState, + > +{ pub fn is_search(&self) -> bool { matches!(self, AppState::Search(_)) } diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 2efc614..f46777d 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -11,7 +11,7 @@ use crate::tui::{ event::{Event, EventError, EventReceiver}, }; -use super::app::{IAppBase, IAppEventFetch}; +use super::app::{IAppBase, IAppEventFetch, IAppInteractInput}; #[cfg_attr(test, automock)] pub trait IEventHandler { @@ -26,6 +26,7 @@ trait IEventHandlerPrivate { fn handle_search_key_event(app: ::SearchState, key_event: KeyEvent) -> APP; fn handle_fetch_key_event(app: ::FetchState, key_event: KeyEvent) -> APP; fn handle_match_key_event(app: ::MatchState, key_event: KeyEvent) -> APP; + fn handle_input_key_event(app: ::InputState, key_event: KeyEvent) -> APP; fn handle_error_key_event(app: ::ErrorState, key_event: KeyEvent) -> APP; fn handle_critical_key_event(app: ::CriticalState, key_event: KeyEvent) -> APP; @@ -75,6 +76,7 @@ impl IEventHandlerPrivate for EventHandler { } AppState::Fetch(fetch_state) => Self::handle_fetch_key_event(fetch_state, key_event), AppState::Match(match_state) => Self::handle_match_key_event(match_state, key_event), + AppState::Input(input_state) => Self::handle_input_key_event(input_state, key_event), AppState::Error(error_state) => Self::handle_error_key_event(error_state, key_event), AppState::Critical(critical_state) => { Self::handle_critical_key_event(critical_state, key_event) @@ -84,14 +86,15 @@ impl IEventHandlerPrivate for EventHandler { fn handle_fetch_result_ready_event(app: APP) -> APP { match app.state() { - AppState::Browse(browse_state) => browse_state.no_op(), - AppState::Info(info_state) => info_state.no_op(), - AppState::Reload(reload_state) => reload_state.no_op(), - AppState::Search(search_state) => search_state.no_op(), + AppState::Browse(state) => state.no_op(), + AppState::Info(state) => state.no_op(), + AppState::Reload(state) => state.no_op(), + AppState::Search(state) => state.no_op(), AppState::Fetch(fetch_state) => fetch_state.fetch_result_ready(), - AppState::Match(match_state) => match_state.no_op(), - AppState::Error(error_state) => error_state.no_op(), - AppState::Critical(critical_state) => critical_state.no_op(), + AppState::Match(state) => state.no_op(), + AppState::Input(state) => state.no_op(), + AppState::Error(state) => state.no_op(), + AppState::Critical(state) => state.no_op(), } } @@ -196,6 +199,25 @@ impl IEventHandlerPrivate for EventHandler { } } + fn handle_input_key_event(app: ::InputState, key_event: KeyEvent) -> APP { + if key_event.modifiers == KeyModifiers::CONTROL { + return match key_event.code { + KeyCode::Char('g') | KeyCode::Char('G') => app.cancel(), + _ => app.no_op(), + }; + } + + match key_event.code { + // Add/remove character. + KeyCode::Char(ch) => app.append_character(ch), + KeyCode::Backspace => app.delete_character(), + // Return. + KeyCode::Esc | KeyCode::Enter => app.confirm(), + // Othey keys. + _ => app.no_op(), + } + } + fn handle_error_key_event(app: ::ErrorState, _key_event: KeyEvent) -> APP { // Any key dismisses the error. app.dismiss_error() diff --git a/src/tui/ui/display.rs b/src/tui/ui/display.rs index 0ff6eb6..1e8fc1c 100644 --- a/src/tui/ui/display.rs +++ b/src/tui/ui/display.rs @@ -137,6 +137,7 @@ impl UiDisplay { match_artist.score, ), MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(), + MatchOption::ManualInputMbid => Self::display_manual_input_mbid().to_string(), } } @@ -153,12 +154,17 @@ impl UiDisplay { match_album.score, ), MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(), + MatchOption::ManualInputMbid => Self::display_manual_input_mbid().to_string(), } } fn display_cannot_have_mbid() -> &'static str { "-- Cannot have a MusicBrainz Identifier --" } + + fn display_manual_input_mbid() -> &'static str { + "-- Manually enter a MusicBrainz Identifier --" + } } #[cfg(test)] diff --git a/src/tui/ui/minibuffer.rs b/src/tui/ui/minibuffer.rs index 95ac560..7247b60 100644 --- a/src/tui/ui/minibuffer.rs +++ b/src/tui/ui/minibuffer.rs @@ -67,6 +67,13 @@ impl Minibuffer<'_> { ], columns: 2, }, + AppState::Input(_) => Minibuffer { + paragraphs: vec![ + Paragraph::new("enter: confirm"), + Paragraph::new("ctrl+g: cancel"), + ], + columns: 2, + }, AppState::Error(_) => Minibuffer { paragraphs: vec![Paragraph::new( "Press any key to dismiss the error message...", diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index 89c191a..a9b58bc 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -211,6 +211,7 @@ mod tests { info: m.info, state: m.state, }), + AppState::Input(s) => AppState::Input(s), AppState::Error(s) => AppState::Error(s), AppState::Critical(s) => AppState::Critical(s), },