Do state tracking via types

This commit is contained in:
Wojciech Kozlowski 2024-09-14 22:10:02 +02:00
parent 8acf208968
commit 20f36bcb0e
13 changed files with 113 additions and 89 deletions

View File

@ -11,7 +11,6 @@ impl AppMachine<BrowseState> {
AppMachine { AppMachine {
inner, inner,
state: BrowseState, state: BrowseState,
input: None,
} }
} }
} }
@ -27,7 +26,6 @@ impl<'a> From<&'a mut AppMachine<BrowseState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Browse(()), state: AppState::Browse(()),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -14,7 +14,6 @@ impl AppMachine<CriticalState> {
state: CriticalState { state: CriticalState {
string: string.into(), string: string.into(),
}, },
input: None
} }
} }
} }
@ -30,7 +29,6 @@ impl<'a> From<&'a mut AppMachine<CriticalState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Critical(&machine.state.string), state: AppState::Critical(&machine.state.string),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -14,7 +14,6 @@ impl AppMachine<ErrorState> {
state: ErrorState { state: ErrorState {
string: string.into(), string: string.into(),
}, },
input: None,
} }
} }
} }
@ -30,7 +29,6 @@ impl<'a> From<&'a mut AppMachine<ErrorState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Error(&machine.state.string), state: AppState::Error(&machine.state.string),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -40,11 +40,7 @@ pub type FetchReceiver = mpsc::Receiver<FetchResult>;
impl AppMachine<FetchState> { impl AppMachine<FetchState> {
fn fetch_state(inner: AppInner, state: FetchState) -> Self { fn fetch_state(inner: AppInner, state: FetchState) -> Self {
AppMachine { AppMachine { inner, state }
inner,
state,
input: None,
}
} }
pub fn app_fetch_new(inner: AppInner) -> App { pub fn app_fetch_new(inner: AppInner) -> App {
@ -177,7 +173,6 @@ impl<'a> From<&'a mut AppMachine<FetchState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Fetch(()), state: AppState::Fetch(()),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -10,7 +10,6 @@ impl AppMachine<InfoState> {
AppMachine { AppMachine {
inner, inner,
state: InfoState, state: InfoState,
input: None,
} }
} }
} }
@ -26,7 +25,6 @@ impl<'a> From<&'a mut AppMachine<InfoState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Info(()), state: AppState::Info(()),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -1,9 +1,8 @@
use tui_input::backend::crossterm::EventHandler; use tui_input::backend::crossterm::EventHandler;
use crate::tui::app::{ use crate::tui::app::{machine::App, AppState, IAppInput, InputEvent, InputPublic};
machine::{App, AppMachine},
IAppInput, InputEvent, InputPublic, use super::AppInputMode;
};
#[derive(Default)] #[derive(Default)]
pub struct Input(tui_input::Input); pub struct Input(tui_input::Input);
@ -14,23 +13,15 @@ impl<'app> From<&'app Input> for InputPublic<'app> {
} }
} }
impl<State> IAppInput for AppMachine<State> impl IAppInput for AppInputMode {
where
AppMachine<State>: Into<App>,
{
type APP = App; type APP = App;
fn taking_input(&self) -> bool {
self.input.is_some()
}
fn input(mut self, input: InputEvent) -> Self::APP { fn input(mut self, input: InputEvent) -> Self::APP {
self.input self.input
.as_mut()
.unwrap() // FIXME
.0 .0
.handle_event(&crossterm::event::Event::Key(input)); .handle_event(&crossterm::event::Event::Key(input));
self.into() self.app.inner_mut().input.replace(self.input);
self.app
} }
fn confirm(self) -> Self::APP { fn confirm(self) -> Self::APP {
@ -38,7 +29,10 @@ where
} }
fn cancel(mut self) -> Self::APP { fn cancel(mut self) -> Self::APP {
self.input = None; match &mut self.app {
self.into() AppState::Match(state) => state.submit_input(self.input),
_ => {}
}
self.app
} }
} }

View File

