Compare commits

..

No commits in common. "d5716b79e27d99edac912729699de4f9d57a2b01" and "8b008292cb143858649797e9689a9547f0d428e6" have entirely different histories.

11 changed files with 152 additions and 429 deletions

128
Cargo.lock generated
View File

@ -65,7 +65,7 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi 0.1.19",
"hermit-abi",
"libc",
"winapi",
]
@ -129,9 +129,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "castaway"
version = "0.2.3"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
dependencies = [
"rustversion",
]
@ -168,14 +168,13 @@ dependencies = [
[[package]]
name = "compact_str"
version = "0.8.0"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644"
checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
@ -198,15 +197,15 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "crossterm"
version = "0.28.1"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.4.2",
"crossterm_winapi",
"mio 1.0.2",
"libc",
"mio",
"parking_lot",
"rustix",
"signal-hook",
"signal-hook-mio",
"winapi",
@ -394,9 +393,9 @@ dependencies = [
[[package]]
name = "heck"
version = "0.5.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
@ -407,12 +406,6 @@ 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"
@ -505,14 +498,10 @@ dependencies = [
]
[[package]]
name = "instability"
version = "0.3.2"
name = "indoc"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c"
dependencies = [
"quote",
"syn 2.0.48",
]
checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
[[package]]
name = "ipnet"
@ -522,9 +511,9 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "itertools"
version = "0.13.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
@ -552,15 +541,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.158"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "lock_api"
@ -614,22 +603,10 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
dependencies = [
"libc",
"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",
"windows-sys 0.48.0",
]
[[package]]
@ -676,7 +653,6 @@ dependencies = [
"structopt",
"tempfile",
"tokio",
"tui-input",
"url",
"uuid",
"version_check",
@ -935,22 +911,21 @@ dependencies = [
[[package]]
name = "ratatui"
version = "0.28.1"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d"
checksum = "154b85ef15a5d1719bcaa193c3c81fe645cd120c156874cd660fe49fd21d1373"
dependencies = [
"bitflags 2.4.2",
"cassowary",
"compact_str",
"crossterm",
"instability",
"indoc",
"itertools",
"lru",
"paste",
"stability",
"strum",
"strum_macros",
"unicode-segmentation",
"unicode-truncate",
"unicode-width",
]
@ -1011,9 +986,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustix"
version = "0.38.37"
version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
"bitflags 2.4.2",
"errno",
@ -1152,12 +1127,12 @@ dependencies = [
[[package]]
name = "signal-hook-mio"
version = "0.2.4"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
dependencies = [
"libc",
"mio 1.0.2",
"mio",
"signal-hook",
]
@ -1214,6 +1189,16 @@ 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"
@ -1261,11 +1246,11 @@ dependencies = [
[[package]]
name = "strum_macros"
version = "0.26.4"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18"
dependencies = [
"heck 0.5.0",
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
@ -1392,7 +1377,7 @@ dependencies = [
"backtrace",
"bytes",
"libc",
"mio 0.8.10",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@ -1485,16 +1470,6 @@ 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"
@ -1542,22 +1517,11 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-truncate"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
dependencies = [
"itertools",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "unicode-width"
version = "0.1.13"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "url"

View File

@ -7,18 +7,16 @@ edition = "2021"
[dependencies]
aho-corasick = { version = "1.1.2", optional = true }
crossterm = { version = "0.28.1", optional = true}
crossterm = { version = "0.27.0", 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.28.1", optional = true}
ratatui = { version = "0.26.0", 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" }
@ -37,7 +35,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-input"]
tui = ["aho-corasick", "crossterm", "once_cell", "ratatui"]
[[bin]]
name = "musichoard"

View File

@ -1,37 +0,0 @@
use tui_input::backend::crossterm::EventHandler;
use crate::tui::app::{machine::App, AppState, IAppInput, InputEvent, InputPublic};
use super::AppInputMode;
#[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 AppInputMode {
type APP = App;
fn input(mut self, input: InputEvent) -> Self::APP {
self.input
.0
.handle_event(&crossterm::event::Event::Key(input));
self.app.inner_mut().input.replace(self.input);
self.app
}
fn confirm(self) -> Self::APP {
self.cancel()
}
fn cancel(mut self) -> Self::APP {
if let AppState::Match(state) = &mut self.app {
state.submit_input(self.input)
};
self.app
}
}

View File

@ -6,7 +6,7 @@ use crate::tui::app::{
MatchStateInfo, MatchStatePublic, WidgetState,
};
use super::{fetch_state::FetchState, input::Input};
use super::fetch_state::FetchState;
impl ArtistMatches {
fn len(&self) -> usize {
@ -16,14 +16,6 @@ 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 {
@ -34,14 +26,6 @@ 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 {
@ -52,26 +36,12 @@ impl MatchStateInfo {
}
}
fn push_cannot_have_mbid(&mut self) {
pub 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 {
@ -86,7 +56,6 @@ 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,
@ -100,8 +69,6 @@ impl AppMachine<MatchState> {
pub fn match_state(inner: AppInner, state: MatchState) -> Self {
AppMachine { inner, state }
}
pub fn submit_input(&mut self, _input: Input) {}
}
impl From<AppMachine<MatchState>> for App {
@ -110,20 +77,14 @@ impl From<AppMachine<MatchState>> 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<MatchState>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MatchState>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Match((&mut machine.state).into()),
state: AppState::Match(MatchStatePublic {
info: machine.state.current.as_ref().map(Into::into),
state: &mut machine.state.state,
}),
}
}
}
@ -154,20 +115,7 @@ impl IAppInteractMatch for AppMachine<MatchState> {
self.into()
}
fn select(mut 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)
{
self.inner.input = Some(Input::default());
return self.into();
}
}
fn select(self) -> Self::APP {
AppMachine::app_fetch_next(self.inner, self.state.fetch)
}

View File

@ -3,7 +3,6 @@ mod critical_state;
mod error_state;
mod fetch_state;
mod info_state;
mod input;
mod match_state;
mod reload_state;
mod search_state;
@ -21,12 +20,11 @@ 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 super::{AppMode, IAppBase};
use super::IAppBase;
pub type App = AppState<
AppMachine<BrowseState>,
@ -50,18 +48,6 @@ pub struct AppInner {
musicbrainz: Arc<Mutex<dyn IMusicBrainz + Send>>,
selection: Selection,
events: EventSender,
input: Option<Input>,
}
pub struct AppInputMode {
input: Input,
app: App,
}
impl AppInputMode {
fn new(input: Input, app: App) -> Self {
AppInputMode { input, app }
}
}
impl App {
@ -119,7 +105,6 @@ impl IApp for App {
type MatchState = AppMachine<MatchState>;
type ErrorState = AppMachine<ErrorState>;
type CriticalState = AppMachine<CriticalState>;
type InputMode = AppInputMode;
fn is_running(&self) -> bool {
self.inner_ref().running
@ -144,28 +129,6 @@ 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::State(self.state())
}
}
}
impl<T: Into<App>> IAppBase for T {
@ -179,14 +142,14 @@ impl<T: Into<App>> IAppBase for T {
impl IAppAccess for App {
fn get(&mut self) -> AppPublic {
match self {
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::Error(state) => state.into(),
AppState::Critical(state) => state.into(),
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(),
}
}
}
@ -204,7 +167,6 @@ impl AppInner {
musicbrainz: Arc::new(Mutex::new(musicbrainz)),
selection,
events,
input: None,
}
}
}
@ -214,7 +176,6 @@ 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),
}
}
}
@ -233,77 +194,57 @@ mod tests {
use super::*;
impl<
BrowseState,
InfoState,
ReloadState,
SearchState,
FetchState,
MatchState,
ErrorState,
CriticalState,
>
AppState<
BrowseState,
InfoState,
ReloadState,
SearchState,
FetchState,
MatchState,
ErrorState,
CriticalState,
>
{
pub fn unwrap_browse(self) -> BrowseState {
impl<BS, IS, RS, SS, FS, MS, ES, CS> AppState<BS, IS, RS, SS, FS, MS, ES, CS> {
pub fn unwrap_browse(self) -> BS {
match self {
AppState::Browse(browse) => browse,
_ => panic!(),
}
}
pub fn unwrap_info(self) -> InfoState {
pub fn unwrap_info(self) -> IS {
match self {
AppState::Info(info) => info,
_ => panic!(),
}
}
pub fn unwrap_reload(self) -> ReloadState {
pub fn unwrap_reload(self) -> RS {
match self {
AppState::Reload(reload) => reload,
_ => panic!(),
}
}
pub fn unwrap_search(self) -> SearchState {
pub fn unwrap_search(self) -> SS {
match self {
AppState::Search(search) => search,
_ => panic!(),
}
}
pub fn unwrap_fetch(self) -> FetchState {
pub fn unwrap_fetch(self) -> FS {
match self {
AppState::Fetch(fetch) => fetch,
_ => panic!(),
}
}
pub fn unwrap_match(self) -> MatchState {
pub fn unwrap_match(self) -> MS {
match self {
AppState::Match(matches) => matches,
_ => panic!(),
}
}
pub fn unwrap_error(self) -> ErrorState {
pub fn unwrap_error(self) -> ES {
match self {
AppState::Error(error) => error,
_ => panic!(),
}
}
pub fn unwrap_critical(self) -> CriticalState {
pub fn unwrap_critical(self) -> CS {
match self {
AppState::Critical(critical) => critical,
_ => panic!(),
@ -462,7 +403,7 @@ mod tests {
}
#[test]
fn state_match() {
fn state_matches() {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
assert!(app.is_running());

View File

@ -8,27 +8,24 @@ use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection};
use crate::tui::lib::interface::musicbrainz::Match;
pub enum AppState<B, I, R, S, F, M, E, C> {
Browse(B),
Info(I),
Reload(R),
Search(S),
Fetch(F),
Match(M),
Error(E),
Critical(C),
}
pub enum AppMode<StateMode, InputMode> {
State(StateMode),
Input(InputMode),
}
macro_rules! IAppState {
() => {
AppState<Self::BrowseState, Self::InfoState, Self::ReloadState, Self::SearchState,
Self::FetchState, Self::MatchState, Self::ErrorState, Self::CriticalState>
};
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 trait IApp {
@ -42,15 +39,23 @@ pub trait IApp {
type MatchState: IAppBase<APP = Self> + IAppInteractMatch<APP = Self>;
type ErrorState: IAppBase<APP = Self> + IAppInteractError<APP = Self>;
type CriticalState: IAppBase<APP = Self>;
type InputMode: IAppInput<APP = Self>;
fn is_running(&self) -> bool;
fn force_quit(self) -> Self;
fn state(self) -> IAppState!();
#[allow(clippy::type_complexity)]
fn mode(self) -> AppMode<IAppState!(), Self::InputMode>;
fn state(
self,
) -> AppState<
Self::BrowseState,
Self::InfoState,
Self::ReloadState,
Self::SearchState,
Self::FetchState,
Self::MatchState,
Self::ErrorState,
Self::CriticalState,
>;
}
pub trait IAppBase {
@ -124,15 +129,6 @@ pub trait IAppInteractMatch {
fn abort(self) -> Self::APP;
}
type InputEvent = crossterm::event::KeyEvent;
pub trait IAppInput {
type APP: IApp;
fn input(self, input: InputEvent) -> Self::APP;
fn confirm(self) -> Self::APP;
fn cancel(self) -> Self::APP;
}
pub trait IAppInteractError {
type APP: IApp;
@ -155,16 +151,12 @@ pub struct AppPublic<'app> {
pub struct AppPublicInner<'app> {
pub collection: &'app Collection,
pub selection: &'app mut Selection,
pub input: Option<InputPublic<'app>>,
}
pub type InputPublic<'app> = &'app tui_input::Input;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MatchOption<T> {
Match(Match<T>),
CannotHaveMbid,
ManualInputMbid,
}
impl<T> From<Match<T>> for MatchOption<T> {
@ -211,7 +203,7 @@ pub struct MatchStatePublic<'app> {
pub type AppPublicState<'app> =
AppState<(), (), (), &'app str, (), MatchStatePublic<'app>, &'app str, &'app str>;
impl<B, I, R, S, F, M, E, C> AppState<B, I, R, S, F, M, E, C> {
impl<BS, IS, RS, SS, FS, MS, ES, CS> AppState<BS, IS, RS, SS, FS, MS, ES, CS> {
pub fn is_search(&self) -> bool {
matches!(self, AppState::Search(_))
}

View File

@ -11,7 +11,7 @@ use crate::tui::{
event::{Event, EventError, EventReceiver},
};
use super::app::{AppMode, IAppBase, IAppEventFetch, IAppInput};
use super::app::{IAppBase, IAppEventFetch};
#[cfg_attr(test, automock)]
pub trait IEventHandler<APP: IApp> {
@ -30,8 +30,6 @@ trait IEventHandlerPrivate<APP: IApp> {
fn handle_critical_key_event(app: <APP as IApp>::CriticalState, key_event: KeyEvent) -> APP;
fn handle_fetch_result_ready_event(app: APP) -> APP;
fn handle_input_key_event<Input: IAppInput<APP = APP>>(app: Input, key_event: KeyEvent) -> APP;
}
pub struct EventHandler {
@ -64,9 +62,7 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
};
}
match app.mode() {
AppMode::Input(input_mode) => Self::handle_input_key_event(input_mode, key_event),
AppMode::State(state_mode) => match state_mode {
match app.state() {
AppState::Browse(browse_state) => {
Self::handle_browse_key_event(browse_state, key_event)
}
@ -77,32 +73,25 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
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::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)
}
},
}
}
fn handle_fetch_result_ready_event(app: APP) -> APP {
match app.state() {
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::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::Fetch(fetch_state) => fetch_state.fetch_result_ready(),
AppState::Match(state) => state.no_op(),
AppState::Error(state) => state.no_op(),
AppState::Critical(state) => state.no_op(),
AppState::Match(match_state) => match_state.no_op(),
AppState::Error(error_state) => error_state.no_op(),
AppState::Critical(critical_state) => critical_state.no_op(),
}
}
@ -186,13 +175,6 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
}
fn handle_fetch_key_event(app: <APP as IApp>::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(),
@ -202,13 +184,6 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
}
fn handle_match_key_event(app: <APP as IApp>::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(),
@ -230,22 +205,5 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
// No action is allowed.
app.no_op()
}
fn handle_input_key_event<Input: IAppInput<APP = APP>>(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

View File

@ -28,14 +28,10 @@ pub struct TrackArea {
pub info: Rect,
}
pub struct BrowseArea {
pub struct FrameArea {
pub artist: ArtistArea,
pub album: AlbumArea,
pub track: TrackArea,
}
pub struct FrameArea {
pub browse: BrowseArea,
pub minibuffer: Rect,
}
@ -95,7 +91,6 @@ impl FrameArea {
};
FrameArea {
browse: BrowseArea {
artist: ArtistArea { list: artist_list },
album: AlbumArea {
list: album_list,
@ -105,7 +100,6 @@ impl FrameArea {
list: track_list,
info: track_info,
},
},
minibuffer,
}
}

View File

@ -137,7 +137,6 @@ impl UiDisplay {
match_artist.score,
),
MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(),
MatchOption::ManualInputMbid => Self::display_manual_input_mbid().to_string(),
}
}
@ -154,17 +153,12 @@ 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)]

View File

@ -57,16 +57,13 @@ impl Minibuffer<'_> {
columns,
},
AppState::Fetch(()) => Minibuffer {
paragraphs: vec![
Paragraph::new("fetching..."),
Paragraph::new("ctrl+g: abort"),
],
paragraphs: vec![Paragraph::new("fetching..."), Paragraph::new("q: abort")],
columns: 2,
},
AppState::Match(public) => Minibuffer {
paragraphs: vec![
Paragraph::new(UiDisplay::display_matching_info(public.info)),
Paragraph::new("ctrl+g: abort"),
Paragraph::new("q: abort"),
],
columns: 2,
},

View File

@ -10,7 +10,6 @@ mod reload_state;
mod style;
mod widgets;
use browse_state::BrowseArea;
use ratatui::{layout::Rect, widgets::Paragraph, Frame};
use musichoard::collection::{album::Album, Collection};
@ -33,8 +32,6 @@ use crate::tui::{
},
};
use super::app::InputPublic;
pub trait IUi {
fn render<APP: IAppAccess>(app: &mut APP, frame: &mut Frame);
}
@ -67,10 +64,11 @@ impl Ui {
fn render_browse_frame(
artists: &Collection,
selection: &mut Selection,
areas: BrowseArea,
state: &AppPublicState,
frame: &mut Frame,
) {
let active = selection.category();
let areas = FrameArea::new(frame.size());
let artist_state = ArtistState::new(
active == Category::Artist,
@ -103,10 +101,12 @@ 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) {
let area = OverlayBuilder::default().build(frame.area());
let area = OverlayBuilder::default().build(frame.size());
if selection.category() == Category::Artist {
let overlay = ArtistOverlay::new(artists, &selection.widget_state_artist().list);
@ -126,13 +126,13 @@ impl Ui {
let area = OverlayBuilder::default()
.with_width(OverlaySize::Value(39))
.with_height(OverlaySize::Value(4))
.build(frame.area());
.build(frame.size());
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.area());
let area = OverlayBuilder::default().build(frame.size());
let fetch_text = FetchOverlay::paragraph();
UiWidget::render_overlay_widget("Fetching", fetch_text, area, false, frame)
}
@ -142,31 +142,15 @@ impl Ui {
state: &mut WidgetState,
frame: &mut Frame,
) {
let area = OverlayBuilder::default().build(frame.area());
let area = OverlayBuilder::default().build(frame.size());
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: InputPublic, frame: &mut Frame) {
let area = OverlayBuilder::default()
.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,
))
}
fn render_error_overlay<S: AsRef<str>>(title: S, msg: S, frame: &mut Frame) {
let area = OverlayBuilder::default()
.with_height(OverlaySize::Value(4))
.build(frame.area());
.build(frame.size());
let error_text = ErrorOverlay::paragraph(msg.as_ref());
UiWidget::render_overlay_widget(title.as_ref(), error_text, area, true, frame);
}
@ -180,11 +164,7 @@ impl IUi for Ui {
let selection = app.inner.selection;
let state = app.state;
let areas = FrameArea::new(frame.area());
Self::render_browse_frame(collection, selection, areas.browse, frame);
Self::render_minibuffer(&state, areas.minibuffer, frame);
Self::render_browse_frame(collection, selection, &state, frame);
match state {
AppState::Info(()) => Self::render_info_overlay(collection, selection, frame),
AppState::Reload(()) => Self::render_reload_overlay(frame),
@ -194,10 +174,6 @@ impl IUi for Ui {
AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame),
_ => {}
}
if let Some(input) = app.inner.input {
Self::render_input_overlay(input, frame);
}
}
}
@ -224,7 +200,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(()),
@ -250,7 +225,6 @@ mod tests {
AppPublicInner {
collection,
selection,
input: None,
}
}