From cba1f2dff64e0eced83a44165d5502b9d664a909 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 14 Sep 2024 13:04:39 +0200 Subject: [PATCH 01/17] 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), }, -- 2.45.2 From 664950d5a3e0fd8a007b04f81154b777aff9d1d6 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 14 Sep 2024 14:50:22 +0200 Subject: [PATCH 02/17] Proof of concept works --- Cargo.lock | 130 ++++++++++++++++++----------- Cargo.toml | 8 +- src/tui/app/machine/input_state.rs | 19 ++--- src/tui/app/mod.rs | 21 ++++- src/tui/handler.rs | 11 +-- src/tui/ui/mod.rs | 31 +++++-- 6 files changed, 141 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 327ce37..587b63f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,7 +65,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -129,9 +129,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" dependencies = [ "rustversion", ] @@ -168,13 +168,14 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" dependencies = [ "castaway", "cfg-if", "itoa", + "rustversion", "ryu", "static_assertions", ] @@ -197,15 +198,15 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "crossterm" -version = "0.27.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.4.2", "crossterm_winapi", - "libc", - "mio", + "mio 1.0.2", "parking_lot", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -393,9 +394,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -406,6 +407,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "http" version = "0.2.12" @@ -498,10 +505,14 @@ dependencies = [ ] [[package]] -name = "indoc" -version = "2.0.4" +name = "instability" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +dependencies = [ + "quote", + "syn 2.0.48", +] [[package]] name = "ipnet" @@ -511,9 +522,9 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -541,15 +552,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" @@ -604,11 +615,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", - "log", "wasi", "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "mockall" version = "0.12.1" @@ -653,6 +676,7 @@ dependencies = [ "structopt", "tempfile", "tokio", + "tui-input", "url", "uuid", "version_check", @@ -911,21 +935,22 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "154b85ef15a5d1719bcaa193c3c81fe645cd120c156874cd660fe49fd21d1373" +checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" dependencies = [ "bitflags 2.4.2", "cassowary", "compact_str", "crossterm", - "indoc", + "instability", "itertools", "lru", "paste", - "stability", "strum", + "strum_macros", "unicode-segmentation", + "unicode-truncate", "unicode-width", ] @@ -986,9 +1011,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags 2.4.2", "errno", @@ -1127,12 +1152,12 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 1.0.2", "signal-hook", ] @@ -1189,16 +1214,6 @@ dependencies = [ "serde", ] -[[package]] -name = "stability" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" -dependencies = [ - "quote", - "syn 1.0.109", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -1246,11 +1261,11 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.26.1" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -1377,7 +1392,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 0.8.10", "pin-project-lite", "signal-hook-registry", "socket2", @@ -1470,6 +1485,16 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tui-input" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd137780d743c103a391e06fe952487f914b299a4fe2c3626677f6a6339a7c6b" +dependencies = [ + "ratatui", + "unicode-width", +] + [[package]] name = "typed-builder" version = "0.18.1" @@ -1518,10 +1543,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] -name = "unicode-width" -version = "0.1.11" +name = "unicode-truncate" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "url" diff --git a/Cargo.toml b/Cargo.toml index ac138f4..d758e31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,16 +7,18 @@ edition = "2021" [dependencies] aho-corasick = { version = "1.1.2", optional = true } -crossterm = { version = "0.27.0", optional = true} +crossterm = { version = "0.28.1", optional = true} once_cell = { version = "1.19.0", optional = true} openssh = { version = "0.10.3", features = ["native-mux"], default-features = false, optional = true} paste = { version = "1.0.15", optional = true } -ratatui = { version = "0.26.0", optional = true} +ratatui = { version = "0.28.1", optional = true} reqwest = { version = "0.11.25", features = ["blocking", "json"], optional = true } serde = { version = "1.0.196", features = ["derive"], optional = true } serde_json = { version = "1.0.113", optional = true} structopt = { version = "0.3.26", optional = true} tokio = { version = "1.36.0", features = ["rt"], optional = true} +# ratatui/crossterm dependency version must match with musichhoard's ratatui/crossterm +tui-input = { version = "0.10.1", optional = true } url = { version = "2.5.0" } uuid = { version = "1.7.0" } @@ -35,7 +37,7 @@ database-json = ["serde", "serde_json"] library-beets = [] library-beets-ssh = ["openssh", "tokio"] musicbrainz = ["paste", "reqwest", "serde", "serde_json"] -tui = ["aho-corasick", "crossterm", "once_cell", "ratatui"] +tui = ["aho-corasick", "crossterm", "once_cell", "ratatui", "tui-input"] [[bin]] name = "musichoard" diff --git a/src/tui/app/machine/input_state.rs b/src/tui/app/machine/input_state.rs index 1f1abb4..52e0e7d 100644 --- a/src/tui/app/machine/input_state.rs +++ b/src/tui/app/machine/input_state.rs @@ -1,12 +1,14 @@ +use tui_input::{backend::crossterm::EventHandler, Input}; + use crate::tui::app::{ machine::{App, AppInner, AppMachine}, - AppPublic, AppState, IAppInteractInput, + AppPublic, AppState, IAppInteractInput, InputEvent, }; use super::match_state::MatchState; pub struct InputState { - string: String, + input: Input, client: InputClient, } @@ -19,7 +21,7 @@ impl AppMachine { AppMachine { inner, state: InputState { - string: String::new(), + input: Input::default(), client, }, } @@ -36,7 +38,7 @@ 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), + state: AppState::Input(&machine.state.input), } } } @@ -44,13 +46,8 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { 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(); + fn input(mut self, input: InputEvent) -> Self::APP { + self.state.input.handle_event(&crossterm::event::Event::Key(input)); self.into() } diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index ae99314..aaaaedd 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -1,10 +1,12 @@ mod machine; mod selection; +use crossterm::event::KeyEvent; pub use machine::App; pub use selection::{Category, Delta, Selection, WidgetState}; use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection}; +use tui_input::Input; use crate::tui::lib::interface::musicbrainz::Match; @@ -133,11 +135,11 @@ pub trait IAppInteractMatch { fn abort(self) -> Self::APP; } +type InputEvent = KeyEvent; pub trait IAppInteractInput { type APP: IApp; - fn append_character(self, ch: char) -> Self::APP; - fn delete_character(self) -> Self::APP; + fn input(self, input: InputEvent) -> Self::APP; fn confirm(self) -> Self::APP; fn cancel(self) -> Self::APP; } @@ -214,8 +216,19 @@ pub struct MatchStatePublic<'app> { pub state: &'app mut WidgetState, } -pub type AppPublicState<'app> = - AppState<(), (), (), &'app str, (), MatchStatePublic<'app>, &'app str, &'app str, &'app str>; +pub type InputStatePublic<'app> = &'app Input; + +pub type AppPublicState<'app> = AppState< + (), + (), + (), + &'app str, + (), + MatchStatePublic<'app>, + InputStatePublic<'app>, + &'app str, + &'app str, +>; impl< BrowseState, diff --git a/src/tui/handler.rs b/src/tui/handler.rs index f46777d..e06f964 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -201,20 +201,17 @@ 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 { + KeyCode::Char('g') | KeyCode::Char('G') => return app.cancel(), + _ => {}, }; } 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(), + _ => app.input(key_event), } } diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index a9b58bc..c78815b 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -32,6 +32,8 @@ use crate::tui::{ }, }; +use super::app::InputStatePublic; + pub trait IUi { fn render(app: &mut APP, frame: &mut Frame); } @@ -68,7 +70,7 @@ impl Ui { frame: &mut Frame, ) { let active = selection.category(); - let areas = FrameArea::new(frame.size()); + let areas = FrameArea::new(frame.area()); let artist_state = ArtistState::new( active == Category::Artist, @@ -106,7 +108,7 @@ impl Ui { } fn render_info_overlay(artists: &Collection, selection: &mut Selection, frame: &mut Frame) { - let area = OverlayBuilder::default().build(frame.size()); + let area = OverlayBuilder::default().build(frame.area()); if selection.category() == Category::Artist { let overlay = ArtistOverlay::new(artists, &selection.widget_state_artist().list); @@ -126,13 +128,13 @@ impl Ui { let area = OverlayBuilder::default() .with_width(OverlaySize::Value(39)) .with_height(OverlaySize::Value(4)) - .build(frame.size()); + .build(frame.area()); let reload_text = ReloadOverlay::paragraph(); UiWidget::render_overlay_widget("Reload", reload_text, area, false, frame); } fn render_fetch_overlay(frame: &mut Frame) { - let area = OverlayBuilder::default().build(frame.size()); + let area = OverlayBuilder::default().build(frame.area()); let fetch_text = FetchOverlay::paragraph(); UiWidget::render_overlay_widget("Fetching", fetch_text, area, false, frame) } @@ -142,15 +144,29 @@ impl Ui { state: &mut WidgetState, frame: &mut Frame, ) { - let area = OverlayBuilder::default().build(frame.size()); + let area = OverlayBuilder::default().build(frame.area()); let st = MatchOverlay::new(info, state); UiWidget::render_overlay_list_widget(&st.matching, st.list, st.state, true, area, frame) } + fn render_input_overlay(input: InputStatePublic, frame: &mut Frame) { + let area = OverlayBuilder::default() + .with_height(OverlaySize::Value(3)) + .build(frame.area()); + UiWidget::render_overlay_widget("Input", Paragraph::new(input.value()), area, false, frame); + + let width = area.width.max(3) - 3; // keep 2 for borders and 1 for cursor + let scroll = input.visual_scroll(width as usize); + frame.set_cursor_position(( + area.x + ((input.visual_cursor()).max(scroll) - scroll) as u16 + 1, + area.y + 1, + )) + } + fn render_error_overlay>(title: S, msg: S, frame: &mut Frame) { let area = OverlayBuilder::default() .with_height(OverlaySize::Value(4)) - .build(frame.size()); + .build(frame.area()); let error_text = ErrorOverlay::paragraph(msg.as_ref()); UiWidget::render_overlay_widget(title.as_ref(), error_text, area, true, frame); } @@ -170,6 +186,7 @@ impl IUi for Ui { AppState::Reload(()) => Self::render_reload_overlay(frame), AppState::Fetch(()) => Self::render_fetch_overlay(frame), AppState::Match(public) => Self::render_match_overlay(public.info, public.state, frame), + AppState::Input(input) => Self::render_input_overlay(input, frame), AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame), AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame), _ => {} @@ -211,7 +228,7 @@ mod tests { info: m.info, state: m.state, }), - AppState::Input(s) => AppState::Input(s), + AppState::Input(ref i) => AppState::Input(i), AppState::Error(s) => AppState::Error(s), AppState::Critical(s) => AppState::Critical(s), }, -- 2.45.2 From 09e1cb7fdaee600f050846dba8c31e08edf397aa Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 14 Sep 2024 15:49:39 +0200 Subject: [PATCH 03/17] Visuals good enough --- src/tui/app/machine/input_state.rs | 23 ++++++++++++++---- src/tui/app/machine/match_state.rs | 24 ++++++++++++++----- src/tui/app/machine/mod.rs | 37 ++++++++++++++++++++++------- src/tui/app/mod.rs | 14 +++++++---- src/tui/ui/mod.rs | 38 +++++++++++++++++++++++------- 5 files changed, 105 insertions(+), 31 deletions(-) diff --git a/src/tui/app/machine/input_state.rs b/src/tui/app/machine/input_state.rs index 52e0e7d..9595759 100644 --- a/src/tui/app/machine/input_state.rs +++ b/src/tui/app/machine/input_state.rs @@ -2,7 +2,7 @@ use tui_input::{backend::crossterm::EventHandler, Input}; use crate::tui::app::{ machine::{App, AppInner, AppMachine}, - AppPublic, AppState, IAppInteractInput, InputEvent, + AppPublic, AppState, IAppInteractInput, InputClientPublic, InputEvent, InputStatePublic, }; use super::match_state::MatchState; @@ -34,11 +34,22 @@ impl From> for App { } } +impl<'a> From<&'a mut InputClient> for InputClientPublic<'a> { + fn from(client: &'a mut InputClient) -> Self { + match client { + InputClient::Match(state) => InputClientPublic::Match(state.into()), + } + } +} + 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.input), + state: AppState::Input(InputStatePublic { + input: &machine.state.input, + client: (&mut machine.state.client).into(), + }), } } } @@ -47,19 +58,21 @@ impl IAppInteractInput for AppMachine { type APP = App; fn input(mut self, input: InputEvent) -> Self::APP { - self.state.input.handle_event(&crossterm::event::Event::Key(input)); + self.state + .input + .handle_event(&crossterm::event::Event::Key(input)); self.into() } fn confirm(self) -> Self::APP { match self.state.client { - InputClient::Match(state) => AppMachine::match_state(self.inner, state).into() + 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() + 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 ee8c593..4729ede 100644 --- a/src/tui/app/machine/match_state.rs +++ b/src/tui/app/machine/match_state.rs @@ -108,14 +108,20 @@ impl From> for App { } } +impl<'a> From<&'a mut MatchState> for MatchStatePublic<'a> { + fn from(state: &'a mut MatchState) -> Self { + MatchStatePublic { + info: state.current.as_ref().map(Into::into), + state: &mut state.state, + } + } +} + impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { fn from(machine: &'a mut AppMachine) -> Self { AppPublic { inner: (&mut machine.inner).into(), - state: AppState::Match(MatchStatePublic { - info: machine.state.current.as_ref().map(Into::into), - state: &mut machine.state.state, - }), + state: AppState::Match((&mut machine.state).into()), } } } @@ -149,8 +155,14 @@ 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() + 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 e56ec0b..d6b91dd 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -192,6 +192,7 @@ impl<'a> From<&'a mut AppInner> for AppPublicInner<'a> { mod tests { use std::sync::mpsc; + use input_state::InputClient; use musichoard::collection::Collection; use crate::tui::{ @@ -267,13 +268,6 @@ mod tests { } } - 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, @@ -440,7 +434,7 @@ mod tests { } #[test] - fn state_matches() { + fn state_match() { let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); assert!(app.is_running()); @@ -465,6 +459,33 @@ mod tests { assert!(!app.is_running()); } + #[test] + fn state_input() { + let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); + assert!(app.is_running()); + + let (_, rx) = mpsc::channel(); + let fetch = FetchState::new(rx); + let match_state = MatchState::new(None, fetch); + let input_client = InputClient::Match(match_state); + app = AppMachine::input_state(app.unwrap_browse().inner, input_client).into(); + + let state = app.state(); + assert!(matches!(state, AppState::Input(_))); + app = state; + + app = app.no_op(); + let state = app.state(); + assert!(matches!(state, AppState::Input(_))); + app = state; + + let public = app.get(); + assert!(matches!(public.state, AppState::Input(_))); + + app = app.force_quit(); + assert!(!app.is_running()); + } + #[test] fn state_error() { let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index aaaaedd..85e79bc 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -1,12 +1,10 @@ mod machine; mod selection; -use crossterm::event::KeyEvent; pub use machine::App; pub use selection::{Category, Delta, Selection, WidgetState}; use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection}; -use tui_input::Input; use crate::tui::lib::interface::musicbrainz::Match; @@ -135,7 +133,7 @@ pub trait IAppInteractMatch { fn abort(self) -> Self::APP; } -type InputEvent = KeyEvent; +type InputEvent = crossterm::event::KeyEvent; pub trait IAppInteractInput { type APP: IApp; @@ -216,7 +214,15 @@ pub struct MatchStatePublic<'app> { pub state: &'app mut WidgetState, } -pub type InputStatePublic<'app> = &'app Input; +pub enum InputClientPublic<'app> { + Match(MatchStatePublic<'app>), +} + +pub type Input = tui_input::Input; +pub struct InputStatePublic<'app> { + pub input: &'app Input, + pub client: InputClientPublic<'app> +} pub type AppPublicState<'app> = AppState< (), diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index c78815b..5bacefe 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -32,7 +32,7 @@ use crate::tui::{ }, }; -use super::app::InputStatePublic; +use super::app::{Input, InputClientPublic}; pub trait IUi { fn render(app: &mut APP, frame: &mut Frame); @@ -149,16 +149,18 @@ impl Ui { UiWidget::render_overlay_list_widget(&st.matching, st.list, st.state, true, area, frame) } - fn render_input_overlay(input: InputStatePublic, frame: &mut Frame) { + fn render_input_overlay(input: &Input, frame: &mut Frame) { let area = OverlayBuilder::default() + .with_width(OverlaySize::MarginFactor(4)) .with_height(OverlaySize::Value(3)) .build(frame.area()); - UiWidget::render_overlay_widget("Input", Paragraph::new(input.value()), area, false, frame); + let text_area = format!(" {}", input.value()); + UiWidget::render_overlay_widget("Input", Paragraph::new(text_area), area, false, frame); - let width = area.width.max(3) - 3; // keep 2 for borders and 1 for cursor + let width = area.width.max(4) - 4; // keep 2 for borders, 1 for left-pad, and 1 for cursor let scroll = input.visual_scroll(width as usize); frame.set_cursor_position(( - area.x + ((input.visual_cursor()).max(scroll) - scroll) as u16 + 1, + area.x + ((input.visual_cursor()).max(scroll) - scroll) as u16 + 2, area.y + 1, )) } @@ -186,7 +188,14 @@ impl IUi for Ui { AppState::Reload(()) => Self::render_reload_overlay(frame), AppState::Fetch(()) => Self::render_fetch_overlay(frame), AppState::Match(public) => Self::render_match_overlay(public.info, public.state, frame), - AppState::Input(input) => Self::render_input_overlay(input, frame), + AppState::Input(input) => { + match input.client { + InputClientPublic::Match(public) => { + Self::render_match_overlay(public.info, public.state, frame) + } + } + Self::render_input_overlay(input.input, frame); + } AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame), AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame), _ => {} @@ -202,7 +211,10 @@ mod tests { }; use crate::tui::{ - app::{AppPublic, AppPublicInner, Delta, MatchOption, MatchStatePublic}, + app::{ + AppPublic, AppPublicInner, Delta, InputClientPublic, InputStatePublic, MatchOption, + MatchStatePublic, + }, lib::interface::musicbrainz::Match, testmod::COLLECTION, tests::terminal, @@ -228,7 +240,17 @@ mod tests { info: m.info, state: m.state, }), - AppState::Input(ref i) => AppState::Input(i), + AppState::Input(ref mut i) => AppState::Input(InputStatePublic { + input: i.input, + client: match i.client { + InputClientPublic::Match(ref mut m) => { + InputClientPublic::Match(MatchStatePublic { + info: m.info, + state: m.state, + }) + } + }, + }), AppState::Error(s) => AppState::Error(s), AppState::Critical(s) => AppState::Critical(s), }, -- 2.45.2 From 8acf20896834f3240e49c3b22d7565a00ea1949d Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 14 Sep 2024 20:55:17 +0200 Subject: [PATCH 04/17] Input no longer a state --- src/tui/app/machine/browse_state.rs | 2 + src/tui/app/machine/critical_state.rs | 2 + src/tui/app/machine/error_state.rs | 2 + src/tui/app/machine/fetch_state.rs | 7 ++- src/tui/app/machine/info_state.rs | 2 + src/tui/app/machine/input.rs | 44 +++++++++++++++ src/tui/app/machine/input_state.rs | 78 --------------------------- src/tui/app/machine/match_state.rs | 14 +++-- src/tui/app/machine/mod.rs | 49 ++++------------- src/tui/app/machine/reload_state.rs | 2 + src/tui/app/machine/search_state.rs | 2 + src/tui/app/mod.rs | 39 ++++---------- src/tui/handler.rs | 62 +++++++++++++-------- src/tui/ui/browse_state.rs | 24 +++++---- src/tui/ui/minibuffer.rs | 14 ++--- src/tui/ui/mod.rs | 49 +++++++---------- 16 files changed, 172 insertions(+), 220 deletions(-) create mode 100644 src/tui/app/machine/input.rs delete mode 100644 src/tui/app/machine/input_state.rs diff --git a/src/tui/app/machine/browse_state.rs b/src/tui/app/machine/browse_state.rs index 2e71f73..f1bd64d 100644 --- a/src/tui/app/machine/browse_state.rs +++ b/src/tui/app/machine/browse_state.rs @@ -11,6 +11,7 @@ impl AppMachine { AppMachine { inner, state: BrowseState, + input: None, } } } @@ -26,6 +27,7 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Browse(()), + input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/machine/critical_state.rs b/src/tui/app/machine/critical_state.rs index 9242cbc..80ef163 100644 --- a/src/tui/app/machine/critical_state.rs +++ b/src/tui/app/machine/critical_state.rs @@ -14,6 +14,7 @@ impl AppMachine { state: CriticalState { string: string.into(), }, + input: None } } } @@ -29,6 +30,7 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Critical(&machine.state.string), + input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/machine/error_state.rs b/src/tui/app/machine/error_state.rs index 2150b2d..42a7a46 100644 --- a/src/tui/app/machine/error_state.rs +++ b/src/tui/app/machine/error_state.rs @@ -14,6 +14,7 @@ impl AppMachine { state: ErrorState { string: string.into(), }, + input: None, } } } @@ -29,6 +30,7 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Error(&machine.state.string), + input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/machine/fetch_state.rs b/src/tui/app/machine/fetch_state.rs index 78d09eb..a16327c 100644 --- a/src/tui/app/machine/fetch_state.rs +++ b/src/tui/app/machine/fetch_state.rs @@ -40,7 +40,11 @@ pub type FetchReceiver = mpsc::Receiver; impl AppMachine { fn fetch_state(inner: AppInner, state: FetchState) -> Self { - AppMachine { inner, state } + AppMachine { + inner, + state, + input: None, + } } pub fn app_fetch_new(inner: AppInner) -> App { @@ -173,6 +177,7 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Fetch(()), + input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/machine/info_state.rs b/src/tui/app/machine/info_state.rs index a4e5ef6..c066bcf 100644 --- a/src/tui/app/machine/info_state.rs +++ b/src/tui/app/machine/info_state.rs @@ -10,6 +10,7 @@ impl AppMachine { AppMachine { inner, state: InfoState, + input: None, } } } @@ -25,6 +26,7 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Info(()), + input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/machine/input.rs b/src/tui/app/machine/input.rs new file mode 100644 index 0000000..ee5ea9d --- /dev/null +++ b/src/tui/app/machine/input.rs @@ -0,0 +1,44 @@ +use tui_input::backend::crossterm::EventHandler; + +use crate::tui::app::{ + machine::{App, AppMachine}, + IAppInput, InputEvent, InputPublic, +}; + +#[derive(Default)] +pub struct Input(tui_input::Input); + +impl<'app> From<&'app Input> for InputPublic<'app> { + fn from(value: &'app Input) -> Self { + &value.0 + } +} + +impl IAppInput for AppMachine +where + AppMachine: Into, +{ + type APP = App; + + fn taking_input(&self) -> bool { + self.input.is_some() + } + + fn input(mut self, input: InputEvent) -> Self::APP { + self.input + .as_mut() + .unwrap() // FIXME + .0 + .handle_event(&crossterm::event::Event::Key(input)); + self.into() + } + + fn confirm(self) -> Self::APP { + self.cancel() + } + + fn cancel(mut self) -> Self::APP { + self.input = None; + self.into() + } +} diff --git a/src/tui/app/machine/input_state.rs b/src/tui/app/machine/input_state.rs deleted file mode 100644 index 9595759..0000000 --- a/src/tui/app/machine/input_state.rs +++ /dev/null @@ -1,78 +0,0 @@ -use tui_input::{backend::crossterm::EventHandler, Input}; - -use crate::tui::app::{ - machine::{App, AppInner, AppMachine}, - AppPublic, AppState, IAppInteractInput, InputClientPublic, InputEvent, InputStatePublic, -}; - -use super::match_state::MatchState; - -pub struct InputState { - input: Input, - client: InputClient, -} - -pub enum InputClient { - Match(MatchState), -} - -impl AppMachine { - pub fn input_state(inner: AppInner, client: InputClient) -> Self { - AppMachine { - inner, - state: InputState { - input: Input::default(), - client, - }, - } - } -} - -impl From> for App { - fn from(machine: AppMachine) -> Self { - AppState::Input(machine) - } -} - -impl<'a> From<&'a mut InputClient> for InputClientPublic<'a> { - fn from(client: &'a mut InputClient) -> Self { - match client { - InputClient::Match(state) => InputClientPublic::Match(state.into()), - } - } -} - -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(InputStatePublic { - input: &machine.state.input, - client: (&mut machine.state.client).into(), - }), - } - } -} - -impl IAppInteractInput for AppMachine { - type APP = App; - - fn input(mut self, input: InputEvent) -> Self::APP { - self.state - .input - .handle_event(&crossterm::event::Event::Key(input)); - 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 4729ede..2ab81d2 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, input_state::InputClient}; +use super::{fetch_state::FetchState, input::Input}; impl ArtistMatches { fn len(&self) -> usize { @@ -98,7 +98,11 @@ impl MatchState { impl AppMachine { pub fn match_state(inner: AppInner, state: MatchState) -> Self { - AppMachine { inner, state } + AppMachine { + inner, + state, + input: None, + } } } @@ -122,6 +126,7 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Match((&mut machine.state).into()), + input: (&machine.input.as_ref()).map(Into::into), } } } @@ -152,7 +157,7 @@ impl IAppInteractMatch for AppMachine { self.into() } - fn select(self) -> Self::APP { + fn select(mut self) -> Self::APP { if let Some(index) = self.state.state.list.selected() { // selected() implies current exists if self @@ -162,7 +167,8 @@ impl IAppInteractMatch for AppMachine { .unwrap() .is_manual_input_mbid(index) { - return AppMachine::input_state(self.inner, InputClient::Match(self.state)).into(); + self.input = Some(Input::default()); + return self.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 d6b91dd..5598c76 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -3,7 +3,7 @@ mod critical_state; mod error_state; mod fetch_state; mod info_state; -mod input_state; +mod input; mod match_state; mod reload_state; mod search_state; @@ -21,11 +21,12 @@ 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; +use input::Input; + use super::IAppBase; pub type App = AppState< @@ -35,7 +36,6 @@ pub type App = AppState< AppMachine, AppMachine, AppMachine, - AppMachine, AppMachine, AppMachine, >; @@ -43,6 +43,7 @@ pub type App = AppState< pub struct AppMachine { inner: AppInner, state: STATE, + input: Option, } pub struct AppInner { @@ -80,7 +81,6 @@ 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, } @@ -94,7 +94,6 @@ 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, } @@ -108,7 +107,6 @@ impl IApp for App { type SearchState = AppMachine; type FetchState = AppMachine; type MatchState = AppMachine; - type InputState = AppMachine; type ErrorState = AppMachine; type CriticalState = AppMachine; @@ -130,7 +128,6 @@ impl IApp for App { Self::SearchState, Self::FetchState, Self::MatchState, - Self::InputState, Self::ErrorState, Self::CriticalState, > { @@ -155,7 +152,6 @@ impl IAppAccess for App { 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(), } @@ -192,7 +188,6 @@ impl<'a> From<&'a mut AppInner> for AppPublicInner<'a> { mod tests { use std::sync::mpsc; - use input_state::InputClient; use musichoard::collection::Collection; use crate::tui::{ @@ -210,7 +205,6 @@ mod tests { SearchState, FetchState, MatchState, - InputState, ErrorState, CriticalState, > @@ -221,7 +215,6 @@ mod tests { SearchState, FetchState, MatchState, - InputState, ErrorState, CriticalState, > @@ -415,7 +408,12 @@ mod tests { let (_, rx) = mpsc::channel(); let inner = app.unwrap_browse().inner; let state = FetchState::new(rx); - app = AppMachine { inner, state }.into(); + app = AppMachine { + inner, + state, + input: None, + } + .into(); let state = app.state(); assert!(matches!(state, AppState::Fetch(_))); @@ -459,33 +457,6 @@ mod tests { assert!(!app.is_running()); } - #[test] - fn state_input() { - let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); - assert!(app.is_running()); - - let (_, rx) = mpsc::channel(); - let fetch = FetchState::new(rx); - let match_state = MatchState::new(None, fetch); - let input_client = InputClient::Match(match_state); - app = AppMachine::input_state(app.unwrap_browse().inner, input_client).into(); - - let state = app.state(); - assert!(matches!(state, AppState::Input(_))); - app = state; - - app = app.no_op(); - let state = app.state(); - assert!(matches!(state, AppState::Input(_))); - app = state; - - let public = app.get(); - assert!(matches!(public.state, AppState::Input(_))); - - app = app.force_quit(); - assert!(!app.is_running()); - } - #[test] fn state_error() { let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); diff --git a/src/tui/app/machine/reload_state.rs b/src/tui/app/machine/reload_state.rs index 4c9323f..ad45f40 100644 --- a/src/tui/app/machine/reload_state.rs +++ b/src/tui/app/machine/reload_state.rs @@ -11,6 +11,7 @@ impl AppMachine { AppMachine { inner, state: ReloadState, + input: None, } } } @@ -25,6 +26,7 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Reload(()), + input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/machine/search_state.rs b/src/tui/app/machine/search_state.rs index a658dd1..9185e48 100644 --- a/src/tui/app/machine/search_state.rs +++ b/src/tui/app/machine/search_state.rs @@ -40,6 +40,7 @@ impl AppMachine { orig, memo: vec![], }, + input: None, } } } @@ -55,6 +56,7 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Search(&machine.state.string), + input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 85e79bc..754c089 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -15,7 +15,6 @@ pub enum AppState< SearchState, FetchState, MatchState, - InputState, ErrorState, CriticalState, > { @@ -25,7 +24,6 @@ pub enum AppState< Search(SearchState), Fetch(FetchState), Match(MatchState), - Input(InputState), Error(ErrorState), Critical(CriticalState), } @@ -38,8 +36,7 @@ pub trait IApp { type FetchState: IAppBase + IAppInteractFetch + IAppEventFetch; - type MatchState: IAppBase + IAppInteractMatch; - type InputState: IAppBase + IAppInteractInput; + type MatchState: IAppBase + IAppInteractMatch + IAppInput; type ErrorState: IAppBase + IAppInteractError; type CriticalState: IAppBase; @@ -56,7 +53,6 @@ pub trait IApp { Self::SearchState, Self::FetchState, Self::MatchState, - Self::InputState, Self::ErrorState, Self::CriticalState, >; @@ -134,9 +130,11 @@ pub trait IAppInteractMatch { } type InputEvent = crossterm::event::KeyEvent; -pub trait IAppInteractInput { +pub trait IAppInput { type APP: IApp; + fn taking_input(&self) -> bool; + fn input(self, input: InputEvent) -> Self::APP; fn confirm(self) -> Self::APP; fn cancel(self) -> Self::APP; @@ -159,6 +157,7 @@ pub trait IAppAccess { pub struct AppPublic<'app> { pub inner: AppPublicInner<'app>, pub state: AppPublicState<'app>, + pub input: AppPublicInput<'app>, } pub struct AppPublicInner<'app> { @@ -214,27 +213,8 @@ pub struct MatchStatePublic<'app> { pub state: &'app mut WidgetState, } -pub enum InputClientPublic<'app> { - Match(MatchStatePublic<'app>), -} - -pub type Input = tui_input::Input; -pub struct InputStatePublic<'app> { - pub input: &'app Input, - pub client: InputClientPublic<'app> -} - -pub type AppPublicState<'app> = AppState< - (), - (), - (), - &'app str, - (), - MatchStatePublic<'app>, - InputStatePublic<'app>, - &'app str, - &'app str, ->; +pub type AppPublicState<'app> = + AppState<(), (), (), &'app str, (), MatchStatePublic<'app>, &'app str, &'app str>; impl< BrowseState, @@ -243,7 +223,6 @@ impl< SearchState, FetchState, MatchState, - InputState, ErrorState, CriticalState, > @@ -254,7 +233,6 @@ impl< SearchState, FetchState, MatchState, - InputState, ErrorState, CriticalState, > @@ -264,6 +242,9 @@ impl< } } +pub type InputPublic<'app> = &'app tui_input::Input; +pub type AppPublicInput<'app> = Option>; + #[cfg(test)] mod tests { use super::*; diff --git a/src/tui/handler.rs b/src/tui/handler.rs index e06f964..a112b7d 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, IAppInteractInput}; +use super::app::{IAppBase, IAppEventFetch, IAppInput}; #[cfg_attr(test, automock)] pub trait IEventHandler { @@ -26,11 +26,12 @@ 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; fn handle_fetch_result_ready_event(app: APP) -> APP; + + fn handle_input_key_event>(app: Input, key_event: KeyEvent) -> APP; } pub struct EventHandler { @@ -75,8 +76,13 @@ impl IEventHandlerPrivate for EventHandler { Self::handle_search_key_event(search_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::Input(input_state) => Self::handle_input_key_event(input_state, key_event), + AppState::Match(match_state) => { + if match_state.taking_input() { + Self::handle_input_key_event(match_state, key_event) + } else { + Self::handle_match_key_event(match_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) @@ -92,7 +98,6 @@ impl IEventHandlerPrivate for EventHandler { AppState::Search(state) => state.no_op(), AppState::Fetch(fetch_state) => fetch_state.fetch_result_ready(), AppState::Match(state) => state.no_op(), - AppState::Input(state) => state.no_op(), AppState::Error(state) => state.no_op(), AppState::Critical(state) => state.no_op(), } @@ -178,6 +183,13 @@ impl IEventHandlerPrivate for EventHandler { } fn handle_fetch_key_event(app: ::FetchState, key_event: KeyEvent) -> APP { + if key_event.modifiers == KeyModifiers::CONTROL { + return match key_event.code { + KeyCode::Char('g') | KeyCode::Char('G') => app.abort(), + _ => app.no_op(), + } + } + match key_event.code { // Abort. KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(), @@ -187,6 +199,13 @@ impl IEventHandlerPrivate for EventHandler { } fn handle_match_key_event(app: ::MatchState, key_event: KeyEvent) -> APP { + if key_event.modifiers == KeyModifiers::CONTROL { + return match key_event.code { + KeyCode::Char('g') | KeyCode::Char('G') => app.abort(), + _ => app.no_op(), + } + } + match key_event.code { // Abort. KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(), @@ -199,22 +218,6 @@ impl IEventHandlerPrivate for EventHandler { } } - fn handle_input_key_event(app: ::InputState, key_event: KeyEvent) -> APP { - if key_event.modifiers == KeyModifiers::CONTROL { - match key_event.code { - KeyCode::Char('g') | KeyCode::Char('G') => return app.cancel(), - _ => {}, - }; - } - - match key_event.code { - // Return. - KeyCode::Esc | KeyCode::Enter => app.confirm(), - // Othey keys. - _ => app.input(key_event), - } - } - fn handle_error_key_event(app: ::ErrorState, _key_event: KeyEvent) -> APP { // Any key dismisses the error. app.dismiss_error() @@ -224,5 +227,22 @@ impl IEventHandlerPrivate for EventHandler { // No action is allowed. app.no_op() } + + fn handle_input_key_event>(app: Input, key_event: KeyEvent) -> APP { + if key_event.modifiers == KeyModifiers::CONTROL { + match key_event.code { + KeyCode::Char('g') | KeyCode::Char('G') => return app.cancel(), + _ => {} + }; + } + + match key_event.code { + // Return. + KeyCode::Esc => app.cancel(), + KeyCode::Enter => app.confirm(), + // Othey keys. + _ => app.input(key_event), + } + } } // GRCOV_EXCL_STOP diff --git a/src/tui/ui/browse_state.rs b/src/tui/ui/browse_state.rs index 22df107..e01845c 100644 --- a/src/tui/ui/browse_state.rs +++ b/src/tui/ui/browse_state.rs @@ -28,10 +28,14 @@ pub struct TrackArea { pub info: Rect, } -pub struct FrameArea { +pub struct BrowseArea { pub artist: ArtistArea, pub album: AlbumArea, pub track: TrackArea, +} + +pub struct FrameArea { + pub browse: BrowseArea, pub minibuffer: Rect, } @@ -91,14 +95,16 @@ impl FrameArea { }; FrameArea { - artist: ArtistArea { list: artist_list }, - album: AlbumArea { - list: album_list, - info: album_info, - }, - track: TrackArea { - list: track_list, - info: track_info, + browse: BrowseArea { + artist: ArtistArea { list: artist_list }, + album: AlbumArea { + list: album_list, + info: album_info, + }, + track: TrackArea { + list: track_list, + info: track_info, + }, }, minibuffer, } diff --git a/src/tui/ui/minibuffer.rs b/src/tui/ui/minibuffer.rs index 7247b60..7db8361 100644 --- a/src/tui/ui/minibuffer.rs +++ b/src/tui/ui/minibuffer.rs @@ -57,20 +57,16 @@ impl Minibuffer<'_> { columns, }, AppState::Fetch(()) => Minibuffer { - paragraphs: vec![Paragraph::new("fetching..."), Paragraph::new("q: abort")], + paragraphs: vec![ + Paragraph::new("fetching..."), + Paragraph::new("ctrl+g: abort"), + ], columns: 2, }, AppState::Match(public) => Minibuffer { paragraphs: vec![ Paragraph::new(UiDisplay::display_matching_info(public.info)), - Paragraph::new("q: abort"), - ], - columns: 2, - }, - AppState::Input(_) => Minibuffer { - paragraphs: vec![ - Paragraph::new("enter: confirm"), - Paragraph::new("ctrl+g: cancel"), + Paragraph::new("ctrl+g: abort"), ], columns: 2, }, diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index 5bacefe..0d14dfb 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -10,6 +10,7 @@ mod reload_state; mod style; mod widgets; +use browse_state::BrowseArea; use ratatui::{layout::Rect, widgets::Paragraph, Frame}; use musichoard::collection::{album::Album, Collection}; @@ -32,7 +33,7 @@ use crate::tui::{ }, }; -use super::app::{Input, InputClientPublic}; +use super::app::InputPublic; pub trait IUi { fn render(app: &mut APP, frame: &mut Frame); @@ -66,11 +67,10 @@ impl Ui { fn render_browse_frame( artists: &Collection, selection: &mut Selection, - state: &AppPublicState, + areas: BrowseArea, frame: &mut Frame, ) { let active = selection.category(); - let areas = FrameArea::new(frame.area()); let artist_state = ArtistState::new( active == Category::Artist, @@ -103,8 +103,6 @@ impl Ui { ); Self::render_track_column(track_state, areas.track, frame); - - Self::render_minibuffer(state, areas.minibuffer, frame); } fn render_info_overlay(artists: &Collection, selection: &mut Selection, frame: &mut Frame) { @@ -149,7 +147,7 @@ impl Ui { UiWidget::render_overlay_list_widget(&st.matching, st.list, st.state, true, area, frame) } - fn render_input_overlay(input: &Input, frame: &mut Frame) { + fn render_input_overlay(input: InputPublic, frame: &mut Frame) { let area = OverlayBuilder::default() .with_width(OverlaySize::MarginFactor(4)) .with_height(OverlaySize::Value(3)) @@ -182,24 +180,24 @@ impl IUi for Ui { let selection = app.inner.selection; let state = app.state; - Self::render_browse_frame(collection, selection, &state, frame); + let areas = FrameArea::new(frame.area()); + + Self::render_browse_frame(collection, selection, areas.browse, frame); + Self::render_minibuffer(&state, areas.minibuffer, frame); + match state { AppState::Info(()) => Self::render_info_overlay(collection, selection, frame), AppState::Reload(()) => Self::render_reload_overlay(frame), AppState::Fetch(()) => Self::render_fetch_overlay(frame), AppState::Match(public) => Self::render_match_overlay(public.info, public.state, frame), - AppState::Input(input) => { - match input.client { - InputClientPublic::Match(public) => { - Self::render_match_overlay(public.info, public.state, frame) - } - } - Self::render_input_overlay(input.input, frame); - } AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame), AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame), _ => {} } + + if let Some(input) = app.input { + Self::render_input_overlay(input, frame); + } } } @@ -211,10 +209,7 @@ mod tests { }; use crate::tui::{ - app::{ - AppPublic, AppPublicInner, Delta, InputClientPublic, InputStatePublic, MatchOption, - MatchStatePublic, - }, + app::{AppPublic, AppPublicInner, Delta, MatchOption, MatchStatePublic}, lib::interface::musicbrainz::Match, testmod::COLLECTION, tests::terminal, @@ -240,20 +235,10 @@ mod tests { info: m.info, state: m.state, }), - AppState::Input(ref mut i) => AppState::Input(InputStatePublic { - input: i.input, - client: match i.client { - InputClientPublic::Match(ref mut m) => { - InputClientPublic::Match(MatchStatePublic { - info: m.info, - state: m.state, - }) - } - }, - }), AppState::Error(s) => AppState::Error(s), AppState::Critical(s) => AppState::Critical(s), }, + input: self.input, } } } @@ -286,6 +271,7 @@ mod tests { let mut app = AppPublic { inner: public_inner(collection, selection), state: AppState::Browse(()), + input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); @@ -364,6 +350,7 @@ mod tests { info: None, state: &mut widget_state, }), + input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } @@ -393,6 +380,7 @@ mod tests { info: Some(&artist_matches), state: &mut widget_state, }), + input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } @@ -427,6 +415,7 @@ mod tests { info: Some(&album_matches), state: &mut widget_state, }), + input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } -- 2.45.2 From 20f36bcb0e145034d10ef9b74ebcdec610e51209 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 14 Sep 2024 22:10:02 +0200 Subject: [PATCH 05/17] Do state tracking via types --- src/tui/app/machine/browse_state.rs | 2 -- src/tui/app/machine/critical_state.rs | 2 -- src/tui/app/machine/error_state.rs | 2 -- src/tui/app/machine/fetch_state.rs | 7 +--- src/tui/app/machine/info_state.rs | 2 -- src/tui/app/machine/input.rs | 28 ++++++--------- src/tui/app/machine/match_state.rs | 11 +++--- src/tui/app/machine/mod.rs | 50 ++++++++++++++++++++------ src/tui/app/machine/reload_state.rs | 2 -- src/tui/app/machine/search_state.rs | 2 -- src/tui/app/mod.rs | 34 ++++++++++++++---- src/tui/handler.rs | 51 ++++++++++++++------------- src/tui/ui/mod.rs | 9 ++--- 13 files changed, 113 insertions(+), 89 deletions(-) diff --git a/src/tui/app/machine/browse_state.rs b/src/tui/app/machine/browse_state.rs index f1bd64d..2e71f73 100644 --- a/src/tui/app/machine/browse_state.rs +++ b/src/tui/app/machine/browse_state.rs @@ -11,7 +11,6 @@ impl AppMachine { AppMachine { inner, state: BrowseState, - input: None, } } } @@ -27,7 +26,6 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Browse(()), - input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/machine/critical_state.rs b/src/tui/app/machine/critical_state.rs index 80ef163..9242cbc 100644 --- a/src/tui/app/machine/critical_state.rs +++ b/src/tui/app/machine/critical_state.rs @@ -14,7 +14,6 @@ impl AppMachine { state: CriticalState { string: string.into(), }, - input: None } } } @@ -30,7 +29,6 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Critical(&machine.state.string), - input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/machine/error_state.rs b/src/tui/app/machine/error_state.rs index 42a7a46..2150b2d 100644 --- a/src/tui/app/machine/error_state.rs +++ b/src/tui/app/machine/error_state.rs @@ -14,7 +14,6 @@ impl AppMachine { state: ErrorState { string: string.into(), }, - input: None, } } } @@ -30,7 +29,6 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Error(&machine.state.string), - input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/machine/fetch_state.rs b/src/tui/app/machine/fetch_state.rs index a16327c..78d09eb 100644 --- a/src/tui/app/machine/fetch_state.rs +++ b/src/tui/app/machine/fetch_state.rs @@ -40,11 +40,7 @@ pub type FetchReceiver = mpsc::Receiver; impl AppMachine { fn fetch_state(inner: AppInner, state: FetchState) -> Self { - AppMachine { - inner, - state, - input: None, - } + AppMachine { inner, state } } pub fn app_fetch_new(inner: AppInner) -> App { @@ -177,7 +173,6 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Fetch(()), - input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/machine/info_state.rs b/src/tui/app/machine/info_state.rs index c066bcf..a4e5ef6 100644 --- a/src/tui/app/machine/info_state.rs +++ b/src/tui/app/machine/info_state.rs @@ -10,7 +10,6 @@ impl AppMachine { AppMachine { inner, state: InfoState, - input: None, } } } @@ -26,7 +25,6 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Info(()), - input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/machine/input.rs b/src/tui/app/machine/input.rs index ee5ea9d..5cd5a86 100644 --- a/src/tui/app/machine/input.rs +++ b/src/tui/app/machine/input.rs @@ -1,9 +1,8 @@ use tui_input::backend::crossterm::EventHandler; -use crate::tui::app::{ - machine::{App, AppMachine}, - IAppInput, InputEvent, InputPublic, -}; +use crate::tui::app::{machine::App, AppState, IAppInput, InputEvent, InputPublic}; + +use super::AppInputMode; #[derive(Default)] pub struct Input(tui_input::Input); @@ -14,23 +13,15 @@ impl<'app> From<&'app Input> for InputPublic<'app> { } } -impl IAppInput for AppMachine -where - AppMachine: Into, -{ +impl IAppInput for AppInputMode { type APP = App; - fn taking_input(&self) -> bool { - self.input.is_some() - } - fn input(mut self, input: InputEvent) -> Self::APP { self.input - .as_mut() - .unwrap() // FIXME .0 .handle_event(&crossterm::event::Event::Key(input)); - self.into() + self.app.inner_mut().input.replace(self.input); + self.app } fn confirm(self) -> Self::APP { @@ -38,7 +29,10 @@ where } fn cancel(mut self) -> Self::APP { - self.input = None; - self.into() + match &mut self.app { + AppState::Match(state) => state.submit_input(self.input), + _ => {} + } + self.app } } diff --git a/src/tui/app/machine/match_state.rs b/src/tui/app/machine/match_state.rs index 2ab81d2..ff67f06 100644 --- a/src/tui/app/machine/match_state.rs +++ b/src/tui/app/machine/match_state.rs @@ -98,12 +98,10 @@ impl MatchState { impl AppMachine { pub fn match_state(inner: AppInner, state: MatchState) -> Self { - AppMachine { - inner, - state, - input: None, - } + AppMachine { inner, state } } + + pub fn submit_input(&mut self, _input: Input) {} } impl From> for App { @@ -126,7 +124,6 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Match((&mut machine.state).into()), - input: (&machine.input.as_ref()).map(Into::into), } } } @@ -167,7 +164,7 @@ impl IAppInteractMatch for AppMachine { .unwrap() .is_manual_input_mbid(index) { - self.input = Some(Input::default()); + self.inner.input = Some(Input::default()); return self.into(); } } diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index 5598c76..8e89a8e 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -21,13 +21,12 @@ use critical_state::CriticalState; use error_state::ErrorState; use fetch_state::FetchState; use info_state::InfoState; +use input::Input; use match_state::MatchState; use reload_state::ReloadState; use search_state::SearchState; -use input::Input; - -use super::IAppBase; +use super::{AppMode, IAppBase}; pub type App = AppState< AppMachine, @@ -43,7 +42,6 @@ pub type App = AppState< pub struct AppMachine { inner: AppInner, state: STATE, - input: Option, } pub struct AppInner { @@ -52,6 +50,18 @@ pub struct AppInner { musicbrainz: Arc>, selection: Selection, events: EventSender, + input: Option, +} + +pub struct AppInputMode { + input: Input, + app: App, +} + +impl AppInputMode { + fn new(input: Input, app: App) -> Self { + AppInputMode { input, app } + } } impl App { @@ -109,6 +119,7 @@ impl IApp for App { type MatchState = AppMachine; type ErrorState = AppMachine; type CriticalState = AppMachine; + type InputMode = AppInputMode; fn is_running(&self) -> bool { self.inner_ref().running @@ -133,6 +144,28 @@ impl IApp for App { > { 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> IAppBase for T { @@ -171,6 +204,7 @@ impl AppInner { musicbrainz: Arc::new(Mutex::new(musicbrainz)), selection, events, + input: None, } } } @@ -180,6 +214,7 @@ impl<'a> From<&'a mut AppInner> for AppPublicInner<'a> { AppPublicInner { collection: inner.music_hoard.get_collection(), selection: &mut inner.selection, + input: inner.input.as_ref().map(Into::into), } } } @@ -408,12 +443,7 @@ mod tests { let (_, rx) = mpsc::channel(); let inner = app.unwrap_browse().inner; let state = FetchState::new(rx); - app = AppMachine { - inner, - state, - input: None, - } - .into(); + app = AppMachine { inner, state }.into(); let state = app.state(); assert!(matches!(state, AppState::Fetch(_))); diff --git a/src/tui/app/machine/reload_state.rs b/src/tui/app/machine/reload_state.rs index ad45f40..4c9323f 100644 --- a/src/tui/app/machine/reload_state.rs +++ b/src/tui/app/machine/reload_state.rs @@ -11,7 +11,6 @@ impl AppMachine { AppMachine { inner, state: ReloadState, - input: None, } } } @@ -26,7 +25,6 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Reload(()), - input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/machine/search_state.rs b/src/tui/app/machine/search_state.rs index 9185e48..a658dd1 100644 --- a/src/tui/app/machine/search_state.rs +++ b/src/tui/app/machine/search_state.rs @@ -40,7 +40,6 @@ impl AppMachine { orig, memo: vec![], }, - input: None, } } } @@ -56,7 +55,6 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { AppPublic { inner: (&mut machine.inner).into(), state: AppState::Search(&machine.state.string), - input: (&machine.input.as_ref()).map(Into::into), } } } diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 754c089..2cefc08 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -28,6 +28,11 @@ pub enum AppState< Critical(CriticalState), } +pub enum AppMode { + Browse(BrowseMode), + Input(InputMode), +} + pub trait IApp { type BrowseState: IAppBase + IAppInteractBrowse; type InfoState: IAppBase + IAppInteractInfo; @@ -36,9 +41,10 @@ pub trait IApp { type FetchState: IAppBase + IAppInteractFetch + IAppEventFetch; - type MatchState: IAppBase + IAppInteractMatch + IAppInput; + type MatchState: IAppBase + IAppInteractMatch; type ErrorState: IAppBase + IAppInteractError; type CriticalState: IAppBase; + type InputMode: IAppInput; fn is_running(&self) -> bool; fn force_quit(self) -> Self; @@ -56,6 +62,23 @@ pub trait IApp { Self::ErrorState, 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 { @@ -133,8 +156,6 @@ type InputEvent = crossterm::event::KeyEvent; pub trait IAppInput { type APP: IApp; - fn taking_input(&self) -> bool; - fn input(self, input: InputEvent) -> Self::APP; fn confirm(self) -> Self::APP; fn cancel(self) -> Self::APP; @@ -157,14 +178,16 @@ pub trait IAppAccess { pub struct AppPublic<'app> { pub inner: AppPublicInner<'app>, pub state: AppPublicState<'app>, - pub input: AppPublicInput<'app>, } pub struct AppPublicInner<'app> { pub collection: &'app Collection, pub selection: &'app mut Selection, + pub input: Option>, } +pub type InputPublic<'app> = &'app tui_input::Input; + #[derive(Clone, Debug, PartialEq, Eq)] pub enum MatchOption { Match(Match), @@ -242,9 +265,6 @@ impl< } } -pub type InputPublic<'app> = &'app tui_input::Input; -pub type AppPublicInput<'app> = Option>; - #[cfg(test)] mod tests { use super::*; diff --git a/src/tui/handler.rs b/src/tui/handler.rs index a112b7d..ef0b5fc 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, IAppInput}; +use super::app::{AppMode, IAppBase, IAppEventFetch, IAppInput}; #[cfg_attr(test, automock)] pub trait IEventHandler { @@ -64,29 +64,32 @@ impl IEventHandlerPrivate for EventHandler { }; } - match app.state() { - AppState::Browse(browse_state) => { - Self::handle_browse_key_event(browse_state, key_event) - } - AppState::Info(info_state) => Self::handle_info_key_event(info_state, key_event), - AppState::Reload(reload_state) => { - Self::handle_reload_key_event(reload_state, key_event) - } - AppState::Search(search_state) => { - Self::handle_search_key_event(search_state, key_event) - } - AppState::Fetch(fetch_state) => Self::handle_fetch_key_event(fetch_state, key_event), - AppState::Match(match_state) => { - if match_state.taking_input() { - Self::handle_input_key_event(match_state, key_event) - } else { + match app.mode() { + AppMode::Input(input_mode) => Self::handle_input_key_event(input_mode, key_event), + AppMode::Browse(browse_mode) => match browse_mode { + AppState::Browse(browse_state) => { + Self::handle_browse_key_event(browse_state, key_event) + } + AppState::Info(info_state) => Self::handle_info_key_event(info_state, key_event), + AppState::Reload(reload_state) => { + Self::handle_reload_key_event(reload_state, key_event) + } + AppState::Search(search_state) => { + Self::handle_search_key_event(search_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::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) - } + 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) + } + }, } } @@ -187,7 +190,7 @@ impl IEventHandlerPrivate for EventHandler { return match key_event.code { KeyCode::Char('g') | KeyCode::Char('G') => app.abort(), _ => app.no_op(), - } + }; } match key_event.code { @@ -203,7 +206,7 @@ impl IEventHandlerPrivate for EventHandler { return match key_event.code { KeyCode::Char('g') | KeyCode::Char('G') => app.abort(), _ => app.no_op(), - } + }; } match key_event.code { diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index 0d14dfb..76f1b65 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -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); } } @@ -224,6 +224,7 @@ mod tests { inner: AppPublicInner { collection: self.inner.collection, selection: self.inner.selection, + input: self.inner.input, }, state: match self.state { AppState::Browse(()) => AppState::Browse(()), @@ -238,7 +239,6 @@ mod tests { AppState::Error(s) => AppState::Error(s), AppState::Critical(s) => AppState::Critical(s), }, - input: self.input, } } } @@ -250,6 +250,7 @@ mod tests { AppPublicInner { collection, selection, + input: None, } } @@ -271,7 +272,6 @@ mod tests { let mut app = AppPublic { inner: public_inner(collection, selection), state: AppState::Browse(()), - input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); @@ -350,7 +350,6 @@ mod tests { info: None, state: &mut widget_state, }), - input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } @@ -380,7 +379,6 @@ mod tests { info: Some(&artist_matches), state: &mut widget_state, }), - input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } @@ -415,7 +413,6 @@ mod tests { info: Some(&album_matches), state: &mut widget_state, }), - input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } -- 2.45.2 From e8db0e98d52a3de37e8759c61c1feb8f0088e0fb Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 14 Sep 2024 22:57:48 +0200 Subject: [PATCH 06/17] Don't be verbose if not needed --- src/tui/app/machine/input.rs | 7 ++- src/tui/app/mod.rs | 86 ++++++++---------------------------- 2 files changed, 22 insertions(+), 71 deletions(-) diff --git a/src/tui/app/machine/input.rs b/src/tui/app/machine/input.rs index 5cd5a86..169bf58 100644 --- a/src/tui/app/machine/input.rs +++ b/src/tui/app/machine/input.rs @@ -29,10 +29,9 @@ impl IAppInput for AppInputMode { } fn cancel(mut self) -> Self::APP { - match &mut self.app { - AppState::Match(state) => state.submit_input(self.input), - _ => {} - } + if let AppState::Match(state) = &mut self.app { + state.submit_input(self.input) + }; self.app } } diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 2cefc08..deb5e6c 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -8,24 +8,15 @@ use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection}; use crate::tui::lib::interface::musicbrainz::Match; -pub enum AppState< - BrowseState, - InfoState, - ReloadState, - SearchState, - FetchState, - MatchState, - ErrorState, - CriticalState, -> { - Browse(BrowseState), - Info(InfoState), - Reload(ReloadState), - Search(SearchState), - Fetch(FetchState), - Match(MatchState), - Error(ErrorState), - Critical(CriticalState), +pub enum AppState { + Browse(B), + Info(I), + Reload(R), + Search(S), + Fetch(F), + Match(M), + Error(E), + Critical(C), } pub enum AppMode { @@ -33,6 +24,13 @@ pub enum AppMode { Input(InputMode), } +macro_rules! IAppState { + () => { + AppState + }; +} + pub trait IApp { type BrowseState: IAppBase + IAppInteractBrowse; type InfoState: IAppBase + IAppInteractInfo; @@ -49,36 +47,10 @@ pub trait IApp { fn is_running(&self) -> bool; fn force_quit(self) -> Self; - #[allow(clippy::type_complexity)] - fn state( - self, - ) -> AppState< - Self::BrowseState, - Self::InfoState, - Self::ReloadState, - Self::SearchState, - Self::FetchState, - Self::MatchState, - Self::ErrorState, - Self::CriticalState, - >; + fn state(self) -> IAppState!(); #[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, - >; + fn mode(self) -> AppMode; } pub trait IAppBase { @@ -239,27 +211,7 @@ pub struct MatchStatePublic<'app> { pub type AppPublicState<'app> = AppState<(), (), (), &'app str, (), MatchStatePublic<'app>, &'app str, &'app str>; -impl< - BrowseState, - InfoState, - ReloadState, - SearchState, - FetchState, - MatchState, - ErrorState, - CriticalState, - > - AppState< - BrowseState, - InfoState, - ReloadState, - SearchState, - FetchState, - MatchState, - ErrorState, - CriticalState, - > -{ +impl AppState { pub fn is_search(&self) -> bool { matches!(self, AppState::Search(_)) } -- 2.45.2 From d5716b79e27d99edac912729699de4f9d57a2b01 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 14 Sep 2024 22:58:45 +0200 Subject: [PATCH 07/17] Match naming --- src/tui/app/machine/mod.rs | 2 +- src/tui/app/mod.rs | 4 ++-- src/tui/handler.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index 8e89a8e..0295e16 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -163,7 +163,7 @@ impl IApp for App { if let Some(input) = self.inner_mut().input.take() { AppMode::Input(AppInputMode::new(input, self.state())) } else { - AppMode::Browse(self.state()) + AppMode::State(self.state()) } } } diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index deb5e6c..177860b 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -19,8 +19,8 @@ pub enum AppState { Critical(C), } -pub enum AppMode { - Browse(BrowseMode), +pub enum AppMode { + State(StateMode), Input(InputMode), } diff --git a/src/tui/handler.rs b/src/tui/handler.rs index ef0b5fc..d26d455 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -66,7 +66,7 @@ impl IEventHandlerPrivate for EventHandler { match app.mode() { AppMode::Input(input_mode) => Self::handle_input_key_event(input_mode, key_event), - AppMode::Browse(browse_mode) => match browse_mode { + AppMode::State(state_mode) => match state_mode { AppState::Browse(browse_state) => { Self::handle_browse_key_event(browse_state, key_event) } -- 2.45.2 From 4c39988d964094706ad26dac404047270ff3bc44 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 14 Sep 2024 23:08:06 +0200 Subject: [PATCH 08/17] More verbosity reduction --- src/tui/app/machine/mod.rs | 31 +++---------------------------- src/tui/app/mod.rs | 1 + 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index 0295e16..5aee12b 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -26,7 +26,7 @@ use match_state::MatchState; use reload_state::ReloadState; use search_state::SearchState; -use super::{AppMode, IAppBase}; +use super::{AppMode, IAppBase, IAppState}; pub type App = AppState< AppMachine, @@ -130,36 +130,11 @@ impl IApp for App { self } - fn state( - self, - ) -> AppState< - Self::BrowseState, - Self::InfoState, - Self::ReloadState, - Self::SearchState, - Self::FetchState, - Self::MatchState, - Self::ErrorState, - Self::CriticalState, - > { + fn state(self) -> IAppState!() { 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, - > { + fn mode(mut self) -> super::AppMode { if let Some(input) = self.inner_mut().input.take() { AppMode::Input(AppInputMode::new(input, self.state())) } else { diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 177860b..644f7f7 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -30,6 +30,7 @@ macro_rules! IAppState { Self::FetchState, Self::MatchState, Self::ErrorState, Self::CriticalState> }; } +use IAppState; pub trait IApp { type BrowseState: IAppBase + IAppInteractBrowse; -- 2.45.2 From 48e19c1f7f73ccf5d229cb85215897a1d23ee7e9 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 14 Sep 2024 23:09:47 +0200 Subject: [PATCH 09/17] Submit on confirm not cancel --- src/tui/app/machine/input.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tui/app/machine/input.rs b/src/tui/app/machine/input.rs index 169bf58..5653cf9 100644 --- a/src/tui/app/machine/input.rs +++ b/src/tui/app/machine/input.rs @@ -24,14 +24,14 @@ impl IAppInput for AppInputMode { self.app } - fn confirm(self) -> Self::APP { - self.cancel() - } - - fn cancel(mut self) -> Self::APP { + fn confirm(mut self) -> Self::APP { if let AppState::Match(state) = &mut self.app { state.submit_input(self.input) }; self.app } + + fn cancel(self) -> Self::APP { + self.app + } } -- 2.45.2 From 18bbf78a926621d1ccd6d25a32a29e99075f7632 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 14 Sep 2024 23:18:09 +0200 Subject: [PATCH 10/17] Fix unit tests --- src/tui/app/machine/match_state.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/tui/app/machine/match_state.rs b/src/tui/app/machine/match_state.rs index ff67f06..085d7ce 100644 --- a/src/tui/app/machine/match_state.rs +++ b/src/tui/app/machine/match_state.rs @@ -273,6 +273,7 @@ mod tests { match_state(Some(album_match.clone())), ); album_match.push_cannot_have_mbid(); + album_match.push_manual_input_mbid(); let mut widget_state = WidgetState::default(); widget_state.list.select(Some(0)); @@ -296,6 +297,7 @@ mod tests { let matches = AppMachine::match_state(inner(music_hoard(vec![])), app_matches); matches_info.push_cannot_have_mbid(); + matches_info.push_manual_input_mbid(); let mut widget_state = WidgetState::default(); widget_state.list.select(Some(0)); @@ -319,12 +321,19 @@ mod tests { assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); assert_eq!(matches.state.state.list.selected(), Some(2)); + // Next is ManualInputMbid let matches = matches.next_match().unwrap_match(); assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); - assert_eq!(matches.state.state.list.selected(), Some(2)); + assert_eq!(matches.state.state.list.selected(), Some(3)); - // And it's done + let matches = matches.next_match().unwrap_match(); + + assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); + assert_eq!(matches.state.state.list.selected(), Some(3)); + + // Go prev_match first as selecting on manual input does not go back to fetch. + let matches = matches.prev_match().unwrap_match(); matches.select().unwrap_fetch(); } @@ -346,6 +355,7 @@ mod tests { match_state(Some(album_match.clone())), ); album_match.push_cannot_have_mbid(); + album_match.push_manual_input_mbid(); let mut widget_state = WidgetState::default(); widget_state.list.select(Some(0)); -- 2.45.2 From 694242e386b20d2f9839c4877beb39d2ca2a387f Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 14 Sep 2024 23:26:50 +0200 Subject: [PATCH 11/17] Isolate code --- src/tui/ui/input.rs | 20 ++++++++++++++++++++ src/tui/ui/mod.rs | 14 +++++--------- 2 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 src/tui/ui/input.rs diff --git a/src/tui/ui/input.rs b/src/tui/ui/input.rs new file mode 100644 index 0000000..3a4995f --- /dev/null +++ b/src/tui/ui/input.rs @@ -0,0 +1,20 @@ +use ratatui::{layout::Rect, widgets::Paragraph, Frame}; + +use crate::tui::app::InputPublic; + +pub struct InputOverlay; + +impl InputOverlay { + pub fn paragraph<'a>(text: &str) -> Paragraph<'a> { + Paragraph::new(format!(" {text}")) + } + + pub fn place_cursor(input: InputPublic, area: Rect, frame: &mut Frame) { + let width = area.width.max(4) - 4; // keep 2 for borders, 1 for left-pad, and 1 for cursor + let scroll = input.visual_scroll(width as usize); + frame.set_cursor_position(( + area.x + ((input.visual_cursor()).max(scroll) - scroll) as u16 + 2, + area.y + 1, + )) + } +} diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index 76f1b65..70573c8 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -3,6 +3,7 @@ mod display; mod error_state; mod fetch_state; mod info_state; +mod input; mod match_state; mod minibuffer; mod overlay; @@ -25,6 +26,7 @@ use crate::tui::{ error_state::ErrorOverlay, fetch_state::FetchOverlay, info_state::{AlbumOverlay, ArtistOverlay}, + input::InputOverlay, match_state::MatchOverlay, minibuffer::Minibuffer, overlay::{OverlayBuilder, OverlaySize}, @@ -152,15 +154,9 @@ impl Ui { .with_width(OverlaySize::MarginFactor(4)) .with_height(OverlaySize::Value(3)) .build(frame.area()); - let text_area = format!(" {}", input.value()); - UiWidget::render_overlay_widget("Input", Paragraph::new(text_area), area, false, frame); - - let width = area.width.max(4) - 4; // keep 2 for borders, 1 for left-pad, and 1 for cursor - let scroll = input.visual_scroll(width as usize); - frame.set_cursor_position(( - area.x + ((input.visual_cursor()).max(scroll) - scroll) as u16 + 2, - area.y + 1, - )) + let input_text = InputOverlay::paragraph(input.value()); + UiWidget::render_overlay_widget("Input", input_text, area, false, frame); + InputOverlay::place_cursor(input, area, frame); } fn render_error_overlay>(title: S, msg: S, frame: &mut Frame) { -- 2.45.2 From e4c77c982d0c1a7be3ccb111001e9048abb14e5e Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 15 Sep 2024 10:23:44 +0200 Subject: [PATCH 12/17] Make input part of machine to reset between states --- src/tui/app/machine/browse_state.rs | 16 +++------- src/tui/app/machine/critical_state.rs | 26 +++++++-------- src/tui/app/machine/error_state.rs | 26 +++++++-------- src/tui/app/machine/fetch_state.rs | 13 +++----- src/tui/app/machine/info_state.rs | 16 +++------- src/tui/app/machine/input.rs | 2 +- src/tui/app/machine/match_state.rs | 21 ++++-------- src/tui/app/machine/mod.rs | 46 +++++++++++++++++++++++---- src/tui/app/machine/reload_state.rs | 17 ++++------ src/tui/app/machine/search_state.rs | 30 ++++++++--------- src/tui/app/mod.rs | 2 +- src/tui/ui/mod.rs | 9 ++++-- 12 files changed, 116 insertions(+), 108 deletions(-) diff --git a/src/tui/app/machine/browse_state.rs b/src/tui/app/machine/browse_state.rs index 2e71f73..06c8cfb 100644 --- a/src/tui/app/machine/browse_state.rs +++ b/src/tui/app/machine/browse_state.rs @@ -1,17 +1,14 @@ use crate::tui::app::{ machine::{App, AppInner, AppMachine}, selection::{Delta, ListSelection}, - AppPublic, AppState, IAppInteractBrowse, + AppPublicState, AppState, IAppInteractBrowse, }; pub struct BrowseState; impl AppMachine { pub fn browse_state(inner: AppInner) -> Self { - AppMachine { - inner, - state: BrowseState, - } + AppMachine::new(inner, BrowseState) } } @@ -21,12 +18,9 @@ impl From> for App { } } -impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { - fn from(machine: &'a mut AppMachine) -> Self { - AppPublic { - inner: (&mut machine.inner).into(), - state: AppState::Browse(()), - } +impl<'a> From<&'a mut BrowseState> for AppPublicState<'a> { + fn from(_state: &'a mut BrowseState) -> Self { + AppState::Browse(()) } } diff --git a/src/tui/app/machine/critical_state.rs b/src/tui/app/machine/critical_state.rs index 9242cbc..b6268c3 100644 --- a/src/tui/app/machine/critical_state.rs +++ b/src/tui/app/machine/critical_state.rs @@ -1,20 +1,23 @@ use crate::tui::app::{ machine::{App, AppInner, AppMachine}, - AppPublic, AppState, + AppPublicState, AppState, }; pub struct CriticalState { string: String, } +impl CriticalState { + fn new>(string: S) -> Self { + CriticalState { + string: string.into(), + } + } +} + impl AppMachine { pub fn critical_state>(inner: AppInner, string: S) -> Self { - AppMachine { - inner, - state: CriticalState { - string: string.into(), - }, - } + AppMachine::new(inner, CriticalState::new(string)) } } @@ -24,11 +27,8 @@ impl From> for App { } } -impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { - fn from(machine: &'a mut AppMachine) -> Self { - AppPublic { - inner: (&mut machine.inner).into(), - state: AppState::Critical(&machine.state.string), - } +impl<'a> From<&'a mut CriticalState> for AppPublicState<'a> { + fn from(state: &'a mut CriticalState) -> Self { + AppState::Critical(&state.string) } } diff --git a/src/tui/app/machine/error_state.rs b/src/tui/app/machine/error_state.rs index 2150b2d..0ff6561 100644 --- a/src/tui/app/machine/error_state.rs +++ b/src/tui/app/machine/error_state.rs @@ -1,20 +1,23 @@ use crate::tui::app::{ machine::{App, AppInner, AppMachine}, - AppPublic, AppState, IAppInteractError, + AppPublicState, AppState, IAppInteractError, }; pub struct ErrorState { string: String, } +impl ErrorState { + fn new>(string: S) -> Self { + ErrorState { + string: string.into(), + } + } +} + impl AppMachine { pub fn error_state>(inner: AppInner, string: S) -> Self { - AppMachine { - inner, - state: ErrorState { - string: string.into(), - }, - } + AppMachine::new(inner, ErrorState::new(string)) } } @@ -24,12 +27,9 @@ impl From> for App { } } -impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { - fn from(machine: &'a mut AppMachine) -> Self { - AppPublic { - inner: (&mut machine.inner).into(), - state: AppState::Error(&machine.state.string), - } +impl<'a> From<&'a mut ErrorState> for AppPublicState<'a> { + fn from(state: &'a mut ErrorState) -> Self { + AppState::Error(&state.string) } } diff --git a/src/tui/app/machine/fetch_state.rs b/src/tui/app/machine/fetch_state.rs index 78d09eb..275e5f9 100644 --- a/src/tui/app/machine/fetch_state.rs +++ b/src/tui/app/machine/fetch_state.rs @@ -15,7 +15,7 @@ use musichoard::collection::{ use crate::tui::{ app::{ machine::{App, AppInner, AppMachine}, - AppPublic, AppState, IAppEventFetch, IAppInteractFetch, MatchStateInfo, + AppPublicState, AppState, IAppEventFetch, IAppInteractFetch, MatchStateInfo, }, event::{Event, EventSender}, lib::interface::musicbrainz::{self, Error as MbError, IMusicBrainz}, @@ -40,7 +40,7 @@ pub type FetchReceiver = mpsc::Receiver; impl AppMachine { fn fetch_state(inner: AppInner, state: FetchState) -> Self { - AppMachine { inner, state } + AppMachine::new(inner, state) } pub fn app_fetch_new(inner: AppInner) -> App { @@ -168,12 +168,9 @@ impl From> for App { } } -impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { - fn from(machine: &'a mut AppMachine) -> Self { - AppPublic { - inner: (&mut machine.inner).into(), - state: AppState::Fetch(()), - } +impl<'a> From<&'a mut FetchState> for AppPublicState<'a> { + fn from(_state: &'a mut FetchState) -> Self { + AppState::Fetch(()) } } diff --git a/src/tui/app/machine/info_state.rs b/src/tui/app/machine/info_state.rs index a4e5ef6..9dd7ddc 100644 --- a/src/tui/app/machine/info_state.rs +++ b/src/tui/app/machine/info_state.rs @@ -1,16 +1,13 @@ use crate::tui::app::{ machine::{App, AppInner, AppMachine}, - AppPublic, AppState, IAppInteractInfo, + AppPublicState, AppState, IAppInteractInfo, }; pub struct InfoState; impl AppMachine { pub fn info_state(inner: AppInner) -> Self { - AppMachine { - inner, - state: InfoState, - } + AppMachine::new(inner, InfoState) } } @@ -20,12 +17,9 @@ impl From> for App { } } -impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { - fn from(machine: &'a mut AppMachine) -> Self { - AppPublic { - inner: (&mut machine.inner).into(), - state: AppState::Info(()), - } +impl<'a> From<&'a mut InfoState> for AppPublicState<'a> { + fn from(_state: &'a mut InfoState) -> Self { + AppState::Info(()) } } diff --git a/src/tui/app/machine/input.rs b/src/tui/app/machine/input.rs index 5653cf9..d1873ff 100644 --- a/src/tui/app/machine/input.rs +++ b/src/tui/app/machine/input.rs @@ -20,7 +20,7 @@ impl IAppInput for AppInputMode { self.input .0 .handle_event(&crossterm::event::Event::Key(input)); - self.app.inner_mut().input.replace(self.input); + self.app.input_mut().replace(self.input); self.app } diff --git a/src/tui/app/machine/match_state.rs b/src/tui/app/machine/match_state.rs index 085d7ce..3414cbb 100644 --- a/src/tui/app/machine/match_state.rs +++ b/src/tui/app/machine/match_state.rs @@ -2,7 +2,7 @@ use std::cmp; use crate::tui::app::{ machine::{App, AppInner, AppMachine}, - AlbumMatches, AppPublic, AppState, ArtistMatches, IAppInteractMatch, MatchOption, + AlbumMatches, AppPublicState, AppState, ArtistMatches, IAppInteractMatch, MatchOption, MatchStateInfo, MatchStatePublic, WidgetState, }; @@ -98,7 +98,7 @@ impl MatchState { impl AppMachine { pub fn match_state(inner: AppInner, state: MatchState) -> Self { - AppMachine { inner, state } + AppMachine::new(inner, state) } pub fn submit_input(&mut self, _input: Input) {} @@ -110,21 +110,12 @@ impl From> for App { } } -impl<'a> From<&'a mut MatchState> for MatchStatePublic<'a> { +impl<'a> From<&'a mut MatchState> for AppPublicState<'a> { fn from(state: &'a mut MatchState) -> Self { - MatchStatePublic { + AppState::Match(MatchStatePublic { info: state.current.as_ref().map(Into::into), state: &mut state.state, - } - } -} - -impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { - fn from(machine: &'a mut AppMachine) -> Self { - AppPublic { - inner: (&mut machine.inner).into(), - state: AppState::Match((&mut machine.state).into()), - } + }) } } @@ -164,7 +155,7 @@ impl IAppInteractMatch for AppMachine { .unwrap() .is_manual_input_mbid(index) { - self.inner.input = Some(Input::default()); + self.input.replace(Input::default()); return self.into(); } } diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index 5aee12b..acabcd1 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -26,7 +26,7 @@ use match_state::MatchState; use reload_state::ReloadState; use search_state::SearchState; -use super::{AppMode, IAppBase, IAppState}; +use super::{AppMode, AppPublicState, IAppBase, IAppState}; pub type App = AppState< AppMachine, @@ -42,6 +42,7 @@ pub type App = AppState< pub struct AppMachine { inner: AppInner, state: STATE, + input: Option, } pub struct AppInner { @@ -50,7 +51,6 @@ pub struct AppInner { musicbrainz: Arc>, selection: Selection, events: EventSender, - input: Option, } pub struct AppInputMode { @@ -108,6 +108,19 @@ impl App { AppState::Critical(critical_state) => &mut critical_state.inner, } } + + fn input_mut(&mut self) -> &mut Option { + match self { + AppState::Browse(state) => &mut state.input, + AppState::Info(state) => &mut state.input, + AppState::Reload(state) => &mut state.input, + AppState::Search(state) => &mut state.input, + AppState::Fetch(state) => &mut state.input, + AppState::Match(state) => &mut state.input, + AppState::Error(state) => &mut state.input, + AppState::Critical(state) => &mut state.input, + } + } } impl IApp for App { @@ -135,7 +148,7 @@ impl IApp for App { } fn mode(mut self) -> super::AppMode { - if let Some(input) = self.inner_mut().input.take() { + if let Some(input) = self.input_mut().take() { AppMode::Input(AppInputMode::new(input, self.state())) } else { AppMode::State(self.state()) @@ -179,7 +192,6 @@ impl AppInner { musicbrainz: Arc::new(Mutex::new(musicbrainz)), selection, events, - input: None, } } } @@ -189,7 +201,29 @@ impl<'a> From<&'a mut AppInner> for AppPublicInner<'a> { AppPublicInner { collection: inner.music_hoard.get_collection(), selection: &mut inner.selection, - input: inner.input.as_ref().map(Into::into), + } + } +} + +impl AppMachine { + pub fn new(inner: AppInner, state: State) -> Self { + AppMachine { + inner, + state, + input: None, + } + } +} + +impl<'a, State> From<&'a mut AppMachine> for AppPublic<'a> +where + &'a mut State: Into>, +{ + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: (&mut machine.state).into(), + input: machine.input.as_ref().map(Into::into), } } } @@ -418,7 +452,7 @@ mod tests { let (_, rx) = mpsc::channel(); let inner = app.unwrap_browse().inner; let state = FetchState::new(rx); - app = AppMachine { inner, state }.into(); + app = AppMachine::new(inner, state).into(); let state = app.state(); assert!(matches!(state, AppState::Fetch(_))); diff --git a/src/tui/app/machine/reload_state.rs b/src/tui/app/machine/reload_state.rs index 4c9323f..25def5b 100644 --- a/src/tui/app/machine/reload_state.rs +++ b/src/tui/app/machine/reload_state.rs @@ -1,17 +1,14 @@ use crate::tui::app::{ machine::{App, AppInner, AppMachine}, selection::KeySelection, - AppPublic, AppState, IAppInteractReload, + AppPublicState, AppState, IAppInteractReload, }; pub struct ReloadState; impl AppMachine { pub fn reload_state(inner: AppInner) -> Self { - AppMachine { - inner, - state: ReloadState, - } + AppMachine::new(inner, ReloadState) } } @@ -20,12 +17,10 @@ impl From> for App { AppState::Reload(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::Reload(()), - } + +impl<'a> From<&'a mut ReloadState> for AppPublicState<'a> { + fn from(_state: &'a mut ReloadState) -> Self { + AppState::Reload(()) } } diff --git a/src/tui/app/machine/search_state.rs b/src/tui/app/machine/search_state.rs index a658dd1..dbcb9d8 100644 --- a/src/tui/app/machine/search_state.rs +++ b/src/tui/app/machine/search_state.rs @@ -6,7 +6,7 @@ use musichoard::collection::{album::Album, artist::Artist, track::Track}; use crate::tui::app::{ machine::{App, AppInner, AppMachine}, selection::{ListSelection, SelectionState}, - AppPublic, AppState, Category, IAppInteractSearch, + AppPublicState, AppState, Category, IAppInteractSearch, }; // Unlikely that this covers all possible strings, but it should at least cover strings @@ -31,16 +31,19 @@ struct SearchStateMemo { char: bool, } +impl SearchState { + fn new(orig: ListSelection) -> Self { + SearchState { + string: String::new(), + orig, + memo: vec![], + } + } +} + impl AppMachine { pub fn search_state(inner: AppInner, orig: ListSelection) -> Self { - AppMachine { - inner, - state: SearchState { - string: String::new(), - orig, - memo: vec![], - }, - } + AppMachine::new(inner, SearchState::new(orig)) } } @@ -50,12 +53,9 @@ impl From> for App { } } -impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { - fn from(machine: &'a mut AppMachine) -> Self { - AppPublic { - inner: (&mut machine.inner).into(), - state: AppState::Search(&machine.state.string), - } +impl<'a> From<&'a mut SearchState> for AppPublicState<'a> { + fn from(state: &'a mut SearchState) -> Self { + AppState::Search(&state.string) } } diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 644f7f7..3506fd5 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -151,12 +151,12 @@ pub trait IAppAccess { pub struct AppPublic<'app> { pub inner: AppPublicInner<'app>, pub state: AppPublicState<'app>, + pub input: Option>, } pub struct AppPublicInner<'app> { pub collection: &'app Collection, pub selection: &'app mut Selection, - pub input: Option>, } pub type InputPublic<'app> = &'app tui_input::Input; diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index 70573c8..51306c6 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -191,7 +191,7 @@ impl IUi for Ui { _ => {} } - if let Some(input) = app.inner.input { + if let Some(input) = app.input { Self::render_input_overlay(input, frame); } } @@ -220,7 +220,6 @@ mod tests { inner: AppPublicInner { collection: self.inner.collection, selection: self.inner.selection, - input: self.inner.input, }, state: match self.state { AppState::Browse(()) => AppState::Browse(()), @@ -235,6 +234,7 @@ mod tests { AppState::Error(s) => AppState::Error(s), AppState::Critical(s) => AppState::Critical(s), }, + input: self.input, } } } @@ -246,7 +246,6 @@ mod tests { AppPublicInner { collection, selection, - input: None, } } @@ -268,6 +267,7 @@ mod tests { let mut app = AppPublic { inner: public_inner(collection, selection), state: AppState::Browse(()), + input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); @@ -346,6 +346,7 @@ mod tests { info: None, state: &mut widget_state, }), + input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } @@ -375,6 +376,7 @@ mod tests { info: Some(&artist_matches), state: &mut widget_state, }), + input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } @@ -409,6 +411,7 @@ mod tests { info: Some(&album_matches), state: &mut widget_state, }), + input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } -- 2.45.2 From 4b92254a4d4b84dd7e79906f9ead4950e204bf58 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 15 Sep 2024 10:44:12 +0200 Subject: [PATCH 13/17] Isolate input lifecycle in one place --- src/tui/app/machine/input.rs | 25 ++++++++++++++++++++++--- src/tui/app/machine/mod.rs | 21 +++------------------ 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/tui/app/machine/input.rs b/src/tui/app/machine/input.rs index d1873ff..189b113 100644 --- a/src/tui/app/machine/input.rs +++ b/src/tui/app/machine/input.rs @@ -1,8 +1,6 @@ use tui_input::backend::crossterm::EventHandler; -use crate::tui::app::{machine::App, AppState, IAppInput, InputEvent, InputPublic}; - -use super::AppInputMode; +use crate::tui::app::{machine::App, AppMode, AppState, IAppInput, InputEvent, InputPublic}; #[derive(Default)] pub struct Input(tui_input::Input); @@ -13,6 +11,27 @@ impl<'app> From<&'app Input> for InputPublic<'app> { } } +impl From for AppMode { + fn from(mut app: App) -> Self { + if let Some(input) = app.input_mut().take() { + AppMode::Input(AppInputMode::new(input, app)) + } else { + AppMode::State(app) + } + } +} + +pub struct AppInputMode { + input: Input, + app: App, +} + +impl AppInputMode { + pub fn new(input: Input, app: App) -> Self { + AppInputMode { input, app } + } +} + impl IAppInput for AppInputMode { type APP = App; diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index acabcd1..4c6b447 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -21,7 +21,7 @@ use critical_state::CriticalState; use error_state::ErrorState; use fetch_state::FetchState; use info_state::InfoState; -use input::Input; +use input::{AppInputMode, Input}; use match_state::MatchState; use reload_state::ReloadState; use search_state::SearchState; @@ -53,17 +53,6 @@ pub struct AppInner { events: EventSender, } -pub struct AppInputMode { - input: Input, - app: App, -} - -impl AppInputMode { - fn new(input: Input, app: App) -> Self { - AppInputMode { input, app } - } -} - impl App { pub fn new( mut music_hoard: MH, @@ -147,12 +136,8 @@ impl IApp for App { self } - fn mode(mut self) -> super::AppMode { - if let Some(input) = self.input_mut().take() { - AppMode::Input(AppInputMode::new(input, self.state())) - } else { - AppMode::State(self.state()) - } + fn mode(self) -> AppMode { + self.into() } } -- 2.45.2 From dd3633cd5fe893063dcd036184c359fa9c77cba7 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 15 Sep 2024 10:56:28 +0200 Subject: [PATCH 14/17] Don't use super:: imports --- src/external/database/json/mod.rs | 11 ++++++----- src/external/musicbrainz/api/search/artist.rs | 7 ++++--- src/external/musicbrainz/api/search/release_group.rs | 3 +-- src/tui/app/machine/fetch_state.rs | 4 +--- src/tui/app/machine/match_state.rs | 4 +--- src/tui/app/machine/mod.rs | 7 ++++--- src/tui/handler.rs | 7 +++---- src/tui/listener.rs | 2 +- src/tui/ui/mod.rs | 7 ++++--- 9 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/external/database/json/mod.rs b/src/external/database/json/mod.rs index 924220e..126a4bd 100644 --- a/src/external/database/json/mod.rs +++ b/src/external/database/json/mod.rs @@ -5,13 +5,14 @@ pub mod backend; #[cfg(test)] use mockall::automock; -use crate::core::{ - collection::Collection, - interface::database::{IDatabase, LoadError, SaveError}, +use crate::{ + core::{ + collection::Collection, + interface::database::{IDatabase, LoadError, SaveError}, + }, + external::database::serde::{deserialize::DeserializeDatabase, serialize::SerializeDatabase}, }; -use super::serde::{deserialize::DeserializeDatabase, serialize::SerializeDatabase}; - impl From for LoadError { fn from(err: serde_json::Error) -> LoadError { LoadError::SerDeError(err.to_string()) diff --git a/src/external/musicbrainz/api/search/artist.rs b/src/external/musicbrainz/api/search/artist.rs index 6ad7a41..7d0f805 100644 --- a/src/external/musicbrainz/api/search/artist.rs +++ b/src/external/musicbrainz/api/search/artist.rs @@ -4,11 +4,12 @@ use serde::Deserialize; use crate::{ collection::{artist::ArtistId, musicbrainz::Mbid}, - external::musicbrainz::api::SerdeMbid, + external::musicbrainz::api::{ + search::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin}, + SerdeMbid, + }, }; -use super::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin}; - pub enum SearchArtist<'a> { String(&'a str), } diff --git a/src/external/musicbrainz/api/search/release_group.rs b/src/external/musicbrainz/api/search/release_group.rs index 4192630..ae9394e 100644 --- a/src/external/musicbrainz/api/search/release_group.rs +++ b/src/external/musicbrainz/api/search/release_group.rs @@ -8,12 +8,11 @@ use crate::{ musicbrainz::Mbid, }, external::musicbrainz::api::{ + search::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin}, ApiDisplay, SerdeAlbumDate, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, SerdeMbid, }, }; -use super::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin}; - pub enum SearchReleaseGroup<'a> { String(&'a str), Arid(&'a Mbid), diff --git a/src/tui/app/machine/fetch_state.rs b/src/tui/app/machine/fetch_state.rs index 275e5f9..da01165 100644 --- a/src/tui/app/machine/fetch_state.rs +++ b/src/tui/app/machine/fetch_state.rs @@ -14,15 +14,13 @@ use musichoard::collection::{ use crate::tui::{ app::{ - machine::{App, AppInner, AppMachine}, + machine::{match_state::MatchState, App, AppInner, AppMachine}, AppPublicState, AppState, IAppEventFetch, IAppInteractFetch, MatchStateInfo, }, event::{Event, EventSender}, lib::interface::musicbrainz::{self, Error as MbError, IMusicBrainz}, }; -use super::match_state::MatchState; - pub struct FetchState { fetch_rx: FetchReceiver, } diff --git a/src/tui/app/machine/match_state.rs b/src/tui/app/machine/match_state.rs index 3414cbb..89964d0 100644 --- a/src/tui/app/machine/match_state.rs +++ b/src/tui/app/machine/match_state.rs @@ -1,13 +1,11 @@ use std::cmp; use crate::tui::app::{ - machine::{App, AppInner, AppMachine}, + machine::{fetch_state::FetchState, input::Input, App, AppInner, AppMachine}, AlbumMatches, AppPublicState, AppState, ArtistMatches, IAppInteractMatch, MatchOption, MatchStateInfo, MatchStatePublic, WidgetState, }; -use super::{fetch_state::FetchState, input::Input}; - impl ArtistMatches { fn len(&self) -> usize { self.list.len() diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index 4c6b447..9f611ab 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -11,7 +11,10 @@ mod search_state; use std::sync::{Arc, Mutex}; use crate::tui::{ - app::{selection::Selection, AppPublic, AppPublicInner, AppState, IApp, IAppAccess}, + app::{ + selection::Selection, AppMode, AppPublic, AppPublicInner, AppPublicState, AppState, IApp, + IAppAccess, IAppBase, IAppState, + }, event::EventSender, lib::{interface::musicbrainz::IMusicBrainz, IMusicHoard}, }; @@ -26,8 +29,6 @@ use match_state::MatchState; use reload_state::ReloadState; use search_state::SearchState; -use super::{AppMode, AppPublicState, IAppBase, IAppState}; - pub type App = AppState< AppMachine, AppMachine, diff --git a/src/tui/handler.rs b/src/tui/handler.rs index d26d455..83c4813 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -5,14 +5,13 @@ use mockall::automock; use crate::tui::{ app::{ - AppState, Delta, IApp, IAppInteractBrowse, IAppInteractError, IAppInteractFetch, - IAppInteractInfo, IAppInteractMatch, IAppInteractReload, IAppInteractSearch, + AppMode, AppState, Delta, IApp, IAppBase, IAppEventFetch, IAppInput, IAppInteractBrowse, + IAppInteractError, IAppInteractFetch, IAppInteractInfo, IAppInteractMatch, + IAppInteractReload, IAppInteractSearch, }, event::{Event, EventError, EventReceiver}, }; -use super::app::{AppMode, IAppBase, IAppEventFetch, IAppInput}; - #[cfg_attr(test, automock)] pub trait IEventHandler { fn handle_next_event(&self, app: APP) -> Result; diff --git a/src/tui/listener.rs b/src/tui/listener.rs index 1bcbbe2..f49a147 100644 --- a/src/tui/listener.rs +++ b/src/tui/listener.rs @@ -4,7 +4,7 @@ use std::thread; #[cfg(test)] use mockall::automock; -use super::event::{Event, EventError, EventSender}; +use crate::tui::event::{Event, EventError, EventSender}; #[cfg_attr(test, automock)] pub trait IEventListener { diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index 51306c6..c45862d 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -17,7 +17,10 @@ use ratatui::{layout::Rect, widgets::Paragraph, Frame}; use musichoard::collection::{album::Album, Collection}; use crate::tui::{ - app::{AppPublicState, AppState, Category, IAppAccess, MatchStateInfo, Selection, WidgetState}, + app::{ + AppPublicState, AppState, Category, IAppAccess, InputPublic, MatchStateInfo, Selection, + WidgetState, + }, ui::{ browse_state::{ AlbumArea, AlbumState, ArtistArea, ArtistState, FrameArea, TrackArea, TrackState, @@ -35,8 +38,6 @@ use crate::tui::{ }, }; -use super::app::InputPublic; - pub trait IUi { fn render(app: &mut APP, frame: &mut Frame); } -- 2.45.2 From cb09f47eb25c076687f8b8faf8cc8672994a8ffb Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 15 Sep 2024 11:00:12 +0200 Subject: [PATCH 15/17] Ui unit tests --- src/tui/ui/mod.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index c45862d..b87ddfa 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -253,12 +253,14 @@ mod tests { fn artist_matches(matching: ArtistMeta, list: Vec>) -> MatchStateInfo { let mut list: Vec> = list.into_iter().map(Into::into).collect(); list.push(MatchOption::CannotHaveMbid); + list.push(MatchOption::ManualInputMbid); MatchStateInfo::artist(matching, list) } fn album_matches(matching: AlbumMeta, list: Vec>) -> MatchStateInfo { let mut list: Vec> = list.into_iter().map(Into::into).collect(); list.push(MatchOption::CannotHaveMbid); + list.push(MatchOption::ManualInputMbid); MatchStateInfo::album(matching, list) } @@ -380,6 +382,10 @@ mod tests { input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); + + let input = tui_input::Input::default(); + app.input = Some(&input); + terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } #[test] @@ -415,5 +421,9 @@ mod tests { input: None, }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); + + let input = tui_input::Input::default(); + app.input = Some(&input); + terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } } -- 2.45.2 From b597edc86f75bdd7e0866b68a37959f3df4e8308 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 15 Sep 2024 11:44:30 +0200 Subject: [PATCH 16/17] Complete unit tests --- src/tui/app/machine/input.rs | 46 ++++++++++- src/tui/app/machine/match_state.rs | 19 ++++- src/tui/app/machine/mod.rs | 119 ++++++++++++++++++++--------- 3 files changed, 147 insertions(+), 37 deletions(-) diff --git a/src/tui/app/machine/input.rs b/src/tui/app/machine/input.rs index 189b113..9981777 100644 --- a/src/tui/app/machine/input.rs +++ b/src/tui/app/machine/input.rs @@ -45,8 +45,8 @@ impl IAppInput for AppInputMode { fn confirm(mut self) -> Self::APP { if let AppState::Match(state) = &mut self.app { - state.submit_input(self.input) - }; + state.submit_input(self.input); + } self.app } @@ -54,3 +54,45 @@ impl IAppInput for AppInputMode { self.app } } + +#[cfg(test)] +mod tests { + use crate::tui::app::{ + machine::tests::{events, mb_api, music_hoard_init}, + IApp, + }; + + use super::*; + + fn input_event(c: char) -> InputEvent { + InputEvent::new( + crossterm::event::KeyCode::Char(c), + crossterm::event::KeyModifiers::empty(), + ) + } + + #[test] + fn handle_input() { + let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); + app.input_mut().replace(Input::default()); + + let input = app.mode().unwrap_input(); + let app = input.input(input_event('H')); + + let input = app.mode().unwrap_input(); + let app = input.input(input_event('e')); + + let input = app.mode().unwrap_input(); + let app = input.input(input_event('l')); + + let input = app.mode().unwrap_input(); + let app = input.input(input_event('l')); + + let input = app.mode().unwrap_input(); + let app = input.input(input_event('o')); + + assert_eq!(app.input_ref().as_ref().unwrap().0.value(), "Hello"); + + app.mode().unwrap_input().confirm().unwrap_browse(); + } +} diff --git a/src/tui/app/machine/match_state.rs b/src/tui/app/machine/match_state.rs index 89964d0..efcf7b7 100644 --- a/src/tui/app/machine/match_state.rs +++ b/src/tui/app/machine/match_state.rs @@ -177,7 +177,7 @@ mod tests { use crate::tui::{ app::{ machine::tests::{inner, music_hoard}, - IAppAccess, + IApp, IAppAccess, IAppInput, }, lib::interface::musicbrainz::Match, }; @@ -362,4 +362,21 @@ mod tests { let matches = AppMachine::match_state(inner(music_hoard(vec![])), match_state(None)); matches.select().unwrap_browse(); } + + #[test] + fn select_manual_input() { + let matches = + AppMachine::match_state(inner(music_hoard(vec![])), match_state(Some(album_match()))); + + // album_match has two matches which means that the fourth option should be manual input. + let matches = matches.next_match().unwrap_match(); + let matches = matches.next_match().unwrap_match(); + let matches = matches.next_match().unwrap_match(); + let matches = matches.next_match().unwrap_match(); + + let app = matches.select(); + + let input = app.mode().unwrap_input(); + input.confirm().unwrap_match(); + } } diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index 9f611ab..f7c5d08 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -54,6 +54,36 @@ pub struct AppInner { events: EventSender, } +macro_rules! app_field_ref { + ($app:ident, $field:ident) => { + match $app { + AppState::Browse(state) => &state.$field, + AppState::Info(state) => &state.$field, + AppState::Reload(state) => &state.$field, + AppState::Search(state) => &state.$field, + AppState::Fetch(state) => &state.$field, + AppState::Match(state) => &state.$field, + AppState::Error(state) => &state.$field, + AppState::Critical(state) => &state.$field, + } + }; +} + +macro_rules! app_field_mut { + ($app:ident, $field:ident) => { + match $app { + AppState::Browse(state) => &mut state.$field, + AppState::Info(state) => &mut state.$field, + AppState::Reload(state) => &mut state.$field, + AppState::Search(state) => &mut state.$field, + AppState::Fetch(state) => &mut state.$field, + AppState::Match(state) => &mut state.$field, + AppState::Error(state) => &mut state.$field, + AppState::Critical(state) => &mut state.$field, + } + }; +} + impl App { pub fn new( mut music_hoard: MH, @@ -74,42 +104,20 @@ impl App { } fn inner_ref(&self) -> &AppInner { - match self { - AppState::Browse(browse_state) => &browse_state.inner, - AppState::Info(info_state) => &info_state.inner, - AppState::Reload(reload_state) => &reload_state.inner, - AppState::Search(search_state) => &search_state.inner, - AppState::Fetch(fetch_state) => &fetch_state.inner, - AppState::Match(match_state) => &match_state.inner, - AppState::Error(error_state) => &error_state.inner, - AppState::Critical(critical_state) => &critical_state.inner, - } + app_field_ref!(self, inner) } fn inner_mut(&mut self) -> &mut AppInner { - match self { - AppState::Browse(browse_state) => &mut browse_state.inner, - AppState::Info(info_state) => &mut info_state.inner, - AppState::Reload(reload_state) => &mut reload_state.inner, - 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::Error(error_state) => &mut error_state.inner, - AppState::Critical(critical_state) => &mut critical_state.inner, - } + app_field_mut!(self, inner) + } + + #[cfg(test)] + fn input_ref(&self) -> &Option { + app_field_ref!(self, input) } fn input_mut(&mut self) -> &mut Option { - match self { - AppState::Browse(state) => &mut state.input, - AppState::Info(state) => &mut state.input, - AppState::Reload(state) => &mut state.input, - AppState::Search(state) => &mut state.input, - AppState::Fetch(state) => &mut state.input, - AppState::Match(state) => &mut state.input, - AppState::Error(state) => &mut state.input, - AppState::Critical(state) => &mut state.input, - } + app_field_mut!(self, input) } } @@ -221,13 +229,29 @@ mod tests { use musichoard::collection::Collection; use crate::tui::{ - app::{AppState, IApp, IAppInteractBrowse}, + app::{AppState, IApp, IAppInput, IAppInteractBrowse}, lib::{interface::musicbrainz::MockIMusicBrainz, MockIMusicHoard}, EventChannel, }; use super::*; + impl AppMode { + fn unwrap_state(self) -> StateMode { + match self { + AppMode::State(state) => state, + _ => panic!(), + } + } + + pub fn unwrap_input(self) -> InputMode { + match self { + AppMode::Input(input) => input, + _ => panic!(), + } + } + } + impl< BrowseState, InfoState, @@ -313,7 +337,7 @@ mod tests { music_hoard } - fn music_hoard_init(collection: Collection) -> MockIMusicHoard { + pub fn music_hoard_init(collection: Collection) -> MockIMusicHoard { let mut music_hoard = music_hoard(collection); music_hoard @@ -324,11 +348,11 @@ mod tests { music_hoard } - fn mb_api() -> MockIMusicBrainz { + pub fn mb_api() -> MockIMusicBrainz { MockIMusicBrainz::new() } - fn events() -> EventSender { + pub fn events() -> EventSender { EventChannel::new().sender() } @@ -340,6 +364,33 @@ mod tests { AppInner::new(music_hoard, mb_api, events()) } + #[test] + fn input_mode() { + let app = App::new(music_hoard_init(vec![]), mb_api(), events()); + assert!(app.is_running()); + + let mode = app.mode(); + assert!(matches!(mode, AppMode::State(_))); + + let state = mode.unwrap_state(); + assert!(matches!(state, AppState::Browse(_))); + + let mut app = state; + app.input_mut().replace(Input::default()); + + let public = app.get(); + assert!(public.input.is_some()); + + let mode = app.mode(); + assert!(matches!(mode, AppMode::Input(_))); + + let mut app = mode.unwrap_input().cancel(); + assert!(matches!(app, AppState::Browse(_))); + + let public = app.get(); + assert!(public.input.is_none()); + } + #[test] fn state_browse() { let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); -- 2.45.2 From 11d3df8f68811045b2c4b39718d03cb8491afc3d Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 15 Sep 2024 15:15:27 +0200 Subject: [PATCH 17/17] Don't use crossterm type directly --- src/tui/app/machine/input.rs | 5 +++-- src/tui/app/mod.rs | 15 ++++++++++++++- src/tui/handler.rs | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/tui/app/machine/input.rs b/src/tui/app/machine/input.rs index 9981777..0494235 100644 --- a/src/tui/app/machine/input.rs +++ b/src/tui/app/machine/input.rs @@ -38,7 +38,7 @@ impl IAppInput for AppInputMode { fn input(mut self, input: InputEvent) -> Self::APP { self.input .0 - .handle_event(&crossterm::event::Event::Key(input)); + .handle_event(&crossterm::event::Event::Key(input.into())); self.app.input_mut().replace(self.input); self.app } @@ -65,10 +65,11 @@ mod tests { use super::*; fn input_event(c: char) -> InputEvent { - InputEvent::new( + crossterm::event::KeyEvent::new( crossterm::event::KeyCode::Char(c), crossterm::event::KeyModifiers::empty(), ) + .into() } #[test] diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 3506fd5..dd6b3a9 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -125,7 +125,20 @@ pub trait IAppInteractMatch { fn abort(self) -> Self::APP; } -type InputEvent = crossterm::event::KeyEvent; +pub struct InputEvent(crossterm::event::KeyEvent); + +impl From for InputEvent { + fn from(value: crossterm::event::KeyEvent) -> Self { + InputEvent(value) + } +} + +impl From for crossterm::event::KeyEvent { + fn from(value: InputEvent) -> Self { + value.0 + } +} + pub trait IAppInput { type APP: IApp; diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 83c4813..9adea18 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -243,7 +243,7 @@ impl IEventHandlerPrivate for EventHandler { KeyCode::Esc => app.cancel(), KeyCode::Enter => app.confirm(), // Othey keys. - _ => app.input(key_event), + _ => app.input(key_event.into()), } } } -- 2.45.2