Add manual input elements to the app an ui (#216)
Part 2 of #188 Reviewed-on: #216
This commit is contained in:
parent
8b008292cb
commit
38517caf4e
130
Cargo.lock
generated
130
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
11
src/external/database/json/mod.rs
vendored
11
src/external/database/json/mod.rs
vendored
@ -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<serde_json::Error> for LoadError {
|
||||
fn from(err: serde_json::Error) -> LoadError {
|
||||
LoadError::SerDeError(err.to_string())
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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<BrowseState> {
|
||||
pub fn browse_state(inner: AppInner) -> Self {
|
||||
AppMachine {
|
||||
inner,
|
||||
state: BrowseState,
|
||||
}
|
||||
AppMachine::new(inner, BrowseState)
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,12 +18,9 @@ impl From<AppMachine<BrowseState>> for App {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut AppMachine<BrowseState>> for AppPublic<'a> {
|
||||
fn from(machine: &'a mut AppMachine<BrowseState>) -> 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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<S: Into<String>>(string: S) -> Self {
|
||||
CriticalState {
|
||||
string: string.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppMachine<CriticalState> {
|
||||
pub fn critical_state<S: Into<String>>(inner: AppInner, string: S) -> Self {
|
||||
AppMachine {
|
||||
inner,
|
||||
state: CriticalState {
|
||||
string: string.into(),
|
||||
},
|
||||
}
|
||||
AppMachine::new(inner, CriticalState::new(string))
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,11 +27,8 @@ impl From<AppMachine<CriticalState>> for App {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut AppMachine<CriticalState>> for AppPublic<'a> {
|
||||
fn from(machine: &'a mut AppMachine<CriticalState>) -> 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)
|
||||
}
|
||||
}
|
||||
|
@ -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<S: Into<String>>(string: S) -> Self {
|
||||
ErrorState {
|
||||
string: string.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppMachine<ErrorState> {
|
||||
pub fn error_state<S: Into<String>>(inner: AppInner, string: S) -> Self {
|
||||
AppMachine {
|
||||
inner,
|
||||
state: ErrorState {
|
||||
string: string.into(),
|
||||
},
|
||||
}
|
||||
AppMachine::new(inner, ErrorState::new(string))
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,12 +27,9 @@ impl From<AppMachine<ErrorState>> for App {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut AppMachine<ErrorState>> for AppPublic<'a> {
|
||||
fn from(machine: &'a mut AppMachine<ErrorState>) -> 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,15 +14,13 @@ use musichoard::collection::{
|
||||
|
||||
use crate::tui::{
|
||||
app::{
|
||||
machine::{App, AppInner, AppMachine},
|
||||
AppPublic, AppState, IAppEventFetch, IAppInteractFetch, MatchStateInfo,
|
||||
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,
|
||||
}
|
||||
@ -40,7 +38,7 @@ pub type FetchReceiver = mpsc::Receiver<FetchResult>;
|
||||
|
||||
impl AppMachine<FetchState> {
|
||||
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 +166,9 @@ impl From<AppMachine<FetchState>> for App {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut AppMachine<FetchState>> for AppPublic<'a> {
|
||||
fn from(machine: &'a mut AppMachine<FetchState>) -> 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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,13 @@
|
||||
use crate::tui::app::{
|
||||
machine::{App, AppInner, AppMachine},
|
||||
AppPublic, AppState, IAppInteractInfo,
|
||||
AppPublicState, AppState, IAppInteractInfo,
|
||||
};
|
||||
|
||||
pub struct InfoState;
|
||||
|
||||
impl AppMachine<InfoState> {
|
||||
pub fn info_state(inner: AppInner) -> Self {
|
||||
AppMachine {
|
||||
inner,
|
||||
state: InfoState,
|
||||
}
|
||||
AppMachine::new(inner, InfoState)
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,12 +17,9 @@ impl From<AppMachine<InfoState>> for App {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut AppMachine<InfoState>> for AppPublic<'a> {
|
||||
fn from(machine: &'a mut AppMachine<InfoState>) -> 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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
99
src/tui/app/machine/input.rs
Normal file
99
src/tui/app/machine/input.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use tui_input::backend::crossterm::EventHandler;
|
||||
|
||||
use crate::tui::app::{machine::App, AppMode, AppState, 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 From<App> for AppMode<App, AppInputMode> {
|
||||
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;
|
||||
|
||||
fn input(mut self, input: InputEvent) -> Self::APP {
|
||||
self.input
|
||||
.0
|
||||
.handle_event(&crossterm::event::Event::Key(input.into()));
|
||||
self.app.input_mut().replace(self.input);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
crossterm::event::KeyEvent::new(
|
||||
crossterm::event::KeyCode::Char(c),
|
||||
crossterm::event::KeyModifiers::empty(),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[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();
|
||||
}
|
||||
}
|
@ -1,13 +1,11 @@
|
||||
use std::cmp;
|
||||
|
||||
use crate::tui::app::{
|
||||
machine::{App, AppInner, AppMachine},
|
||||
AlbumMatches, AppPublic, AppState, ArtistMatches, IAppInteractMatch, MatchOption,
|
||||
machine::{fetch_state::FetchState, input::Input, App, AppInner, AppMachine},
|
||||
AlbumMatches, AppPublicState, AppState, ArtistMatches, IAppInteractMatch, MatchOption,
|
||||
MatchStateInfo, MatchStatePublic, WidgetState,
|
||||
};
|
||||
|
||||
use super::fetch_state::FetchState;
|
||||
|
||||
impl ArtistMatches {
|
||||
fn len(&self) -> usize {
|
||||
self.list.len()
|
||||
@ -16,6 +14,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 +32,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 +50,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 +84,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,
|
||||
@ -67,8 +96,10 @@ impl MatchState {
|
||||
|
||||
impl AppMachine<MatchState> {
|
||||
pub fn match_state(inner: AppInner, state: MatchState) -> Self {
|
||||
AppMachine { inner, state }
|
||||
AppMachine::new(inner, state)
|
||||
}
|
||||
|
||||
pub fn submit_input(&mut self, _input: Input) {}
|
||||
}
|
||||
|
||||
impl From<AppMachine<MatchState>> for App {
|
||||
@ -77,15 +108,12 @@ impl From<AppMachine<MatchState>> for App {
|
||||
}
|
||||
}
|
||||
|
||||
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(MatchStatePublic {
|
||||
info: machine.state.current.as_ref().map(Into::into),
|
||||
state: &mut machine.state.state,
|
||||
}),
|
||||
}
|
||||
impl<'a> From<&'a mut MatchState> for AppPublicState<'a> {
|
||||
fn from(state: &'a mut MatchState) -> Self {
|
||||
AppState::Match(MatchStatePublic {
|
||||
info: state.current.as_ref().map(Into::into),
|
||||
state: &mut state.state,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,7 +143,20 @@ impl IAppInteractMatch for AppMachine<MatchState> {
|
||||
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
|
||||
.state
|
||||
.current
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_manual_input_mbid(index)
|
||||
{
|
||||
self.input.replace(Input::default());
|
||||
return self.into();
|
||||
}
|
||||
}
|
||||
AppMachine::app_fetch_next(self.inner, self.state.fetch)
|
||||
}
|
||||
|
||||
@ -136,7 +177,7 @@ mod tests {
|
||||
use crate::tui::{
|
||||
app::{
|
||||
machine::tests::{inner, music_hoard},
|
||||
IAppAccess,
|
||||
IApp, IAppAccess, IAppInput,
|
||||
},
|
||||
lib::interface::musicbrainz::Match,
|
||||
};
|
||||
@ -221,6 +262,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));
|
||||
@ -244,6 +286,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));
|
||||
@ -267,12 +310,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();
|
||||
}
|
||||
|
||||
@ -294,6 +344,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));
|
||||
@ -311,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();
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ mod critical_state;
|
||||
mod error_state;
|
||||
mod fetch_state;
|
||||
mod info_state;
|
||||
mod input;
|
||||
mod match_state;
|
||||
mod reload_state;
|
||||
mod search_state;
|
||||
@ -10,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},
|
||||
};
|
||||
@ -20,12 +24,11 @@ use critical_state::CriticalState;
|
||||
use error_state::ErrorState;
|
||||
use fetch_state::FetchState;
|
||||
use info_state::InfoState;
|
||||
use input::{AppInputMode, Input};
|
||||
use match_state::MatchState;
|
||||
use reload_state::ReloadState;
|
||||
use search_state::SearchState;
|
||||
|
||||
use super::IAppBase;
|
||||
|
||||
pub type App = AppState<
|
||||
AppMachine<BrowseState>,
|
||||
AppMachine<InfoState>,
|
||||
@ -40,6 +43,7 @@ pub type App = AppState<
|
||||
pub struct AppMachine<STATE> {
|
||||
inner: AppInner,
|
||||
state: STATE,
|
||||
input: Option<Input>,
|
||||
}
|
||||
|
||||
pub struct AppInner {
|
||||
@ -50,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<MH: IMusicHoard + 'static, MB: IMusicBrainz + Send + 'static>(
|
||||
mut music_hoard: MH,
|
||||
@ -70,29 +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<Input> {
|
||||
app_field_ref!(self, input)
|
||||
}
|
||||
|
||||
fn input_mut(&mut self) -> &mut Option<Input> {
|
||||
app_field_mut!(self, input)
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,6 +130,7 @@ 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
|
||||
@ -115,20 +141,13 @@ 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(self) -> AppMode<IAppState!(), Self::InputMode> {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<App>> IAppBase for T {
|
||||
@ -142,14 +161,14 @@ impl<T: Into<App>> IAppBase for T {
|
||||
impl IAppAccess for App {
|
||||
fn get(&mut self) -> AppPublic {
|
||||
match self {
|
||||
AppState::Browse(browse) => browse.into(),
|
||||
AppState::Info(info) => info.into(),
|
||||
AppState::Reload(reload) => reload.into(),
|
||||
AppState::Search(search) => search.into(),
|
||||
AppState::Fetch(fetch) => fetch.into(),
|
||||
AppState::Match(matches) => matches.into(),
|
||||
AppState::Error(error) => error.into(),
|
||||
AppState::Critical(critical) => critical.into(),
|
||||
AppState::Browse(state) => state.into(),
|
||||
AppState::Info(state) => state.into(),
|
||||
AppState::Reload(state) => state.into(),
|
||||
AppState::Search(state) => state.into(),
|
||||
AppState::Fetch(state) => state.into(),
|
||||
AppState::Match(state) => state.into(),
|
||||
AppState::Error(state) => state.into(),
|
||||
AppState::Critical(state) => state.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -180,6 +199,29 @@ impl<'a> From<&'a mut AppInner> for AppPublicInner<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<State> AppMachine<State> {
|
||||
pub fn new(inner: AppInner, state: State) -> Self {
|
||||
AppMachine {
|
||||
inner,
|
||||
state,
|
||||
input: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, State> From<&'a mut AppMachine<State>> for AppPublic<'a>
|
||||
where
|
||||
&'a mut State: Into<AppPublicState<'a>>,
|
||||
{
|
||||
fn from(machine: &'a mut AppMachine<State>) -> Self {
|
||||
AppPublic {
|
||||
inner: (&mut machine.inner).into(),
|
||||
state: (&mut machine.state).into(),
|
||||
input: machine.input.as_ref().map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::mpsc;
|
||||
@ -187,64 +229,100 @@ 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<BS, IS, RS, SS, FS, MS, ES, CS> AppState<BS, IS, RS, SS, FS, MS, ES, CS> {
|
||||
pub fn unwrap_browse(self) -> BS {
|
||||
impl<StateMode, InputMode> AppMode<StateMode, InputMode> {
|
||||
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,
|
||||
ReloadState,
|
||||
SearchState,
|
||||
FetchState,
|
||||
MatchState,
|
||||
ErrorState,
|
||||
CriticalState,
|
||||
>
|
||||
AppState<
|
||||
BrowseState,
|
||||
InfoState,
|
||||
ReloadState,
|
||||
SearchState,
|
||||
FetchState,
|
||||
MatchState,
|
||||
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_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!(),
|
||||
@ -259,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
|
||||
@ -270,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()
|
||||
}
|
||||
|
||||
@ -286,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());
|
||||
@ -384,7 +489,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(_)));
|
||||
@ -403,7 +508,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());
|
||||
|
||||
|
@ -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<ReloadState> {
|
||||
pub fn reload_state(inner: AppInner) -> Self {
|
||||
AppMachine {
|
||||
inner,
|
||||
state: ReloadState,
|
||||
}
|
||||
AppMachine::new(inner, ReloadState)
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,12 +17,10 @@ impl From<AppMachine<ReloadState>> for App {
|
||||
AppState::Reload(machine)
|
||||
}
|
||||
}
|
||||
impl<'a> From<&'a mut AppMachine<ReloadState>> for AppPublic<'a> {
|
||||
fn from(machine: &'a mut AppMachine<ReloadState>) -> 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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<SearchState> {
|
||||
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<AppMachine<SearchState>> for App {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut AppMachine<SearchState>> for AppPublic<'a> {
|
||||
fn from(machine: &'a mut AppMachine<SearchState>) -> 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,26 +8,30 @@ 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<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>
|
||||
};
|
||||
}
|
||||
use IAppState;
|
||||
|
||||
pub trait IApp {
|
||||
type BrowseState: IAppBase<APP = Self> + IAppInteractBrowse<APP = Self>;
|
||||
type InfoState: IAppBase<APP = Self> + IAppInteractInfo<APP = Self>;
|
||||
@ -39,23 +43,15 @@ 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 state(
|
||||
self,
|
||||
) -> AppState<
|
||||
Self::BrowseState,
|
||||
Self::InfoState,
|
||||
Self::ReloadState,
|
||||
Self::SearchState,
|
||||
Self::FetchState,
|
||||
Self::MatchState,
|
||||
Self::ErrorState,
|
||||
Self::CriticalState,
|
||||
>;
|
||||
fn mode(self) -> AppMode<IAppState!(), Self::InputMode>;
|
||||
}
|
||||
|
||||
pub trait IAppBase {
|
||||
@ -129,6 +125,28 @@ pub trait IAppInteractMatch {
|
||||
fn abort(self) -> Self::APP;
|
||||
}
|
||||
|
||||
pub struct InputEvent(crossterm::event::KeyEvent);
|
||||
|
||||
impl From<crossterm::event::KeyEvent> for InputEvent {
|
||||
fn from(value: crossterm::event::KeyEvent) -> Self {
|
||||
InputEvent(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InputEvent> for crossterm::event::KeyEvent {
|
||||
fn from(value: InputEvent) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@ -146,6 +164,7 @@ pub trait IAppAccess {
|
||||
pub struct AppPublic<'app> {
|
||||
pub inner: AppPublicInner<'app>,
|
||||
pub state: AppPublicState<'app>,
|
||||
pub input: Option<InputPublic<'app>>,
|
||||
}
|
||||
|
||||
pub struct AppPublicInner<'app> {
|
||||
@ -153,10 +172,13 @@ pub struct AppPublicInner<'app> {
|
||||
pub selection: &'app mut Selection,
|
||||
}
|
||||
|
||||
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> {
|
||||
@ -203,7 +225,7 @@ pub struct MatchStatePublic<'app> {
|
||||
pub type AppPublicState<'app> =
|
||||
AppState<(), (), (), &'app str, (), MatchStatePublic<'app>, &'app str, &'app str>;
|
||||
|
||||
impl<BS, IS, RS, SS, FS, MS, ES, CS> AppState<BS, IS, RS, SS, FS, MS, ES, CS> {
|
||||
impl<B, I, R, S, F, M, E, C> AppState<B, I, R, S, F, M, E, C> {
|
||||
pub fn is_search(&self) -> bool {
|
||||
matches!(self, AppState::Search(_))
|
||||
}
|
||||
|
@ -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::{IAppBase, IAppEventFetch};
|
||||
|
||||
#[cfg_attr(test, automock)]
|
||||
pub trait IEventHandler<APP: IApp> {
|
||||
fn handle_next_event(&self, app: APP) -> Result<APP, EventError>;
|
||||
@ -30,6 +29,8 @@ 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 {
|
||||
@ -62,36 +63,45 @@ impl<APP: IApp> IEventHandlerPrivate<APP> 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) => 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)
|
||||
}
|
||||
match app.mode() {
|
||||
AppMode::Input(input_mode) => Self::handle_input_key_event(input_mode, key_event),
|
||||
AppMode::State(state_mode) => match state_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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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::Error(state) => state.no_op(),
|
||||
AppState::Critical(state) => state.no_op(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,6 +185,13 @@ 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(),
|
||||
@ -184,6 +201,13 @@ 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(),
|
||||
@ -205,5 +229,22 @@ 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.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
// GRCOV_EXCL_STOP
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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)]
|
||||
|
20
src/tui/ui/input.rs
Normal file
20
src/tui/ui/input.rs
Normal file
@ -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,
|
||||
))
|
||||
}
|
||||
}
|
@ -57,13 +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"),
|
||||
Paragraph::new("ctrl+g: abort"),
|
||||
],
|
||||
columns: 2,
|
||||
},
|
||||
|
@ -3,6 +3,7 @@ mod display;
|
||||
mod error_state;
|
||||
mod fetch_state;
|
||||
mod info_state;
|
||||
mod input;
|
||||
mod match_state;
|
||||
mod minibuffer;
|
||||
mod overlay;
|
||||
@ -10,12 +11,16 @@ mod reload_state;
|
||||
mod style;
|
||||
mod widgets;
|
||||
|
||||
use browse_state::BrowseArea;
|
||||
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,
|
||||
@ -24,6 +29,7 @@ use crate::tui::{
|
||||
error_state::ErrorOverlay,
|
||||
fetch_state::FetchOverlay,
|
||||
info_state::{AlbumOverlay, ArtistOverlay},
|
||||
input::InputOverlay,
|
||||
match_state::MatchOverlay,
|
||||
minibuffer::Minibuffer,
|
||||
overlay::{OverlayBuilder, OverlaySize},
|
||||
@ -64,11 +70,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.size());
|
||||
|
||||
let artist_state = ArtistState::new(
|
||||
active == Category::Artist,
|
||||
@ -101,12 +106,10 @@ 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.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 +129,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 +145,25 @@ 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: InputPublic, frame: &mut Frame) {
|
||||
let area = OverlayBuilder::default()
|
||||
.with_width(OverlaySize::MarginFactor(4))
|
||||
.with_height(OverlaySize::Value(3))
|
||||
.build(frame.area());
|
||||
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<S: AsRef<str>>(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);
|
||||
}
|
||||
@ -164,7 +177,11 @@ 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),
|
||||
@ -174,6 +191,10 @@ impl IUi for Ui {
|
||||
AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(input) = app.input {
|
||||
Self::render_input_overlay(input, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,6 +235,7 @@ mod tests {
|
||||
AppState::Error(s) => AppState::Error(s),
|
||||
AppState::Critical(s) => AppState::Critical(s),
|
||||
},
|
||||
input: self.input,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -231,12 +253,14 @@ mod tests {
|
||||
fn artist_matches(matching: ArtistMeta, list: Vec<Match<ArtistMeta>>) -> MatchStateInfo {
|
||||
let mut list: Vec<MatchOption<ArtistMeta>> = 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<Match<AlbumMeta>>) -> MatchStateInfo {
|
||||
let mut list: Vec<MatchOption<AlbumMeta>> = list.into_iter().map(Into::into).collect();
|
||||
list.push(MatchOption::CannotHaveMbid);
|
||||
list.push(MatchOption::ManualInputMbid);
|
||||
MatchStateInfo::album(matching, list)
|
||||
}
|
||||
|
||||
@ -246,6 +270,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();
|
||||
|
||||
@ -324,6 +349,7 @@ mod tests {
|
||||
info: None,
|
||||
state: &mut widget_state,
|
||||
}),
|
||||
input: None,
|
||||
};
|
||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||
}
|
||||
@ -353,8 +379,13 @@ mod tests {
|
||||
info: Some(&artist_matches),
|
||||
state: &mut widget_state,
|
||||
}),
|
||||
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]
|
||||
@ -387,7 +418,12 @@ mod tests {
|
||||
info: Some(&album_matches),
|
||||
state: &mut widget_state,
|
||||
}),
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user