Add manual input elements to the app an ui #216
68
src/tui/app/machine/input_state.rs
Normal file
68
src/tui/app/machine/input_state.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<MatchState> {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -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<SearchState>,
|
||||
AppMachine<FetchState>,
|
||||
AppMachine<MatchState>,
|
||||
AppMachine<InputState>,
|
||||
AppMachine<ErrorState>,
|
||||
AppMachine<CriticalState>,
|
||||
>;
|
||||
@ -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<SearchState>;
|
||||
type FetchState = AppMachine<FetchState>;
|
||||
type MatchState = AppMachine<MatchState>;
|
||||
type InputState = AppMachine<InputState>;
|
||||
type ErrorState = AppMachine<ErrorState>;
|
||||
type CriticalState = AppMachine<CriticalState>;
|
||||
|
||||
@ -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<T: Into<App>> 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<BS, IS, RS, SS, FS, MS, ES, CS> AppState<BS, IS, RS, SS, FS, MS, ES, CS> {
|
||||
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!(),
|
||||
|
@ -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<APP = Self>
|
||||
+ IAppEventFetch<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 CriticalState: IAppBase<APP = Self>;
|
||||
|
||||
@ -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<T> {
|
||||
Match(Match<T>),
|
||||
CannotHaveMbid,
|
||||
ManualInputMbid,
|
||||
}
|
||||
|
||||
impl<T> From<Match<T>> for MatchOption<T> {
|
||||
@ -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<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 {
|
||||
matches!(self, AppState::Search(_))
|
||||
}
|
||||
|
@ -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<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_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_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_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::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<APP: IApp> IEventHandlerPrivate<APP> 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<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 {
|
||||
// Any key dismisses the error.
|
||||
app.dismiss_error()
|
||||
|
@ -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)]
|
||||
|
@ -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...",
|
||||
|
@ -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),
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user