@ -98,12 +98,10 @@ impl MatchState {
impl AppMachine<MatchState> { impl AppMachine<MatchState> {
pub fn match_state(inner: AppInner, state: MatchState) -> Self { pub fn match_state(inner: AppInner, state: MatchState) -> Self {
AppMachine { AppMachine { inner, state }
inner,
state,
input: None,
}
} }
pub fn submit_input(&mut self, _input: Input) {}
} }
impl From<AppMachine<MatchState>> for App { impl From<AppMachine<MatchState>> for App {
@ -126,7 +124,6 @@ impl<'a> From<&'a mut AppMachine<MatchState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Match((&mut machine.state).into()), state: AppState::Match((&mut machine.state).into()),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }
@ -167,7 +164,7 @@ impl IAppInteractMatch for AppMachine<MatchState> {
.unwrap() .unwrap()
.is_manual_input_mbid(index) .is_manual_input_mbid(index)
{ {
self.input = Some(Input::default()); self.inner.input = Some(Input::default());
return self.into(); return self.into();
} }
} }

View File

@ -21,13 +21,12 @@ 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::Input;
use match_state::MatchState; use match_state::MatchState;
use reload_state::ReloadState; use reload_state::ReloadState;
use search_state::SearchState; use search_state::SearchState;
use input::Input; use super::{AppMode, IAppBase};
use super::IAppBase;
pub type App = AppState< pub type App = AppState<
AppMachine<BrowseState>, AppMachine<BrowseState>,
@ -43,7 +42,6 @@ pub type App = AppState<
pub struct AppMachine<STATE> { pub struct AppMachine<STATE> {
inner: AppInner, inner: AppInner,
state: STATE, state: STATE,
input: Option<Input>,
} }
pub struct AppInner { pub struct AppInner {
@ -52,6 +50,18 @@ pub struct AppInner {
musicbrainz: Arc<Mutex<dyn IMusicBrainz + Send>>, musicbrainz: Arc<Mutex<dyn IMusicBrainz + Send>>,
selection: Selection, selection: Selection,
events: EventSender, events: EventSender,
input: Option<Input>,
}
pub struct AppInputMode {
input: Input,
app: App,
}
impl AppInputMode {
fn new(input: Input, app: App) -> Self {
AppInputMode { input, app }
}
} }
impl App { impl App {
@ -109,6 +119,7 @@ impl IApp for App {
type MatchState = AppMachine<MatchState>; type MatchState = AppMachine<MatchState>;
type ErrorState = AppMachine<ErrorState>; type ErrorState = AppMachine<ErrorState>;
type CriticalState = AppMachine<CriticalState>; type CriticalState = AppMachine<CriticalState>;
type InputMode = AppInputMode;
fn is_running(&self) -> bool { fn is_running(&self) -> bool {
self.inner_ref().running self.inner_ref().running
@ -133,6 +144,28 @@ impl IApp for App {
> { > {
self self
} }
fn mode(
mut self,
) -> super::AppMode<
AppState<
Self::BrowseState,
Self::InfoState,
Self::ReloadState,
Self::SearchState,
Self::FetchState,
Self::MatchState,
Self::ErrorState,
Self::CriticalState,
>,
Self::InputMode,
> {
if let Some(input) = self.inner_mut().input.take() {
AppMode::Input(AppInputMode::new(input, self.state()))
} else {
AppMode::Browse(self.state())
}
}
} }
impl<T: Into<App>> IAppBase for T { impl<T: Into<App>> IAppBase for T {
@ -171,6 +204,7 @@ impl AppInner {
musicbrainz: Arc::new(Mutex::new(musicbrainz)), musicbrainz: Arc::new(Mutex::new(musicbrainz)),
selection, selection,
events, events,
input: None,
} }
} }
} }
@ -180,6 +214,7 @@ impl<'a> From<&'a mut AppInner> for AppPublicInner<'a> {
AppPublicInner { AppPublicInner {
collection: inner.music_hoard.get_collection(), collection: inner.music_hoard.get_collection(),
selection: &mut inner.selection, selection: &mut inner.selection,
input: inner.input.as_ref().map(Into::into),
} }
} }
} }
@ -408,12 +443,7 @@ mod tests {
let (_, rx) = mpsc::channel(); let (_, rx) = mpsc::channel();
let inner = app.unwrap_browse().inner; let inner = app.unwrap_browse().inner;
let state = FetchState::new(rx); let state = FetchState::new(rx);
app = AppMachine { app = AppMachine { inner, state }.into();
inner,
state,
input: None,
}
.into();
let state = app.state(); let state = app.state();
assert!(matches!(state, AppState::Fetch(_))); assert!(matches!(state, AppState::Fetch(_)));

View File

@ -11,7 +11,6 @@ impl AppMachine<ReloadState> {
AppMachine { AppMachine {
inner, inner,
state: ReloadState, state: ReloadState,
input: None,
} }
} }
} }
@ -26,7 +25,6 @@ impl<'a> From<&'a mut AppMachine<ReloadState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Reload(()), state: AppState::Reload(()),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -40,7 +40,6 @@ impl AppMachine<SearchState> {
orig, orig,
memo: vec![], memo: vec![],
}, },
input: None,
} }
} }
} }
@ -56,7 +55,6 @@ impl<'a> From<&'a mut AppMachine<SearchState>> for AppPublic<'a> {
AppPublic { AppPublic {
inner: (&mut machine.inner).into(), inner: (&mut machine.inner).into(),
state: AppState::Search(&machine.state.string), state: AppState::Search(&machine.state.string),
input: (&machine.input.as_ref()).map(Into::into),
} }
} }
} }

