Add manual input elements to the app an ui #216

Merged
wojtek merged 17 commits from 188---add-option-for-manual-input-during-fetch into main 2024-09-15 15:20:11 +02:00
8 changed files with 243 additions and 29 deletions
Showing only changes of commit cba1f2dff6 - Show all commits

View File

@ -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<InputState> {
pub fn input_state(inner: AppInner, client: InputClient) -> Self {
AppMachine {
inner,
state: InputState {
string: String::new(),
client,
},
}
}
}
impl From<AppMachine<InputState>> for App {
fn from(machine: AppMachine<InputState>) -> Self {
AppState::Input(machine)
}
}
impl<'a> From<&'a mut AppMachine<InputState>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<InputState>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Input(&machine.state.string),
}
}
}
impl IAppInteractInput for AppMachine<InputState> {
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()
}
}
}

View File

@ -6,7 +6,7 @@ use crate::tui::app::{
MatchStateInfo, MatchStatePublic, WidgetState, MatchStateInfo, MatchStatePublic, WidgetState,
}; };
use super::fetch_state::FetchState; use super::{fetch_state::FetchState, input_state::InputClient};
impl ArtistMatches { impl ArtistMatches {
fn len(&self) -> usize { fn len(&self) -> usize {
@ -16,6 +16,14 @@ impl ArtistMatches {
fn push_cannot_have_mbid(&mut self) { fn push_cannot_have_mbid(&mut self) {
self.list.push(MatchOption::CannotHaveMbid) 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 { impl AlbumMatches {
@ -26,6 +34,14 @@ impl AlbumMatches {
fn push_cannot_have_mbid(&mut self) { fn push_cannot_have_mbid(&mut self) {
self.list.push(MatchOption::CannotHaveMbid) 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 { 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 { match self {
Self::Artist(a) => a.push_cannot_have_mbid(), Self::Artist(a) => a.push_cannot_have_mbid(),
Self::Album(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 { pub struct MatchState {
@ -56,6 +86,7 @@ impl MatchState {
if let Some(ref mut current) = current { if let Some(ref mut current) = current {
state.list.select(Some(0)); state.list.select(Some(0));
current.push_cannot_have_mbid(); current.push_cannot_have_mbid();
current.push_manual_input_mbid();
} }
MatchState { MatchState {
current, current,
@ -116,6 +147,12 @@ impl IAppInteractMatch for AppMachine<MatchState> {
} }
fn select(self) -> Self::APP { 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) AppMachine::app_fetch_next(self.inner, self.state.fetch)
} }

View File

@ -3,6 +3,7 @@ mod critical_state;
mod error_state; mod error_state;
mod fetch_state; mod fetch_state;
mod info_state; mod info_state;
mod input_state;
mod match_state; mod match_state;
mod reload_state; mod reload_state;
mod search_state; mod search_state;
@ -20,6 +21,7 @@ use critical_state::CriticalState;
use error_state::ErrorState; use error_state::ErrorState;
use fetch_state::FetchState; use fetch_state::FetchState;
use info_state::InfoState; use info_state::InfoState;
use input_state::InputState;
use match_state::MatchState; use match_state::MatchState;
use reload_state::ReloadState; use reload_state::ReloadState;
use search_state::SearchState; use search_state::SearchState;
@ -33,6 +35,7 @@ pub type App = AppState<
AppMachine<SearchState>, AppMachine<SearchState>,
AppMachine<FetchState>, AppMachine<FetchState>,
AppMachine<MatchState>, AppMachine<MatchState>,
AppMachine<InputState>,
AppMachine<ErrorState>, AppMachine<ErrorState>,
AppMachine<CriticalState>, AppMachine<CriticalState>,
>; >;
@ -77,6 +80,7 @@ impl App {
AppState::Search(search_state) => &search_state.inner, AppState::Search(search_state) => &search_state.inner,
AppState::Fetch(fetch_state) => &fetch_state.inner, AppState::Fetch(fetch_state) => &fetch_state.inner,
AppState::Match(match_state) => &match_state.inner, AppState::Match(match_state) => &match_state.inner,
AppState::Input(input_state) => &input_state.inner,
AppState::Error(error_state) => &error_state.inner, AppState::Error(error_state) => &error_state.inner,
AppState::Critical(critical_state) => &critical_state.inner, AppState::Critical(critical_state) => &critical_state.inner,
} }
@ -90,6 +94,7 @@ impl App {
AppState::Search(search_state) => &mut search_state.inner, AppState::Search(search_state) => &mut search_state.inner,
AppState::Fetch(fetch_state) => &mut fetch_state.inner, AppState::Fetch(fetch_state) => &mut fetch_state.inner,
AppState::Match(match_state) => &mut match_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::Error(error_state) => &mut error_state.inner,
AppState::Critical(critical_state) => &mut critical_state.inner, AppState::Critical(critical_state) => &mut critical_state.inner,
} }
@ -103,6 +108,7 @@ impl IApp for App {
type SearchState = AppMachine<SearchState>; type SearchState = AppMachine<SearchState>;
type FetchState = AppMachine<FetchState>; type FetchState = AppMachine<FetchState>;
type MatchState = AppMachine<MatchState>; type MatchState = AppMachine<MatchState>;
type InputState = AppMachine<InputState>;
type ErrorState = AppMachine<ErrorState>; type ErrorState = AppMachine<ErrorState>;
type CriticalState = AppMachine<CriticalState>; type CriticalState = AppMachine<CriticalState>;
@ -124,6 +130,7 @@ impl IApp for App {
Self::SearchState, Self::SearchState,
Self::FetchState, Self::FetchState,
Self::MatchState, Self::MatchState,
Self::InputState,
Self::ErrorState, Self::ErrorState,
Self::CriticalState, Self::CriticalState,
> { > {
@ -142,14 +149,15 @@ impl<T: Into<App>> IAppBase for T {
impl IAppAccess for App { impl IAppAccess for App {
fn get(&mut self) -> AppPublic { fn get(&mut self) -> AppPublic {
match self { match self {
AppState::Browse(browse) => browse.into(), AppState::Browse(state) => state.into(),
AppState::Info(info) => info.into(), AppState::Info(state) => state.into(),
AppState::Reload(reload) => reload.into(), AppState::Reload(state) => state.into(),
AppState::Search(search) => search.into(), AppState::Search(state) => state.into(),
AppState::Fetch(fetch) => fetch.into(), AppState::Fetch(state) => state.into(),
AppState::Match(matches) => matches.into(), AppState::Match(state) => state.into(),
AppState::Error(error) => error.into(), AppState::Input(state) => state.into(),
AppState::Critical(critical) => critical.into(), AppState::Error(state) => state.into(),
AppState::Critical(state) => state.into(),
} }
} }
} }
@ -194,57 +202,86 @@ mod tests {
use super::*; use super::*;
impl<BS, IS, RS, SS, FS, MS, ES, CS> AppState<BS, IS, RS, SS, FS, MS, ES, CS> { impl<
pub fn unwrap_browse(self) -> BS { 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 { match self {
AppState::Browse(browse) => browse, AppState::Browse(browse) => browse,
_ => panic!(), _ => panic!(),
} }
} }
pub fn unwrap_info(self) -> IS { pub fn unwrap_info(self) -> InfoState {
match self { match self {
AppState::Info(info) => info, AppState::Info(info) => info,
_ => panic!(), _ => panic!(),
} }
} }
pub fn unwrap_reload(self) -> RS { pub fn unwrap_reload(self) -> ReloadState {
match self { match self {
AppState::Reload(reload) => reload, AppState::Reload(reload) => reload,
_ => panic!(), _ => panic!(),
} }
} }
pub fn unwrap_search(self) -> SS { pub fn unwrap_search(self) -> SearchState {
match self { match self {
AppState::Search(search) => search, AppState::Search(search) => search,
_ => panic!(), _ => panic!(),
} }
} }
pub fn unwrap_fetch(self) -> FS { pub fn unwrap_fetch(self) -> FetchState {
match self { match self {
AppState::Fetch(fetch) => fetch, AppState::Fetch(fetch) => fetch,
_ => panic!(), _ => panic!(),
} }
} }
pub fn unwrap_match(self) -> MS { pub fn unwrap_match(self) -> MatchState {
match self { match self {
AppState::Match(matches) => matches, AppState::Match(matches) => matches,
_ => panic!(), _ => 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 { match self {
AppState::Error(error) => error, AppState::Error(error) => error,
_ => panic!(), _ => panic!(),
} }
} }
pub fn unwrap_critical(self) -> CS { pub fn unwrap_critical(self) -> CriticalState {
match self { match self {
AppState::Critical(critical) => critical, AppState::Critical(critical) => critical,
_ => panic!(), _ => panic!(),

View File

@ -15,6 +15,7 @@ pub enum AppState<
SearchState, SearchState,
FetchState, FetchState,
MatchState, MatchState,
InputState,
ErrorState, ErrorState,
CriticalState, CriticalState,
> { > {
@ -24,6 +25,7 @@ pub enum AppState<
Search(SearchState), Search(SearchState),
Fetch(FetchState), Fetch(FetchState),
Match(MatchState), Match(MatchState),
Input(InputState),
Error(ErrorState), Error(ErrorState),
Critical(CriticalState), Critical(CriticalState),
} }
@ -37,6 +39,7 @@ pub trait IApp {
+ IAppInteractFetch<APP = Self> + IAppInteractFetch<APP = Self>
+ IAppEventFetch<APP = Self>; + IAppEventFetch<APP = Self>;
type MatchState: IAppBase<APP = Self> + IAppInteractMatch<APP = Self>; type MatchState: IAppBase<APP = Self> + IAppInteractMatch<APP = Self>;
type InputState: IAppBase<APP = Self> + IAppInteractInput<APP = Self>;
type ErrorState: IAppBase<APP = Self> + IAppInteractError<APP = Self>; type ErrorState: IAppBase<APP = Self> + IAppInteractError<APP = Self>;
type CriticalState: IAppBase<APP = Self>; type CriticalState: IAppBase<APP = Self>;
@ -53,6 +56,7 @@ pub trait IApp {
Self::SearchState, Self::SearchState,
Self::FetchState, Self::FetchState,
Self::MatchState, Self::MatchState,
Self::InputState,
Self::ErrorState, Self::ErrorState,
Self::CriticalState, Self::CriticalState,
>; >;
@ -129,6 +133,15 @@ pub trait IAppInteractMatch {
fn abort(self) -> Self::APP; 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 { pub trait IAppInteractError {
type APP: IApp; type APP: IApp;
@ -157,6 +170,7 @@ pub struct AppPublicInner<'app> {
pub enum MatchOption<T> { pub enum MatchOption<T> {
Match(Match<T>), Match(Match<T>),
CannotHaveMbid, CannotHaveMbid,
ManualInputMbid,
} }
impl<T> From<Match<T>> for MatchOption<T> { impl<T> From<Match<T>> for MatchOption<T> {
@ -201,9 +215,31 @@ pub struct MatchStatePublic<'app> {
} }
pub type AppPublicState<'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<BS, IS, RS, SS, FS, MS, ES, CS> AppState<BS, IS, RS, SS, FS, MS, ES, CS> { 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 { pub fn is_search(&self) -> bool {
matches!(self, AppState::Search(_)) matches!(self, AppState::Search(_))
} }

View File

@ -11,7 +11,7 @@ use crate::tui::{
event::{Event, EventError, EventReceiver}, event::{Event, EventError, EventReceiver},
}; };
use super::app::{IAppBase, IAppEventFetch}; use super::app::{IAppBase, IAppEventFetch, IAppInteractInput};
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IEventHandler<APP: IApp> { pub trait IEventHandler<APP: IApp> {
@ -26,6 +26,7 @@ trait IEventHandlerPrivate<APP: IApp> {
fn handle_search_key_event(app: <APP as IApp>::SearchState, key_event: KeyEvent) -> APP; fn handle_search_key_event(app: <APP as IApp>::SearchState, key_event: KeyEvent) -> APP;
fn handle_fetch_key_event(app: <APP as IApp>::FetchState, key_event: KeyEvent) -> APP; fn handle_fetch_key_event(app: <APP as IApp>::FetchState, key_event: KeyEvent) -> APP;
fn handle_match_key_event(app: <APP as IApp>::MatchState, key_event: KeyEvent) -> APP; fn handle_match_key_event(app: <APP as IApp>::MatchState, key_event: KeyEvent) -> APP;
fn handle_input_key_event(app: <APP as IApp>::InputState, key_event: KeyEvent) -> APP;
fn handle_error_key_event(app: <APP as IApp>::ErrorState, key_event: KeyEvent) -> APP; fn handle_error_key_event(app: <APP as IApp>::ErrorState, key_event: KeyEvent) -> APP;
fn handle_critical_key_event(app: <APP as IApp>::CriticalState, key_event: KeyEvent) -> APP; fn handle_critical_key_event(app: <APP as IApp>::CriticalState, key_event: KeyEvent) -> APP;
@ -75,6 +76,7 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
} }
AppState::Fetch(fetch_state) => Self::handle_fetch_key_event(fetch_state, key_event), 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::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::Error(error_state) => Self::handle_error_key_event(error_state, key_event),
AppState::Critical(critical_state) => { AppState::Critical(critical_state) => {
Self::handle_critical_key_event(critical_state, key_event) Self::handle_critical_key_event(critical_state, key_event)
@ -84,14 +86,15 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
fn handle_fetch_result_ready_event(app: APP) -> APP { fn handle_fetch_result_ready_event(app: APP) -> APP {
match app.state() { match app.state() {
AppState::Browse(browse_state) => browse_state.no_op(), AppState::Browse(state) => state.no_op(),
AppState::Info(info_state) => info_state.no_op(), AppState::Info(state) => state.no_op(),
AppState::Reload(reload_state) => reload_state.no_op(), AppState::Reload(state) => state.no_op(),
AppState::Search(search_state) => search_state.no_op(), AppState::Search(state) => state.no_op(),
AppState::Fetch(fetch_state) => fetch_state.fetch_result_ready(), AppState::Fetch(fetch_state) => fetch_state.fetch_result_ready(),
AppState::Match(match_state) => match_state.no_op(), AppState::Match(state) => state.no_op(),
AppState::Error(error_state) => error_state.no_op(), AppState::Input(state) => state.no_op(),
AppState::Critical(critical_state) => critical_state.no_op(), AppState::Error(state) => state.no_op(),
AppState::Critical(state) => state.no_op(),
} }
} }
@ -196,6 +199,25 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
} }
} }
fn handle_input_key_event(app: <APP as IApp>::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: <APP as IApp>::ErrorState, _key_event: KeyEvent) -> APP { fn handle_error_key_event(app: <APP as IApp>::ErrorState, _key_event: KeyEvent) -> APP {
// Any key dismisses the error. // Any key dismisses the error.
app.dismiss_error() app.dismiss_error()

View File

@ -137,6 +137,7 @@ impl UiDisplay {
match_artist.score, match_artist.score,
), ),
MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(), 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, match_album.score,
), ),
MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(), 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 { fn display_cannot_have_mbid() -> &'static str {
"-- Cannot have a MusicBrainz Identifier --" "-- Cannot have a MusicBrainz Identifier --"
} }
fn display_manual_input_mbid() -> &'static str {
"-- Manually enter a MusicBrainz Identifier --"
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -67,6 +67,13 @@ impl Minibuffer<'_> {
], ],
columns: 2, columns: 2,
}, },
AppState::Input(_) => Minibuffer {
paragraphs: vec![
Paragraph::new("enter: confirm"),
Paragraph::new("ctrl+g: cancel"),
],
columns: 2,
},
AppState::Error(_) => Minibuffer { AppState::Error(_) => Minibuffer {
paragraphs: vec![Paragraph::new( paragraphs: vec![Paragraph::new(
"Press any key to dismiss the error message...", "Press any key to dismiss the error message...",

View File

@ -211,6 +211,7 @@ mod tests {
info: m.info, info: m.info,
state: m.state, state: m.state,
}), }),
AppState::Input(s) => AppState::Input(s),
AppState::Error(s) => AppState::Error(s), AppState::Error(s) => AppState::Error(s),
AppState::Critical(s) => AppState::Critical(s), AppState::Critical(s) => AppState::Critical(s),
}, },