Provide search functionality through the TUI #134

Merged
wojtek merged 35 commits from 24---provide-search-functionality-through-the-tui into main 2024-02-18 22:12:42 +01:00
12 changed files with 130 additions and 167 deletions
Showing only changes of commit c1e634b473 - Show all commits

View File

@ -1,96 +0,0 @@
// FIXME: combine with mod state into a mod machine
#![allow(clippy::module_inception)]
use crate::tui::{
app::{
machine::{
browse::AppBrowse, critical::AppCritical, error::AppError, info::AppInfo,
reload::AppReload, search::AppSearch, AppInner, AppMachine,
},
AppPublic, AppState, IAppAccess, IAppInteract,
},
lib::IMusicHoard,
};
pub type App<MH> = AppState<
AppMachine<MH, AppBrowse>,
AppMachine<MH, AppInfo>,
AppMachine<MH, AppReload>,
AppMachine<MH, AppSearch>,
AppMachine<MH, AppError>,
AppMachine<MH, AppCritical>,
>;
impl<MH: IMusicHoard> App<MH> {
pub fn new(mut music_hoard: MH) -> Self {
let init_result = Self::init(&mut music_hoard);
let inner = AppInner::new(music_hoard);
match init_result {
Ok(()) => AppMachine::browse(inner).into(),
Err(err) => AppMachine::critical(inner, err.to_string()).into(),
}
}
fn init(music_hoard: &mut MH) -> Result<(), musichoard::Error> {
music_hoard.load_from_database()?;
music_hoard.rescan_library()?;
Ok(())
}
fn inner_ref(&self) -> &AppInner<MH> {
match self {
AppState::Browse(browse) => browse.inner_ref(),
AppState::Info(info) => info.inner_ref(),
AppState::Reload(reload) => reload.inner_ref(),
AppState::Search(search) => search.inner_ref(),
AppState::Error(error) => error.inner_ref(),
AppState::Critical(critical) => critical.inner_ref(),
}
}
fn inner_mut(&mut self) -> &mut AppInner<MH> {
match self {
AppState::Browse(browse) => browse.inner_mut(),
AppState::Info(info) => info.inner_mut(),
AppState::Reload(reload) => reload.inner_mut(),
AppState::Search(search) => search.inner_mut(),
AppState::Error(error) => error.inner_mut(),
AppState::Critical(critical) => critical.inner_mut(),
}
}
}
impl<MH: IMusicHoard> IAppInteract for App<MH> {
type BS = AppMachine<MH, AppBrowse>;
type IS = AppMachine<MH, AppInfo>;
type RS = AppMachine<MH, AppReload>;
type SS = AppMachine<MH, AppSearch>;
type ES = AppMachine<MH, AppError>;
type CS = AppMachine<MH, AppCritical>;
fn is_running(&self) -> bool {
self.inner_ref().is_running()
}
fn force_quit(mut self) -> Self {
self.inner_mut().stop();
self
}
fn state(self) -> AppState<Self::BS, Self::IS, Self::RS, Self::SS, Self::ES, Self::CS> {
self
}
}
impl<MH: IMusicHoard> IAppAccess for App<MH> {
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::Error(error) => error.into(),
AppState::Critical(critical) => critical.into(),
}
}
}

View File

