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::{
app::{
app::App,
machine::{AppInner, AppMachine},
machine::{App, AppInner, AppMachine},
selection::{Delta, ListSelection},
AppPublic, AppState, IAppInteractBrowse,
},

View File

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

View File

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

View File

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

View File

@ -1,36 +1,116 @@
pub mod browse;
pub mod critical;
pub mod error;
pub mod info;
pub mod reload;
pub mod search;
mod browse;
mod critical;
mod error;
mod info;
mod reload;
mod search;
use crate::tui::{
app::{selection::Selection, AppPublicInner},
app::{selection::Selection, AppPublic, AppPublicInner, AppState, IAppAccess, IAppInteract},
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> {
inner: AppInner<MH>,
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> {
running: bool,
music_hoard: MH,
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> {
pub fn new(music_hoard: MH) -> Self {
let selection = Selection::new(music_hoard.get_collection());
@ -40,14 +120,6 @@ impl<MH: IMusicHoard> AppInner<MH> {
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> {
@ -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)]
mod tests {
use musichoard::collection::Collection;
use crate::tui::{
app::{app::App, AppState, IAppInteract},
app::{AppState, IAppInteract},
lib::MockIMusicHoard,
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();
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
.expect_load_from_database()
@ -128,14 +205,6 @@ mod tests {
.expect_rescan_library()
.times(1)
.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
}
@ -145,8 +214,8 @@ mod tests {
}
#[test]
fn running_force_quit() {
let app = App::new(music_hoard_app(COLLECTION.to_owned()));
fn force_quit() {
let app = App::new(music_hoard_init(COLLECTION.to_owned()));
assert!(app.is_running());
let app = app.force_quit();
@ -155,7 +224,7 @@ mod tests {
#[test]
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());
app = AppMachine::error(app.unwrap_browse().inner, "get rekt").into();

View File

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

View File

@ -2,9 +2,8 @@ use musichoard::collection::artist::Artist;
use crate::tui::{
app::{
app::App,
machine::{App, AppInner, AppMachine},
selection::ListSelection,
machine::{AppInner, AppMachine},
AppPublic, AppState, IAppInteractSearch,
},
lib::IMusicHoard,
@ -99,7 +98,7 @@ trait IAppInteractSearchPrivate {
fn incremental_search_predicate(
case_sensitive: bool,
char_sensitive: bool,
search_name: &String,
search_name: &str,
probe: &Artist,
) -> bool;
@ -137,7 +136,7 @@ impl<MH: IMusicHoard> IAppInteractSearchPrivate for AppMachine<MH, AppSearch> {
fn incremental_search_predicate(
case_sensitive: bool,
char_sensitive: bool,
search_name: &String,
search_name: &str,
probe: &Artist,
) -> bool {
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 selection;
pub use machine::App;
pub use selection::{Category, Delta, Selection, WidgetState};
use musichoard::collection::Collection;
use selection::{Delta, Selection};
pub enum AppState<BS, IS, RS, SS, ES, CS> {
Browse(BS),
Info(IS),

View File

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

View File

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

View File

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