View File

@ -28,6 +28,11 @@ pub enum AppState<
Critical(CriticalState), Critical(CriticalState),
} }
pub enum AppMode<BrowseMode, InputMode> {
Browse(BrowseMode),
Input(InputMode),
}
pub trait IApp { pub trait IApp {
type BrowseState: IAppBase<APP = Self> + IAppInteractBrowse<APP = Self>; type BrowseState: IAppBase<APP = Self> + IAppInteractBrowse<APP = Self>;
type InfoState: IAppBase<APP = Self> + IAppInteractInfo<APP = Self>; type InfoState: IAppBase<APP = Self> + IAppInteractInfo<APP = Self>;
@ -36,9 +41,10 @@ pub trait IApp {
type FetchState: IAppBase<APP = Self> type FetchState: IAppBase<APP = Self>
+ IAppInteractFetch<APP = Self> + IAppInteractFetch<APP = Self>
+ IAppEventFetch<APP = Self>; + IAppEventFetch<APP = Self>;
type MatchState: IAppBase<APP = Self> + IAppInteractMatch<APP = Self> + IAppInput<APP = Self>; type MatchState: IAppBase<APP = Self> + IAppInteractMatch<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>;
type InputMode: IAppInput<APP = Self>;
fn is_running(&self) -> bool; fn is_running(&self) -> bool;
fn force_quit(self) -> Self; fn force_quit(self) -> Self;
@ -56,6 +62,23 @@ pub trait IApp {
Self::ErrorState, Self::ErrorState,
Self::CriticalState, Self::CriticalState,
>; >;
#[allow(clippy::type_complexity)]
fn mode(
self,
) -> AppMode<
AppState<
Self::BrowseState,
Self::InfoState,
Self::ReloadState,
Self::SearchState,
Self::FetchState,
Self::MatchState,
Self::ErrorState,
Self::CriticalState,
>,
Self::InputMode,
>;
} }
pub trait IAppBase { pub trait IAppBase {
@ -133,8 +156,6 @@ type InputEvent = crossterm::event::KeyEvent;
pub trait IAppInput { pub trait IAppInput {
type APP: IApp; type APP: IApp;
fn taking_input(&self) -> bool;
fn input(self, input: InputEvent) -> Self::APP; fn input(self, input: InputEvent) -> Self::APP;
fn confirm(self) -> Self::APP; fn confirm(self) -> Self::APP;
fn cancel(self) -> Self::APP; fn cancel(self) -> Self::APP;
@ -157,14 +178,16 @@ pub trait IAppAccess {
pub struct AppPublic<'app> { pub struct AppPublic<'app> {
pub inner: AppPublicInner<'app>, pub inner: AppPublicInner<'app>,
pub state: AppPublicState<'app>, pub state: AppPublicState<'app>,
pub input: AppPublicInput<'app>,
} }
pub struct AppPublicInner<'app> { pub struct AppPublicInner<'app> {
pub collection: &'app Collection, pub collection: &'app Collection,
pub selection: &'app mut Selection, pub selection: &'app mut Selection,
pub input: Option<InputPublic<'app>>,
} }
pub type InputPublic<'app> = &'app tui_input::Input;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum MatchOption<T> { pub enum MatchOption<T> {
Match(Match<T>), Match(Match<T>),
@ -242,9 +265,6 @@ impl<
} }
} }
pub type InputPublic<'app> = &'app tui_input::Input;
pub type AppPublicInput<'app> = Option<InputPublic<'app>>;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -11,7 +11,7 @@ use crate::tui::{
event::{Event, EventError, EventReceiver}, event::{Event, EventError, EventReceiver},
}; };
use super::app::{IAppBase, IAppEventFetch, IAppInput}; use super::app::{AppMode, IAppBase, IAppEventFetch, IAppInput};
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IEventHandler<APP: IApp> { pub trait IEventHandler<APP: IApp> {
@ -64,29 +64,32 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
}; };
} }
match app.state() { match app.mode() {
AppState::Browse(browse_state) => { AppMode::Input(input_mode) => Self::handle_input_key_event(input_mode, key_event),
Self::handle_browse_key_event(browse_state, key_event) AppMode::Browse(browse_mode) => match browse_mode {
} AppState::Browse(browse_state) => {
AppState::Info(info_state) => Self::handle_info_key_event(info_state, key_event), Self::handle_browse_key_event(browse_state, key_event)
AppState::Reload(reload_state) => { }
Self::handle_reload_key_event(reload_state, key_event) AppState::Info(info_state) => Self::handle_info_key_event(info_state, key_event),
} AppState::Reload(reload_state) => {
AppState::Search(search_state) => { Self::handle_reload_key_event(reload_state, key_event)
Self::handle_search_key_event(search_state, key_event) }
} AppState::Search(search_state) => {
AppState::Fetch(fetch_state) => Self::handle_fetch_key_event(fetch_state, key_event), Self::handle_search_key_event(search_state, key_event)
AppState::Match(match_state) => { }
if match_state.taking_input() { AppState::Fetch(fetch_state) => {
Self::handle_input_key_event(match_state, key_event) Self::handle_fetch_key_event(fetch_state, key_event)
} else { }
AppState::Match(match_state) => {
Self::handle_match_key_event(match_state, key_event) Self::handle_match_key_event(match_state, key_event)
} }
} AppState::Error(error_state) => {
AppState::Error(error_state) => Self::handle_error_key_event(error_state, key_event), Self::handle_error_key_event(error_state, key_event)
AppState::Critical(critical_state) => { }
Self::handle_critical_key_event(critical_state, key_event) AppState::Critical(critical_state) => {
} Self::handle_critical_key_event(critical_state, key_event)
}
},
} }
} }
@ -187,7 +190,7 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
return match key_event.code { return match key_event.code {
KeyCode::Char('g') | KeyCode::Char('G') => app.abort(), KeyCode::Char('g') | KeyCode::Char('G') => app.abort(),
_ => app.no_op(), _ => app.no_op(),
} };
} }
match key_event.code { match key_event.code {
@ -203,7 +206,7 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
return match key_event.code { return match key_event.code {
KeyCode::Char('g') | KeyCode::Char('G') => app.abort(), KeyCode::Char('g') | KeyCode::Char('G') => app.abort(),
_ => app.no_op(), _ => app.no_op(),
} };
} }
match key_event.code { match key_event.code {

View File

@ -195,7 +195,7 @@ impl IUi for Ui {
_ => {} _ => {}
} }
if let Some(input) = app.input { if let Some(input) = app.inner.input {
Self::render_input_overlay(input, frame); Self::render_input_overlay(input, frame);
} }
} }
@ -224,6 +224,7 @@ mod tests {
inner: AppPublicInner { inner: AppPublicInner {
collection: self.inner.collection, collection: self.inner.collection,
selection: self.inner.selection, selection: self.inner.selection,
input: self.inner.input,
}, },
state: match self.state { state: match self.state {
AppState::Browse(()) => AppState::Browse(()), AppState::Browse(()) => AppState::Browse(()),
@ -238,7 +239,6 @@ mod tests {
AppState::Error(s) => AppState::Error(s), AppState::Error(s) => AppState::Error(s),
AppState::Critical(s) => AppState::Critical(s), AppState::Critical(s) => AppState::Critical(s),
}, },
input: self.input,
} }
} }
} }
@ -250,6 +250,7 @@ mod tests {
AppPublicInner { AppPublicInner {
collection, collection,
selection, selection,
input: None,
} }
} }
@ -271,7 +272,6 @@ mod tests {
let mut app = AppPublic { let mut app = AppPublic {
inner: public_inner(collection, selection), inner: public_inner(collection, selection),
state: AppState::Browse(()), state: AppState::Browse(()),
input: None,
}; };
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
@ -350,7 +350,6 @@ mod tests {
info: None, info: None,
state: &mut widget_state, state: &mut widget_state,
}), }),
input: None,
}; };
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
} }
@ -380,7 +379,6 @@ mod tests {
info: Some(&artist_matches), info: Some(&artist_matches),
state: &mut widget_state, state: &mut widget_state,
}), }),
input: None,
}; };
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
} }
@ -415,7 +413,6 @@ mod tests {
info: Some(&album_matches), info: Some(&album_matches),
state: &mut widget_state, state: &mut widget_state,
}), }),
input: None,
}; };
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
} }