@ -1,7 +1,6 @@
use crate::tui::{ use crate::tui::{
app::{ app::{
app::App, machine::{App, AppInner, AppMachine},
machine::{AppInner, AppMachine},
selection::{Delta, ListSelection}, selection::{Delta, ListSelection},
AppPublic, AppState, IAppInteractBrowse, AppPublic, AppState, IAppInteractBrowse,
}, },

View File

@ -1,7 +1,6 @@
use crate::tui::{ use crate::tui::{
app::{ app::{
app::App, machine::{App, AppInner, AppMachine},
machine::{AppInner, AppMachine},
AppPublic, AppState, IAppInteractCritical, AppPublic, AppState, IAppInteractCritical,
}, },
lib::IMusicHoard, lib::IMusicHoard,
@ -47,7 +46,7 @@ impl<MH: IMusicHoard> IAppInteractCritical for AppMachine<MH, AppCritical> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tui::app::machine::tests::{music_hoard, inner}; use crate::tui::app::machine::tests::{inner, music_hoard};
use super::*; use super::*;

View File

@ -1,7 +1,6 @@
use crate::tui::{ use crate::tui::{
app::{ app::{
app::App, machine::{App, AppInner, AppMachine},
machine::{AppInner, AppMachine},
AppPublic, AppState, IAppInteractError, AppPublic, AppState, IAppInteractError,
}, },
lib::IMusicHoard, lib::IMusicHoard,

View File

@ -1,7 +1,6 @@
use crate::tui::{ use crate::tui::{
app::{ app::{
app::App, machine::{App, AppInner, AppMachine},
machine::{AppInner, AppMachine},
AppPublic, AppState, IAppInteractInfo, AppPublic, AppState, IAppInteractInfo,
}, },
lib::IMusicHoard, lib::IMusicHoard,
@ -47,7 +46,7 @@ impl<MH: IMusicHoard> IAppInteractInfo for AppMachine<MH, AppInfo> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tui::app::machine::tests::{music_hoard, inner}; use crate::tui::app::machine::tests::{inner, music_hoard};
use super::*; use super::*;

View File

@ -1,36 +1,116 @@
pub mod browse; mod browse;
pub mod critical; mod critical;
pub mod error; mod error;
pub mod info; mod info;
pub mod reload; mod reload;
pub mod search; mod search;
use crate::tui::{ use crate::tui::{
app::{selection::Selection, AppPublicInner}, app::{selection::Selection, AppPublic, AppPublicInner, AppState, IAppAccess, IAppInteract},
lib::IMusicHoard, lib::IMusicHoard,
}; };
use browse::AppBrowse;
use critical::AppCritical;
use error::AppError;
use info::AppInfo;
use reload::AppReload;
use search::AppSearch;
pub type App<MH> = AppState<
AppMachine<MH, AppBrowse>,
AppMachine<MH, AppInfo>,
AppMachine<MH, AppReload>,
AppMachine<MH, AppSearch>,
AppMachine<MH, AppError>,
AppMachine<MH, AppCritical>,
>;
pub struct AppMachine<MH: IMusicHoard, STATE> { pub struct AppMachine<MH: IMusicHoard, STATE> {
inner: AppInner<MH>, inner: AppInner<MH>,
state: STATE, state: STATE,
} }
impl<MH: IMusicHoard, STATE> AppMachine<MH, STATE> {
pub fn inner_ref(&self) -> &AppInner<MH> {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut AppInner<MH> {
&mut self.inner
}
}
pub struct AppInner<MH: IMusicHoard> { pub struct AppInner<MH: IMusicHoard> {
running: bool, running: bool,
music_hoard: MH, music_hoard: MH,
selection: Selection, selection: Selection,
} }
impl<MH: IMusicHoard> App<MH> {
pub fn new(mut music_hoard: MH) -> Self {
let init_result = Self::init(&mut music_hoard);
let inner = AppInner::new(music_hoard);
match init_result {
Ok(()) => AppMachine::browse(inner).into(),
Err(err) => AppMachine::critical(inner, err.to_string()).into(),
}
}
fn init(music_hoard: &mut MH) -> Result<(), musichoard::Error> {
music_hoard.load_from_database()?;
music_hoard.rescan_library()?;
Ok(())
}
fn inner_ref(&self) -> &AppInner<MH> {
match self {
AppState::Browse(browse) => &browse.inner,
AppState::Info(info) => &info.inner,
AppState::Reload(reload) => &reload.inner,
AppState::Search(search) => &search.inner,
AppState::Error(error) => &error.inner,
AppState::Critical(critical) => &critical.inner,
}
}
fn inner_mut(&mut self) -> &mut AppInner<MH> {
match self {
AppState::Browse(browse) => &mut browse.inner,
AppState::Info(info) => &mut info.inner,
AppState::Reload(reload) => &mut reload.inner,
AppState::Search(search) => &mut search.inner,
AppState::Error(error) => &mut error.inner,
AppState::Critical(critical) => &mut critical.inner,
}
}
}
impl<MH: IMusicHoard> IAppInteract for App<MH> {
type BS = AppMachine<MH, AppBrowse>;
type IS = AppMachine<MH, AppInfo>;
type RS = AppMachine<MH, AppReload>;
type SS = AppMachine<MH, AppSearch>;
type ES = AppMachine<MH, AppError>;
type CS = AppMachine<MH, AppCritical>;
fn is_running(&self) -> bool {
self.inner_ref().running
}
fn force_quit(mut self) -> Self {
self.inner_mut().running = false;
self
}
fn state(self) -> AppState<Self::BS, Self::IS, Self::RS, Self::SS, Self::ES, Self::CS> {
self
}
}
impl<MH: IMusicHoard> IAppAccess for App<MH> {
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::Error(error) => error.into(),
AppState::Critical(critical) => critical.into(),
}
}
}
impl<MH: IMusicHoard> AppInner<MH> { impl<MH: IMusicHoard> AppInner<MH> {
pub fn new(music_hoard: MH) -> Self { pub fn new(music_hoard: MH) -> Self {
let selection = Selection::new(music_hoard.get_collection()); let selection = Selection::new(music_hoard.get_collection());
@ -40,14 +120,6 @@ impl<MH: IMusicHoard> AppInner<MH> {
selection, selection,
} }
} }
pub fn is_running(&self) -> bool {
self.running
}
pub fn stop(&mut self) {
self.running = false;
}
} }
impl<'a, MH: IMusicHoard> From<&'a mut AppInner<MH>> for AppPublicInner<'a> { impl<'a, MH: IMusicHoard> From<&'a mut AppInner<MH>> for AppPublicInner<'a> {
@ -59,14 +131,12 @@ impl<'a, MH: IMusicHoard> From<&'a mut AppInner<MH>> for AppPublicInner<'a> {
} }
} }
// FIXME: split tests - into parts that test functionality in isolation and move those where
// appropriate, and parts that verify transitions between states.
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use musichoard::collection::Collection; use musichoard::collection::Collection;
use crate::tui::{ use crate::tui::{
app::{app::App, AppState, IAppInteract}, app::{AppState, IAppInteract},
lib::MockIMusicHoard, lib::MockIMusicHoard,
testmod::COLLECTION, testmod::COLLECTION,
}; };
@ -117,8 +187,15 @@ mod tests {
} }
} }
fn music_hoard_app(collection: Collection) -> MockIMusicHoard { pub fn music_hoard(collection: Collection) -> MockIMusicHoard {
let mut music_hoard = MockIMusicHoard::new(); let mut music_hoard = MockIMusicHoard::new();
music_hoard.expect_get_collection().return_const(collection);
music_hoard
}
fn music_hoard_init(collection: Collection) -> MockIMusicHoard {
let mut music_hoard = music_hoard(collection);
music_hoard music_hoard
.expect_load_from_database() .expect_load_from_database()
@ -128,14 +205,6 @@ mod tests {
.expect_rescan_library() .expect_rescan_library()
.times(1) .times(1)
.return_once(|| Ok(())); .return_once(|| Ok(()));
music_hoard.expect_get_collection().return_const(collection);
music_hoard
}
pub fn music_hoard(collection: Collection) -> MockIMusicHoard {
let mut music_hoard = MockIMusicHoard::new();
music_hoard.expect_get_collection().return_const(collection);
music_hoard music_hoard
} }
@ -145,8 +214,8 @@ mod tests {
} }
#[test] #[test]
fn running_force_quit() { fn force_quit() {
let app = App::new(music_hoard_app(COLLECTION.to_owned())); let app = App::new(music_hoard_init(COLLECTION.to_owned()));
assert!(app.is_running()); assert!(app.is_running());
let app = app.force_quit(); let app = app.force_quit();
@ -155,7 +224,7 @@ mod tests {
#[test] #[test]
fn error_force_quit() { fn error_force_quit() {
let mut app = App::new(music_hoard_app(COLLECTION.to_owned())); let mut app = App::new(music_hoard_init(COLLECTION.to_owned()));
assert!(app.is_running()); assert!(app.is_running());
app = AppMachine::error(app.unwrap_browse().inner, "get rekt").into(); app = AppMachine::error(app.unwrap_browse().inner, "get rekt").into();

View File

@ -1,8 +1,7 @@
use crate::tui::{ use crate::tui::{
app::{ app::{
app::App, machine::{App, AppInner, AppMachine},
selection::IdSelection, selection::IdSelection,
machine::{AppInner, AppMachine},
AppPublic, AppState, IAppInteractReload, AppPublic, AppState, IAppInteractReload,
}, },
lib::IMusicHoard, lib::IMusicHoard,
@ -94,7 +93,6 @@ mod tests {
app.unwrap_browse(); app.unwrap_browse();
} }
#[test] #[test]
fn reload_database() { fn reload_database() {
let mut music_hoard = music_hoard(vec![]); let mut music_hoard = music_hoard(vec![]);

View File

@ -2,9 +2,8 @@ use musichoard::collection::artist::Artist;
use crate::tui::{ use crate::tui::{
app::{ app::{
app::App, machine::{App, AppInner, AppMachine},
selection::ListSelection, selection::ListSelection,
machine::{AppInner, AppMachine},
AppPublic, AppState, IAppInteractSearch, AppPublic, AppState, IAppInteractSearch,
}, },
lib::IMusicHoard, lib::IMusicHoard,
@ -99,7 +98,7 @@ trait IAppInteractSearchPrivate {
fn incremental_search_predicate( fn incremental_search_predicate(
case_sensitive: bool, case_sensitive: bool,
char_sensitive: bool, char_sensitive: bool,
search_name: &String, search_name: &str,
probe: &Artist, probe: &Artist,
) -> bool; ) -> bool;
@ -137,7 +136,7 @@ impl<MH: IMusicHoard> IAppInteractSearchPrivate for AppMachine<MH, AppSearch> {
fn incremental_search_predicate( fn incremental_search_predicate(
case_sensitive: bool, case_sensitive: bool,
char_sensitive: bool, char_sensitive: bool,
search_name: &String, search_name: &str,
probe: &Artist, probe: &Artist,
) -> bool { ) -> bool {
let name = Self::normalize_search(&probe.id.name, !case_sensitive, !char_sensitive); let name = Self::normalize_search(&probe.id.name, !case_sensitive, !char_sensitive);

View File

@ -1,11 +1,11 @@
pub mod app;
pub mod selection;
mod machine; mod machine;
mod selection;
pub use machine::App;
pub use selection::{Category, Delta, Selection, WidgetState};
use musichoard::collection::Collection; use musichoard::collection::Collection;
use selection::{Delta, Selection};
pub enum AppState<BS, IS, RS, SS, ES, CS> { pub enum AppState<BS, IS, RS, SS, ES, CS> {
Browse(BS), Browse(BS),
Info(IS), Info(IS),

View File

@ -5,8 +5,8 @@ use mockall::automock;
use crate::tui::{ use crate::tui::{
app::{ app::{
selection::Delta, AppState, IAppInteract, IAppInteractBrowse, IAppInteractCritical, AppState, Delta, IAppInteract, IAppInteractBrowse, IAppInteractCritical, IAppInteractError,
IAppInteractError, IAppInteractInfo, IAppInteractReload, IAppInteractSearch, IAppInteractInfo, IAppInteractReload, IAppInteractSearch,
}, },
event::{Event, EventError, EventReceiver}, event::{Event, EventError, EventReceiver},
}; };

View File

@ -5,7 +5,7 @@ mod lib;
mod listener; mod listener;
mod ui; mod ui;
pub use app::app::App; pub use app::App;
pub use event::EventChannel; pub use event::EventChannel;
pub use handler::EventHandler; pub use handler::EventHandler;
pub use listener::EventListener; pub use listener::EventListener;
@ -178,8 +178,8 @@ mod tests {
use musichoard::collection::Collection; use musichoard::collection::Collection;
use crate::tui::{ use crate::tui::{
app::app::App, handler::MockIEventHandler, lib::MockIMusicHoard, app::App, handler::MockIEventHandler, lib::MockIMusicHoard, listener::MockIEventListener,
listener::MockIEventListener, ui::Ui, ui::Ui,
}; };
use super::*; use super::*;

View File

@ -13,10 +13,7 @@ use ratatui::{
Frame, Frame,
}; };
use crate::tui::app::{ use crate::tui::app::{AppPublicState, AppState, Category, IAppAccess, Selection, WidgetState};
selection::{Category, Selection, WidgetState},
AppPublicState, AppState, IAppAccess,
};
pub trait IUi { pub trait IUi {
fn render<APP: IAppAccess>(app: &mut APP, frame: &mut Frame); fn render<APP: IAppAccess>(app: &mut APP, frame: &mut Frame);
@ -695,7 +692,7 @@ impl IUi for Ui {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tui::{ use crate::tui::{
app::{selection::Delta, AppPublic, AppPublicInner}, app::{AppPublic, AppPublicInner, Delta},
testmod::COLLECTION, testmod::COLLECTION,
tests::terminal, tests::terminal,
}; };