From 973db2a7534ffe6ec6d5460e5b002e61ee478a82 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 11 Feb 2024 08:37:20 +0100 Subject: [PATCH 01/35] No need to require column number --- src/tui/ui.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 8ed4686..af602d3 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -353,8 +353,8 @@ struct Minibuffer<'a> { impl Minibuffer<'_> { fn paragraphs(state: &AppPublicState) -> Self { - let columns = 3; - let mb = match state { + let columns = 2; + match state { AppState::Browse(_) => Minibuffer { paragraphs: vec![ Paragraph::new("m: show info overlay"), @@ -384,11 +384,7 @@ impl Minibuffer<'_> { paragraphs: vec![Paragraph::new("Press ctrl+c to terminate the program...")], columns: 1, }, - }; - - // Callers will assume this. - assert!(mb.columns >= mb.paragraphs.len() as u16); - mb + } } } -- 2.45.2 From 8fd11c4f5710f5b7b1ecab7058f158d0db9ba308 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 11 Feb 2024 09:05:04 +0100 Subject: [PATCH 02/35] Search mode input --- src/tui/app/app.rs | 76 ++++++++++++++++++++++++++++++++++++++++------ src/tui/handler.rs | 28 +++++++++++++++-- src/tui/ui.rs | 31 +++++++++++++------ 3 files changed, 112 insertions(+), 23 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index 114abca..915666b 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -7,28 +7,33 @@ use crate::tui::{ lib::IMusicHoard, }; -pub enum AppState { +pub enum AppState { Browse(BS), Info(IS), Reload(RS), + Search(SS), Error(ES), Critical(CS), } -impl AppState { - fn is_browse(&self) -> bool { +impl AppState { + pub fn is_browse(&self) -> bool { matches!(self, AppState::Browse(_)) } - fn is_info(&self) -> bool { + pub fn is_info(&self) -> bool { matches!(self, AppState::Info(_)) } - fn is_reload(&self) -> bool { + pub fn is_reload(&self) -> bool { matches!(self, AppState::Reload(_)) } - fn is_error(&self) -> bool { + pub fn is_search(&self) -> bool { + matches!(self, AppState::Search(_)) + } + + pub fn is_error(&self) -> bool { matches!(self, AppState::Error(_)) } } @@ -37,6 +42,7 @@ pub trait IAppInteract { type BS: IAppInteractBrowse; type IS: IAppInteractInfo; type RS: IAppInteractReload; + type SS: IAppInteractSearch; type ES: IAppInteractError; type CS: IAppInteractCritical; @@ -46,7 +52,14 @@ pub trait IAppInteract { #[allow(clippy::type_complexity)] fn state( &mut self, - ) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES, &mut Self::CS>; + ) -> AppState< + &mut Self::BS, + &mut Self::IS, + &mut Self::RS, + &mut Self::SS, + &mut Self::ES, + &mut Self::CS, + >; } pub trait IAppInteractBrowse { @@ -61,6 +74,8 @@ pub trait IAppInteractBrowse { fn show_info_overlay(&mut self); fn show_reload_menu(&mut self); + + fn begin_search(&mut self); } pub trait IAppInteractInfo { @@ -73,6 +88,12 @@ pub trait IAppInteractReload { fn hide_reload_menu(&mut self); } +pub trait IAppInteractSearch { + fn append_character(&mut self, ch: char); + fn remove_character(&mut self); + fn finish_search(&mut self); +} + pub trait IAppInteractError { fn dismiss_error(&mut self); } @@ -87,7 +108,7 @@ pub trait IAppAccess { fn get(&mut self) -> AppPublic; } -pub type AppPublicState = AppState<(), (), (), String, String>; +pub type AppPublicState = AppState<(), (), (), String, String, String>; pub struct AppPublic<'app> { pub collection: &'app Collection, @@ -99,7 +120,7 @@ pub struct App { running: bool, music_hoard: MH, selection: Selection, - state: AppState<(), (), (), String, String>, + state: AppState<(), (), (), String, String, String>, } impl App { @@ -128,6 +149,7 @@ impl IAppInteract for App { type BS = Self; type IS = Self; type RS = Self; + type SS = Self; type ES = Self; type CS = Self; @@ -141,11 +163,19 @@ impl IAppInteract for App { fn state( &mut self, - ) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES, &mut Self::CS> { + ) -> AppState< + &mut Self::BS, + &mut Self::IS, + &mut Self::RS, + &mut Self::SS, + &mut Self::ES, + &mut Self::CS, + > { match self.state { AppState::Browse(_) => AppState::Browse(self), AppState::Info(_) => AppState::Info(self), AppState::Reload(_) => AppState::Reload(self), + AppState::Search(_) => AppState::Search(self), AppState::Error(_) => AppState::Error(self), AppState::Critical(_) => AppState::Critical(self), } @@ -190,6 +220,11 @@ impl IAppInteractBrowse for App { assert!(self.state.is_browse()); self.state = AppState::Reload(()); } + + fn begin_search(&mut self) { + assert!(self.state.is_browse()); + self.state = AppState::Search(String::new()); + } } impl IAppInteractInfo for App { @@ -236,6 +271,27 @@ impl IAppInteractReloadPrivate for App { } } +impl IAppInteractSearch for App { + fn append_character(&mut self, ch: char) { + match self.state { + AppState::Search(ref mut s) => s.push(ch), + _ => unreachable!(), + } + } + + fn remove_character(&mut self) { + match self.state { + AppState::Search(ref mut s) => s.pop(), + _ => unreachable!(), + }; + } + + fn finish_search(&mut self) { + assert!(self.state.is_search()); + self.state = AppState::Browse(()); + } +} + impl IAppInteractError for App { fn dismiss_error(&mut self) { assert!(self.state.is_error()); diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 322206e..a1dbc7d 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -7,7 +7,7 @@ use crate::tui::{ app::{ app::{ AppState, IAppInteract, IAppInteractBrowse, IAppInteractError, IAppInteractInfo, - IAppInteractReload, + IAppInteractReload, IAppInteractSearch, }, selection::Delta, }, @@ -24,6 +24,7 @@ trait IEventHandlerPrivate { fn handle_browse_key_event(app: &mut ::BS, key_event: KeyEvent); fn handle_info_key_event(app: &mut ::IS, key_event: KeyEvent); fn handle_reload_key_event(app: &mut ::RS, key_event: KeyEvent); + fn handle_search_key_event(app: &mut ::SS, key_event: KeyEvent); fn handle_error_key_event(app: &mut ::ES, key_event: KeyEvent); fn handle_critical_key_event(app: &mut ::CS, key_event: KeyEvent); } @@ -69,6 +70,9 @@ impl IEventHandlerPrivate for EventHandler { AppState::Reload(reload) => { >::handle_reload_key_event(reload, key_event); } + AppState::Search(search) => { + >::handle_search_key_event(search, key_event); + } AppState::Error(error) => { >::handle_error_key_event(error, key_event); } @@ -96,10 +100,16 @@ impl IEventHandlerPrivate for EventHandler { KeyCode::Down => app.increment_selection(Delta::Line), KeyCode::PageUp => app.decrement_selection(Delta::Page), KeyCode::PageDown => app.increment_selection(Delta::Page), - // Toggle overlay. + // Toggle info overlay. KeyCode::Char('m') | KeyCode::Char('M') => app.show_info_overlay(), - // Toggle Reload + // Toggle reload meny. KeyCode::Char('g') | KeyCode::Char('G') => app.show_reload_menu(), + // Toggle search. + KeyCode::Char('s') | KeyCode::Char('S') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.begin_search(); + } + } // Othey keys. _ => {} } @@ -134,6 +144,18 @@ impl IEventHandlerPrivate for EventHandler { } } + fn handle_search_key_event(app: &mut ::SS, key_event: KeyEvent) { + match key_event.code { + // Add/remove character to search. + KeyCode::Char(ch) => app.append_character(ch), + KeyCode::Backspace => app.remove_character(), + // Return. + KeyCode::Esc | KeyCode::Enter => app.finish_search(), + // Othey keys. + _ => {} + } + } + fn handle_error_key_event(app: &mut ::ES, _key_event: KeyEvent) { // Any key dismisses the error. app.dismiss_error(); diff --git a/src/tui/ui.rs b/src/tui/ui.rs index af602d3..ea1e022 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -353,12 +353,13 @@ struct Minibuffer<'a> { impl Minibuffer<'_> { fn paragraphs(state: &AppPublicState) -> Self { - let columns = 2; - match state { + let columns = 3; + let mut mb = match state { AppState::Browse(_) => Minibuffer { paragraphs: vec![ Paragraph::new("m: show info overlay"), Paragraph::new("g: show reload menu"), + Paragraph::new("ctrl+s: search artist"), ], columns, }, @@ -374,6 +375,10 @@ impl Minibuffer<'_> { ], columns, }, + AppState::Search(ref s) => Minibuffer { + paragraphs: vec![Paragraph::new(format!("I-search: {s}"))], + columns: 1, + }, AppState::Error(_) => Minibuffer { paragraphs: vec![Paragraph::new( "Press any key to dismiss the error message...", @@ -384,7 +389,17 @@ impl Minibuffer<'_> { paragraphs: vec![Paragraph::new("Press ctrl+c to terminate the program...")], columns: 1, }, + }; + + if !state.is_search() { + mb.paragraphs = mb + .paragraphs + .into_iter() + .map(|p| p.alignment(Alignment::Center)) + .collect(); } + + mb } } @@ -553,17 +568,13 @@ impl Ui { } fn render_minibuffer(state: &AppPublicState, ar: Rect, fr: &mut Frame) { - let mut mb = Minibuffer::paragraphs(state); - mb.paragraphs = mb - .paragraphs - .into_iter() - .map(|p| p.alignment(Alignment::Center)) - .collect(); + let mb = Minibuffer::paragraphs(state); + let space = 3; let area = Rect { - x: ar.x + 1, + x: ar.x + 1 + space, y: ar.y + 1, - width: ar.width.saturating_sub(2), + width: ar.width.saturating_sub(2 + 2 * space), height: 1, }; -- 2.45.2 From cea3516c60395d3aa1a8abfc028b09c79f9db330 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 11 Feb 2024 10:03:49 +0100 Subject: [PATCH 03/35] Add incremental search --- src/tui/app/app.rs | 16 +++++++++-- src/tui/app/selection.rs | 57 ++++++++++++++++++++++++++++++++++------ src/tui/handler.rs | 52 ++++++++++++++++++------------------ 3 files changed, 89 insertions(+), 36 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index 915666b..63156af 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -224,6 +224,8 @@ impl IAppInteractBrowse for App { fn begin_search(&mut self) { assert!(self.state.is_browse()); self.state = AppState::Search(String::new()); + self.selection + .reset_artist(self.music_hoard.get_collection()); } } @@ -274,14 +276,24 @@ impl IAppInteractReloadPrivate for App { impl IAppInteractSearch for App { fn append_character(&mut self, ch: char) { match self.state { - AppState::Search(ref mut s) => s.push(ch), + AppState::Search(ref mut s) => { + s.push(ch); + self.selection + .incremental_artist_search(self.music_hoard.get_collection(), s); + } _ => unreachable!(), } } fn remove_character(&mut self) { match self.state { - AppState::Search(ref mut s) => s.pop(), + AppState::Search(ref mut s) => { + s.pop(); + self.selection + .reset_artist(self.music_hoard.get_collection()); + self.selection + .incremental_artist_search(self.music_hoard.get_collection(), s); + } _ => unreachable!(), }; } diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index 6438bd0..ea0cf89 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -73,6 +73,12 @@ impl Selection { self.artist.reinitialise(artists, selected.artist); } + pub fn reset_artist(&mut self, artists: &[Artist]) { + if self.artist.state.list.selected() != Some(0) { + self.select(artists, ActiveSelection { artist: None }); + } + } + pub fn increment_category(&mut self) { self.active = match self.active { Category::Artist => Category::Album, @@ -89,6 +95,10 @@ impl Selection { }; } + pub fn incremental_artist_search(&mut self, collection: &Collection, artist_name: &str) { + self.artist.incremental_search(collection, artist_name); + } + pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) { match self.active { Category::Artist => self.increment_artist(collection, delta), @@ -172,16 +182,47 @@ impl ArtistSelection { } } + fn select_to(&mut self, artists: &[Artist], mut to: usize) { + if to >= artists.len() { + to = artists.len() - 1; + } + if self.state.list.selected() != Some(to) { + self.state.list.select(Some(to)); + self.album = AlbumSelection::initialise(&artists[to].albums); + } + } + + fn incremental_search(&mut self, artists: &[Artist], artist_name: &str) { + if let Some(index) = self.state.list.selected() { + let case_sensitive = artist_name + .chars() + .any(|ch| !(ch.is_lowercase() || ch.is_whitespace())); + let slice = &artists[index..]; + + let result = if case_sensitive { + slice.binary_search_by(|probe| probe.get_sort_key().name.as_str().cmp(artist_name)) + } else { + slice.binary_search_by(|probe| { + probe + .get_sort_key() + .name + .to_lowercase() + .as_str() + .cmp(artist_name) + }) + }; + + let new_index = match result { + Ok(slice_index) | Err(slice_index) => index + slice_index, + }; + self.select_to(artists, new_index); + } + } + fn increment_by(&mut self, artists: &[Artist], by: usize) { if let Some(index) = self.state.list.selected() { - let mut result = index.saturating_add(by); - if result >= artists.len() { - result = artists.len() - 1; - } - if self.state.list.selected() != Some(result) { - self.state.list.select(Some(result)); - self.album = AlbumSelection::initialise(&artists[result].albums); - } + let result = index.saturating_add(by); + self.select_to(artists, result); } } diff --git a/src/tui/handler.rs b/src/tui/handler.rs index a1dbc7d..5a0d967 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -53,35 +53,35 @@ impl IEventHandler for EventHandler { impl IEventHandlerPrivate for EventHandler { fn handle_key_event(app: &mut APP, key_event: KeyEvent) { - match key_event.code { - // Exit application on `Ctrl-C`. - KeyCode::Char('c') | KeyCode::Char('C') => { - if key_event.modifiers == KeyModifiers::CONTROL { + if key_event.modifiers == KeyModifiers::CONTROL { + match key_event.code { + // Exit application on `Ctrl-C`. + KeyCode::Char('c') | KeyCode::Char('C') => { app.force_quit(); } + _ => {} + } + } + + match app.state() { + AppState::Browse(browse) => { + >::handle_browse_key_event(browse, key_event); + } + AppState::Info(info) => { + >::handle_info_key_event(info, key_event); + } + AppState::Reload(reload) => { + >::handle_reload_key_event(reload, key_event); + } + AppState::Search(search) => { + >::handle_search_key_event(search, key_event); + } + AppState::Error(error) => { + >::handle_error_key_event(error, key_event); + } + AppState::Critical(critical) => { + >::handle_critical_key_event(critical, key_event); } - _ => match app.state() { - AppState::Browse(browse) => { - >::handle_browse_key_event(browse, key_event); - } - AppState::Info(info) => { - >::handle_info_key_event(info, key_event); - } - AppState::Reload(reload) => { - >::handle_reload_key_event(reload, key_event); - } - AppState::Search(search) => { - >::handle_search_key_event(search, key_event); - } - AppState::Error(error) => { - >::handle_error_key_event(error, key_event); - } - AppState::Critical(critical) => { - >::handle_critical_key_event( - critical, key_event, - ); - } - }, } } -- 2.45.2 From 0b697f5484b4b06af904094ca8a5f9eb9fe57cd7 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 11 Feb 2024 10:58:38 +0100 Subject: [PATCH 04/35] Unit tests --- src/core/database/json/testmod.rs | 8 +- src/core/library/beets/testmod.rs | 44 +++++----- src/core/library/testmod.rs | 44 +++++----- src/core/musichoard/musichoard.rs | 2 +- src/tests.rs | 14 +-- src/tui/app/app.rs | 138 ++++++++++++++++++++++++++---- src/tui/app/selection.rs | 50 ++++++++++- src/tui/ui.rs | 4 + 8 files changed, 229 insertions(+), 75 deletions(-) diff --git a/src/core/database/json/testmod.rs b/src/core/database/json/testmod.rs index de0a457..4c3031e 100644 --- a/src/core/database/json/testmod.rs +++ b/src/core/database/json/testmod.rs @@ -2,7 +2,7 @@ pub static DATABASE_JSON: &str = "{\ \"V20240210\":\ [\ {\ - \"name\":\"album_artist a\",\ + \"name\":\"Album_Artist A\",\ \"sort\":null,\ \"musicbrainz\":\"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000\",\ \"properties\":{\ @@ -11,7 +11,7 @@ pub static DATABASE_JSON: &str = "{\ }\ },\ {\ - \"name\":\"album_artist b\",\ + \"name\":\"Album_Artist B\",\ \"sort\":null,\ \"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\ \"properties\":{\ @@ -24,13 +24,13 @@ pub static DATABASE_JSON: &str = "{\ }\ },\ {\ - \"name\":\"album_artist c\",\ + \"name\":\"Album_Artist C\",\ \"sort\":null,\ \"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\ \"properties\":{}\ },\ {\ - \"name\":\"album_artist d\",\ + \"name\":\"Album_Artist D\",\ \"sort\":null,\ \"musicbrainz\":null,\ \"properties\":{}\ diff --git a/src/core/library/beets/testmod.rs b/src/core/library/beets/testmod.rs index e52045b..a2a38a7 100644 --- a/src/core/library/beets/testmod.rs +++ b/src/core/library/beets/testmod.rs @@ -2,27 +2,27 @@ use once_cell::sync::Lazy; pub static LIBRARY_BEETS: Lazy> = Lazy::new(|| -> Vec { vec![ - String::from("album_artist a -*^- -*^- 1998 -*^- album_title a.a -*^- 1 -*^- track a.a.1 -*^- artist a.a.1 -*^- FLAC -*^- 992"), - String::from("album_artist a -*^- -*^- 1998 -*^- album_title a.a -*^- 2 -*^- track a.a.2 -*^- artist a.a.2.1; artist a.a.2.2 -*^- MP3 -*^- 320"), - String::from("album_artist a -*^- -*^- 1998 -*^- album_title a.a -*^- 3 -*^- track a.a.3 -*^- artist a.a.3 -*^- FLAC -*^- 1061"), - String::from("album_artist a -*^- -*^- 1998 -*^- album_title a.a -*^- 4 -*^- track a.a.4 -*^- artist a.a.4 -*^- FLAC -*^- 1042"), - String::from("album_artist a -*^- -*^- 2015 -*^- album_title a.b -*^- 1 -*^- track a.b.1 -*^- artist a.b.1 -*^- FLAC -*^- 1004"), - String::from("album_artist a -*^- -*^- 2015 -*^- album_title a.b -*^- 2 -*^- track a.b.2 -*^- artist a.b.2 -*^- FLAC -*^- 1077"), - String::from("album_artist b -*^- -*^- 2003 -*^- album_title b.a -*^- 1 -*^- track b.a.1 -*^- artist b.a.1 -*^- MP3 -*^- 190"), - String::from("album_artist b -*^- -*^- 2003 -*^- album_title b.a -*^- 2 -*^- track b.a.2 -*^- artist b.a.2.1; artist b.a.2.2 -*^- MP3 -*^- 120"), - String::from("album_artist b -*^- -*^- 2008 -*^- album_title b.b -*^- 1 -*^- track b.b.1 -*^- artist b.b.1 -*^- FLAC -*^- 1077"), - String::from("album_artist b -*^- -*^- 2008 -*^- album_title b.b -*^- 2 -*^- track b.b.2 -*^- artist b.b.2.1; artist b.b.2.2 -*^- MP3 -*^- 320"), - String::from("album_artist b -*^- -*^- 2009 -*^- album_title b.c -*^- 1 -*^- track b.c.1 -*^- artist b.c.1 -*^- MP3 -*^- 190"), - String::from("album_artist b -*^- -*^- 2009 -*^- album_title b.c -*^- 2 -*^- track b.c.2 -*^- artist b.c.2.1; artist b.c.2.2 -*^- MP3 -*^- 120"), - String::from("album_artist b -*^- -*^- 2015 -*^- album_title b.d -*^- 1 -*^- track b.d.1 -*^- artist b.d.1 -*^- MP3 -*^- 190"), - String::from("album_artist b -*^- -*^- 2015 -*^- album_title b.d -*^- 2 -*^- track b.d.2 -*^- artist b.d.2.1; artist b.d.2.2 -*^- MP3 -*^- 120"), - String::from("album_artist c -*^- -*^- 1985 -*^- album_title c.a -*^- 1 -*^- track c.a.1 -*^- artist c.a.1 -*^- MP3 -*^- 320"), - String::from("album_artist c -*^- -*^- 1985 -*^- album_title c.a -*^- 2 -*^- track c.a.2 -*^- artist c.a.2.1; artist c.a.2.2 -*^- MP3 -*^- 120"), - String::from("album_artist c -*^- -*^- 2018 -*^- album_title c.b -*^- 1 -*^- track c.b.1 -*^- artist c.b.1 -*^- FLAC -*^- 1041"), - String::from("album_artist c -*^- -*^- 2018 -*^- album_title c.b -*^- 2 -*^- track c.b.2 -*^- artist c.b.2.1; artist c.b.2.2 -*^- FLAC -*^- 756"), - String::from("album_artist d -*^- -*^- 1995 -*^- album_title d.a -*^- 1 -*^- track d.a.1 -*^- artist d.a.1 -*^- MP3 -*^- 120"), - String::from("album_artist d -*^- -*^- 1995 -*^- album_title d.a -*^- 2 -*^- track d.a.2 -*^- artist d.a.2.1; artist d.a.2.2 -*^- MP3 -*^- 120"), - String::from("album_artist d -*^- -*^- 2028 -*^- album_title d.b -*^- 1 -*^- track d.b.1 -*^- artist d.b.1 -*^- FLAC -*^- 841"), - String::from("album_artist d -*^- -*^- 2028 -*^- album_title d.b -*^- 2 -*^- track d.b.2 -*^- artist d.b.2.1; artist d.b.2.2 -*^- FLAC -*^- 756") + String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 1 -*^- track a.a.1 -*^- artist a.a.1 -*^- FLAC -*^- 992"), + String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 2 -*^- track a.a.2 -*^- artist a.a.2.1; artist a.a.2.2 -*^- MP3 -*^- 320"), + String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 3 -*^- track a.a.3 -*^- artist a.a.3 -*^- FLAC -*^- 1061"), + String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 4 -*^- track a.a.4 -*^- artist a.a.4 -*^- FLAC -*^- 1042"), + String::from("Album_Artist A -*^- -*^- 2015 -*^- album_title a.b -*^- 1 -*^- track a.b.1 -*^- artist a.b.1 -*^- FLAC -*^- 1004"), + String::from("Album_Artist A -*^- -*^- 2015 -*^- album_title a.b -*^- 2 -*^- track a.b.2 -*^- artist a.b.2 -*^- FLAC -*^- 1077"), + String::from("Album_Artist B -*^- -*^- 2003 -*^- album_title b.a -*^- 1 -*^- track b.a.1 -*^- artist b.a.1 -*^- MP3 -*^- 190"), + String::from("Album_Artist B -*^- -*^- 2003 -*^- album_title b.a -*^- 2 -*^- track b.a.2 -*^- artist b.a.2.1; artist b.a.2.2 -*^- MP3 -*^- 120"), + String::from("Album_Artist B -*^- -*^- 2008 -*^- album_title b.b -*^- 1 -*^- track b.b.1 -*^- artist b.b.1 -*^- FLAC -*^- 1077"), + String::from("Album_Artist B -*^- -*^- 2008 -*^- album_title b.b -*^- 2 -*^- track b.b.2 -*^- artist b.b.2.1; artist b.b.2.2 -*^- MP3 -*^- 320"), + String::from("Album_Artist B -*^- -*^- 2009 -*^- album_title b.c -*^- 1 -*^- track b.c.1 -*^- artist b.c.1 -*^- MP3 -*^- 190"), + String::from("Album_Artist B -*^- -*^- 2009 -*^- album_title b.c -*^- 2 -*^- track b.c.2 -*^- artist b.c.2.1; artist b.c.2.2 -*^- MP3 -*^- 120"), + String::from("Album_Artist B -*^- -*^- 2015 -*^- album_title b.d -*^- 1 -*^- track b.d.1 -*^- artist b.d.1 -*^- MP3 -*^- 190"), + String::from("Album_Artist B -*^- -*^- 2015 -*^- album_title b.d -*^- 2 -*^- track b.d.2 -*^- artist b.d.2.1; artist b.d.2.2 -*^- MP3 -*^- 120"), + String::from("Album_Artist C -*^- -*^- 1985 -*^- album_title c.a -*^- 1 -*^- track c.a.1 -*^- artist c.a.1 -*^- MP3 -*^- 320"), + String::from("Album_Artist C -*^- -*^- 1985 -*^- album_title c.a -*^- 2 -*^- track c.a.2 -*^- artist c.a.2.1; artist c.a.2.2 -*^- MP3 -*^- 120"), + String::from("Album_Artist C -*^- -*^- 2018 -*^- album_title c.b -*^- 1 -*^- track c.b.1 -*^- artist c.b.1 -*^- FLAC -*^- 1041"), + String::from("Album_Artist C -*^- -*^- 2018 -*^- album_title c.b -*^- 2 -*^- track c.b.2 -*^- artist c.b.2.1; artist c.b.2.2 -*^- FLAC -*^- 756"), + String::from("Album_Artist D -*^- -*^- 1995 -*^- album_title d.a -*^- 1 -*^- track d.a.1 -*^- artist d.a.1 -*^- MP3 -*^- 120"), + String::from("Album_Artist D -*^- -*^- 1995 -*^- album_title d.a -*^- 2 -*^- track d.a.2 -*^- artist d.a.2.1; artist d.a.2.2 -*^- MP3 -*^- 120"), + String::from("Album_Artist D -*^- -*^- 2028 -*^- album_title d.b -*^- 1 -*^- track d.b.1 -*^- artist d.b.1 -*^- FLAC -*^- 841"), + String::from("Album_Artist D -*^- -*^- 2028 -*^- album_title d.b -*^- 2 -*^- track d.b.2 -*^- artist d.b.2.1; artist d.b.2.2 -*^- FLAC -*^- 756") ] }); diff --git a/src/core/library/testmod.rs b/src/core/library/testmod.rs index f86be94..1eb2787 100644 --- a/src/core/library/testmod.rs +++ b/src/core/library/testmod.rs @@ -5,7 +5,7 @@ use crate::core::{collection::track::Format, library::Item}; pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { vec![ Item { - album_artist: String::from("album_artist a"), + album_artist: String::from("Album_Artist A"), album_artist_sort: None, album_year: 1998, album_title: String::from("album_title a.a"), @@ -16,7 +16,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 992, }, Item { - album_artist: String::from("album_artist a"), + album_artist: String::from("Album_Artist A"), album_artist_sort: None, album_year: 1998, album_title: String::from("album_title a.a"), @@ -30,7 +30,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 320, }, Item { - album_artist: String::from("album_artist a"), + album_artist: String::from("Album_Artist A"), album_artist_sort: None, album_year: 1998, album_title: String::from("album_title a.a"), @@ -41,7 +41,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 1061, }, Item { - album_artist: String::from("album_artist a"), + album_artist: String::from("Album_Artist A"), album_artist_sort: None, album_year: 1998, album_title: String::from("album_title a.a"), @@ -52,7 +52,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 1042, }, Item { - album_artist: String::from("album_artist a"), + album_artist: String::from("Album_Artist A"), album_artist_sort: None, album_year: 2015, album_title: String::from("album_title a.b"), @@ -63,7 +63,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 1004, }, Item { - album_artist: String::from("album_artist a"), + album_artist: String::from("Album_Artist A"), album_artist_sort: None, album_year: 2015, album_title: String::from("album_title a.b"), @@ -74,7 +74,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 1077, }, Item { - album_artist: String::from("album_artist b"), + album_artist: String::from("Album_Artist B"), album_artist_sort: None, album_year: 2003, album_title: String::from("album_title b.a"), @@ -85,7 +85,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 190, }, Item { - album_artist: String::from("album_artist b"), + album_artist: String::from("Album_Artist B"), album_artist_sort: None, album_year: 2003, album_title: String::from("album_title b.a"), @@ -99,7 +99,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 120, }, Item { - album_artist: String::from("album_artist b"), + album_artist: String::from("Album_Artist B"), album_artist_sort: None, album_year: 2008, album_title: String::from("album_title b.b"), @@ -110,7 +110,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 1077, }, Item { - album_artist: String::from("album_artist b"), + album_artist: String::from("Album_Artist B"), album_artist_sort: None, album_year: 2008, album_title: String::from("album_title b.b"), @@ -124,7 +124,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 320, }, Item { - album_artist: String::from("album_artist b"), + album_artist: String::from("Album_Artist B"), album_artist_sort: None, album_year: 2009, album_title: String::from("album_title b.c"), @@ -135,7 +135,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 190, }, Item { - album_artist: String::from("album_artist b"), + album_artist: String::from("Album_Artist B"), album_artist_sort: None, album_year: 2009, album_title: String::from("album_title b.c"), @@ -149,7 +149,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 120, }, Item { - album_artist: String::from("album_artist b"), + album_artist: String::from("Album_Artist B"), album_artist_sort: None, album_year: 2015, album_title: String::from("album_title b.d"), @@ -160,7 +160,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 190, }, Item { - album_artist: String::from("album_artist b"), + album_artist: String::from("Album_Artist B"), album_artist_sort: None, album_year: 2015, album_title: String::from("album_title b.d"), @@ -174,7 +174,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 120, }, Item { - album_artist: String::from("album_artist c"), + album_artist: String::from("Album_Artist C"), album_artist_sort: None, album_year: 1985, album_title: String::from("album_title c.a"), @@ -185,7 +185,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 320, }, Item { - album_artist: String::from("album_artist c"), + album_artist: String::from("Album_Artist C"), album_artist_sort: None, album_year: 1985, album_title: String::from("album_title c.a"), @@ -199,7 +199,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 120, }, Item { - album_artist: String::from("album_artist c"), + album_artist: String::from("Album_Artist C"), album_artist_sort: None, album_year: 2018, album_title: String::from("album_title c.b"), @@ -210,7 +210,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 1041, }, Item { - album_artist: String::from("album_artist c"), + album_artist: String::from("Album_Artist C"), album_artist_sort: None, album_year: 2018, album_title: String::from("album_title c.b"), @@ -224,7 +224,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 756, }, Item { - album_artist: String::from("album_artist d"), + album_artist: String::from("Album_Artist D"), album_artist_sort: None, album_year: 1995, album_title: String::from("album_title d.a"), @@ -235,7 +235,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 120, }, Item { - album_artist: String::from("album_artist d"), + album_artist: String::from("Album_Artist D"), album_artist_sort: None, album_year: 1995, album_title: String::from("album_title d.a"), @@ -249,7 +249,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 120, }, Item { - album_artist: String::from("album_artist d"), + album_artist: String::from("Album_Artist D"), album_artist_sort: None, album_year: 2028, album_title: String::from("album_title d.b"), @@ -260,7 +260,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 841, }, Item { - album_artist: String::from("album_artist d"), + album_artist: String::from("Album_Artist D"), album_artist_sort: None, album_year: 2028, album_title: String::from("album_title d.b"), diff --git a/src/core/musichoard/musichoard.rs b/src/core/musichoard/musichoard.rs index 552f542..4c2515a 100644 --- a/src/core/musichoard/musichoard.rs +++ b/src/core/musichoard/musichoard.rs @@ -668,7 +668,7 @@ mod tests { let mut right: Vec = vec![left.last().unwrap().clone()]; assert!(right.first().unwrap() > left.first().unwrap()); - let artist_sort = Some(ArtistId::new("album_artist 0")); + let artist_sort = Some(ArtistId::new("Album_Artist 0")); right[0].sort = artist_sort.clone(); assert!(right.first().unwrap() < left.first().unwrap()); diff --git a/src/tests.rs b/src/tests.rs index bfd4383..db2b4f8 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -3,7 +3,7 @@ macro_rules! library_collection { vec![ Artist { id: ArtistId { - name: "album_artist a".to_string(), + name: "Album_Artist A".to_string(), }, sort: None, musicbrainz: None, @@ -98,7 +98,7 @@ macro_rules! library_collection { }, Artist { id: ArtistId { - name: "album_artist b".to_string(), + name: "Album_Artist B".to_string(), }, sort: None, musicbrainz: None, @@ -240,7 +240,7 @@ macro_rules! library_collection { }, Artist { id: ArtistId { - name: "album_artist c".to_string(), + name: "Album_Artist C".to_string(), }, sort: None, musicbrainz: None, @@ -316,7 +316,7 @@ macro_rules! library_collection { }, Artist { id: ArtistId { - name: "album_artist d".to_string(), + name: "Album_Artist D".to_string(), }, sort: None, musicbrainz: None, @@ -400,7 +400,7 @@ macro_rules! full_collection { let mut iter = collection.iter_mut(); let artist_a = iter.next().unwrap(); - assert_eq!(artist_a.id.name, "album_artist a"); + assert_eq!(artist_a.id.name, "Album_Artist A"); artist_a.musicbrainz = Some( MusicBrainz::new( @@ -421,7 +421,7 @@ macro_rules! full_collection { ]); let artist_b = iter.next().unwrap(); - assert_eq!(artist_b.id.name, "album_artist b"); + assert_eq!(artist_b.id.name, "Album_Artist B"); artist_b.musicbrainz = Some( MusicBrainz::new( @@ -443,7 +443,7 @@ macro_rules! full_collection { ]); let artist_c = iter.next().unwrap(); - assert_eq!(artist_c.id.name, "album_artist c"); + assert_eq!(artist_c.id.name, "Album_Artist C"); artist_c.musicbrainz = Some( MusicBrainz::new( diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index 63156af..531ad95 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -33,9 +33,27 @@ impl AppState { matches!(self, AppState::Search(_)) } + pub fn unwrap_search(self) -> SS { + match self { + AppState::Search(ss) => ss, + _ => panic!(), + } + } + pub fn is_error(&self) -> bool { matches!(self, AppState::Error(_)) } + + pub fn as_mut(&mut self) -> AppState<&mut BS, &mut IS, &mut RS, &mut SS, &mut ES, &mut CS> { + match self { + AppState::Browse(ref mut bs) => AppState::Browse(bs), + AppState::Info(ref mut is) => AppState::Info(is), + AppState::Reload(ref mut rs) => AppState::Reload(rs), + AppState::Search(ref mut ss) => AppState::Search(ss), + AppState::Error(ref mut es) => AppState::Error(es), + AppState::Critical(ref mut cs) => AppState::Critical(cs), + } + } } pub trait IAppInteract { @@ -275,27 +293,19 @@ impl IAppInteractReloadPrivate for App { impl IAppInteractSearch for App { fn append_character(&mut self, ch: char) { - match self.state { - AppState::Search(ref mut s) => { - s.push(ch); - self.selection - .incremental_artist_search(self.music_hoard.get_collection(), s); - } - _ => unreachable!(), - } + let s = self.state.as_mut().unwrap_search(); + s.push(ch); + self.selection + .incremental_artist_search(self.music_hoard.get_collection(), s); } fn remove_character(&mut self) { - match self.state { - AppState::Search(ref mut s) => { - s.pop(); - self.selection - .reset_artist(self.music_hoard.get_collection()); - self.selection - .incremental_artist_search(self.music_hoard.get_collection(), s); - } - _ => unreachable!(), - }; + let s = self.state.as_mut().unwrap_search(); + s.pop(); + self.selection + .reset_artist(self.music_hoard.get_collection()); + self.selection + .incremental_artist_search(self.music_hoard.get_collection(), s); } fn finish_search(&mut self) { @@ -345,6 +355,42 @@ mod tests { music_hoard } + #[test] + fn app_state() { + let mut state = AppPublicState::Browse(()); + assert!(state.is_browse()); + assert!(state.as_mut().is_browse()); + + let mut state = AppPublicState::Info(()); + assert!(state.is_info()); + assert!(state.as_mut().is_info()); + + let mut state = AppPublicState::Reload(()); + assert!(state.is_reload()); + assert!(state.as_mut().is_reload()); + + let mut state = AppPublicState::Search(String::from("get rekt")); + assert!(state.is_search()); + assert!(state.as_mut().is_search()); + assert_eq!(state.unwrap_search().as_str(), "get rekt"); + + let mut state = AppPublicState::Error(String::new()); + assert!(state.is_error()); + assert!(state.as_mut().is_error()); + + let mut state = AppPublicState::Critical(String::new()); + assert!(matches!(state, AppState::Critical(_))); + assert!(matches!(state.as_mut(), AppState::Critical(_))); + } + + #[test] + #[should_panic] + fn app_state_unwrap_search_panic() { + let state = AppPublicState::Browse(()); + assert!(state.is_browse()); + state.unwrap_search(); + } + #[test] fn running_quit() { let mut app = App::new(music_hoard(COLLECTION.to_owned())); @@ -767,4 +813,60 @@ mod tests { app.dismiss_error(); assert!(app.state().is_browse()); } + + #[test] + fn search() { + let mut app = App::new(music_hoard(COLLECTION.to_owned())); + assert!(app.state().is_browse()); + + app.increment_selection(Delta::Line); + app.increment_category(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + + app.begin_search(); + assert!(app.state().is_search()); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + + app.append_character('a'); + app.append_character('l'); + app.append_character('b'); + app.append_character('u'); + app.append_character('m'); + app.append_character('_'); + app.append_character('a'); + app.append_character('r'); + app.append_character('t'); + app.append_character('i'); + app.append_character('s'); + app.append_character('t'); + app.append_character(' '); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + + app.append_character('c'); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(2)); + + app.remove_character(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + + app.append_character('b'); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + + app.finish_search(); + assert!(app.state().is_browse()); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + } } diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index ea0cf89..edc3a96 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -192,11 +192,12 @@ impl ArtistSelection { } } + // FIXME: think about converting punctuation characters to some common character. fn incremental_search(&mut self, artists: &[Artist], artist_name: &str) { if let Some(index) = self.state.list.selected() { let case_sensitive = artist_name .chars() - .any(|ch| !(ch.is_lowercase() || ch.is_whitespace())); + .any(|ch| !(ch.is_lowercase() || ch.is_whitespace() || ch.is_ascii_punctuation())); let slice = &artists[index..]; let result = if case_sensitive { @@ -807,4 +808,51 @@ mod tests { sel.reinitialise(&[], active_artist); assert_eq!(sel, expected); } + + #[test] + fn artist_incremental_search() { + let artists = &COLLECTION; + + let mut sel = ArtistSelection::initialise(&[]); + assert_eq!(sel.state.list.selected(), None); + + sel.incremental_search(artists, "album_artist a"); + assert_eq!(sel.state.list.selected(), None); + + let mut sel = ArtistSelection::initialise(artists); + assert_eq!(sel.state.list.selected(), Some(0)); + + sel.incremental_search(artists, "album_artist a"); + assert_eq!(sel.state.list.selected(), Some(0)); + + sel.reinitialise(artists, None); + assert_eq!(sel.state.list.selected(), Some(0)); + + sel.incremental_search(artists, "album_artist b"); + assert_eq!(sel.state.list.selected(), Some(1)); + + sel.reinitialise(artists, None); + assert_eq!(sel.state.list.selected(), Some(0)); + + sel.incremental_search(artists, "Album_Artist B"); + assert_eq!(sel.state.list.selected(), Some(1)); + + sel.reinitialise(artists, None); + assert_eq!(sel.state.list.selected(), Some(0)); + + sel.incremental_search(artists, "album_artist ba"); + assert_eq!(sel.state.list.selected(), Some(2)); + + sel.reinitialise(artists, None); + assert_eq!(sel.state.list.selected(), Some(0)); + + sel.incremental_search(artists, "album_artist c"); + assert_eq!(sel.state.list.selected(), Some(2)); + + sel.reinitialise(artists, None); + assert_eq!(sel.state.list.selected(), Some(0)); + + sel.incremental_search(artists, "album_artist d"); + assert_eq!(sel.state.list.selected(), Some(3)); + } } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index ea1e022..9525cbf 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -724,6 +724,10 @@ mod tests { app.state = &AppState::Reload(()); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); + let binding = AppState::Search(String::new()); + app.state = &binding; + terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); + let binding = AppState::Error(String::from("get rekt scrub")); app.state = &binding; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); -- 2.45.2 From aca46285944193512ab8c5ad4d6a7c11527e1e18 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 11 Feb 2024 16:54:48 +0100 Subject: [PATCH 05/35] Correct-ish implementation with binary search --- src/tui/app/selection.rs | 44 +++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index edc3a96..147cd50 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -192,26 +192,42 @@ impl ArtistSelection { } } - // FIXME: think about converting punctuation characters to some common character. + fn normalize_search_string(search: &str, lowercase: bool) -> String { + let normalized = if lowercase { + search.to_lowercase() + } else { + search.to_owned() + }; + + // Unlikely that this covers all possible strings, but it should at least cover strings + // relevant for music (at least in English). The list of characters handled is based on + // https://wiki.musicbrainz.org/User:Yurim/Punctuation_and_Special_Characters. + normalized + .replace("‐", "-") // U+2010 hyphen + .replace("‒", "-") // U+2012 figure dash + .replace("–", "-") // U+2013 en dash + .replace("—", "-") // U+2014 em dash + .replace("―", "-") // U+2015 horizontal bar + .replace("‘", "'") // U+2018 + .replace("’", "'") // U+2019 + .replace("“", "\"") // U+201C + .replace("”", "\"") // U+201D + .replace("…", "...") // U+2026 + .replace("−", "-") // U+2212 minus sign + } + fn incremental_search(&mut self, artists: &[Artist], artist_name: &str) { if let Some(index) = self.state.list.selected() { let case_sensitive = artist_name .chars() - .any(|ch| !(ch.is_lowercase() || ch.is_whitespace() || ch.is_ascii_punctuation())); + .any(|ch| ch.is_alphabetic() && ch.is_uppercase()); + let search_name = Self::normalize_search_string(artist_name, !case_sensitive); let slice = &artists[index..]; - let result = if case_sensitive { - slice.binary_search_by(|probe| probe.get_sort_key().name.as_str().cmp(artist_name)) - } else { - slice.binary_search_by(|probe| { - probe - .get_sort_key() - .name - .to_lowercase() - .as_str() - .cmp(artist_name) - }) - }; + let result = slice.binary_search_by(|probe| { + Self::normalize_search_string(&probe.get_sort_key().name, !case_sensitive) + .cmp(&search_name) + }); let new_index = match result { Ok(slice_index) | Err(slice_index) => index + slice_index, -- 2.45.2 From 77790b89448741db2e6c3b0c4797a918e6ad3ba5 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 11 Feb 2024 23:03:58 +0100 Subject: [PATCH 06/35] Use linear search instead for more flexibility --- src/tui/app/app.rs | 18 +++-- src/tui/app/selection.rs | 153 +++++++++++++++++++++++++++++++-------- src/tui/handler.rs | 1 + 3 files changed, 134 insertions(+), 38 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index 531ad95..aded90a 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -7,6 +7,8 @@ use crate::tui::{ lib::IMusicHoard, }; +use super::selection::IncSearch; + pub enum AppState { Browse(BS), Info(IS), @@ -293,19 +295,19 @@ impl IAppInteractReloadPrivate for App { impl IAppInteractSearch for App { fn append_character(&mut self, ch: char) { - let s = self.state.as_mut().unwrap_search(); - s.push(ch); + let collection = self.music_hoard.get_collection(); + let search = self.state.as_mut().unwrap_search(); + search.push(ch); self.selection - .incremental_artist_search(self.music_hoard.get_collection(), s); + .incremental_artist_search(IncSearch::Forward, collection, search); } fn remove_character(&mut self) { - let s = self.state.as_mut().unwrap_search(); - s.pop(); + let collection = self.music_hoard.get_collection(); + let search = self.state.as_mut().unwrap_search(); + search.pop(); self.selection - .reset_artist(self.music_hoard.get_collection()); - self.selection - .incremental_artist_search(self.music_hoard.get_collection(), s); + .incremental_artist_search(IncSearch::Reverse, collection, search); } fn finish_search(&mut self) { diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index 147cd50..b1afdb2 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -61,6 +61,11 @@ impl Delta { } } +pub enum IncSearch { + Forward, + Reverse, +} + impl Selection { pub fn new(artists: &[Artist]) -> Self { Selection { @@ -95,8 +100,14 @@ impl Selection { }; } - pub fn incremental_artist_search(&mut self, collection: &Collection, artist_name: &str) { - self.artist.incremental_search(collection, artist_name); + pub fn incremental_artist_search( + &mut self, + direction: IncSearch, + collection: &Collection, + artist_name: &str, + ) { + self.artist + .incremental_search(direction, collection, artist_name); } pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) { @@ -192,7 +203,8 @@ impl ArtistSelection { } } - fn normalize_search_string(search: &str, lowercase: bool) -> String { + // FIXME: use aho_corasick for normalization + fn normalize_search_string(search: &str, lowercase: bool, asciify: bool) -> String { let normalized = if lowercase { search.to_lowercase() } else { @@ -202,40 +214,121 @@ impl ArtistSelection { // Unlikely that this covers all possible strings, but it should at least cover strings // relevant for music (at least in English). The list of characters handled is based on // https://wiki.musicbrainz.org/User:Yurim/Punctuation_and_Special_Characters. - normalized - .replace("‐", "-") // U+2010 hyphen - .replace("‒", "-") // U+2012 figure dash - .replace("–", "-") // U+2013 en dash - .replace("—", "-") // U+2014 em dash - .replace("―", "-") // U+2015 horizontal bar - .replace("‘", "'") // U+2018 - .replace("’", "'") // U+2019 - .replace("“", "\"") // U+201C - .replace("”", "\"") // U+201D - .replace("…", "...") // U+2026 - .replace("−", "-") // U+2212 minus sign + if asciify { + normalized + .replace("‐", "-") // U+2010 hyphen + .replace("‒", "-") // U+2012 figure dash + .replace("–", "-") // U+2013 en dash + .replace("—", "-") // U+2014 em dash + .replace("―", "-") // U+2015 horizontal bar + .replace("‘", "'") // U+2018 + .replace("’", "'") // U+2019 + .replace("“", "\"") // U+201C + .replace("”", "\"") // U+201D + .replace("…", "...") // U+2026 + .replace("−", "-") // U+2212 minus sign + } else { + normalized + } } - fn incremental_search(&mut self, artists: &[Artist], artist_name: &str) { - if let Some(index) = self.state.list.selected() { - let case_sensitive = artist_name - .chars() - .any(|ch| ch.is_alphabetic() && ch.is_uppercase()); - let search_name = Self::normalize_search_string(artist_name, !case_sensitive); - let slice = &artists[index..]; + fn is_case_sensitive(artist_name: &str) -> bool { + artist_name + .chars() + .any(|ch| ch.is_alphabetic() && ch.is_uppercase()) + } - let result = slice.binary_search_by(|probe| { - Self::normalize_search_string(&probe.get_sort_key().name, !case_sensitive) - .cmp(&search_name) - }); + fn is_char_sensitive(artist_name: &str) -> bool { + let special_chars: &[char] = &['‐', '‒', '–', '—', '―', '‘', '’', '“', '”', '…', '−']; + artist_name.chars().any(|ch| special_chars.contains(&ch)) + } - let new_index = match result { - Ok(slice_index) | Err(slice_index) => index + slice_index, - }; - self.select_to(artists, new_index); + // FIXME: compare against both sort key and actual name + fn incremental_search_predicate( + case_sensitive: bool, + char_sensitive: bool, + search_name: &String, + probe: &Artist, + ) -> bool { + let probe_name = &probe.get_sort_key().name; + match Self::normalize_search_string(probe_name, !case_sensitive, !char_sensitive) + .cmp(search_name) + { + std::cmp::Ordering::Less => false, + std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => true, } } + fn incremental_search(&mut self, direction: IncSearch, artists: &[Artist], artist_name: &str) { + if let Some(index) = self.state.list.selected() { + let case_sensitive = Self::is_case_sensitive(artist_name); + let char_sensitive = Self::is_char_sensitive(artist_name); + let search_name = + Self::normalize_search_string(artist_name, !case_sensitive, !char_sensitive); + + match direction { + IncSearch::Forward => self.forward_incremental_search( + artists, + index, + case_sensitive, + char_sensitive, + &search_name, + ), + IncSearch::Reverse => self.reverse_incremental_search( + artists, + index, + case_sensitive, + char_sensitive, + &search_name, + ), + } + } + } + + fn forward_incremental_search( + &mut self, + artists: &[Artist], + index: usize, + case_sensitive: bool, + char_sensitive: bool, + search: &String, + ) { + let slice = &artists[index..]; + + let result = slice.iter().position(|probe| { + Self::incremental_search_predicate(case_sensitive, char_sensitive, &search, probe) + }); + + let new_index = match result { + Some(slice_index) => index + slice_index, + None => artists.len(), + }; + self.select_to(artists, new_index); + } + + fn reverse_incremental_search( + &mut self, + artists: &[Artist], + index: usize, + case_sensitive: bool, + char_sensitive: bool, + search: &String, + ) { + let slice = &artists[..(index + 1)]; + + // We search using opposite predicate in the reverse direction because what matters is the + // point at which the predicate flips value. + let result = slice.iter().rev().position(|probe| { + !Self::incremental_search_predicate(case_sensitive, char_sensitive, &search, probe) + }); + + let new_index = match result { + Some(slice_index) => index - slice_index + 1, + None => 0, + }; + self.select_to(artists, new_index); + } + fn increment_by(&mut self, artists: &[Artist], by: usize) { if let Some(index) = self.state.list.selected() { let result = index.saturating_add(by); diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 5a0d967..8860ad3 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -58,6 +58,7 @@ impl IEventHandlerPrivate for EventHandler { // Exit application on `Ctrl-C`. KeyCode::Char('c') | KeyCode::Char('C') => { app.force_quit(); + return; } _ => {} } -- 2.45.2 From f8815982f40de6ae4c2a29991e3a05d6d3caad70 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Mon, 12 Feb 2024 20:11:18 +0100 Subject: [PATCH 07/35] More intuitive search behaviour --- src/tui/app/app.rs | 14 +++--- src/tui/app/selection.rs | 105 ++++++++------------------------------- 2 files changed, 30 insertions(+), 89 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index aded90a..b9e58f7 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -7,8 +7,6 @@ use crate::tui::{ lib::IMusicHoard, }; -use super::selection::IncSearch; - pub enum AppState { Browse(BS), Info(IS), @@ -298,16 +296,20 @@ impl IAppInteractSearch for App { let collection = self.music_hoard.get_collection(); let search = self.state.as_mut().unwrap_search(); search.push(ch); - self.selection - .incremental_artist_search(IncSearch::Forward, collection, search); + self.selection.incremental_artist_search(collection, search); } + // Removing a character restarts the search from scratch. For now, the performance impact of + // this is not noticeable. If it does become noticeable, some form of memoization should be used + // as an optimisation. The most intuitive behaviour when removing a character is to return to + // the previous search position. However, it is difficult to construct a search predicate in + // selection.rs that will always correctly reverse to the previous position. fn remove_character(&mut self) { let collection = self.music_hoard.get_collection(); let search = self.state.as_mut().unwrap_search(); search.pop(); - self.selection - .incremental_artist_search(IncSearch::Reverse, collection, search); + self.selection.reset_artist(&collection); + self.selection.incremental_artist_search(collection, search); } fn finish_search(&mut self) { diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index b1afdb2..b95ac1b 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -61,11 +61,6 @@ impl Delta { } } -pub enum IncSearch { - Forward, - Reverse, -} - impl Selection { pub fn new(artists: &[Artist]) -> Self { Selection { @@ -100,14 +95,8 @@ impl Selection { }; } - pub fn incremental_artist_search( - &mut self, - direction: IncSearch, - collection: &Collection, - artist_name: &str, - ) { - self.artist - .incremental_search(direction, collection, artist_name); + pub fn incremental_artist_search(&mut self, collection: &Collection, artist_name: &str) { + self.artist.incremental_search(collection, artist_name); } pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) { @@ -204,7 +193,7 @@ impl ArtistSelection { } // FIXME: use aho_corasick for normalization - fn normalize_search_string(search: &str, lowercase: bool, asciify: bool) -> String { + fn normalize_search(search: &str, lowercase: bool, asciify: bool) -> String { let normalized = if lowercase { search.to_lowercase() } else { @@ -243,92 +232,42 @@ impl ArtistSelection { artist_name.chars().any(|ch| special_chars.contains(&ch)) } - // FIXME: compare against both sort key and actual name fn incremental_search_predicate( case_sensitive: bool, char_sensitive: bool, search_name: &String, probe: &Artist, ) -> bool { - let probe_name = &probe.get_sort_key().name; - match Self::normalize_search_string(probe_name, !case_sensitive, !char_sensitive) - .cmp(search_name) - { - std::cmp::Ordering::Less => false, - std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => true, + let name = Self::normalize_search(&probe.id.name, !case_sensitive, !char_sensitive); + let mut result = name.starts_with(search_name); + + if let Some(ref probe_sort) = probe.sort { + let name = Self::normalize_search(&probe_sort.name, !case_sensitive, !char_sensitive); + result = result || name.starts_with(search_name); } + + result } - fn incremental_search(&mut self, direction: IncSearch, artists: &[Artist], artist_name: &str) { + // FIXME: Use memoization - the only way to have correct behaviour + fn incremental_search(&mut self, artists: &[Artist], artist_name: &str) { if let Some(index) = self.state.list.selected() { let case_sensitive = Self::is_case_sensitive(artist_name); let char_sensitive = Self::is_char_sensitive(artist_name); - let search_name = - Self::normalize_search_string(artist_name, !case_sensitive, !char_sensitive); + let search = Self::normalize_search(artist_name, !case_sensitive, !char_sensitive); - match direction { - IncSearch::Forward => self.forward_incremental_search( - artists, - index, - case_sensitive, - char_sensitive, - &search_name, - ), - IncSearch::Reverse => self.reverse_incremental_search( - artists, - index, - case_sensitive, - char_sensitive, - &search_name, - ), + let slice = &artists[index..]; + + let result = slice.iter().position(|probe| { + Self::incremental_search_predicate(case_sensitive, char_sensitive, &search, probe) + }); + + if let Some(slice_index) = result { + self.select_to(artists, index + slice_index); } } } - fn forward_incremental_search( - &mut self, - artists: &[Artist], - index: usize, - case_sensitive: bool, - char_sensitive: bool, - search: &String, - ) { - let slice = &artists[index..]; - - let result = slice.iter().position(|probe| { - Self::incremental_search_predicate(case_sensitive, char_sensitive, &search, probe) - }); - - let new_index = match result { - Some(slice_index) => index + slice_index, - None => artists.len(), - }; - self.select_to(artists, new_index); - } - - fn reverse_incremental_search( - &mut self, - artists: &[Artist], - index: usize, - case_sensitive: bool, - char_sensitive: bool, - search: &String, - ) { - let slice = &artists[..(index + 1)]; - - // We search using opposite predicate in the reverse direction because what matters is the - // point at which the predicate flips value. - let result = slice.iter().rev().position(|probe| { - !Self::incremental_search_predicate(case_sensitive, char_sensitive, &search, probe) - }); - - let new_index = match result { - Some(slice_index) => index - slice_index + 1, - None => 0, - }; - self.select_to(artists, new_index); - } - fn increment_by(&mut self, artists: &[Artist], by: usize) { if let Some(index) = self.state.list.selected() { let result = index.saturating_add(by); -- 2.45.2 From cc3de25192647cb3f76d4c9e8db8b6c430cf4273 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Mon, 12 Feb 2024 22:30:02 +0100 Subject: [PATCH 08/35] memoization works --- src/tui/app/app.rs | 25 ++++++++++++++++--------- src/tui/app/selection.rs | 34 +++++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index b9e58f7..14aa923 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -139,6 +139,9 @@ pub struct App { music_hoard: MH, selection: Selection, state: AppState<(), (), (), String, String, String>, + // FIXME: is it possible to use a wrapper struct? - when state() is called return a wrapper + // around App which will contain App. + memo: Vec>, } impl App { @@ -153,6 +156,7 @@ impl App { music_hoard, selection, state, + memo: vec![], } } @@ -242,6 +246,7 @@ impl IAppInteractBrowse for App { fn begin_search(&mut self) { assert!(self.state.is_browse()); self.state = AppState::Search(String::new()); + self.memo = vec![self.selection.selected_artist()]; self.selection .reset_artist(self.music_hoard.get_collection()); } @@ -291,25 +296,27 @@ impl IAppInteractReloadPrivate for App { } } +// FIXME: add `search_next` to find next match +// FIXME: once `search_next` is added, backspace should step back. If the previous action was +// `search_next` then no character should be removed. If the previous action was to append +// a character, a character should be removed. When in doubt see how Emacs's isearch works. +// FIXME: Also add a `cancel_search` which returns to the previous selection. impl IAppInteractSearch for App { fn append_character(&mut self, ch: char) { let collection = self.music_hoard.get_collection(); let search = self.state.as_mut().unwrap_search(); search.push(ch); - self.selection.incremental_artist_search(collection, search); + let prev = self.selection.incremental_artist_search(collection, search); + self.memo.push(prev); } - // Removing a character restarts the search from scratch. For now, the performance impact of - // this is not noticeable. If it does become noticeable, some form of memoization should be used - // as an optimisation. The most intuitive behaviour when removing a character is to return to - // the previous search position. However, it is difficult to construct a search predicate in - // selection.rs that will always correctly reverse to the previous position. fn remove_character(&mut self) { let collection = self.music_hoard.get_collection(); let search = self.state.as_mut().unwrap_search(); - search.pop(); - self.selection.reset_artist(&collection); - self.selection.incremental_artist_search(collection, search); + if search.pop().is_some() { + let prev = self.memo.pop().unwrap(); + self.selection.select_artist(collection, prev); + } } fn finish_search(&mut self) { diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index b95ac1b..fcf7f6c 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -5,6 +5,7 @@ use musichoard::collection::{ Collection, }; use ratatui::widgets::ListState; +use std::cmp; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Category { @@ -69,10 +70,19 @@ impl Selection { } } + // FIXME: the name is not suitable in the presence of other `select` methods. pub fn select(&mut self, artists: &[Artist], selected: ActiveSelection) { self.artist.reinitialise(artists, selected.artist); } + pub fn selected_artist(&mut self) -> Option { + self.artist.state.list.selected() + } + + pub fn select_artist(&mut self, artists: &[Artist], index: Option) { + self.artist.select(artists, index); + } + pub fn reset_artist(&mut self, artists: &[Artist]) { if self.artist.state.list.selected() != Some(0) { self.select(artists, ActiveSelection { artist: None }); @@ -95,8 +105,12 @@ impl Selection { }; } - pub fn incremental_artist_search(&mut self, collection: &Collection, artist_name: &str) { - self.artist.incremental_search(collection, artist_name); + pub fn incremental_artist_search( + &mut self, + collection: &Collection, + artist_name: &str, + ) -> Option { + self.artist.incremental_search(collection, artist_name) } pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) { @@ -182,10 +196,13 @@ impl ArtistSelection { } } + fn select(&mut self, artists: &[Artist], mut to: Option) { + to = to.map(|i| cmp::min(i, artists.len() - 1)); + self.state.list.select(to); + } + fn select_to(&mut self, artists: &[Artist], mut to: usize) { - if to >= artists.len() { - to = artists.len() - 1; - } + to = cmp::min(to, artists.len() - 1); if self.state.list.selected() != Some(to) { self.state.list.select(Some(to)); self.album = AlbumSelection::initialise(&artists[to].albums); @@ -249,8 +266,9 @@ impl ArtistSelection { result } - // FIXME: Use memoization - the only way to have correct behaviour - fn incremental_search(&mut self, artists: &[Artist], artist_name: &str) { + fn incremental_search(&mut self, artists: &[Artist], artist_name: &str) -> Option { + let previous = self.state.list.selected(); + if let Some(index) = self.state.list.selected() { let case_sensitive = Self::is_case_sensitive(artist_name); let char_sensitive = Self::is_char_sensitive(artist_name); @@ -266,6 +284,8 @@ impl ArtistSelection { self.select_to(artists, index + slice_index); } } + + previous } fn increment_by(&mut self, artists: &[Artist], by: usize) { -- 2.45.2 From 39e0c7c3f66133917cb0b723fd9d81578abc30b5 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Mon, 12 Feb 2024 22:33:54 +0100 Subject: [PATCH 09/35] Clarify names --- src/tui/app/app.rs | 2 +- src/tui/app/selection.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index 14aa923..ddf2c6d 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -288,7 +288,7 @@ impl IAppInteractReloadPrivate for App { match result { Ok(()) => { self.selection - .select(self.music_hoard.get_collection(), previous); + .select_by_id(self.music_hoard.get_collection(), previous); self.state = AppState::Browse(()) } Err(err) => self.state = AppState::Error(err.to_string()), diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index fcf7f6c..9316432 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -70,8 +70,7 @@ impl Selection { } } - // FIXME: the name is not suitable in the presence of other `select` methods. - pub fn select(&mut self, artists: &[Artist], selected: ActiveSelection) { + pub fn select_by_id(&mut self, artists: &[Artist], selected: ActiveSelection) { self.artist.reinitialise(artists, selected.artist); } @@ -85,7 +84,7 @@ impl Selection { pub fn reset_artist(&mut self, artists: &[Artist]) { if self.artist.state.list.selected() != Some(0) { - self.select(artists, ActiveSelection { artist: None }); + self.select_by_id(artists, ActiveSelection { artist: None }); } } -- 2.45.2 From 488d20273e380e88d618613c8a0a736b64373917 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Mon, 12 Feb 2024 22:52:18 +0100 Subject: [PATCH 10/35] Add search_next --- src/tui/app/app.rs | 12 ++++++++++-- src/tui/app/selection.rs | 15 ++++++++++++--- src/tui/handler.rs | 10 ++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index ddf2c6d..e0223da 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -108,6 +108,7 @@ pub trait IAppInteractReload { pub trait IAppInteractSearch { fn append_character(&mut self, ch: char); + fn search_next(&mut self); fn remove_character(&mut self); fn finish_search(&mut self); } @@ -246,6 +247,7 @@ impl IAppInteractBrowse for App { fn begin_search(&mut self) { assert!(self.state.is_browse()); self.state = AppState::Search(String::new()); + // FIXME: this should be the entire selection - not just the artist. self.memo = vec![self.selection.selected_artist()]; self.selection .reset_artist(self.music_hoard.get_collection()); @@ -296,7 +298,6 @@ impl IAppInteractReloadPrivate for App { } } -// FIXME: add `search_next` to find next match // FIXME: once `search_next` is added, backspace should step back. If the previous action was // `search_next` then no character should be removed. If the previous action was to append // a character, a character should be removed. When in doubt see how Emacs's isearch works. @@ -306,7 +307,14 @@ impl IAppInteractSearch for App { let collection = self.music_hoard.get_collection(); let search = self.state.as_mut().unwrap_search(); search.push(ch); - let prev = self.selection.incremental_artist_search(collection, search); + let prev = self.selection.incremental_artist_search(collection, search, false); + self.memo.push(prev); + } + + fn search_next(&mut self) { + let collection = self.music_hoard.get_collection(); + let search = self.state.as_mut().unwrap_search(); + let prev = self.selection.incremental_artist_search(collection, search, true); self.memo.push(prev); } diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index 9316432..be5136c 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -108,8 +108,9 @@ impl Selection { &mut self, collection: &Collection, artist_name: &str, + next: bool, ) -> Option { - self.artist.incremental_search(collection, artist_name) + self.artist.incremental_search(collection, artist_name, next) } pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) { @@ -265,14 +266,22 @@ impl ArtistSelection { result } - fn incremental_search(&mut self, artists: &[Artist], artist_name: &str) -> Option { + fn incremental_search( + &mut self, + artists: &[Artist], + artist_name: &str, + next: bool, + ) -> Option { let previous = self.state.list.selected(); - if let Some(index) = self.state.list.selected() { + if let Some(mut index) = self.state.list.selected() { let case_sensitive = Self::is_case_sensitive(artist_name); let char_sensitive = Self::is_char_sensitive(artist_name); let search = Self::normalize_search(artist_name, !case_sensitive, !char_sensitive); + if next && ((index + 1) < artists.len()) { + index += 1; + } let slice = &artists[index..]; let result = slice.iter().position(|probe| { diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 8860ad3..687baf2 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -146,6 +146,16 @@ impl IEventHandlerPrivate for EventHandler { } fn handle_search_key_event(app: &mut ::SS, key_event: KeyEvent) { + if key_event.modifiers == KeyModifiers::CONTROL { + match key_event.code { + KeyCode::Char('s') | KeyCode::Char('S') => { + app.search_next(); + }, + _ => {} + } + return; + } + match key_event.code { // Add/remove character to search. KeyCode::Char(ch) => app.append_character(ch), -- 2.45.2 From a29a3fa3ce057d32a6efbfd2c227a36319e664c8 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Mon, 12 Feb 2024 23:09:19 +0100 Subject: [PATCH 11/35] Implement step back --- src/tui/app/app.rs | 36 ++++++++++++++++++++++-------------- src/tui/app/selection.rs | 11 +++++++---- src/tui/handler.rs | 2 +- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index e0223da..8dc5f41 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -109,7 +109,7 @@ pub trait IAppInteractReload { pub trait IAppInteractSearch { fn append_character(&mut self, ch: char); fn search_next(&mut self); - fn remove_character(&mut self); + fn step_back(&mut self); fn finish_search(&mut self); } @@ -142,7 +142,8 @@ pub struct App { state: AppState<(), (), (), String, String, String>, // FIXME: is it possible to use a wrapper struct? - when state() is called return a wrapper // around App which will contain App. - memo: Vec>, + orig: Option, + memo: Vec<(Option, bool)>, } impl App { @@ -157,6 +158,7 @@ impl App { music_hoard, selection, state, + orig: None, memo: vec![], } } @@ -248,7 +250,8 @@ impl IAppInteractBrowse for App { assert!(self.state.is_browse()); self.state = AppState::Search(String::new()); // FIXME: this should be the entire selection - not just the artist. - self.memo = vec![self.selection.selected_artist()]; + self.orig = self.selection.selected_artist(); + self.memo = vec![]; self.selection .reset_artist(self.music_hoard.get_collection()); } @@ -298,31 +301,36 @@ impl IAppInteractReloadPrivate for App { } } -// FIXME: once `search_next` is added, backspace should step back. If the previous action was -// `search_next` then no character should be removed. If the previous action was to append -// a character, a character should be removed. When in doubt see how Emacs's isearch works. // FIXME: Also add a `cancel_search` which returns to the previous selection. impl IAppInteractSearch for App { fn append_character(&mut self, ch: char) { let collection = self.music_hoard.get_collection(); let search = self.state.as_mut().unwrap_search(); search.push(ch); - let prev = self.selection.incremental_artist_search(collection, search, false); - self.memo.push(prev); + let prev = self + .selection + .incremental_artist_search(collection, search, false); + self.memo.push((prev, true)); } fn search_next(&mut self) { let collection = self.music_hoard.get_collection(); let search = self.state.as_mut().unwrap_search(); - let prev = self.selection.incremental_artist_search(collection, search, true); - self.memo.push(prev); + if !search.is_empty() { + let prev = self + .selection + .incremental_artist_search(collection, search, true); + self.memo.push((prev, false)); + } } - fn remove_character(&mut self) { + fn step_back(&mut self) { let collection = self.music_hoard.get_collection(); - let search = self.state.as_mut().unwrap_search(); - if search.pop().is_some() { - let prev = self.memo.pop().unwrap(); + if let Some((prev, pop_char)) = self.memo.pop() { + if pop_char { + let search = self.state.as_mut().unwrap_search(); + search.pop(); + } self.selection.select_artist(collection, prev); } } diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index be5136c..dc598b8 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -110,7 +110,8 @@ impl Selection { artist_name: &str, next: bool, ) -> Option { - self.artist.incremental_search(collection, artist_name, next) + self.artist + .incremental_search(collection, artist_name, next) } pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) { @@ -196,9 +197,11 @@ impl ArtistSelection { } } - fn select(&mut self, artists: &[Artist], mut to: Option) { - to = to.map(|i| cmp::min(i, artists.len() - 1)); - self.state.list.select(to); + fn select(&mut self, artists: &[Artist], to: Option) { + match to { + Some(to) => self.select_to(artists, to), + None => self.state.list.select(None), + } } fn select_to(&mut self, artists: &[Artist], mut to: usize) { diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 687baf2..e52e9cb 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -159,7 +159,7 @@ impl IEventHandlerPrivate for EventHandler { match key_event.code { // Add/remove character to search. KeyCode::Char(ch) => app.append_character(ch), - KeyCode::Backspace => app.remove_character(), + KeyCode::Backspace => app.step_back(), // Return. KeyCode::Esc | KeyCode::Enter => app.finish_search(), // Othey keys. -- 2.45.2 From 5b7263d5d8ffb87e1792964a7dfe9fb3d6630cbd Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Mon, 12 Feb 2024 23:18:03 +0100 Subject: [PATCH 12/35] Implement cancel search --- src/tui/app/app.rs | 9 ++++++++- src/tui/handler.rs | 3 +++ src/tui/ui.rs | 9 +++++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index 8dc5f41..aef8fac 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -111,6 +111,7 @@ pub trait IAppInteractSearch { fn search_next(&mut self); fn step_back(&mut self); fn finish_search(&mut self); + fn cancel_search(&mut self); } pub trait IAppInteractError { @@ -301,7 +302,6 @@ impl IAppInteractReloadPrivate for App { } } -// FIXME: Also add a `cancel_search` which returns to the previous selection. impl IAppInteractSearch for App { fn append_character(&mut self, ch: char) { let collection = self.music_hoard.get_collection(); @@ -339,6 +339,13 @@ impl IAppInteractSearch for App { assert!(self.state.is_search()); self.state = AppState::Browse(()); } + + fn cancel_search(&mut self) { + assert!(self.state.is_search()); + let collection = self.music_hoard.get_collection(); + self.selection.select_artist(collection, self.orig); + self.state = AppState::Browse(()); + } } impl IAppInteractError for App { diff --git a/src/tui/handler.rs b/src/tui/handler.rs index e52e9cb..9ce9411 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -151,6 +151,9 @@ impl IEventHandlerPrivate for EventHandler { KeyCode::Char('s') | KeyCode::Char('S') => { app.search_next(); }, + KeyCode::Char('g') | KeyCode::Char('G') => { + app.cancel_search(); + }, _ => {} } return; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 9525cbf..cea31c6 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -348,6 +348,7 @@ impl<'a, 'b> TrackState<'a, 'b> { struct Minibuffer<'a> { paragraphs: Vec>, + // FIXME: make this an option columns: u16, } @@ -376,8 +377,12 @@ impl Minibuffer<'_> { columns, }, AppState::Search(ref s) => Minibuffer { - paragraphs: vec![Paragraph::new(format!("I-search: {s}"))], - columns: 1, + paragraphs: vec![ + Paragraph::new(format!("I-search: {s}")), + Paragraph::new(format!("ctrl+s: search next")).alignment(Alignment::Center), + Paragraph::new(format!("ctrl+g: cancel search")).alignment(Alignment::Center), + ], + columns, }, AppState::Error(_) => Minibuffer { paragraphs: vec![Paragraph::new( -- 2.45.2 From 9cf29b99b80814e2e15158065210e2ad853b7dde Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Tue, 13 Feb 2024 07:11:48 +0100 Subject: [PATCH 13/35] Save entire list state when beginning search --- src/tui/app/app.rs | 18 +++---- src/tui/app/selection.rs | 112 +++++++++++++++++++++++---------------- 2 files changed, 73 insertions(+), 57 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index aef8fac..a3f324b 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -3,7 +3,7 @@ use musichoard::collection::Collection; use crate::tui::{ - app::selection::{ActiveSelection, Delta, Selection}, + app::selection::{Delta, IdSelection, Selection, ListSelection}, lib::IMusicHoard, }; @@ -143,7 +143,7 @@ pub struct App { state: AppState<(), (), (), String, String, String>, // FIXME: is it possible to use a wrapper struct? - when state() is called return a wrapper // around App which will contain App. - orig: Option, + orig: Option, memo: Vec<(Option, bool)>, } @@ -250,8 +250,7 @@ impl IAppInteractBrowse for App { fn begin_search(&mut self) { assert!(self.state.is_browse()); self.state = AppState::Search(String::new()); - // FIXME: this should be the entire selection - not just the artist. - self.orig = self.selection.selected_artist(); + self.orig = Some(ListSelection::get(&self.selection)); self.memo = vec![]; self.selection .reset_artist(self.music_hoard.get_collection()); @@ -267,13 +266,13 @@ impl IAppInteractInfo for App { impl IAppInteractReload for App { fn reload_library(&mut self) { - let previous = ActiveSelection::get(self.music_hoard.get_collection(), &self.selection); + let previous = IdSelection::get(self.music_hoard.get_collection(), &self.selection); let result = self.music_hoard.rescan_library(); self.refresh(previous, result); } fn reload_database(&mut self) { - let previous = ActiveSelection::get(self.music_hoard.get_collection(), &self.selection); + let previous = IdSelection::get(self.music_hoard.get_collection(), &self.selection); let result = self.music_hoard.load_from_database(); self.refresh(previous, result); } @@ -285,11 +284,11 @@ impl IAppInteractReload for App { } trait IAppInteractReloadPrivate { - fn refresh(&mut self, previous: ActiveSelection, result: Result<(), musichoard::Error>); + fn refresh(&mut self, previous: IdSelection, result: Result<(), musichoard::Error>); } impl IAppInteractReloadPrivate for App { - fn refresh(&mut self, previous: ActiveSelection, result: Result<(), musichoard::Error>) { + fn refresh(&mut self, previous: IdSelection, result: Result<(), musichoard::Error>) { assert!(self.state.is_reload()); match result { Ok(()) => { @@ -342,8 +341,7 @@ impl IAppInteractSearch for App { fn cancel_search(&mut self) { assert!(self.state.is_search()); - let collection = self.music_hoard.get_collection(); - self.selection.select_artist(collection, self.orig); + self.selection.select_by_list(self.orig.take().unwrap()); self.state = AppState::Browse(()); } } diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index dc598b8..c81df44 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -70,12 +70,14 @@ impl Selection { } } - pub fn select_by_id(&mut self, artists: &[Artist], selected: ActiveSelection) { - self.artist.reinitialise(artists, selected.artist); + pub fn select_by_list(&mut self, selected: ListSelection) { + self.artist.state.list = selected.artist; + self.artist.album.state.list = selected.album; + self.artist.album.track.state.list = selected.track; } - pub fn selected_artist(&mut self) -> Option { - self.artist.state.list.selected() + pub fn select_by_id(&mut self, artists: &[Artist], selected: IdSelection) { + self.artist.reinitialise(artists, selected.artist); } pub fn select_artist(&mut self, artists: &[Artist], index: Option) { @@ -84,7 +86,7 @@ impl Selection { pub fn reset_artist(&mut self, artists: &[Artist]) { if self.artist.state.list.selected() != Some(0) { - self.select_by_id(artists, ActiveSelection { artist: None }); + self.select_by_id(artists, IdSelection { artist: None }); } } @@ -165,7 +167,7 @@ impl ArtistSelection { selection } - fn reinitialise(&mut self, artists: &[Artist], active: Option) { + fn reinitialise(&mut self, artists: &[Artist], active: Option) { if let Some(active) = active { let result = artists.binary_search_by(|a| a.get_sort_key().cmp(&active.artist_id)); match result { @@ -181,7 +183,7 @@ impl ArtistSelection { &mut self, artists: &[Artist], index: usize, - active_album: Option, + active_album: Option, ) { if artists.is_empty() { self.state.list.select(None); @@ -359,7 +361,7 @@ impl AlbumSelection { selection } - fn reinitialise(&mut self, albums: &[Album], album: Option) { + fn reinitialise(&mut self, albums: &[Album], album: Option) { if let Some(album) = album { let result = albums.binary_search_by(|a| a.get_sort_key().cmp(&album.album_id)); match result { @@ -375,7 +377,7 @@ impl AlbumSelection { &mut self, albums: &[Album], index: usize, - active_track: Option, + active_track: Option, ) { if albums.is_empty() { self.state.list.select(None); @@ -443,7 +445,7 @@ impl TrackSelection { selection } - fn reinitialise(&mut self, tracks: &[Track], track: Option) { + fn reinitialise(&mut self, tracks: &[Track], track: Option) { if let Some(track) = track { let result = tracks.binary_search_by(|t| t.get_sort_key().cmp(&track.track_id)); match result { @@ -494,61 +496,77 @@ impl TrackSelection { } } -pub struct ActiveSelection { - artist: Option, +pub struct ListSelection { + artist: ListState, + album: ListState, + track: ListState, } -struct ActiveArtist { - artist_id: ArtistId, - album: Option, -} - -struct ActiveAlbum { - album_id: AlbumId, - track: Option, -} - -struct ActiveTrack { - track_id: TrackId, -} - -impl ActiveSelection { - pub fn get(collection: &Collection, selection: &Selection) -> Self { - ActiveSelection { - artist: ActiveArtist::get(collection, &selection.artist), +impl ListSelection { + pub fn get(selection: &Selection) -> Self { + ListSelection { + artist: selection.artist.state.list.clone(), + album: selection.artist.album.state.list.clone(), + track: selection.artist.album.track.state.list.clone(), } } } -impl ActiveArtist { +pub struct IdSelection { + artist: Option, +} + +struct IdSelectArtist { + artist_id: ArtistId, + album: Option, +} + +struct IdSelectAlbum { + album_id: AlbumId, + track: Option, +} + +struct IdSelectTrack { + track_id: TrackId, +} + +impl IdSelection { + pub fn get(collection: &Collection, selection: &Selection) -> Self { + IdSelection { + artist: IdSelectArtist::get(collection, &selection.artist), + } + } +} + +impl IdSelectArtist { fn get(artists: &[Artist], selection: &ArtistSelection) -> Option { selection.state.list.selected().map(|index| { let artist = &artists[index]; - ActiveArtist { + IdSelectArtist { artist_id: artist.get_sort_key().clone(), - album: ActiveAlbum::get(&artist.albums, &selection.album), + album: IdSelectAlbum::get(&artist.albums, &selection.album), } }) } } -impl ActiveAlbum { +impl IdSelectAlbum { fn get(albums: &[Album], selection: &AlbumSelection) -> Option { selection.state.list.selected().map(|index| { let album = &albums[index]; - ActiveAlbum { + IdSelectAlbum { album_id: album.get_sort_key().clone(), - track: ActiveTrack::get(&album.tracks, &selection.track), + track: IdSelectTrack::get(&album.tracks, &selection.track), } }) } } -impl ActiveTrack { +impl IdSelectTrack { fn get(tracks: &[Track], selection: &TrackSelection) -> Option { selection.state.list.selected().map(|index| { let track = &tracks[index]; - ActiveTrack { + IdSelectTrack { track_id: track.get_sort_key().clone(), } }) @@ -626,20 +644,20 @@ mod tests { // Re-initialise. let expected = sel.clone(); - let active_track = ActiveTrack::get(tracks, &sel); + let active_track = IdSelectTrack::get(tracks, &sel); sel.reinitialise(tracks, active_track); assert_eq!(sel, expected); // Re-initialise out-of-bounds. let mut expected = sel.clone(); expected.decrement(tracks, Delta::Line); - let active_track = ActiveTrack::get(tracks, &sel); + let active_track = IdSelectTrack::get(tracks, &sel); sel.reinitialise(&tracks[..(tracks.len() - 1)], active_track); assert_eq!(sel, expected); // Re-initialise empty. let expected = TrackSelection::initialise(&[]); - let active_track = ActiveTrack::get(tracks, &sel); + let active_track = IdSelectTrack::get(tracks, &sel); sel.reinitialise(&[], active_track); assert_eq!(sel, expected); } @@ -748,20 +766,20 @@ mod tests { // Re-initialise. let expected = sel.clone(); - let active_album = ActiveAlbum::get(albums, &sel); + let active_album = IdSelectAlbum::get(albums, &sel); sel.reinitialise(albums, active_album); assert_eq!(sel, expected); // Re-initialise out-of-bounds. let mut expected = sel.clone(); expected.decrement(albums, Delta::Line); - let active_album = ActiveAlbum::get(albums, &sel); + let active_album = IdSelectAlbum::get(albums, &sel); sel.reinitialise(&albums[..(albums.len() - 1)], active_album); assert_eq!(sel, expected); // Re-initialise empty. let expected = AlbumSelection::initialise(&[]); - let active_album = ActiveAlbum::get(albums, &sel); + let active_album = IdSelectAlbum::get(albums, &sel); sel.reinitialise(&[], active_album); assert_eq!(sel, expected); } @@ -870,20 +888,20 @@ mod tests { // Re-initialise. let expected = sel.clone(); - let active_artist = ActiveArtist::get(artists, &sel); + let active_artist = IdSelectArtist::get(artists, &sel); sel.reinitialise(artists, active_artist); assert_eq!(sel, expected); // Re-initialise out-of-bounds. let mut expected = sel.clone(); expected.decrement(artists, Delta::Line); - let active_artist = ActiveArtist::get(artists, &sel); + let active_artist = IdSelectArtist::get(artists, &sel); sel.reinitialise(&artists[..(artists.len() - 1)], active_artist); assert_eq!(sel, expected); // Re-initialise empty. let expected = ArtistSelection::initialise(&[]); - let active_artist = ActiveArtist::get(artists, &sel); + let active_artist = IdSelectArtist::get(artists, &sel); sel.reinitialise(&[], active_artist); assert_eq!(sel, expected); } -- 2.45.2 From 55129ffcfc0b8ecf60729f3e6a439b703b77fdcb Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Tue, 13 Feb 2024 07:44:20 +0100 Subject: [PATCH 14/35] Minor change --- src/tui/ui.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tui/ui.rs b/src/tui/ui.rs index cea31c6..cbc5323 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -348,7 +348,6 @@ impl<'a, 'b> TrackState<'a, 'b> { struct Minibuffer<'a> { paragraphs: Vec>, - // FIXME: make this an option columns: u16, } @@ -388,11 +387,11 @@ impl Minibuffer<'_> { paragraphs: vec![Paragraph::new( "Press any key to dismiss the error message...", )], - columns: 1, + columns: 0, }, AppState::Critical(_) => Minibuffer { paragraphs: vec![Paragraph::new("Press ctrl+c to terminate the program...")], - columns: 1, + columns: 0, }, }; -- 2.45.2 From b34cc9c6629121648d1341172e89b10577b477ff Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 15 Feb 2024 20:44:02 +0100 Subject: [PATCH 15/35] Complete unit tests --- src/core/database/json/testmod.rs | 10 +- src/core/library/beets/testmod.rs | 44 +++---- src/core/library/testmod.rs | 52 ++++----- src/tests.rs | 18 +-- src/tui/app/app.rs | 187 ++++++++++++++++++++++++++++-- src/tui/app/selection.rs | 69 ++++++++--- 6 files changed, 291 insertions(+), 89 deletions(-) diff --git a/src/core/database/json/testmod.rs b/src/core/database/json/testmod.rs index 4c3031e..c492aed 100644 --- a/src/core/database/json/testmod.rs +++ b/src/core/database/json/testmod.rs @@ -2,7 +2,7 @@ pub static DATABASE_JSON: &str = "{\ \"V20240210\":\ [\ {\ - \"name\":\"Album_Artist A\",\ + \"name\":\"Album_Artist ‘A’\",\ \"sort\":null,\ \"musicbrainz\":\"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000\",\ \"properties\":{\ @@ -11,7 +11,7 @@ pub static DATABASE_JSON: &str = "{\ }\ },\ {\ - \"name\":\"Album_Artist B\",\ + \"name\":\"Album_Artist ‘B’\",\ \"sort\":null,\ \"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\ \"properties\":{\ @@ -24,13 +24,13 @@ pub static DATABASE_JSON: &str = "{\ }\ },\ {\ - \"name\":\"Album_Artist C\",\ - \"sort\":null,\ + \"name\":\"The Album_Artist ‘C’\",\ + \"sort\":\"Album_Artist ‘C’, The\",\ \"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\ \"properties\":{}\ },\ {\ - \"name\":\"Album_Artist D\",\ + \"name\":\"Album_Artist ‘D’\",\ \"sort\":null,\ \"musicbrainz\":null,\ \"properties\":{}\ diff --git a/src/core/library/beets/testmod.rs b/src/core/library/beets/testmod.rs index a2a38a7..e4f30f9 100644 --- a/src/core/library/beets/testmod.rs +++ b/src/core/library/beets/testmod.rs @@ -2,27 +2,27 @@ use once_cell::sync::Lazy; pub static LIBRARY_BEETS: Lazy> = Lazy::new(|| -> Vec { vec![ - String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 1 -*^- track a.a.1 -*^- artist a.a.1 -*^- FLAC -*^- 992"), - String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 2 -*^- track a.a.2 -*^- artist a.a.2.1; artist a.a.2.2 -*^- MP3 -*^- 320"), - String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 3 -*^- track a.a.3 -*^- artist a.a.3 -*^- FLAC -*^- 1061"), - String::from("Album_Artist A -*^- -*^- 1998 -*^- album_title a.a -*^- 4 -*^- track a.a.4 -*^- artist a.a.4 -*^- FLAC -*^- 1042"), - String::from("Album_Artist A -*^- -*^- 2015 -*^- album_title a.b -*^- 1 -*^- track a.b.1 -*^- artist a.b.1 -*^- FLAC -*^- 1004"), - String::from("Album_Artist A -*^- -*^- 2015 -*^- album_title a.b -*^- 2 -*^- track a.b.2 -*^- artist a.b.2 -*^- FLAC -*^- 1077"), - String::from("Album_Artist B -*^- -*^- 2003 -*^- album_title b.a -*^- 1 -*^- track b.a.1 -*^- artist b.a.1 -*^- MP3 -*^- 190"), - String::from("Album_Artist B -*^- -*^- 2003 -*^- album_title b.a -*^- 2 -*^- track b.a.2 -*^- artist b.a.2.1; artist b.a.2.2 -*^- MP3 -*^- 120"), - String::from("Album_Artist B -*^- -*^- 2008 -*^- album_title b.b -*^- 1 -*^- track b.b.1 -*^- artist b.b.1 -*^- FLAC -*^- 1077"), - String::from("Album_Artist B -*^- -*^- 2008 -*^- album_title b.b -*^- 2 -*^- track b.b.2 -*^- artist b.b.2.1; artist b.b.2.2 -*^- MP3 -*^- 320"), - String::from("Album_Artist B -*^- -*^- 2009 -*^- album_title b.c -*^- 1 -*^- track b.c.1 -*^- artist b.c.1 -*^- MP3 -*^- 190"), - String::from("Album_Artist B -*^- -*^- 2009 -*^- album_title b.c -*^- 2 -*^- track b.c.2 -*^- artist b.c.2.1; artist b.c.2.2 -*^- MP3 -*^- 120"), - String::from("Album_Artist B -*^- -*^- 2015 -*^- album_title b.d -*^- 1 -*^- track b.d.1 -*^- artist b.d.1 -*^- MP3 -*^- 190"), - String::from("Album_Artist B -*^- -*^- 2015 -*^- album_title b.d -*^- 2 -*^- track b.d.2 -*^- artist b.d.2.1; artist b.d.2.2 -*^- MP3 -*^- 120"), - String::from("Album_Artist C -*^- -*^- 1985 -*^- album_title c.a -*^- 1 -*^- track c.a.1 -*^- artist c.a.1 -*^- MP3 -*^- 320"), - String::from("Album_Artist C -*^- -*^- 1985 -*^- album_title c.a -*^- 2 -*^- track c.a.2 -*^- artist c.a.2.1; artist c.a.2.2 -*^- MP3 -*^- 120"), - String::from("Album_Artist C -*^- -*^- 2018 -*^- album_title c.b -*^- 1 -*^- track c.b.1 -*^- artist c.b.1 -*^- FLAC -*^- 1041"), - String::from("Album_Artist C -*^- -*^- 2018 -*^- album_title c.b -*^- 2 -*^- track c.b.2 -*^- artist c.b.2.1; artist c.b.2.2 -*^- FLAC -*^- 756"), - String::from("Album_Artist D -*^- -*^- 1995 -*^- album_title d.a -*^- 1 -*^- track d.a.1 -*^- artist d.a.1 -*^- MP3 -*^- 120"), - String::from("Album_Artist D -*^- -*^- 1995 -*^- album_title d.a -*^- 2 -*^- track d.a.2 -*^- artist d.a.2.1; artist d.a.2.2 -*^- MP3 -*^- 120"), - String::from("Album_Artist D -*^- -*^- 2028 -*^- album_title d.b -*^- 1 -*^- track d.b.1 -*^- artist d.b.1 -*^- FLAC -*^- 841"), - String::from("Album_Artist D -*^- -*^- 2028 -*^- album_title d.b -*^- 2 -*^- track d.b.2 -*^- artist d.b.2.1; artist d.b.2.2 -*^- FLAC -*^- 756") + String::from("Album_Artist ‘A’ -*^- -*^- 1998 -*^- album_title a.a -*^- 1 -*^- track a.a.1 -*^- artist a.a.1 -*^- FLAC -*^- 992"), + String::from("Album_Artist ‘A’ -*^- -*^- 1998 -*^- album_title a.a -*^- 2 -*^- track a.a.2 -*^- artist a.a.2.1; artist a.a.2.2 -*^- MP3 -*^- 320"), + String::from("Album_Artist ‘A’ -*^- -*^- 1998 -*^- album_title a.a -*^- 3 -*^- track a.a.3 -*^- artist a.a.3 -*^- FLAC -*^- 1061"), + String::from("Album_Artist ‘A’ -*^- -*^- 1998 -*^- album_title a.a -*^- 4 -*^- track a.a.4 -*^- artist a.a.4 -*^- FLAC -*^- 1042"), + String::from("Album_Artist ‘A’ -*^- -*^- 2015 -*^- album_title a.b -*^- 1 -*^- track a.b.1 -*^- artist a.b.1 -*^- FLAC -*^- 1004"), + String::from("Album_Artist ‘A’ -*^- -*^- 2015 -*^- album_title a.b -*^- 2 -*^- track a.b.2 -*^- artist a.b.2 -*^- FLAC -*^- 1077"), + String::from("Album_Artist ‘B’ -*^- -*^- 2003 -*^- album_title b.a -*^- 1 -*^- track b.a.1 -*^- artist b.a.1 -*^- MP3 -*^- 190"), + String::from("Album_Artist ‘B’ -*^- -*^- 2003 -*^- album_title b.a -*^- 2 -*^- track b.a.2 -*^- artist b.a.2.1; artist b.a.2.2 -*^- MP3 -*^- 120"), + String::from("Album_Artist ‘B’ -*^- -*^- 2008 -*^- album_title b.b -*^- 1 -*^- track b.b.1 -*^- artist b.b.1 -*^- FLAC -*^- 1077"), + String::from("Album_Artist ‘B’ -*^- -*^- 2008 -*^- album_title b.b -*^- 2 -*^- track b.b.2 -*^- artist b.b.2.1; artist b.b.2.2 -*^- MP3 -*^- 320"), + String::from("Album_Artist ‘B’ -*^- -*^- 2009 -*^- album_title b.c -*^- 1 -*^- track b.c.1 -*^- artist b.c.1 -*^- MP3 -*^- 190"), + String::from("Album_Artist ‘B’ -*^- -*^- 2009 -*^- album_title b.c -*^- 2 -*^- track b.c.2 -*^- artist b.c.2.1; artist b.c.2.2 -*^- MP3 -*^- 120"), + String::from("Album_Artist ‘B’ -*^- -*^- 2015 -*^- album_title b.d -*^- 1 -*^- track b.d.1 -*^- artist b.d.1 -*^- MP3 -*^- 190"), + String::from("Album_Artist ‘B’ -*^- -*^- 2015 -*^- album_title b.d -*^- 2 -*^- track b.d.2 -*^- artist b.d.2.1; artist b.d.2.2 -*^- MP3 -*^- 120"), + String::from("The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- 1985 -*^- album_title c.a -*^- 1 -*^- track c.a.1 -*^- artist c.a.1 -*^- MP3 -*^- 320"), + String::from("The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- 1985 -*^- album_title c.a -*^- 2 -*^- track c.a.2 -*^- artist c.a.2.1; artist c.a.2.2 -*^- MP3 -*^- 120"), + String::from("The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- 2018 -*^- album_title c.b -*^- 1 -*^- track c.b.1 -*^- artist c.b.1 -*^- FLAC -*^- 1041"), + String::from("The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- 2018 -*^- album_title c.b -*^- 2 -*^- track c.b.2 -*^- artist c.b.2.1; artist c.b.2.2 -*^- FLAC -*^- 756"), + String::from("Album_Artist ‘D’ -*^- -*^- 1995 -*^- album_title d.a -*^- 1 -*^- track d.a.1 -*^- artist d.a.1 -*^- MP3 -*^- 120"), + String::from("Album_Artist ‘D’ -*^- -*^- 1995 -*^- album_title d.a -*^- 2 -*^- track d.a.2 -*^- artist d.a.2.1; artist d.a.2.2 -*^- MP3 -*^- 120"), + String::from("Album_Artist ‘D’ -*^- -*^- 2028 -*^- album_title d.b -*^- 1 -*^- track d.b.1 -*^- artist d.b.1 -*^- FLAC -*^- 841"), + String::from("Album_Artist ‘D’ -*^- -*^- 2028 -*^- album_title d.b -*^- 2 -*^- track d.b.2 -*^- artist d.b.2.1; artist d.b.2.2 -*^- FLAC -*^- 756") ] }); diff --git a/src/core/library/testmod.rs b/src/core/library/testmod.rs index 1eb2787..af2ed68 100644 --- a/src/core/library/testmod.rs +++ b/src/core/library/testmod.rs @@ -5,7 +5,7 @@ use crate::core::{collection::track::Format, library::Item}; pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { vec![ Item { - album_artist: String::from("Album_Artist A"), + album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 1998, album_title: String::from("album_title a.a"), @@ -16,7 +16,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 992, }, Item { - album_artist: String::from("Album_Artist A"), + album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 1998, album_title: String::from("album_title a.a"), @@ -30,7 +30,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 320, }, Item { - album_artist: String::from("Album_Artist A"), + album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 1998, album_title: String::from("album_title a.a"), @@ -41,7 +41,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 1061, }, Item { - album_artist: String::from("Album_Artist A"), + album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 1998, album_title: String::from("album_title a.a"), @@ -52,7 +52,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 1042, }, Item { - album_artist: String::from("Album_Artist A"), + album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 2015, album_title: String::from("album_title a.b"), @@ -63,7 +63,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 1004, }, Item { - album_artist: String::from("Album_Artist A"), + album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 2015, album_title: String::from("album_title a.b"), @@ -74,7 +74,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 1077, }, Item { - album_artist: String::from("Album_Artist B"), + album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2003, album_title: String::from("album_title b.a"), @@ -85,7 +85,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 190, }, Item { - album_artist: String::from("Album_Artist B"), + album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2003, album_title: String::from("album_title b.a"), @@ -99,7 +99,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 120, }, Item { - album_artist: String::from("Album_Artist B"), + album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2008, album_title: String::from("album_title b.b"), @@ -110,7 +110,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 1077, }, Item { - album_artist: String::from("Album_Artist B"), + album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2008, album_title: String::from("album_title b.b"), @@ -124,7 +124,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 320, }, Item { - album_artist: String::from("Album_Artist B"), + album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2009, album_title: String::from("album_title b.c"), @@ -135,7 +135,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 190, }, Item { - album_artist: String::from("Album_Artist B"), + album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2009, album_title: String::from("album_title b.c"), @@ -149,7 +149,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 120, }, Item { - album_artist: String::from("Album_Artist B"), + album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2015, album_title: String::from("album_title b.d"), @@ -160,7 +160,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 190, }, Item { - album_artist: String::from("Album_Artist B"), + album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2015, album_title: String::from("album_title b.d"), @@ -174,8 +174,8 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 120, }, Item { - album_artist: String::from("Album_Artist C"), - album_artist_sort: None, + album_artist: String::from("The Album_Artist ‘C’"), + album_artist_sort: Some(String::from("Album_Artist ‘C’, The")), album_year: 1985, album_title: String::from("album_title c.a"), track_number: 1, @@ -185,8 +185,8 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 320, }, Item { - album_artist: String::from("Album_Artist C"), - album_artist_sort: None, + album_artist: String::from("The Album_Artist ‘C’"), + album_artist_sort: Some(String::from("Album_Artist ‘C’, The")), album_year: 1985, album_title: String::from("album_title c.a"), track_number: 2, @@ -199,8 +199,8 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 120, }, Item { - album_artist: String::from("Album_Artist C"), - album_artist_sort: None, + album_artist: String::from("The Album_Artist ‘C’"), + album_artist_sort: Some(String::from("Album_Artist ‘C’, The")), album_year: 2018, album_title: String::from("album_title c.b"), track_number: 1, @@ -210,8 +210,8 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 1041, }, Item { - album_artist: String::from("Album_Artist C"), - album_artist_sort: None, + album_artist: String::from("The Album_Artist ‘C’"), + album_artist_sort: Some(String::from("Album_Artist ‘C’, The")), album_year: 2018, album_title: String::from("album_title c.b"), track_number: 2, @@ -224,7 +224,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 756, }, Item { - album_artist: String::from("Album_Artist D"), + album_artist: String::from("Album_Artist ‘D’"), album_artist_sort: None, album_year: 1995, album_title: String::from("album_title d.a"), @@ -235,7 +235,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 120, }, Item { - album_artist: String::from("Album_Artist D"), + album_artist: String::from("Album_Artist ‘D’"), album_artist_sort: None, album_year: 1995, album_title: String::from("album_title d.a"), @@ -249,7 +249,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 120, }, Item { - album_artist: String::from("Album_Artist D"), + album_artist: String::from("Album_Artist ‘D’"), album_artist_sort: None, album_year: 2028, album_title: String::from("album_title d.b"), @@ -260,7 +260,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { track_bitrate: 841, }, Item { - album_artist: String::from("Album_Artist D"), + album_artist: String::from("Album_Artist ‘D’"), album_artist_sort: None, album_year: 2028, album_title: String::from("album_title d.b"), diff --git a/src/tests.rs b/src/tests.rs index db2b4f8..9b9ab6e 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -3,7 +3,7 @@ macro_rules! library_collection { vec![ Artist { id: ArtistId { - name: "Album_Artist A".to_string(), + name: "Album_Artist ‘A’".to_string(), }, sort: None, musicbrainz: None, @@ -98,7 +98,7 @@ macro_rules! library_collection { }, Artist { id: ArtistId { - name: "Album_Artist B".to_string(), + name: "Album_Artist ‘B’".to_string(), }, sort: None, musicbrainz: None, @@ -240,9 +240,11 @@ macro_rules! library_collection { }, Artist { id: ArtistId { - name: "Album_Artist C".to_string(), + name: "The Album_Artist ‘C’".to_string(), }, - sort: None, + sort: Some(ArtistId { + name: "Album_Artist ‘C’, The".to_string(), + }), musicbrainz: None, properties: HashMap::new(), albums: vec![ @@ -316,7 +318,7 @@ macro_rules! library_collection { }, Artist { id: ArtistId { - name: "Album_Artist D".to_string(), + name: "Album_Artist ‘D’".to_string(), }, sort: None, musicbrainz: None, @@ -400,7 +402,7 @@ macro_rules! full_collection { let mut iter = collection.iter_mut(); let artist_a = iter.next().unwrap(); - assert_eq!(artist_a.id.name, "Album_Artist A"); + assert_eq!(artist_a.id.name, "Album_Artist ‘A’"); artist_a.musicbrainz = Some( MusicBrainz::new( @@ -421,7 +423,7 @@ macro_rules! full_collection { ]); let artist_b = iter.next().unwrap(); - assert_eq!(artist_b.id.name, "Album_Artist B"); + assert_eq!(artist_b.id.name, "Album_Artist ‘B’"); artist_b.musicbrainz = Some( MusicBrainz::new( @@ -443,7 +445,7 @@ macro_rules! full_collection { ]); let artist_c = iter.next().unwrap(); - assert_eq!(artist_c.id.name, "Album_Artist C"); + assert_eq!(artist_c.id.name, "The Album_Artist ‘C’"); artist_c.musicbrainz = Some( MusicBrainz::new( diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index a3f324b..ef7d842 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -3,7 +3,7 @@ use musichoard::collection::Collection; use crate::tui::{ - app::selection::{Delta, IdSelection, Selection, ListSelection}, + app::selection::{Delta, IdSelection, ListSelection, Selection}, lib::IMusicHoard, }; @@ -144,7 +144,12 @@ pub struct App { // FIXME: is it possible to use a wrapper struct? - when state() is called return a wrapper // around App which will contain App. orig: Option, - memo: Vec<(Option, bool)>, + memo: Vec, +} + +struct AppSearchMemo { + index: Option, + char: bool, } impl App { @@ -306,31 +311,31 @@ impl IAppInteractSearch for App { let collection = self.music_hoard.get_collection(); let search = self.state.as_mut().unwrap_search(); search.push(ch); - let prev = self + let index = self .selection .incremental_artist_search(collection, search, false); - self.memo.push((prev, true)); + self.memo.push(AppSearchMemo { index, char: true }); } fn search_next(&mut self) { let collection = self.music_hoard.get_collection(); let search = self.state.as_mut().unwrap_search(); if !search.is_empty() { - let prev = self + let index = self .selection .incremental_artist_search(collection, search, true); - self.memo.push((prev, false)); + self.memo.push(AppSearchMemo { index, char: false }); } } fn step_back(&mut self) { let collection = self.music_hoard.get_collection(); - if let Some((prev, pop_char)) = self.memo.pop() { - if pop_char { + if let Some(memo) = self.memo.pop() { + if memo.char { let search = self.state.as_mut().unwrap_search(); search.pop(); } - self.selection.select_artist(collection, prev); + self.selection.select_artist(collection, memo.index); } } @@ -880,17 +885,23 @@ mod tests { assert_eq!(app.selection.active, Category::Album); assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + app.append_character('\''); app.append_character('c'); + app.append_character('\''); assert_eq!(app.selection.active, Category::Album); assert_eq!(app.selection.artist.state.list.selected(), Some(2)); - app.remove_character(); + app.step_back(); + app.step_back(); + app.step_back(); assert_eq!(app.selection.active, Category::Album); assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + app.append_character('\''); app.append_character('b'); + app.append_character('\''); assert_eq!(app.selection.active, Category::Album); assert_eq!(app.selection.artist.state.list.selected(), Some(1)); @@ -901,4 +912,160 @@ mod tests { assert_eq!(app.selection.active, Category::Album); assert_eq!(app.selection.artist.state.list.selected(), Some(1)); } + + #[test] + fn search_next() { + let mut app = App::new(music_hoard(COLLECTION.to_owned())); + assert!(app.state().is_browse()); + + app.increment_selection(Delta::Line); + app.increment_category(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + + app.begin_search(); + assert!(app.state().is_search()); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + + app.step_back(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + + app.append_character('a'); + + app.search_next(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + + app.search_next(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(2)); + + app.search_next(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(3)); + + app.search_next(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(3)); + + app.step_back(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(3)); + + app.step_back(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(2)); + + app.step_back(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + + app.step_back(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + + app.step_back(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + } + + #[test] + fn cancel_search() { + let mut app = App::new(music_hoard(COLLECTION.to_owned())); + assert!(app.state().is_browse()); + + app.increment_selection(Delta::Line); + app.increment_category(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + + app.begin_search(); + assert!(app.state().is_search()); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + + app.append_character('a'); + app.append_character('l'); + app.append_character('b'); + app.append_character('u'); + app.append_character('m'); + app.append_character('_'); + app.append_character('a'); + app.append_character('r'); + app.append_character('t'); + app.append_character('i'); + app.append_character('s'); + app.append_character('t'); + app.append_character(' '); + app.append_character('\''); + app.append_character('c'); + app.append_character('\''); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(2)); + + app.cancel_search(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + } + + #[test] + fn empty_search() { + let mut app = App::new(music_hoard(vec![])); + assert!(app.state().is_browse()); + + app.increment_selection(Delta::Line); + app.increment_category(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), None); + + app.begin_search(); + assert!(app.state().is_search()); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), None); + + app.append_character('a'); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), None); + + app.search_next(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), None); + + app.step_back(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), None); + + app.step_back(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), None); + + app.cancel_search(); + + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.list.selected(), None); + } } diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index c81df44..cf14785 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -214,7 +214,8 @@ impl ArtistSelection { } } - // FIXME: use aho_corasick for normalization + // FIXME: use aho_corasick for normalization - AhoCorasick does not implement PartialEq. It + // makes more sense to be places in app.rs as it would make ArtistSelection non-trivial. fn normalize_search(search: &str, lowercase: bool, asciify: bool) -> String { let normalized = if lowercase { search.to_lowercase() @@ -910,46 +911,78 @@ mod tests { fn artist_incremental_search() { let artists = &COLLECTION; + // Empty collection. let mut sel = ArtistSelection::initialise(&[]); assert_eq!(sel.state.list.selected(), None); - sel.incremental_search(artists, "album_artist a"); + sel.incremental_search(artists, "album_artist 'a'", false); assert_eq!(sel.state.list.selected(), None); + // Basic test, first element. let mut sel = ArtistSelection::initialise(artists); assert_eq!(sel.state.list.selected(), Some(0)); - sel.incremental_search(artists, "album_artist a"); + sel.incremental_search(artists, "", false); assert_eq!(sel.state.list.selected(), Some(0)); + sel.incremental_search(artists, "album_artist ", false); + assert_eq!(sel.state.list.selected(), Some(0)); + + sel.incremental_search(artists, "album_artist 'a'", false); + assert_eq!(sel.state.list.selected(), Some(0)); + + // Basic test, non-first element. sel.reinitialise(artists, None); + + sel.incremental_search(artists, "album_artist ", false); assert_eq!(sel.state.list.selected(), Some(0)); - sel.incremental_search(artists, "album_artist b"); - assert_eq!(sel.state.list.selected(), Some(1)); - - sel.reinitialise(artists, None); - assert_eq!(sel.state.list.selected(), Some(0)); - - sel.incremental_search(artists, "Album_Artist B"); - assert_eq!(sel.state.list.selected(), Some(1)); - - sel.reinitialise(artists, None); - assert_eq!(sel.state.list.selected(), Some(0)); - - sel.incremental_search(artists, "album_artist ba"); + sel.incremental_search(artists, "album_artist 'c'", false); assert_eq!(sel.state.list.selected(), Some(2)); + // Non-lowercase. sel.reinitialise(artists, None); + + sel.incremental_search(artists, "Album_Artist ", false); assert_eq!(sel.state.list.selected(), Some(0)); - sel.incremental_search(artists, "album_artist c"); + sel.incremental_search(artists, "Album_Artist 'C'", false); assert_eq!(sel.state.list.selected(), Some(2)); + // Non-ascii. sel.reinitialise(artists, None); + + sel.incremental_search(artists, "album_artist ", false); assert_eq!(sel.state.list.selected(), Some(0)); - sel.incremental_search(artists, "album_artist d"); + sel.incremental_search(artists, "album_artist ‘c’", false); + assert_eq!(sel.state.list.selected(), Some(2)); + + // Stop at name, not sort name. + sel.reinitialise(artists, None); + + sel.incremental_search(artists, "the", false); + assert_eq!(sel.state.list.selected(), Some(2)); + + sel.incremental_search(artists, "the album_artist 'c'", false); + assert_eq!(sel.state.list.selected(), Some(2)); + + // Search next with common prefix. + sel.reinitialise(artists, None); + + sel.incremental_search(artists, "album_artist ", false); + assert_eq!(sel.state.list.selected(), Some(0)); + + sel.incremental_search(artists, "album_artist ", true); + assert_eq!(sel.state.list.selected(), Some(1)); + + sel.incremental_search(artists, "album_artist ", true); + assert_eq!(sel.state.list.selected(), Some(2)); + + sel.incremental_search(artists, "album_artist ", true); + assert_eq!(sel.state.list.selected(), Some(3)); + + sel.incremental_search(artists, "album_artist ", true); assert_eq!(sel.state.list.selected(), Some(3)); } } -- 2.45.2 From 5979e75dcdd8f2a4a97d210fdaf6bf96ef4fa3b4 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 15 Feb 2024 20:52:06 +0100 Subject: [PATCH 16/35] Lints --- src/tests.rs | 2 +- src/tui/app/selection.rs | 18 +++++++----------- src/tui/handler.rs | 4 ++-- src/tui/ui.rs | 5 +++-- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/tests.rs b/src/tests.rs index 9b9ab6e..bbb4de1 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -243,7 +243,7 @@ macro_rules! library_collection { name: "The Album_Artist ‘C’".to_string(), }, sort: Some(ArtistId { - name: "Album_Artist ‘C’, The".to_string(), + name: "Album_Artist ‘C’, The".to_string(), }), musicbrainz: None, properties: HashMap::new(), diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index cf14785..90a57c8 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -228,17 +228,13 @@ impl ArtistSelection { // https://wiki.musicbrainz.org/User:Yurim/Punctuation_and_Special_Characters. if asciify { normalized - .replace("‐", "-") // U+2010 hyphen - .replace("‒", "-") // U+2012 figure dash - .replace("–", "-") // U+2013 en dash - .replace("—", "-") // U+2014 em dash - .replace("―", "-") // U+2015 horizontal bar - .replace("‘", "'") // U+2018 - .replace("’", "'") // U+2019 - .replace("“", "\"") // U+201C - .replace("”", "\"") // U+201D - .replace("…", "...") // U+2026 - .replace("−", "-") // U+2212 minus sign + // U+2010 hyphen, U+2012 figure dash, U+2013 en dash, U+2014 em dash, + // U+2015 horizontal bar + .replace(['‐', '‒', '–', '—', '―'], "-") + .replace(['‘', '’'], "'") // U+2018, U+2019 + .replace(['“', '”'], "\"") // U+201C, U+201D + .replace('…', "...") // U+2026 + .replace('−', "-") // U+2212 minus sign } else { normalized } diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 9ce9411..672c4d6 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -150,10 +150,10 @@ impl IEventHandlerPrivate for EventHandler { match key_event.code { KeyCode::Char('s') | KeyCode::Char('S') => { app.search_next(); - }, + } KeyCode::Char('g') | KeyCode::Char('G') => { app.cancel_search(); - }, + } _ => {} } return; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index cbc5323..7833d13 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -378,8 +378,9 @@ impl Minibuffer<'_> { AppState::Search(ref s) => Minibuffer { paragraphs: vec![ Paragraph::new(format!("I-search: {s}")), - Paragraph::new(format!("ctrl+s: search next")).alignment(Alignment::Center), - Paragraph::new(format!("ctrl+g: cancel search")).alignment(Alignment::Center), + Paragraph::new("ctrl+s: search next".to_string()).alignment(Alignment::Center), + Paragraph::new("ctrl+g: cancel search".to_string()) + .alignment(Alignment::Center), ], columns, }, -- 2.45.2 From c2920aac8154fd0fd2d24ecee7cc5db568b8101d Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 16 Feb 2024 19:20:46 +0100 Subject: [PATCH 17/35] State machine solution works --- src/tui/app/app.rs | 1340 +++++++++++++++++++++++++------------------- src/tui/handler.rs | 92 ++- src/tui/mod.rs | 7 +- src/tui/ui.rs | 21 +- 4 files changed, 815 insertions(+), 645 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index ef7d842..eafa5dd 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -7,6 +7,7 @@ use crate::tui::{ lib::IMusicHoard, }; +#[derive(Copy, Clone)] pub enum AppState { Browse(BS), Info(IS), @@ -17,108 +18,86 @@ pub enum AppState { } impl AppState { - pub fn is_browse(&self) -> bool { - matches!(self, AppState::Browse(_)) - } - - pub fn is_info(&self) -> bool { - matches!(self, AppState::Info(_)) - } - - pub fn is_reload(&self) -> bool { - matches!(self, AppState::Reload(_)) - } - pub fn is_search(&self) -> bool { matches!(self, AppState::Search(_)) } - - pub fn unwrap_search(self) -> SS { - match self { - AppState::Search(ss) => ss, - _ => panic!(), - } - } - - pub fn is_error(&self) -> bool { - matches!(self, AppState::Error(_)) - } - - pub fn as_mut(&mut self) -> AppState<&mut BS, &mut IS, &mut RS, &mut SS, &mut ES, &mut CS> { - match self { - AppState::Browse(ref mut bs) => AppState::Browse(bs), - AppState::Info(ref mut is) => AppState::Info(is), - AppState::Reload(ref mut rs) => AppState::Reload(rs), - AppState::Search(ref mut ss) => AppState::Search(ss), - AppState::Error(ref mut es) => AppState::Error(es), - AppState::Critical(ref mut cs) => AppState::Critical(cs), - } - } } pub trait IAppInteract { - type BS: IAppInteractBrowse; - type IS: IAppInteractInfo; - type RS: IAppInteractReload; - type SS: IAppInteractSearch; - type ES: IAppInteractError; - type CS: IAppInteractCritical; + type BS: IAppInteractBrowse; + type IS: IAppInteractInfo; + type RS: IAppInteractReload; + type SS: IAppInteractSearch; + type ES: IAppInteractError; + type CS: IAppInteractCritical; fn is_running(&self) -> bool; - fn force_quit(&mut self); + fn force_quit(self) -> Self; #[allow(clippy::type_complexity)] - fn state( - &mut self, - ) -> AppState< - &mut Self::BS, - &mut Self::IS, - &mut Self::RS, - &mut Self::SS, - &mut Self::ES, - &mut Self::CS, - >; + fn state(self) -> AppState; } pub trait IAppInteractBrowse { - fn save(&mut self); - fn quit(&mut self); + type APP: IAppInteract; - fn increment_category(&mut self); - fn decrement_category(&mut self); - fn increment_selection(&mut self, delta: Delta); - fn decrement_selection(&mut self, delta: Delta); + fn save_and_quit(self) -> Self::APP; - fn show_info_overlay(&mut self); + fn increment_category(self) -> Self::APP; + fn decrement_category(self) -> Self::APP; + fn increment_selection(self, delta: Delta) -> Self::APP; + fn decrement_selection(self, delta: Delta) -> Self::APP; - fn show_reload_menu(&mut self); + fn show_info_overlay(self) -> Self::APP; - fn begin_search(&mut self); + fn show_reload_menu(self) -> Self::APP; + + fn begin_search(self) -> Self::APP; + + fn no_op(self) -> Self::APP; } pub trait IAppInteractInfo { - fn hide_info_overlay(&mut self); + type APP: IAppInteract; + + fn hide_info_overlay(self) -> Self::APP; + + fn no_op(self) -> Self::APP; } pub trait IAppInteractReload { - fn reload_library(&mut self); - fn reload_database(&mut self); - fn hide_reload_menu(&mut self); + type APP: IAppInteract; + + fn reload_library(self) -> Self::APP; + fn reload_database(self) -> Self::APP; + fn hide_reload_menu(self) -> Self::APP; + + fn no_op(self) -> Self::APP; } pub trait IAppInteractSearch { - fn append_character(&mut self, ch: char); - fn search_next(&mut self); - fn step_back(&mut self); - fn finish_search(&mut self); - fn cancel_search(&mut self); + type APP: IAppInteract; + + fn append_character(self, ch: char) -> Self::APP; + fn search_next(self) -> Self::APP; + fn step_back(self) -> Self::APP; + fn finish_search(self) -> Self::APP; + fn cancel_search(self) -> Self::APP; + + fn no_op(self) -> Self::APP; } pub trait IAppInteractError { - fn dismiss_error(&mut self); + type APP: IAppInteract; + + fn dismiss_error(self) -> Self::APP; } -pub trait IAppInteractCritical {} +pub trait IAppInteractCritical { + type APP: IAppInteract; + + fn no_op(self) -> Self::APP; +} // It would be preferable to have a getter for each field separately. However, the selection field // needs to be mutably accessible requiring a mutable borrow of the entire struct if behind a trait. @@ -128,22 +107,35 @@ pub trait IAppAccess { fn get(&mut self) -> AppPublic; } -pub type AppPublicState = AppState<(), (), (), String, String, String>; +pub type AppPublicState<'app> = AppState<(), (), (), &'app str, &'app str, &'app str>; pub struct AppPublic<'app> { pub collection: &'app Collection, pub selection: &'app mut Selection, - pub state: &'app AppPublicState, + pub state: AppPublicState<'app>, } -pub struct App { +struct AppInner { running: bool, music_hoard: MH, selection: Selection, - state: AppState<(), (), (), String, String, String>, - // FIXME: is it possible to use a wrapper struct? - when state() is called return a wrapper - // around App which will contain App. - orig: Option, +} + +pub type App = AppState< + AppGenericState, + AppGenericState, + AppGenericState, + AppSearchState, + AppErrorState, + AppErrorState, +>; + +pub struct AppGenericState(AppInner); + +pub struct AppSearchState { + inner: AppInner, + search: String, + orig: ListSelection, memo: Vec, } @@ -152,20 +144,26 @@ struct AppSearchMemo { char: bool, } +pub struct AppErrorState { + inner: AppInner, + msg: String, +} + impl App { pub fn new(mut music_hoard: MH) -> Self { - let state = match Self::init(&mut music_hoard) { - Ok(()) => AppState::Browse(()), - Err(err) => AppState::Critical(err.to_string()), - }; + let init_result = Self::init(&mut music_hoard); let selection = Selection::new(music_hoard.get_collection()); - App { + let inner = AppInner { running: true, music_hoard, selection, - state, - orig: None, - memo: vec![], + }; + match init_result { + Ok(()) => Self::Browse(AppGenericState(inner)), + Err(err) => Self::Critical(AppErrorState { + inner, + msg: err.to_string(), + }), } } @@ -174,198 +172,271 @@ impl App { music_hoard.rescan_library()?; Ok(()) } + + fn inner_ref(&self) -> &AppInner { + match self { + AppState::Browse(inner) | AppState::Info(inner) | AppState::Reload(inner) => &inner.0, + AppState::Search(search) => &search.inner, + AppState::Error(error) | AppState::Critical(error) => &error.inner, + } + } + + fn inner_mut(&mut self) -> &mut AppInner { + match self { + AppState::Browse(inner) | AppState::Info(inner) | AppState::Reload(inner) => { + &mut inner.0 + } + AppState::Search(search) => &mut search.inner, + AppState::Error(error) | AppState::Critical(error) => &mut error.inner, + } + } } impl IAppInteract for App { - type BS = Self; - type IS = Self; - type RS = Self; - type SS = Self; - type ES = Self; - type CS = Self; + type BS = AppGenericState; + type IS = AppGenericState; + type RS = AppGenericState; + type SS = AppSearchState; + type ES = AppErrorState; + type CS = AppErrorState; fn is_running(&self) -> bool { - self.running + self.inner_ref().running } - fn force_quit(&mut self) { - self.running = false; + fn force_quit(mut self) -> Self { + self.inner_mut().running = false; + self } - fn state( - &mut self, - ) -> AppState< - &mut Self::BS, - &mut Self::IS, - &mut Self::RS, - &mut Self::SS, - &mut Self::ES, - &mut Self::CS, - > { - match self.state { - AppState::Browse(_) => AppState::Browse(self), - AppState::Info(_) => AppState::Info(self), - AppState::Reload(_) => AppState::Reload(self), - AppState::Search(_) => AppState::Search(self), - AppState::Error(_) => AppState::Error(self), - AppState::Critical(_) => AppState::Critical(self), - } + fn state(self) -> AppState { + self } } -impl IAppInteractBrowse for App { - fn quit(&mut self) { - self.running = false; - } +impl IAppInteractBrowse for AppGenericState { + type APP = App; - fn save(&mut self) { - if let Err(err) = self.music_hoard.save_to_database() { - self.state = AppState::Error(err.to_string()); + fn save_and_quit(mut self) -> Self::APP { + match self.0.music_hoard.save_to_database() { + Ok(_) => { + self.0.running = false; + AppState::Browse(self) + } + Err(err) => AppState::Error(AppErrorState { + inner: self.0, + msg: err.to_string(), + }), } } - fn increment_category(&mut self) { - self.selection.increment_category(); + fn increment_category(mut self) -> Self::APP { + self.0.selection.increment_category(); + AppState::Browse(self) } - fn decrement_category(&mut self) { - self.selection.decrement_category(); + fn decrement_category(mut self) -> Self::APP { + self.0.selection.decrement_category(); + AppState::Browse(self) } - fn increment_selection(&mut self, delta: Delta) { - self.selection - .increment_selection(self.music_hoard.get_collection(), delta); + fn increment_selection(mut self, delta: Delta) -> Self::APP { + self.0 + .selection + .increment_selection(self.0.music_hoard.get_collection(), delta); + AppState::Browse(self) } - fn decrement_selection(&mut self, delta: Delta) { - self.selection - .decrement_selection(self.music_hoard.get_collection(), delta); + fn decrement_selection(mut self, delta: Delta) -> Self::APP { + self.0 + .selection + .decrement_selection(self.0.music_hoard.get_collection(), delta); + AppState::Browse(self) } - fn show_info_overlay(&mut self) { - assert!(self.state.is_browse()); - self.state = AppState::Info(()); + fn show_info_overlay(self) -> Self::APP { + AppState::Info(self) } - fn show_reload_menu(&mut self) { - assert!(self.state.is_browse()); - self.state = AppState::Reload(()); + fn show_reload_menu(self) -> Self::APP { + AppState::Reload(self) } - fn begin_search(&mut self) { - assert!(self.state.is_browse()); - self.state = AppState::Search(String::new()); - self.orig = Some(ListSelection::get(&self.selection)); - self.memo = vec![]; - self.selection - .reset_artist(self.music_hoard.get_collection()); + fn begin_search(mut self) -> Self::APP { + let orig = ListSelection::get(&self.0.selection); + self.0 + .selection + .reset_artist(self.0.music_hoard.get_collection()); + AppState::Search(AppSearchState { + inner: self.0, + search: String::new(), + orig, + memo: vec![], + }) + } + + fn no_op(self) -> Self::APP { + AppState::Browse(self) } } -impl IAppInteractInfo for App { - fn hide_info_overlay(&mut self) { - assert!(self.state.is_info()); - self.state = AppState::Browse(()); +impl IAppInteractInfo for AppGenericState { + type APP = App; + + fn hide_info_overlay(self) -> Self::APP { + AppState::Browse(AppGenericState(self.0)) + } + + fn no_op(self) -> Self::APP { + AppState::Info(self) } } -impl IAppInteractReload for App { - fn reload_library(&mut self) { - let previous = IdSelection::get(self.music_hoard.get_collection(), &self.selection); - let result = self.music_hoard.rescan_library(); - self.refresh(previous, result); +impl IAppInteractReload for AppGenericState { + type APP = App; + + fn reload_library(mut self) -> Self::APP { + let previous = IdSelection::get(self.0.music_hoard.get_collection(), &self.0.selection); + let result = self.0.music_hoard.rescan_library(); + self.refresh(previous, result) } - fn reload_database(&mut self) { - let previous = IdSelection::get(self.music_hoard.get_collection(), &self.selection); - let result = self.music_hoard.load_from_database(); - self.refresh(previous, result); + fn reload_database(mut self) -> Self::APP { + let previous = IdSelection::get(self.0.music_hoard.get_collection(), &self.0.selection); + let result = self.0.music_hoard.load_from_database(); + self.refresh(previous, result) } - fn hide_reload_menu(&mut self) { - assert!(self.state.is_reload()); - self.state = AppState::Browse(()); + fn hide_reload_menu(self) -> Self::APP { + AppState::Browse(AppGenericState(self.0)) + } + + fn no_op(self) -> Self::APP { + AppState::Reload(self) } } -trait IAppInteractReloadPrivate { - fn refresh(&mut self, previous: IdSelection, result: Result<(), musichoard::Error>); +trait IAppInteractReloadPrivate { + fn refresh(self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App; } -impl IAppInteractReloadPrivate for App { - fn refresh(&mut self, previous: IdSelection, result: Result<(), musichoard::Error>) { - assert!(self.state.is_reload()); +impl IAppInteractReloadPrivate for AppGenericState { + fn refresh(mut self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App { match result { Ok(()) => { - self.selection - .select_by_id(self.music_hoard.get_collection(), previous); - self.state = AppState::Browse(()) + self.0 + .selection + .select_by_id(self.0.music_hoard.get_collection(), previous); + AppState::Browse(AppGenericState(self.0)) } - Err(err) => self.state = AppState::Error(err.to_string()), + Err(err) => AppState::Error(AppErrorState { + inner: self.0, + msg: err.to_string(), + }), } } } -impl IAppInteractSearch for App { - fn append_character(&mut self, ch: char) { - let collection = self.music_hoard.get_collection(); - let search = self.state.as_mut().unwrap_search(); - search.push(ch); +impl IAppInteractSearch for AppSearchState { + type APP = App; + + fn append_character(mut self, ch: char) -> Self::APP { + let collection = self.inner.music_hoard.get_collection(); + self.search.push(ch); let index = self + .inner .selection - .incremental_artist_search(collection, search, false); + .incremental_artist_search(collection, &self.search, false); self.memo.push(AppSearchMemo { index, char: true }); + AppState::Search(self) } - fn search_next(&mut self) { - let collection = self.music_hoard.get_collection(); - let search = self.state.as_mut().unwrap_search(); - if !search.is_empty() { - let index = self - .selection - .incremental_artist_search(collection, search, true); + fn search_next(mut self) -> Self::APP { + let collection = self.inner.music_hoard.get_collection(); + if !self.search.is_empty() { + let index = + self.inner + .selection + .incremental_artist_search(collection, &self.search, true); self.memo.push(AppSearchMemo { index, char: false }); } + AppState::Search(self) } - fn step_back(&mut self) { - let collection = self.music_hoard.get_collection(); + fn step_back(mut self) -> Self::APP { + let collection = self.inner.music_hoard.get_collection(); if let Some(memo) = self.memo.pop() { if memo.char { - let search = self.state.as_mut().unwrap_search(); - search.pop(); + self.search.pop(); } - self.selection.select_artist(collection, memo.index); + self.inner.selection.select_artist(collection, memo.index); } + AppState::Search(self) } - fn finish_search(&mut self) { - assert!(self.state.is_search()); - self.state = AppState::Browse(()); + fn finish_search(self) -> Self::APP { + AppState::Browse(AppGenericState(self.inner)) } - fn cancel_search(&mut self) { - assert!(self.state.is_search()); - self.selection.select_by_list(self.orig.take().unwrap()); - self.state = AppState::Browse(()); + fn cancel_search(mut self) -> Self::APP { + self.inner.selection.select_by_list(self.orig); + AppState::Browse(AppGenericState(self.inner)) + } + + fn no_op(self) -> Self::APP { + AppState::Search(self) } } -impl IAppInteractError for App { - fn dismiss_error(&mut self) { - assert!(self.state.is_error()); - self.state = AppState::Browse(()); +impl IAppInteractError for AppErrorState { + type APP = App; + + fn dismiss_error(self) -> Self::APP { + AppState::Browse(AppGenericState(self.inner)) } } -impl IAppInteractCritical for App {} +impl IAppInteractCritical for AppErrorState { + type APP = App; + + fn no_op(self) -> Self::APP { + AppState::Critical(self) + } +} impl IAppAccess for App { fn get(&mut self) -> AppPublic { - AppPublic { - collection: self.music_hoard.get_collection(), - selection: &mut self.selection, - state: &self.state, + match self { + AppState::Browse(generic) => AppPublic { + collection: generic.0.music_hoard.get_collection(), + selection: &mut generic.0.selection, + state: AppState::Browse(()), + }, + AppState::Info(generic) => AppPublic { + collection: generic.0.music_hoard.get_collection(), + selection: &mut generic.0.selection, + state: AppState::Info(()), + }, + AppState::Reload(generic) => AppPublic { + collection: generic.0.music_hoard.get_collection(), + selection: &mut generic.0.selection, + state: AppState::Reload(()), + }, + AppState::Search(search) => AppPublic { + collection: search.inner.music_hoard.get_collection(), + selection: &mut search.inner.selection, + state: AppState::Search(&search.search), + }, + AppState::Error(error) => AppPublic { + collection: error.inner.music_hoard.get_collection(), + selection: &mut error.inner.selection, + state: AppState::Error(&error.msg), + }, + AppState::Critical(critical) => AppPublic { + collection: critical.inner.music_hoard.get_collection(), + selection: &mut critical.inner.selection, + state: AppState::Error(&critical.msg), + }, } } } @@ -376,6 +447,50 @@ mod tests { use super::*; + impl AppState { + fn unwrap_browse(self) -> BS { + match self { + AppState::Browse(browse) => browse, + _ => panic!(), + } + } + + fn unwrap_info(self) -> IS { + match self { + AppState::Info(info) => info, + _ => panic!(), + } + } + + fn unwrap_reload(self) -> RS { + match self { + AppState::Reload(reload) => reload, + _ => panic!(), + } + } + + fn unwrap_search(self) -> SS { + match self { + AppState::Search(search) => search, + _ => panic!(), + } + } + + fn unwrap_error(self) -> ES { + match self { + AppState::Error(error) => error, + _ => panic!(), + } + } + + fn unwrap_critical(self) -> CS { + match self { + AppState::Critical(critical) => critical, + _ => panic!(), + } + } + } + fn music_hoard(collection: Collection) -> MockIMusicHoard { let mut music_hoard = MockIMusicHoard::new(); @@ -393,80 +508,74 @@ mod tests { } #[test] - fn app_state() { - let mut state = AppPublicState::Browse(()); - assert!(state.is_browse()); - assert!(state.as_mut().is_browse()); - - let mut state = AppPublicState::Info(()); - assert!(state.is_info()); - assert!(state.as_mut().is_info()); - - let mut state = AppPublicState::Reload(()); - assert!(state.is_reload()); - assert!(state.as_mut().is_reload()); - - let mut state = AppPublicState::Search(String::from("get rekt")); + fn app_is_state() { + let state = AppPublicState::Search("get rekt"); assert!(state.is_search()); - assert!(state.as_mut().is_search()); - assert_eq!(state.unwrap_search().as_str(), "get rekt"); - - let mut state = AppPublicState::Error(String::new()); - assert!(state.is_error()); - assert!(state.as_mut().is_error()); - - let mut state = AppPublicState::Critical(String::new()); - assert!(matches!(state, AppState::Critical(_))); - assert!(matches!(state.as_mut(), AppState::Critical(_))); - } - - #[test] - #[should_panic] - fn app_state_unwrap_search_panic() { - let state = AppPublicState::Browse(()); - assert!(state.is_browse()); - state.unwrap_search(); } #[test] fn running_quit() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); + let mut music_hoard = music_hoard(COLLECTION.to_owned()); + + music_hoard + .expect_save_to_database() + .times(1) + .return_once(|| Ok(())); + + let app = App::new(music_hoard); assert!(app.is_running()); - app.quit(); + let browse = app.unwrap_browse(); + + let app = browse.save_and_quit(); assert!(!app.is_running()); } #[test] fn error_quit() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); + let mut music_hoard = music_hoard(COLLECTION.to_owned()); + + music_hoard + .expect_save_to_database() + .times(1) + .return_once(|| Ok(())); + + let app = App::new(music_hoard); assert!(app.is_running()); - app.state = AppState::Error(String::from("get rekt")); + let app = App::Error(AppErrorState { + inner: app.unwrap_browse().0, + msg: String::from("get rekt"), + }); - app.dismiss_error(); + let error = app.unwrap_error(); - app.quit(); + let browse = error.dismiss_error().unwrap_browse(); + + let app = browse.save_and_quit(); assert!(!app.is_running()); } #[test] fn running_force_quit() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); + let app = App::new(music_hoard(COLLECTION.to_owned())); assert!(app.is_running()); - app.force_quit(); + let app = app.force_quit(); assert!(!app.is_running()); } #[test] fn error_force_quit() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); + let app = App::new(music_hoard(COLLECTION.to_owned())); assert!(app.is_running()); - app.state = AppState::Error(String::from("get rekt")); + let app = App::Error(AppErrorState { + inner: app.unwrap_browse().0, + msg: String::from("get rekt"), + }); - app.force_quit(); + let app = app.force_quit(); assert!(!app.is_running()); } @@ -479,10 +588,9 @@ mod tests { .times(1) .return_once(|| Ok(())); - let mut app = App::new(music_hoard); + let browse = App::new(music_hoard).unwrap_browse(); - app.save(); - assert!(app.state.is_browse()); + browse.save_and_quit().unwrap_browse(); } #[test] @@ -494,11 +602,9 @@ mod tests { .times(1) .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); - let mut app = App::new(music_hoard); + let browse = App::new(music_hoard).unwrap_browse(); - app.save(); - - assert!(app.state.is_error()); + browse.save_and_quit().unwrap_error(); } #[test] @@ -511,136 +617,177 @@ mod tests { .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); music_hoard.expect_get_collection().return_const(vec![]); - let mut app = App::new(music_hoard); + let app = App::new(music_hoard); assert!(app.is_running()); - assert!(matches!(app.state(), AppState::Critical(_))); + app.unwrap_critical(); } #[test] fn modifiers() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); + let app = App::new(music_hoard(COLLECTION.to_owned())); assert!(app.is_running()); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); + let browse = app.unwrap_browse(); + + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_category(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); + let browse = browse.increment_category().unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_category(); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_category().unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(1) ); - app.increment_category(); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_category().unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(1) ); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_selection(Delta::Line); - app.decrement_category(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.decrement_category().unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(1) ); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_selection(Delta::Line); - app.decrement_category(); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.decrement_category().unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); - app.increment_category(); - app.increment_selection(Delta::Line); - app.decrement_category(); - app.decrement_selection(Delta::Line); - app.decrement_category(); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(1)); + let browse = browse.increment_category().unwrap_browse(); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.decrement_category().unwrap_browse(); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let browse = browse.decrement_category().unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); assert_eq!( - app.selection.artist.album.track.state.list.selected(), + browse.0.selection.artist.album.state.list.selected(), + Some(1) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), Some(0) ); } @@ -650,28 +797,48 @@ mod tests { let mut collection = COLLECTION.to_owned(); collection[0].albums[0].tracks = vec![]; - let mut app = App::new(music_hoard(collection)); + let app = App::new(music_hoard(collection)); assert!(app.is_running()); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = app.unwrap_browse(); - app.increment_category(); - app.increment_category(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!( + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_category().unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!( + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!( + browse.0.selection.artist.album.state.list.selected(), + Some(0) + ); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); } #[test] @@ -679,116 +846,150 @@ mod tests { let mut collection = COLLECTION.to_owned(); collection[0].albums = vec![]; - let mut app = App::new(music_hoard(collection)); + let app = App::new(music_hoard(collection)); assert!(app.is_running()); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = app.unwrap_browse(); - app.increment_category(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_category().unwrap_browse(); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_category(); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_category().unwrap_browse(); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); } #[test] fn no_artists() { - let mut app = App::new(music_hoard(vec![])); + let app = App::new(music_hoard(vec![])); assert!(app.is_running()); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = app.unwrap_browse(); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Artist); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_category(); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Artist); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_category().unwrap_browse(); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_category(); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); - app.increment_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_category().unwrap_browse(); - app.decrement_selection(Delta::Line); - assert_eq!(app.selection.active, Category::Track); - assert_eq!(app.selection.artist.state.list.selected(), None); - assert_eq!(app.selection.artist.album.state.list.selected(), None); - assert_eq!(app.selection.artist.album.track.state.list.selected(), None); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + assert_eq!(browse.0.selection.active, Category::Track); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); + assert_eq!( + browse.0.selection.artist.album.track.state.list.selected(), + None + ); } #[test] fn info_overlay() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - app.show_info_overlay(); - assert!(app.state().is_info()); + let info = browse.show_info_overlay().unwrap_info(); - app.hide_info_overlay(); - assert!(app.state().is_browse()); + info.hide_info_overlay().unwrap_browse(); } #[test] fn reload_hide_menu() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - app.show_reload_menu(); - assert!(app.state().is_reload()); + let reload = browse.show_reload_menu().unwrap_reload(); - app.hide_reload_menu(); - assert!(app.state().is_browse()); + reload.hide_reload_menu().unwrap_browse(); } #[test] @@ -800,14 +1001,11 @@ mod tests { .times(1) .return_once(|| Ok(())); - let mut app = App::new(music_hoard); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard).unwrap_browse(); - app.show_reload_menu(); - assert!(app.state().is_reload()); + let reload = browse.show_reload_menu().unwrap_reload(); - app.reload_database(); - assert!(app.state().is_browse()); + reload.reload_database().unwrap_browse(); } #[test] @@ -819,14 +1017,11 @@ mod tests { .times(1) .return_once(|| Ok(())); - let mut app = App::new(music_hoard); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard).unwrap_browse(); - app.show_reload_menu(); - assert!(app.state().is_reload()); + let reload = browse.show_reload_menu().unwrap_reload(); - app.reload_library(); - assert!(app.state().is_browse()); + reload.reload_library().unwrap_browse(); } #[test] @@ -838,234 +1033,221 @@ mod tests { .times(1) .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); - let mut app = App::new(music_hoard); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard).unwrap_browse(); - app.show_reload_menu(); - assert!(app.state().is_reload()); + let reload = browse.show_reload_menu().unwrap_reload(); - app.reload_database(); - assert!(app.state().is_error()); + let error = reload.reload_database().unwrap_error(); - app.dismiss_error(); - assert!(app.state().is_browse()); + error.dismiss_error().unwrap_browse(); } #[test] fn search() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - app.increment_selection(Delta::Line); - app.increment_category(); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - app.begin_search(); - assert!(app.state().is_search()); + let search = browse.begin_search().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.append_character('a'); - app.append_character('l'); - app.append_character('b'); - app.append_character('u'); - app.append_character('m'); - app.append_character('_'); - app.append_character('a'); - app.append_character('r'); - app.append_character('t'); - app.append_character('i'); - app.append_character('s'); - app.append_character('t'); - app.append_character(' '); + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('l').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + let search = search.append_character('u').unwrap_search(); + let search = search.append_character('m').unwrap_search(); + let search = search.append_character('_').unwrap_search(); + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('r').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character('i').unwrap_search(); + let search = search.append_character('s').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character(' ').unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.append_character('\''); - app.append_character('c'); - app.append_character('\''); + let search = search.append_character('\'').unwrap_search(); + let search = search.append_character('c').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(2)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - app.step_back(); - app.step_back(); - app.step_back(); + let search = search.step_back().unwrap_search(); + let search = search.step_back().unwrap_search(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.append_character('\''); - app.append_character('b'); - app.append_character('\''); + let search = search.append_character('\'').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); - app.finish_search(); - assert!(app.state().is_browse()); + let browse = search.finish_search().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); } #[test] fn search_next() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - app.increment_selection(Delta::Line); - app.increment_category(); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - app.begin_search(); - assert!(app.state().is_search()); + let search = browse.begin_search().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.append_character('a'); + let search = search.append_character('a').unwrap_search(); - app.search_next(); + let search = search.search_next().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); - app.search_next(); + let search = search.search_next().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(2)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - app.search_next(); + let search = search.search_next().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(3)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); - app.search_next(); + let search = search.search_next().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(3)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(3)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(2)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); } #[test] fn cancel_search() { - let mut app = App::new(music_hoard(COLLECTION.to_owned())); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - app.increment_selection(Delta::Line); - app.increment_category(); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - app.begin_search(); - assert!(app.state().is_search()); + let search = browse.begin_search().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(0)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - app.append_character('a'); - app.append_character('l'); - app.append_character('b'); - app.append_character('u'); - app.append_character('m'); - app.append_character('_'); - app.append_character('a'); - app.append_character('r'); - app.append_character('t'); - app.append_character('i'); - app.append_character('s'); - app.append_character('t'); - app.append_character(' '); - app.append_character('\''); - app.append_character('c'); - app.append_character('\''); + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('l').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + let search = search.append_character('u').unwrap_search(); + let search = search.append_character('m').unwrap_search(); + let search = search.append_character('_').unwrap_search(); + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('r').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character('i').unwrap_search(); + let search = search.append_character('s').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character(' ').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); + let search = search.append_character('c').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(2)); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - app.cancel_search(); + let browse = search.cancel_search().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); } #[test] fn empty_search() { - let mut app = App::new(music_hoard(vec![])); - assert!(app.state().is_browse()); + let browse = App::new(music_hoard(vec![])).unwrap_browse(); - app.increment_selection(Delta::Line); - app.increment_category(); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); - app.begin_search(); - assert!(app.state().is_search()); + let search = browse.begin_search().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); - app.append_character('a'); + let search = search.append_character('a').unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); - app.search_next(); + let search = search.search_next().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); - app.step_back(); + let search = search.step_back().unwrap_search(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); - app.cancel_search(); + let browse = search.cancel_search().unwrap_browse(); - assert_eq!(app.selection.active, Category::Album); - assert_eq!(app.selection.artist.state.list.selected(), None); + assert_eq!(browse.0.selection.active, Category::Album); + assert_eq!(browse.0.selection.artist.state.list.selected(), None); } } diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 672c4d6..dfd3d8b 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -14,19 +14,21 @@ use crate::tui::{ event::{Event, EventError, EventReceiver}, }; +use super::app::app::IAppInteractCritical; + #[cfg_attr(test, automock)] pub trait IEventHandler { - fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError>; + fn handle_next_event(&self, app: APP) -> Result; } trait IEventHandlerPrivate { - fn handle_key_event(app: &mut APP, key_event: KeyEvent); - fn handle_browse_key_event(app: &mut ::BS, key_event: KeyEvent); - fn handle_info_key_event(app: &mut ::IS, key_event: KeyEvent); - fn handle_reload_key_event(app: &mut ::RS, key_event: KeyEvent); - fn handle_search_key_event(app: &mut ::SS, key_event: KeyEvent); - fn handle_error_key_event(app: &mut ::ES, key_event: KeyEvent); - fn handle_critical_key_event(app: &mut ::CS, key_event: KeyEvent); + fn handle_key_event(app: APP, key_event: KeyEvent) -> APP; + fn handle_browse_key_event(app: ::BS, key_event: KeyEvent) -> APP; + fn handle_info_key_event(app: ::IS, key_event: KeyEvent) -> APP; + fn handle_reload_key_event(app: ::RS, key_event: KeyEvent) -> APP; + fn handle_search_key_event(app: ::SS, key_event: KeyEvent) -> APP; + fn handle_error_key_event(app: ::ES, key_event: KeyEvent) -> APP; + fn handle_critical_key_event(app: ::CS, key_event: KeyEvent) -> APP; } pub struct EventHandler { @@ -41,58 +43,52 @@ impl EventHandler { } impl IEventHandler for EventHandler { - fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> { + fn handle_next_event(&self, mut app: APP) -> Result { match self.events.recv()? { - Event::Key(key_event) => Self::handle_key_event(app, key_event), + Event::Key(key_event) => app = Self::handle_key_event(app, key_event), Event::Mouse(_) => {} Event::Resize(_, _) => {} }; - Ok(()) + Ok(app) } } impl IEventHandlerPrivate for EventHandler { - fn handle_key_event(app: &mut APP, key_event: KeyEvent) { + fn handle_key_event(app: APP, key_event: KeyEvent) -> APP { if key_event.modifiers == KeyModifiers::CONTROL { match key_event.code { // Exit application on `Ctrl-C`. - KeyCode::Char('c') | KeyCode::Char('C') => { - app.force_quit(); - return; - } + KeyCode::Char('c') | KeyCode::Char('C') => return app.force_quit(), _ => {} - } + }; } match app.state() { AppState::Browse(browse) => { - >::handle_browse_key_event(browse, key_event); + >::handle_browse_key_event(browse, key_event) } AppState::Info(info) => { - >::handle_info_key_event(info, key_event); + >::handle_info_key_event(info, key_event) } AppState::Reload(reload) => { - >::handle_reload_key_event(reload, key_event); + >::handle_reload_key_event(reload, key_event) } AppState::Search(search) => { - >::handle_search_key_event(search, key_event); + >::handle_search_key_event(search, key_event) } AppState::Error(error) => { - >::handle_error_key_event(error, key_event); + >::handle_error_key_event(error, key_event) } AppState::Critical(critical) => { - >::handle_critical_key_event(critical, key_event); + >::handle_critical_key_event(critical, key_event) } } } - fn handle_browse_key_event(app: &mut ::BS, key_event: KeyEvent) { + fn handle_browse_key_event(app: ::BS, key_event: KeyEvent) -> APP { match key_event.code { // Exit application on `ESC` or `q`. - KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => { - app.save(); - app.quit(); - } + KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.save_and_quit(), // Category change. KeyCode::Left => app.decrement_category(), KeyCode::Right => app.increment_category(), @@ -108,15 +104,17 @@ impl IEventHandlerPrivate for EventHandler { // Toggle search. KeyCode::Char('s') | KeyCode::Char('S') => { if key_event.modifiers == KeyModifiers::CONTROL { - app.begin_search(); + app.begin_search() + } else { + app.no_op() } } // Othey keys. - _ => {} + _ => app.no_op(), } } - fn handle_info_key_event(app: &mut ::IS, key_event: KeyEvent) { + fn handle_info_key_event(app: ::IS, key_event: KeyEvent) -> APP { match key_event.code { // Toggle overlay. KeyCode::Esc @@ -125,11 +123,11 @@ impl IEventHandlerPrivate for EventHandler { | KeyCode::Char('m') | KeyCode::Char('M') => app.hide_info_overlay(), // Othey keys. - _ => {} + _ => app.no_op(), } } - fn handle_reload_key_event(app: &mut ::RS, key_event: KeyEvent) { + fn handle_reload_key_event(app: ::RS, key_event: KeyEvent) -> APP { match key_event.code { // Reload keys. KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(), @@ -141,22 +139,17 @@ impl IEventHandlerPrivate for EventHandler { | KeyCode::Char('g') | KeyCode::Char('G') => app.hide_reload_menu(), // Othey keys. - _ => {} + _ => app.no_op(), } } - fn handle_search_key_event(app: &mut ::SS, key_event: KeyEvent) { + fn handle_search_key_event(app: ::SS, key_event: KeyEvent) -> APP { if key_event.modifiers == KeyModifiers::CONTROL { - match key_event.code { - KeyCode::Char('s') | KeyCode::Char('S') => { - app.search_next(); - } - KeyCode::Char('g') | KeyCode::Char('G') => { - app.cancel_search(); - } - _ => {} - } - return; + return match key_event.code { + KeyCode::Char('s') | KeyCode::Char('S') => app.search_next(), + KeyCode::Char('g') | KeyCode::Char('G') => app.cancel_search(), + _ => app.no_op(), + }; } match key_event.code { @@ -166,17 +159,18 @@ impl IEventHandlerPrivate for EventHandler { // Return. KeyCode::Esc | KeyCode::Enter => app.finish_search(), // Othey keys. - _ => {} + _ => app.no_op(), } } - fn handle_error_key_event(app: &mut ::ES, _key_event: KeyEvent) { + fn handle_error_key_event(app: ::ES, _key_event: KeyEvent) -> APP { // Any key dismisses the error. - app.dismiss_error(); + app.dismiss_error() } - fn handle_critical_key_event(_app: &mut ::CS, _key_event: KeyEvent) { + fn handle_critical_key_event(app: ::CS, _key_event: KeyEvent) -> APP { // No action is allowed. + app.no_op() } } // GRCOV_EXCL_STOP diff --git a/src/tui/mod.rs b/src/tui/mod.rs index b48e650..fd1f2c8 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -75,7 +75,7 @@ impl Tui { ) -> Result<(), Error> { while app.is_running() { self.terminal.draw(|frame| UI::render(&mut app, frame))?; - handler.handle_next_event(&mut app)?; + app = handler.handle_next_event(app)?; } Ok(()) @@ -219,10 +219,7 @@ mod tests { let mut handler = MockIEventHandler::new(); handler .expect_handle_next_event() - .return_once(|app: &mut App| { - app.force_quit(); - Ok(()) - }); + .return_once(|app: App| Ok(app.force_quit())); handler } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 7833d13..378d31c 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -681,12 +681,12 @@ impl IUi for Ui { let selection = app.selection; let state = app.state; - Self::render_main_frame(collection, selection, state, frame); + Self::render_main_frame(collection, selection, &state, frame); match state { AppState::Info(_) => Self::render_info_overlay(collection, selection, frame), AppState::Reload(_) => Self::render_reload_overlay(frame), - AppState::Error(ref msg) => Self::render_error_overlay("Error", msg, frame), - AppState::Critical(ref msg) => Self::render_error_overlay("Critical Error", msg, frame), + AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame), + AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame), _ => {} } } @@ -719,26 +719,23 @@ mod tests { let mut app = AppPublic { collection, selection, - state: &AppState::Browse(()), + state: AppState::Browse(()), }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); - app.state = &AppState::Info(()); + app.state = AppState::Info(()); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); - app.state = &AppState::Reload(()); + app.state = AppState::Reload(()); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); - let binding = AppState::Search(String::new()); - app.state = &binding; + app.state = AppState::Search(""); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); - let binding = AppState::Error(String::from("get rekt scrub")); - app.state = &binding; + app.state = AppState::Error("get rekt scrub"); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); - let binding = AppState::Critical(String::from("get critically rekt scrub")); - app.state = &binding; + app.state = AppState::Critical("get critically rekt scrub"); terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } -- 2.45.2 From e96963f37a77061bf80c82dde1f4a275de324770 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 17 Feb 2024 21:44:22 +0100 Subject: [PATCH 18/35] Improvements --- src/tui/app/app.rs | 555 +++++++++++++++++---------------------- src/tui/app/selection.rs | 2 + src/tui/ui.rs | 30 ++- 3 files changed, 258 insertions(+), 329 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index eafa5dd..f41cdf9 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -1,3 +1,4 @@ +// FIXME: Split file apart #![allow(clippy::module_inception)] use musichoard::collection::Collection; @@ -7,7 +8,6 @@ use crate::tui::{ lib::IMusicHoard, }; -#[derive(Copy, Clone)] pub enum AppState { Browse(BS), Info(IS), @@ -110,9 +110,13 @@ pub trait IAppAccess { pub type AppPublicState<'app> = AppState<(), (), (), &'app str, &'app str, &'app str>; pub struct AppPublic<'app> { + pub inner: AppPublicInner<'app>, + pub state: AppPublicState<'app>, +} + +pub struct AppPublicInner<'app> { pub collection: &'app Collection, pub selection: &'app mut Selection, - pub state: AppPublicState<'app>, } struct AppInner { @@ -130,11 +134,13 @@ pub type App = AppState< AppErrorState, >; -pub struct AppGenericState(AppInner); +pub struct AppGenericState { + inner: AppInner, +} pub struct AppSearchState { inner: AppInner, - search: String, + string: String, orig: ListSelection, memo: Vec, } @@ -146,7 +152,7 @@ struct AppSearchMemo { pub struct AppErrorState { inner: AppInner, - msg: String, + string: String, } impl App { @@ -159,10 +165,10 @@ impl App { selection, }; match init_result { - Ok(()) => Self::Browse(AppGenericState(inner)), + Ok(()) => Self::Browse(AppGenericState { inner }), Err(err) => Self::Critical(AppErrorState { inner, - msg: err.to_string(), + string: err.to_string(), }), } } @@ -175,7 +181,9 @@ impl App { fn inner_ref(&self) -> &AppInner { match self { - AppState::Browse(inner) | AppState::Info(inner) | AppState::Reload(inner) => &inner.0, + AppState::Browse(generic) | AppState::Info(generic) | AppState::Reload(generic) => { + &generic.inner + } AppState::Search(search) => &search.inner, AppState::Error(error) | AppState::Critical(error) => &error.inner, } @@ -183,8 +191,8 @@ impl App { fn inner_mut(&mut self) -> &mut AppInner { match self { - AppState::Browse(inner) | AppState::Info(inner) | AppState::Reload(inner) => { - &mut inner.0 + AppState::Browse(generic) | AppState::Info(generic) | AppState::Reload(generic) => { + &mut generic.inner } AppState::Search(search) => &mut search.inner, AppState::Error(error) | AppState::Critical(error) => &mut error.inner, @@ -218,39 +226,39 @@ impl IAppInteractBrowse for AppGenericState { type APP = App; fn save_and_quit(mut self) -> Self::APP { - match self.0.music_hoard.save_to_database() { + match self.inner.music_hoard.save_to_database() { Ok(_) => { - self.0.running = false; + self.inner.running = false; AppState::Browse(self) } Err(err) => AppState::Error(AppErrorState { - inner: self.0, - msg: err.to_string(), + inner: self.inner, + string: err.to_string(), }), } } fn increment_category(mut self) -> Self::APP { - self.0.selection.increment_category(); + self.inner.selection.increment_category(); AppState::Browse(self) } fn decrement_category(mut self) -> Self::APP { - self.0.selection.decrement_category(); + self.inner.selection.decrement_category(); AppState::Browse(self) } fn increment_selection(mut self, delta: Delta) -> Self::APP { - self.0 + self.inner .selection - .increment_selection(self.0.music_hoard.get_collection(), delta); + .increment_selection(self.inner.music_hoard.get_collection(), delta); AppState::Browse(self) } fn decrement_selection(mut self, delta: Delta) -> Self::APP { - self.0 + self.inner .selection - .decrement_selection(self.0.music_hoard.get_collection(), delta); + .decrement_selection(self.inner.music_hoard.get_collection(), delta); AppState::Browse(self) } @@ -263,13 +271,13 @@ impl IAppInteractBrowse for AppGenericState { } fn begin_search(mut self) -> Self::APP { - let orig = ListSelection::get(&self.0.selection); - self.0 + let orig = ListSelection::get(&self.inner.selection); + self.inner .selection - .reset_artist(self.0.music_hoard.get_collection()); + .reset_artist(self.inner.music_hoard.get_collection()); AppState::Search(AppSearchState { - inner: self.0, - search: String::new(), + inner: self.inner, + string: String::new(), orig, memo: vec![], }) @@ -284,7 +292,7 @@ impl IAppInteractInfo for AppGenericState { type APP = App; fn hide_info_overlay(self) -> Self::APP { - AppState::Browse(AppGenericState(self.0)) + AppState::Browse(AppGenericState { inner: self.inner }) } fn no_op(self) -> Self::APP { @@ -296,19 +304,25 @@ impl IAppInteractReload for AppGenericState { type APP = App; fn reload_library(mut self) -> Self::APP { - let previous = IdSelection::get(self.0.music_hoard.get_collection(), &self.0.selection); - let result = self.0.music_hoard.rescan_library(); + let previous = IdSelection::get( + self.inner.music_hoard.get_collection(), + &self.inner.selection, + ); + let result = self.inner.music_hoard.rescan_library(); self.refresh(previous, result) } fn reload_database(mut self) -> Self::APP { - let previous = IdSelection::get(self.0.music_hoard.get_collection(), &self.0.selection); - let result = self.0.music_hoard.load_from_database(); + let previous = IdSelection::get( + self.inner.music_hoard.get_collection(), + &self.inner.selection, + ); + let result = self.inner.music_hoard.load_from_database(); self.refresh(previous, result) } fn hide_reload_menu(self) -> Self::APP { - AppState::Browse(AppGenericState(self.0)) + AppState::Browse(AppGenericState { inner: self.inner }) } fn no_op(self) -> Self::APP { @@ -324,14 +338,14 @@ impl IAppInteractReloadPrivate for AppGenericState { fn refresh(mut self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App { match result { Ok(()) => { - self.0 + self.inner .selection - .select_by_id(self.0.music_hoard.get_collection(), previous); - AppState::Browse(AppGenericState(self.0)) + .select_by_id(self.inner.music_hoard.get_collection(), previous); + AppState::Browse(AppGenericState { inner: self.inner }) } Err(err) => AppState::Error(AppErrorState { - inner: self.0, - msg: err.to_string(), + inner: self.inner, + string: err.to_string(), }), } } @@ -342,22 +356,22 @@ impl IAppInteractSearch for AppSearchState { fn append_character(mut self, ch: char) -> Self::APP { let collection = self.inner.music_hoard.get_collection(); - self.search.push(ch); + self.string.push(ch); let index = self .inner .selection - .incremental_artist_search(collection, &self.search, false); + .incremental_artist_search(collection, &self.string, false); self.memo.push(AppSearchMemo { index, char: true }); AppState::Search(self) } fn search_next(mut self) -> Self::APP { let collection = self.inner.music_hoard.get_collection(); - if !self.search.is_empty() { + if !self.string.is_empty() { let index = self.inner .selection - .incremental_artist_search(collection, &self.search, true); + .incremental_artist_search(collection, &self.string, true); self.memo.push(AppSearchMemo { index, char: false }); } AppState::Search(self) @@ -367,7 +381,7 @@ impl IAppInteractSearch for AppSearchState { let collection = self.inner.music_hoard.get_collection(); if let Some(memo) = self.memo.pop() { if memo.char { - self.search.pop(); + self.string.pop(); } self.inner.selection.select_artist(collection, memo.index); } @@ -375,12 +389,12 @@ impl IAppInteractSearch for AppSearchState { } fn finish_search(self) -> Self::APP { - AppState::Browse(AppGenericState(self.inner)) + AppState::Browse(AppGenericState { inner: self.inner }) } fn cancel_search(mut self) -> Self::APP { self.inner.selection.select_by_list(self.orig); - AppState::Browse(AppGenericState(self.inner)) + AppState::Browse(AppGenericState { inner: self.inner }) } fn no_op(self) -> Self::APP { @@ -392,7 +406,7 @@ impl IAppInteractError for AppErrorState { type APP = App; fn dismiss_error(self) -> Self::APP { - AppState::Browse(AppGenericState(self.inner)) + AppState::Browse(AppGenericState { inner: self.inner }) } } @@ -408,39 +422,42 @@ impl IAppAccess for App { fn get(&mut self) -> AppPublic { match self { AppState::Browse(generic) => AppPublic { - collection: generic.0.music_hoard.get_collection(), - selection: &mut generic.0.selection, + inner: (&mut generic.inner).into(), state: AppState::Browse(()), }, AppState::Info(generic) => AppPublic { - collection: generic.0.music_hoard.get_collection(), - selection: &mut generic.0.selection, + inner: (&mut generic.inner).into(), state: AppState::Info(()), }, AppState::Reload(generic) => AppPublic { - collection: generic.0.music_hoard.get_collection(), - selection: &mut generic.0.selection, + inner: (&mut generic.inner).into(), state: AppState::Reload(()), }, AppState::Search(search) => AppPublic { - collection: search.inner.music_hoard.get_collection(), - selection: &mut search.inner.selection, - state: AppState::Search(&search.search), + inner: (&mut search.inner).into(), + state: AppState::Search(&search.string), }, AppState::Error(error) => AppPublic { - collection: error.inner.music_hoard.get_collection(), - selection: &mut error.inner.selection, - state: AppState::Error(&error.msg), + inner: (&mut error.inner).into(), + state: AppState::Error(&error.string), }, AppState::Critical(critical) => AppPublic { - collection: critical.inner.music_hoard.get_collection(), - selection: &mut critical.inner.selection, - state: AppState::Error(&critical.msg), + inner: (&mut critical.inner).into(), + state: AppState::Error(&critical.string), }, } } } +impl<'app, MH: IMusicHoard> From<&'app mut AppInner> for AppPublicInner<'app> { + fn from(inner: &'app mut AppInner) -> Self { + AppPublicInner { + collection: inner.music_hoard.get_collection(), + selection: &mut inner.selection, + } + } +} + #[cfg(test)] mod tests { use crate::tui::{app::selection::Category, lib::MockIMusicHoard, testmod::COLLECTION}; @@ -544,8 +561,8 @@ mod tests { assert!(app.is_running()); let app = App::Error(AppErrorState { - inner: app.unwrap_browse().0, - msg: String::from("get rekt"), + inner: app.unwrap_browse().inner, + string: String::from("get rekt"), }); let error = app.unwrap_error(); @@ -571,8 +588,8 @@ mod tests { assert!(app.is_running()); let app = App::Error(AppErrorState { - inner: app.unwrap_browse().0, - msg: String::from("get rekt"), + inner: app.unwrap_browse().inner, + string: String::from("get rekt"), }); let app = app.force_quit(); @@ -630,166 +647,101 @@ mod tests { let browse = app.unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Artist); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(0) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - Some(0) - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Artist); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(0) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - Some(0) - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); let browse = browse.increment_category().unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(0) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - Some(0) - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(1) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - Some(0) - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); let browse = browse.increment_category().unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Track); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(1) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - Some(0) - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Track); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(1) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - Some(1) - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); let browse = browse.increment_category().unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Track); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(1) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - Some(1) - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Track); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(1) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - Some(0) - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); let browse = browse.increment_selection(Delta::Line).unwrap_browse(); let browse = browse.decrement_category().unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(1) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - Some(1) - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(0) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - Some(0) - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); let browse = browse.increment_selection(Delta::Line).unwrap_browse(); let browse = browse.decrement_category().unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Artist); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(1) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - Some(0) - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Artist); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(0) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - Some(0) - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); let browse = browse.increment_category().unwrap_browse(); let browse = browse.increment_selection(Delta::Line).unwrap_browse(); let browse = browse.decrement_category().unwrap_browse(); let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); let browse = browse.decrement_category().unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Artist); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(1) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - Some(0) - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); } #[test] @@ -802,43 +754,28 @@ mod tests { let browse = app.unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Artist); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(0) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), None); let browse = browse.increment_category().unwrap_browse(); let browse = browse.increment_category().unwrap_browse(); let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Track); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(0) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), None); let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Track); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); - assert_eq!( - browse.0.selection.artist.album.state.list.selected(), - Some(0) - ); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), None); } #[test] @@ -851,53 +788,43 @@ mod tests { let browse = app.unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Artist); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); - assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); let browse = browse.increment_category().unwrap_browse(); let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); - assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); - assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); let browse = browse.increment_category().unwrap_browse(); let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Track); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); - assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Track); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(0)); - assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); } #[test] @@ -907,71 +834,57 @@ mod tests { let browse = app.unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Artist); - assert_eq!(browse.0.selection.artist.state.list.selected(), None); - assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Artist); - assert_eq!(browse.0.selection.artist.state.list.selected(), None); - assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Artist); - assert_eq!(browse.0.selection.artist.state.list.selected(), None); - assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); let browse = browse.increment_category().unwrap_browse(); let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), None); - assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), None); - assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); let browse = browse.increment_category().unwrap_browse(); let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Track); - assert_eq!(browse.0.selection.artist.state.list.selected(), None); - assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Track); - assert_eq!(browse.0.selection.artist.state.list.selected(), None); - assert_eq!(browse.0.selection.artist.album.state.list.selected(), None); - assert_eq!( - browse.0.selection.artist.album.track.state.list.selected(), - None - ); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); } #[test] @@ -1049,8 +962,8 @@ mod tests { let browse = browse.increment_selection(Delta::Line).unwrap_browse(); let browse = browse.increment_category().unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); let search = browse.begin_search().unwrap_search(); @@ -1097,8 +1010,8 @@ mod tests { let browse = search.finish_search().unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); } #[test] @@ -1108,8 +1021,8 @@ mod tests { let browse = browse.increment_selection(Delta::Line).unwrap_browse(); let browse = browse.increment_category().unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); let search = browse.begin_search().unwrap_search(); @@ -1176,8 +1089,8 @@ mod tests { let browse = browse.increment_selection(Delta::Line).unwrap_browse(); let browse = browse.increment_category().unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); let search = browse.begin_search().unwrap_search(); @@ -1206,8 +1119,8 @@ mod tests { let browse = search.cancel_search().unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), Some(1)); + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); } #[test] @@ -1217,8 +1130,8 @@ mod tests { let browse = browse.increment_selection(Delta::Line).unwrap_browse(); let browse = browse.increment_category().unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), None); let search = browse.begin_search().unwrap_search(); @@ -1247,7 +1160,7 @@ mod tests { let browse = search.cancel_search().unwrap_browse(); - assert_eq!(browse.0.selection.active, Category::Album); - assert_eq!(browse.0.selection.artist.state.list.selected(), None); + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), None); } } diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index 90a57c8..82a3663 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -48,6 +48,7 @@ pub struct TrackSelection { pub state: WidgetState, } +// FIXME: should be with browse state (maybe?) pub enum Delta { Line, Page, @@ -268,6 +269,7 @@ impl ArtistSelection { result } + // FIXME: search logic should be with the search state fn incremental_search( &mut self, artists: &[Artist], diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 378d31c..b03b8b6 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -677,8 +677,8 @@ impl IUi for Ui { fn render(app: &mut APP, frame: &mut Frame) { let app = app.get(); - let collection = app.collection; - let selection = app.selection; + let collection = app.inner.collection; + let selection = app.inner.selection; let state = app.state; Self::render_main_frame(collection, selection, &state, frame); @@ -695,7 +695,10 @@ impl IUi for Ui { #[cfg(test)] mod tests { use crate::tui::{ - app::{app::AppPublic, selection::Delta}, + app::{ + app::{AppPublic, AppPublicInner}, + selection::Delta, + }, testmod::COLLECTION, tests::terminal, }; @@ -706,9 +709,18 @@ mod tests { impl IAppAccess for AppPublic<'_> { fn get(&mut self) -> AppPublic { AppPublic { - collection: self.collection, - selection: self.selection, - state: self.state, + inner: AppPublicInner { + collection: self.inner.collection, + selection: self.inner.selection, + }, + state: match self.state { + AppState::Browse(()) => AppState::Browse(()), + AppState::Info(()) => AppState::Info(()), + AppState::Reload(()) => AppState::Reload(()), + AppState::Search(s) => AppState::Search(s), + AppState::Error(s) => AppState::Error(s), + AppState::Critical(s) => AppState::Critical(s), + }, } } } @@ -717,8 +729,10 @@ mod tests { let mut terminal = terminal(); let mut app = AppPublic { - collection, - selection, + inner: AppPublicInner { + collection, + selection, + }, state: AppState::Browse(()), }; terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); -- 2.45.2 From 001bc81b0d6bde70a1d86396aacfefea8b703ff1 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 17 Feb 2024 22:34:37 +0100 Subject: [PATCH 19/35] Extract public interface of app --- src/tui/app/app.rs | 128 +++++---------------------------------------- src/tui/app/mod.rs | 116 ++++++++++++++++++++++++++++++++++++++++ src/tui/handler.rs | 9 +--- src/tui/mod.rs | 2 +- src/tui/ui.rs | 7 +-- 5 files changed, 134 insertions(+), 128 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index f41cdf9..6dc8361 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -1,124 +1,16 @@ // FIXME: Split file apart #![allow(clippy::module_inception)] -use musichoard::collection::Collection; - use crate::tui::{ - app::selection::{Delta, IdSelection, ListSelection, Selection}, + app::{ + selection::{Delta, IdSelection, ListSelection, Selection}, + AppPublic, AppPublicInner, AppState, IAppAccess, IAppInteract, IAppInteractBrowse, + IAppInteractCritical, IAppInteractError, IAppInteractInfo, IAppInteractReload, + IAppInteractSearch, + }, lib::IMusicHoard, }; -pub enum AppState { - Browse(BS), - Info(IS), - Reload(RS), - Search(SS), - Error(ES), - Critical(CS), -} - -impl AppState { - pub fn is_search(&self) -> bool { - matches!(self, AppState::Search(_)) - } -} - -pub trait IAppInteract { - type BS: IAppInteractBrowse; - type IS: IAppInteractInfo; - type RS: IAppInteractReload; - type SS: IAppInteractSearch; - type ES: IAppInteractError; - type CS: IAppInteractCritical; - - fn is_running(&self) -> bool; - fn force_quit(self) -> Self; - - #[allow(clippy::type_complexity)] - fn state(self) -> AppState; -} - -pub trait IAppInteractBrowse { - type APP: IAppInteract; - - fn save_and_quit(self) -> Self::APP; - - fn increment_category(self) -> Self::APP; - fn decrement_category(self) -> Self::APP; - fn increment_selection(self, delta: Delta) -> Self::APP; - fn decrement_selection(self, delta: Delta) -> Self::APP; - - fn show_info_overlay(self) -> Self::APP; - - fn show_reload_menu(self) -> Self::APP; - - fn begin_search(self) -> Self::APP; - - fn no_op(self) -> Self::APP; -} - -pub trait IAppInteractInfo { - type APP: IAppInteract; - - fn hide_info_overlay(self) -> Self::APP; - - fn no_op(self) -> Self::APP; -} - -pub trait IAppInteractReload { - type APP: IAppInteract; - - fn reload_library(self) -> Self::APP; - fn reload_database(self) -> Self::APP; - fn hide_reload_menu(self) -> Self::APP; - - fn no_op(self) -> Self::APP; -} - -pub trait IAppInteractSearch { - type APP: IAppInteract; - - fn append_character(self, ch: char) -> Self::APP; - fn search_next(self) -> Self::APP; - fn step_back(self) -> Self::APP; - fn finish_search(self) -> Self::APP; - fn cancel_search(self) -> Self::APP; - - fn no_op(self) -> Self::APP; -} - -pub trait IAppInteractError { - type APP: IAppInteract; - - fn dismiss_error(self) -> Self::APP; -} - -pub trait IAppInteractCritical { - type APP: IAppInteract; - - fn no_op(self) -> Self::APP; -} - -// It would be preferable to have a getter for each field separately. However, the selection field -// needs to be mutably accessible requiring a mutable borrow of the entire struct if behind a trait. -// This in turn complicates simultaneous field access since only a single mutable borrow is allowed. -// Therefore, all fields are grouped into a single struct and returned as a batch. -pub trait IAppAccess { - fn get(&mut self) -> AppPublic; -} - -pub type AppPublicState<'app> = AppState<(), (), (), &'app str, &'app str, &'app str>; - -pub struct AppPublic<'app> { - pub inner: AppPublicInner<'app>, - pub state: AppPublicState<'app>, -} - -pub struct AppPublicInner<'app> { - pub collection: &'app Collection, - pub selection: &'app mut Selection, -} - struct AppInner { running: bool, music_hoard: MH, @@ -460,7 +352,13 @@ impl<'app, MH: IMusicHoard> From<&'app mut AppInner> for AppPublicInner<'app #[cfg(test)] mod tests { - use crate::tui::{app::selection::Category, lib::MockIMusicHoard, testmod::COLLECTION}; + use musichoard::collection::Collection; + + use crate::tui::{ + app::{selection::Category, AppPublicState}, + lib::MockIMusicHoard, + testmod::COLLECTION, + }; use super::*; diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 57892fa..2cb6d31 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -1,2 +1,118 @@ pub mod app; pub mod selection; + +use musichoard::collection::Collection; + +use selection::{Delta, Selection}; + +pub enum AppState { + Browse(BS), + Info(IS), + Reload(RS), + Search(SS), + Error(ES), + Critical(CS), +} + +pub trait IAppInteract { + type BS: IAppInteractBrowse; + type IS: IAppInteractInfo; + type RS: IAppInteractReload; + type SS: IAppInteractSearch; + type ES: IAppInteractError; + type CS: IAppInteractCritical; + + fn is_running(&self) -> bool; + fn force_quit(self) -> Self; + + #[allow(clippy::type_complexity)] + fn state(self) -> AppState; +} + +// FIXME: move to returning specific states - for errors, return Result +pub trait IAppInteractBrowse { + type APP: IAppInteract; + + fn save_and_quit(self) -> Self::APP; + + fn increment_category(self) -> Self::APP; + fn decrement_category(self) -> Self::APP; + fn increment_selection(self, delta: Delta) -> Self::APP; + fn decrement_selection(self, delta: Delta) -> Self::APP; + + fn show_info_overlay(self) -> Self::APP; + + fn show_reload_menu(self) -> Self::APP; + + fn begin_search(self) -> Self::APP; + + fn no_op(self) -> Self::APP; +} + +pub trait IAppInteractInfo { + type APP: IAppInteract; + + fn hide_info_overlay(self) -> Self::APP; + + fn no_op(self) -> Self::APP; +} + +pub trait IAppInteractReload { + type APP: IAppInteract; + + fn reload_library(self) -> Self::APP; + fn reload_database(self) -> Self::APP; + fn hide_reload_menu(self) -> Self::APP; + + fn no_op(self) -> Self::APP; +} + +pub trait IAppInteractSearch { + type APP: IAppInteract; + + fn append_character(self, ch: char) -> Self::APP; + fn search_next(self) -> Self::APP; + fn step_back(self) -> Self::APP; + fn finish_search(self) -> Self::APP; + fn cancel_search(self) -> Self::APP; + + fn no_op(self) -> Self::APP; +} + +pub trait IAppInteractError { + type APP: IAppInteract; + + fn dismiss_error(self) -> Self::APP; +} + +pub trait IAppInteractCritical { + type APP: IAppInteract; + + fn no_op(self) -> Self::APP; +} + +// It would be preferable to have a getter for each field separately. However, the selection field +// needs to be mutably accessible requiring a mutable borrow of the entire struct if behind a trait. +// This in turn complicates simultaneous field access since only a single mutable borrow is allowed. +// Therefore, all fields are grouped into a single struct and returned as a batch. +pub trait IAppAccess { + fn get(&mut self) -> AppPublic; +} + +pub struct AppPublic<'app> { + pub inner: AppPublicInner<'app>, + pub state: AppPublicState<'app>, +} + +pub struct AppPublicInner<'app> { + pub collection: &'app Collection, + pub selection: &'app mut Selection, +} + +pub type AppPublicState<'app> = AppState<(), (), (), &'app str, &'app str, &'app str>; + +impl AppState { + pub fn is_search(&self) -> bool { + matches!(self, AppState::Search(_)) + } +} diff --git a/src/tui/handler.rs b/src/tui/handler.rs index dfd3d8b..be88bb2 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -5,17 +5,12 @@ use mockall::automock; use crate::tui::{ app::{ - app::{ - AppState, IAppInteract, IAppInteractBrowse, IAppInteractError, IAppInteractInfo, - IAppInteractReload, IAppInteractSearch, - }, - selection::Delta, + selection::Delta, AppState, IAppInteract, IAppInteractBrowse, IAppInteractCritical, + IAppInteractError, IAppInteractInfo, IAppInteractReload, IAppInteractSearch, }, event::{Event, EventError, EventReceiver}, }; -use super::app::app::IAppInteractCritical; - #[cfg_attr(test, automock)] pub trait IEventHandler { fn handle_next_event(&self, app: APP) -> Result; diff --git a/src/tui/mod.rs b/src/tui/mod.rs index fd1f2c8..629ecaa 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -19,7 +19,7 @@ use std::io; use std::marker::PhantomData; use crate::tui::{ - app::app::{IAppAccess, IAppInteract}, + app::{IAppAccess, IAppInteract}, event::EventError, handler::IEventHandler, listener::IEventListener, diff --git a/src/tui/ui.rs b/src/tui/ui.rs index b03b8b6..80536a9 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -14,8 +14,8 @@ use ratatui::{ }; use crate::tui::app::{ - app::{AppPublicState, AppState, IAppAccess}, selection::{Category, Selection, WidgetState}, + AppPublicState, AppState, IAppAccess, }; pub trait IUi { @@ -695,10 +695,7 @@ impl IUi for Ui { #[cfg(test)] mod tests { use crate::tui::{ - app::{ - app::{AppPublic, AppPublicInner}, - selection::Delta, - }, + app::{selection::Delta, AppPublic, AppPublicInner}, testmod::COLLECTION, tests::terminal, }; -- 2.45.2 From 6b7e3ad23228f7fa1569cd2953ab424f06e4fe84 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 17 Feb 2024 22:53:33 +0100 Subject: [PATCH 20/35] Split states into own structs --- src/tui/app/app.rs | 79 +++++++++++++++++++++++++++------------------- src/tui/app/mod.rs | 1 - 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index 6dc8361..8a2fc1e 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -18,15 +18,23 @@ struct AppInner { } pub type App = AppState< - AppGenericState, - AppGenericState, - AppGenericState, + AppBrowseState, + AppInfoState, + AppReloadState, AppSearchState, AppErrorState, - AppErrorState, + AppCriticalState, >; -pub struct AppGenericState { +pub struct AppBrowseState { + inner: AppInner, +} + +pub struct AppInfoState { + inner: AppInner, +} + +pub struct AppReloadState { inner: AppInner, } @@ -47,6 +55,11 @@ pub struct AppErrorState { string: String, } +pub struct AppCriticalState { + inner: AppInner, + string: String, +} + impl App { pub fn new(mut music_hoard: MH) -> Self { let init_result = Self::init(&mut music_hoard); @@ -57,8 +70,8 @@ impl App { selection, }; match init_result { - Ok(()) => Self::Browse(AppGenericState { inner }), - Err(err) => Self::Critical(AppErrorState { + Ok(()) => Self::Browse(AppBrowseState { inner }), + Err(err) => Self::Critical(AppCriticalState { inner, string: err.to_string(), }), @@ -73,32 +86,34 @@ impl App { fn inner_ref(&self) -> &AppInner { match self { - AppState::Browse(generic) | AppState::Info(generic) | AppState::Reload(generic) => { - &generic.inner - } + AppState::Browse(browse) => &browse.inner, + AppState::Info(info) => &info.inner, + AppState::Reload(reload) => &reload.inner, AppState::Search(search) => &search.inner, - AppState::Error(error) | AppState::Critical(error) => &error.inner, + AppState::Error(error) => &error.inner, + AppState::Critical(critical) => &critical.inner, } } fn inner_mut(&mut self) -> &mut AppInner { match self { - AppState::Browse(generic) | AppState::Info(generic) | AppState::Reload(generic) => { - &mut generic.inner - } + 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) | AppState::Critical(error) => &mut error.inner, + AppState::Error(error) => &mut error.inner, + AppState::Critical(critical) => &mut critical.inner, } } } impl IAppInteract for App { - type BS = AppGenericState; - type IS = AppGenericState; - type RS = AppGenericState; + type BS = AppBrowseState; + type IS = AppInfoState; + type RS = AppReloadState; type SS = AppSearchState; type ES = AppErrorState; - type CS = AppErrorState; + type CS = AppCriticalState; fn is_running(&self) -> bool { self.inner_ref().running @@ -114,7 +129,7 @@ impl IAppInteract for App { } } -impl IAppInteractBrowse for AppGenericState { +impl IAppInteractBrowse for AppBrowseState { type APP = App; fn save_and_quit(mut self) -> Self::APP { @@ -155,11 +170,11 @@ impl IAppInteractBrowse for AppGenericState { } fn show_info_overlay(self) -> Self::APP { - AppState::Info(self) + AppState::Info(AppInfoState { inner: self.inner }) } fn show_reload_menu(self) -> Self::APP { - AppState::Reload(self) + AppState::Reload(AppReloadState { inner: self.inner }) } fn begin_search(mut self) -> Self::APP { @@ -180,11 +195,11 @@ impl IAppInteractBrowse for AppGenericState { } } -impl IAppInteractInfo for AppGenericState { +impl IAppInteractInfo for AppInfoState { type APP = App; fn hide_info_overlay(self) -> Self::APP { - AppState::Browse(AppGenericState { inner: self.inner }) + AppState::Browse(AppBrowseState { inner: self.inner }) } fn no_op(self) -> Self::APP { @@ -192,7 +207,7 @@ impl IAppInteractInfo for AppGenericState { } } -impl IAppInteractReload for AppGenericState { +impl IAppInteractReload for AppReloadState { type APP = App; fn reload_library(mut self) -> Self::APP { @@ -214,7 +229,7 @@ impl IAppInteractReload for AppGenericState { } fn hide_reload_menu(self) -> Self::APP { - AppState::Browse(AppGenericState { inner: self.inner }) + AppState::Browse(AppBrowseState { inner: self.inner }) } fn no_op(self) -> Self::APP { @@ -226,14 +241,14 @@ trait IAppInteractReloadPrivate { fn refresh(self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App; } -impl IAppInteractReloadPrivate for AppGenericState { +impl IAppInteractReloadPrivate for AppReloadState { fn refresh(mut self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App { match result { Ok(()) => { self.inner .selection .select_by_id(self.inner.music_hoard.get_collection(), previous); - AppState::Browse(AppGenericState { inner: self.inner }) + AppState::Browse(AppBrowseState { inner: self.inner }) } Err(err) => AppState::Error(AppErrorState { inner: self.inner, @@ -281,12 +296,12 @@ impl IAppInteractSearch for AppSearchState { } fn finish_search(self) -> Self::APP { - AppState::Browse(AppGenericState { inner: self.inner }) + AppState::Browse(AppBrowseState { inner: self.inner }) } fn cancel_search(mut self) -> Self::APP { self.inner.selection.select_by_list(self.orig); - AppState::Browse(AppGenericState { inner: self.inner }) + AppState::Browse(AppBrowseState { inner: self.inner }) } fn no_op(self) -> Self::APP { @@ -298,11 +313,11 @@ impl IAppInteractError for AppErrorState { type APP = App; fn dismiss_error(self) -> Self::APP { - AppState::Browse(AppGenericState { inner: self.inner }) + AppState::Browse(AppBrowseState { inner: self.inner }) } } -impl IAppInteractCritical for AppErrorState { +impl IAppInteractCritical for AppCriticalState { type APP = App; fn no_op(self) -> Self::APP { diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 2cb6d31..d0f4caf 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -29,7 +29,6 @@ pub trait IAppInteract { fn state(self) -> AppState; } -// FIXME: move to returning specific states - for errors, return Result pub trait IAppInteractBrowse { type APP: IAppInteract; -- 2.45.2 From 8ca6c4d36bbd6ce43e88923586dded92d1e5e8d1 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 17 Feb 2024 22:53:46 +0100 Subject: [PATCH 21/35] Make states generic --- src/tui/app/app.rs | 182 +++++++++++++++++++++++++++------------------ 1 file changed, 109 insertions(+), 73 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index 8a2fc1e..853dad0 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -18,28 +18,26 @@ struct AppInner { } pub type App = AppState< - AppBrowseState, - AppInfoState, - AppReloadState, - AppSearchState, - AppErrorState, - AppCriticalState, + AppMachine, + AppMachine, + AppMachine, + AppMachine, + AppMachine, + AppMachine, >; -pub struct AppBrowseState { +pub struct AppMachine { inner: AppInner, + state: STATE, } -pub struct AppInfoState { - inner: AppInner, -} +pub struct AppBrowse; -pub struct AppReloadState { - inner: AppInner, -} +pub struct AppInfo; -pub struct AppSearchState { - inner: AppInner, +pub struct AppReload; + +pub struct AppSearch { string: String, orig: ListSelection, memo: Vec, @@ -50,13 +48,11 @@ struct AppSearchMemo { char: bool, } -pub struct AppErrorState { - inner: AppInner, +pub struct AppError { string: String, } -pub struct AppCriticalState { - inner: AppInner, +pub struct AppCritical { string: String, } @@ -70,10 +66,15 @@ impl App { selection, }; match init_result { - Ok(()) => Self::Browse(AppBrowseState { inner }), - Err(err) => Self::Critical(AppCriticalState { + Ok(()) => Self::Browse(AppMachine { inner, - string: err.to_string(), + state: AppBrowse, + }), + Err(err) => Self::Critical(AppMachine { + inner, + state: AppCritical { + string: err.to_string(), + }, }), } } @@ -108,12 +109,12 @@ impl App { } impl IAppInteract for App { - type BS = AppBrowseState; - type IS = AppInfoState; - type RS = AppReloadState; - type SS = AppSearchState; - type ES = AppErrorState; - type CS = AppCriticalState; + type BS = AppMachine; + type IS = AppMachine; + type RS = AppMachine; + type SS = AppMachine; + type ES = AppMachine; + type CS = AppMachine; fn is_running(&self) -> bool { self.inner_ref().running @@ -129,7 +130,7 @@ impl IAppInteract for App { } } -impl IAppInteractBrowse for AppBrowseState { +impl IAppInteractBrowse for AppMachine { type APP = App; fn save_and_quit(mut self) -> Self::APP { @@ -138,9 +139,11 @@ impl IAppInteractBrowse for AppBrowseState { self.inner.running = false; AppState::Browse(self) } - Err(err) => AppState::Error(AppErrorState { + Err(err) => AppState::Error(AppMachine { inner: self.inner, - string: err.to_string(), + state: AppError { + string: err.to_string(), + }, }), } } @@ -170,11 +173,17 @@ impl IAppInteractBrowse for AppBrowseState { } fn show_info_overlay(self) -> Self::APP { - AppState::Info(AppInfoState { inner: self.inner }) + AppState::Info(AppMachine { + inner: self.inner, + state: AppInfo, + }) } fn show_reload_menu(self) -> Self::APP { - AppState::Reload(AppReloadState { inner: self.inner }) + AppState::Reload(AppMachine { + inner: self.inner, + state: AppReload, + }) } fn begin_search(mut self) -> Self::APP { @@ -182,11 +191,13 @@ impl IAppInteractBrowse for AppBrowseState { self.inner .selection .reset_artist(self.inner.music_hoard.get_collection()); - AppState::Search(AppSearchState { + AppState::Search(AppMachine { inner: self.inner, - string: String::new(), - orig, - memo: vec![], + state: AppSearch { + string: String::new(), + orig, + memo: vec![], + }, }) } @@ -195,11 +206,14 @@ impl IAppInteractBrowse for AppBrowseState { } } -impl IAppInteractInfo for AppInfoState { +impl IAppInteractInfo for AppMachine { type APP = App; fn hide_info_overlay(self) -> Self::APP { - AppState::Browse(AppBrowseState { inner: self.inner }) + AppState::Browse(AppMachine { + inner: self.inner, + state: AppBrowse, + }) } fn no_op(self) -> Self::APP { @@ -207,7 +221,7 @@ impl IAppInteractInfo for AppInfoState { } } -impl IAppInteractReload for AppReloadState { +impl IAppInteractReload for AppMachine { type APP = App; fn reload_library(mut self) -> Self::APP { @@ -229,7 +243,10 @@ impl IAppInteractReload for AppReloadState { } fn hide_reload_menu(self) -> Self::APP { - AppState::Browse(AppBrowseState { inner: self.inner }) + AppState::Browse(AppMachine { + inner: self.inner, + state: AppBrowse, + }) } fn no_op(self) -> Self::APP { @@ -241,54 +258,60 @@ trait IAppInteractReloadPrivate { fn refresh(self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App; } -impl IAppInteractReloadPrivate for AppReloadState { +impl IAppInteractReloadPrivate for AppMachine { fn refresh(mut self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App { match result { Ok(()) => { self.inner .selection .select_by_id(self.inner.music_hoard.get_collection(), previous); - AppState::Browse(AppBrowseState { inner: self.inner }) + AppState::Browse(AppMachine { + inner: self.inner, + state: AppBrowse, + }) } - Err(err) => AppState::Error(AppErrorState { + Err(err) => AppState::Error(AppMachine { inner: self.inner, - string: err.to_string(), + state: AppError { + string: err.to_string(), + }, }), } } } -impl IAppInteractSearch for AppSearchState { +impl IAppInteractSearch for AppMachine { type APP = App; fn append_character(mut self, ch: char) -> Self::APP { let collection = self.inner.music_hoard.get_collection(); - self.string.push(ch); - let index = self - .inner - .selection - .incremental_artist_search(collection, &self.string, false); - self.memo.push(AppSearchMemo { index, char: true }); + self.state.string.push(ch); + let index = + self.inner + .selection + .incremental_artist_search(collection, &self.state.string, false); + self.state.memo.push(AppSearchMemo { index, char: true }); AppState::Search(self) } fn search_next(mut self) -> Self::APP { let collection = self.inner.music_hoard.get_collection(); - if !self.string.is_empty() { - let index = - self.inner - .selection - .incremental_artist_search(collection, &self.string, true); - self.memo.push(AppSearchMemo { index, char: false }); + if !self.state.string.is_empty() { + let index = self.inner.selection.incremental_artist_search( + collection, + &self.state.string, + true, + ); + self.state.memo.push(AppSearchMemo { index, char: false }); } AppState::Search(self) } fn step_back(mut self) -> Self::APP { let collection = self.inner.music_hoard.get_collection(); - if let Some(memo) = self.memo.pop() { + if let Some(memo) = self.state.memo.pop() { if memo.char { - self.string.pop(); + self.state.string.pop(); } self.inner.selection.select_artist(collection, memo.index); } @@ -296,12 +319,18 @@ impl IAppInteractSearch for AppSearchState { } fn finish_search(self) -> Self::APP { - AppState::Browse(AppBrowseState { inner: self.inner }) + AppState::Browse(AppMachine { + inner: self.inner, + state: AppBrowse, + }) } fn cancel_search(mut self) -> Self::APP { - self.inner.selection.select_by_list(self.orig); - AppState::Browse(AppBrowseState { inner: self.inner }) + self.inner.selection.select_by_list(self.state.orig); + AppState::Browse(AppMachine { + inner: self.inner, + state: AppBrowse, + }) } fn no_op(self) -> Self::APP { @@ -309,15 +338,18 @@ impl IAppInteractSearch for AppSearchState { } } -impl IAppInteractError for AppErrorState { +impl IAppInteractError for AppMachine { type APP = App; fn dismiss_error(self) -> Self::APP { - AppState::Browse(AppBrowseState { inner: self.inner }) + AppState::Browse(AppMachine { + inner: self.inner, + state: AppBrowse, + }) } } -impl IAppInteractCritical for AppCriticalState { +impl IAppInteractCritical for AppMachine { type APP = App; fn no_op(self) -> Self::APP { @@ -342,15 +374,15 @@ impl IAppAccess for App { }, AppState::Search(search) => AppPublic { inner: (&mut search.inner).into(), - state: AppState::Search(&search.string), + state: AppState::Search(&search.state.string), }, AppState::Error(error) => AppPublic { inner: (&mut error.inner).into(), - state: AppState::Error(&error.string), + state: AppState::Error(&error.state.string), }, AppState::Critical(critical) => AppPublic { inner: (&mut critical.inner).into(), - state: AppState::Error(&critical.string), + state: AppState::Error(&critical.state.string), }, } } @@ -473,9 +505,11 @@ mod tests { let app = App::new(music_hoard); assert!(app.is_running()); - let app = App::Error(AppErrorState { + let app = App::Error(AppMachine { inner: app.unwrap_browse().inner, - string: String::from("get rekt"), + state: AppError { + string: String::from("get rekt"), + }, }); let error = app.unwrap_error(); @@ -500,9 +534,11 @@ mod tests { let app = App::new(music_hoard(COLLECTION.to_owned())); assert!(app.is_running()); - let app = App::Error(AppErrorState { + let app = App::Error(AppMachine { inner: app.unwrap_browse().inner, - string: String::from("get rekt"), + state: AppError { + string: String::from("get rekt"), + }, }); let app = app.force_quit(); -- 2.45.2 From a68063e7c887c1a9f55de79259b543f33e0ad962 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 17 Feb 2024 23:32:06 +0100 Subject: [PATCH 22/35] Convenience --- src/tui/app/app.rs | 219 +++++++++++++++++++++++++++------------------ 1 file changed, 132 insertions(+), 87 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index 853dad0..e3a4928 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -33,10 +33,55 @@ pub struct AppMachine { pub struct AppBrowse; +impl AppMachine { + fn browse(inner: AppInner) -> Self { + AppMachine { + inner, + state: AppBrowse, + } + } +} + +impl From> for App { + fn from(value: AppMachine) -> Self { + AppState::Browse(value) + } +} + pub struct AppInfo; +impl AppMachine { + fn info(inner: AppInner) -> Self { + AppMachine { + inner, + state: AppInfo, + } + } +} + +impl From> for App { + fn from(value: AppMachine) -> Self { + AppState::Info(value) + } +} + pub struct AppReload; +impl AppMachine { + fn reload(inner: AppInner) -> Self { + AppMachine { + inner, + state: AppReload, + } + } +} + +impl From> for App { + fn from(value: AppMachine) -> Self { + AppState::Reload(value) + } +} + pub struct AppSearch { string: String, orig: ListSelection, @@ -48,14 +93,67 @@ struct AppSearchMemo { char: bool, } +impl AppMachine { + fn search(inner: AppInner, orig: ListSelection) -> Self { + AppMachine { + inner, + state: AppSearch { + string: String::new(), + orig, + memo: vec![], + }, + } + } +} + +impl From> for App { + fn from(value: AppMachine) -> Self { + AppState::Search(value) + } +} + pub struct AppError { string: String, } +impl AppMachine { + fn error>(inner: AppInner, string: S) -> Self { + AppMachine { + inner, + state: AppError { + string: string.into(), + }, + } + } +} + +impl From> for App { + fn from(value: AppMachine) -> Self { + AppState::Error(value) + } +} + pub struct AppCritical { string: String, } +impl AppMachine { + fn critical>(inner: AppInner, string: S) -> Self { + AppMachine { + inner, + state: AppCritical { + string: string.into(), + }, + } + } +} + +impl From> for App { + fn from(value: AppMachine) -> Self { + AppState::Critical(value) + } +} + impl App { pub fn new(mut music_hoard: MH) -> Self { let init_result = Self::init(&mut music_hoard); @@ -66,16 +164,8 @@ impl App { selection, }; match init_result { - Ok(()) => Self::Browse(AppMachine { - inner, - state: AppBrowse, - }), - Err(err) => Self::Critical(AppMachine { - inner, - state: AppCritical { - string: err.to_string(), - }, - }), + Ok(()) => AppMachine::browse(inner).into(), + Err(err) => AppMachine::critical(inner, err.to_string()).into(), } } @@ -137,53 +227,42 @@ impl IAppInteractBrowse for AppMachine { match self.inner.music_hoard.save_to_database() { Ok(_) => { self.inner.running = false; - AppState::Browse(self) + self.into() } - Err(err) => AppState::Error(AppMachine { - inner: self.inner, - state: AppError { - string: err.to_string(), - }, - }), + Err(err) => AppMachine::error(self.inner, err.to_string()).into(), } } fn increment_category(mut self) -> Self::APP { self.inner.selection.increment_category(); - AppState::Browse(self) + self.into() } fn decrement_category(mut self) -> Self::APP { self.inner.selection.decrement_category(); - AppState::Browse(self) + self.into() } fn increment_selection(mut self, delta: Delta) -> Self::APP { self.inner .selection .increment_selection(self.inner.music_hoard.get_collection(), delta); - AppState::Browse(self) + self.into() } fn decrement_selection(mut self, delta: Delta) -> Self::APP { self.inner .selection .decrement_selection(self.inner.music_hoard.get_collection(), delta); - AppState::Browse(self) + self.into() } fn show_info_overlay(self) -> Self::APP { - AppState::Info(AppMachine { - inner: self.inner, - state: AppInfo, - }) + AppMachine::info(self.inner).into() } fn show_reload_menu(self) -> Self::APP { - AppState::Reload(AppMachine { - inner: self.inner, - state: AppReload, - }) + AppMachine::reload(self.inner).into() } fn begin_search(mut self) -> Self::APP { @@ -191,18 +270,11 @@ impl IAppInteractBrowse for AppMachine { self.inner .selection .reset_artist(self.inner.music_hoard.get_collection()); - AppState::Search(AppMachine { - inner: self.inner, - state: AppSearch { - string: String::new(), - orig, - memo: vec![], - }, - }) + AppMachine::search(self.inner, orig).into() } fn no_op(self) -> Self::APP { - AppState::Browse(self) + self.into() } } @@ -210,14 +282,11 @@ impl IAppInteractInfo for AppMachine { type APP = App; fn hide_info_overlay(self) -> Self::APP { - AppState::Browse(AppMachine { - inner: self.inner, - state: AppBrowse, - }) + AppMachine::browse(self.inner).into() } fn no_op(self) -> Self::APP { - AppState::Info(self) + self.into() } } @@ -243,14 +312,11 @@ impl IAppInteractReload for AppMachine { } fn hide_reload_menu(self) -> Self::APP { - AppState::Browse(AppMachine { - inner: self.inner, - state: AppBrowse, - }) + AppMachine::browse(self.inner).into() } fn no_op(self) -> Self::APP { - AppState::Reload(self) + self.into() } } @@ -265,17 +331,9 @@ impl IAppInteractReloadPrivate for AppMachine AppState::Error(AppMachine { - inner: self.inner, - state: AppError { - string: err.to_string(), - }, - }), + Err(err) => AppMachine::error(self.inner, err.to_string()).into(), } } } @@ -291,7 +349,7 @@ impl IAppInteractSearch for AppMachine { .selection .incremental_artist_search(collection, &self.state.string, false); self.state.memo.push(AppSearchMemo { index, char: true }); - AppState::Search(self) + self.into() } fn search_next(mut self) -> Self::APP { @@ -304,7 +362,7 @@ impl IAppInteractSearch for AppMachine { ); self.state.memo.push(AppSearchMemo { index, char: false }); } - AppState::Search(self) + self.into() } fn step_back(mut self) -> Self::APP { @@ -315,26 +373,20 @@ impl IAppInteractSearch for AppMachine { } self.inner.selection.select_artist(collection, memo.index); } - AppState::Search(self) + self.into() } fn finish_search(self) -> Self::APP { - AppState::Browse(AppMachine { - inner: self.inner, - state: AppBrowse, - }) + AppMachine::browse(self.inner).into() } fn cancel_search(mut self) -> Self::APP { self.inner.selection.select_by_list(self.state.orig); - AppState::Browse(AppMachine { - inner: self.inner, - state: AppBrowse, - }) + AppMachine::browse(self.inner).into() } fn no_op(self) -> Self::APP { - AppState::Search(self) + self.into() } } @@ -342,10 +394,7 @@ impl IAppInteractError for AppMachine { type APP = App; fn dismiss_error(self) -> Self::APP { - AppState::Browse(AppMachine { - inner: self.inner, - state: AppBrowse, - }) + AppMachine::browse(self.inner).into() } } @@ -353,7 +402,7 @@ impl IAppInteractCritical for AppMachine { type APP = App; fn no_op(self) -> Self::APP { - AppState::Critical(self) + self.into() } } @@ -505,12 +554,10 @@ mod tests { let app = App::new(music_hoard); assert!(app.is_running()); - let app = App::Error(AppMachine { - inner: app.unwrap_browse().inner, - state: AppError { - string: String::from("get rekt"), - }, - }); + let app = App::Error(AppMachine::error( + app.unwrap_browse().inner, + String::from("get rekt"), + )); let error = app.unwrap_error(); @@ -534,12 +581,10 @@ mod tests { let app = App::new(music_hoard(COLLECTION.to_owned())); assert!(app.is_running()); - let app = App::Error(AppMachine { - inner: app.unwrap_browse().inner, - state: AppError { - string: String::from("get rekt"), - }, - }); + let app = App::Error(AppMachine::error( + app.unwrap_browse().inner, + String::from("get rekt"), + )); let app = app.force_quit(); assert!(!app.is_running()); -- 2.45.2 From 954199b0011f41834cbf7ef31567cc4661e0d7f9 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 18 Feb 2024 01:39:18 +0100 Subject: [PATCH 23/35] Split code for states --- src/tui/app/app.rs | 1131 +-------------------------------- src/tui/app/mod.rs | 1 + src/tui/app/state/browse.rs | 108 ++++ src/tui/app/state/critical.rs | 46 ++ src/tui/app/state/error.rs | 46 ++ src/tui/app/state/info.rs | 46 ++ src/tui/app/state/mod.rs | 778 +++++++++++++++++++++++ src/tui/app/state/reload.rs | 82 +++ src/tui/app/state/search.rs | 100 +++ 9 files changed, 1236 insertions(+), 1102 deletions(-) create mode 100644 src/tui/app/state/browse.rs create mode 100644 src/tui/app/state/critical.rs create mode 100644 src/tui/app/state/error.rs create mode 100644 src/tui/app/state/info.rs create mode 100644 src/tui/app/state/mod.rs create mode 100644 src/tui/app/state/reload.rs create mode 100644 src/tui/app/state/search.rs diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index e3a4928..5b0d8bb 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -1,22 +1,16 @@ -// FIXME: Split file apart #![allow(clippy::module_inception)] use crate::tui::{ app::{ - selection::{Delta, IdSelection, ListSelection, Selection}, - AppPublic, AppPublicInner, AppState, IAppAccess, IAppInteract, IAppInteractBrowse, - IAppInteractCritical, IAppInteractError, IAppInteractInfo, IAppInteractReload, - IAppInteractSearch, + state::{ + browse::AppBrowse, critical::AppCritical, error::AppError, info::AppInfo, + reload::AppReload, search::AppSearch, AppInner, AppMachine, + }, + AppPublic, AppState, IAppAccess, IAppInteract, }, lib::IMusicHoard, }; -struct AppInner { - running: bool, - music_hoard: MH, - selection: Selection, -} - pub type App = AppState< AppMachine, AppMachine, @@ -26,174 +20,33 @@ pub type App = AppState< AppMachine, >; -pub struct AppMachine { - inner: AppInner, - state: STATE, -} - -pub struct AppBrowse; - -impl AppMachine { - fn browse(inner: AppInner) -> Self { - AppMachine { - inner, - state: AppBrowse, - } - } -} - -impl From> for App { - fn from(value: AppMachine) -> Self { - AppState::Browse(value) - } -} - -pub struct AppInfo; - -impl AppMachine { - fn info(inner: AppInner) -> Self { - AppMachine { - inner, - state: AppInfo, - } - } -} - -impl From> for App { - fn from(value: AppMachine) -> Self { - AppState::Info(value) - } -} - -pub struct AppReload; - -impl AppMachine { - fn reload(inner: AppInner) -> Self { - AppMachine { - inner, - state: AppReload, - } - } -} - -impl From> for App { - fn from(value: AppMachine) -> Self { - AppState::Reload(value) - } -} - -pub struct AppSearch { - string: String, - orig: ListSelection, - memo: Vec, -} - -struct AppSearchMemo { - index: Option, - char: bool, -} - -impl AppMachine { - fn search(inner: AppInner, orig: ListSelection) -> Self { - AppMachine { - inner, - state: AppSearch { - string: String::new(), - orig, - memo: vec![], - }, - } - } -} - -impl From> for App { - fn from(value: AppMachine) -> Self { - AppState::Search(value) - } -} - -pub struct AppError { - string: String, -} - -impl AppMachine { - fn error>(inner: AppInner, string: S) -> Self { - AppMachine { - inner, - state: AppError { - string: string.into(), - }, - } - } -} - -impl From> for App { - fn from(value: AppMachine) -> Self { - AppState::Error(value) - } -} - -pub struct AppCritical { - string: String, -} - -impl AppMachine { - fn critical>(inner: AppInner, string: S) -> Self { - AppMachine { - inner, - state: AppCritical { - string: string.into(), - }, - } - } -} - -impl From> for App { - fn from(value: AppMachine) -> Self { - AppState::Critical(value) - } -} - impl App { - pub fn new(mut music_hoard: MH) -> Self { - let init_result = Self::init(&mut music_hoard); - let selection = Selection::new(music_hoard.get_collection()); - let inner = AppInner { - running: true, - music_hoard, - selection, - }; - match init_result { - Ok(()) => AppMachine::browse(inner).into(), - Err(err) => AppMachine::critical(inner, err.to_string()).into(), + pub fn new(music_hoard: MH) -> Self { + match AppMachine::new(music_hoard) { + Ok(browse) => browse.into(), + Err(critical) => critical.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 { 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, + 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 { 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, + 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(), } } } @@ -207,11 +60,11 @@ impl IAppInteract for App { type CS = AppMachine; fn is_running(&self) -> bool { - self.inner_ref().running + self.inner_ref().is_running() } fn force_quit(mut self) -> Self { - self.inner_mut().running = false; + self.inner_mut().stop(); self } @@ -220,941 +73,15 @@ impl IAppInteract for App { } } -impl IAppInteractBrowse for AppMachine { - type APP = App; - - fn save_and_quit(mut self) -> Self::APP { - match self.inner.music_hoard.save_to_database() { - Ok(_) => { - self.inner.running = false; - self.into() - } - Err(err) => AppMachine::error(self.inner, err.to_string()).into(), - } - } - - fn increment_category(mut self) -> Self::APP { - self.inner.selection.increment_category(); - self.into() - } - - fn decrement_category(mut self) -> Self::APP { - self.inner.selection.decrement_category(); - self.into() - } - - fn increment_selection(mut self, delta: Delta) -> Self::APP { - self.inner - .selection - .increment_selection(self.inner.music_hoard.get_collection(), delta); - self.into() - } - - fn decrement_selection(mut self, delta: Delta) -> Self::APP { - self.inner - .selection - .decrement_selection(self.inner.music_hoard.get_collection(), delta); - self.into() - } - - fn show_info_overlay(self) -> Self::APP { - AppMachine::info(self.inner).into() - } - - fn show_reload_menu(self) -> Self::APP { - AppMachine::reload(self.inner).into() - } - - fn begin_search(mut self) -> Self::APP { - let orig = ListSelection::get(&self.inner.selection); - self.inner - .selection - .reset_artist(self.inner.music_hoard.get_collection()); - AppMachine::search(self.inner, orig).into() - } - - fn no_op(self) -> Self::APP { - self.into() - } -} - -impl IAppInteractInfo for AppMachine { - type APP = App; - - fn hide_info_overlay(self) -> Self::APP { - AppMachine::browse(self.inner).into() - } - - fn no_op(self) -> Self::APP { - self.into() - } -} - -impl IAppInteractReload for AppMachine { - type APP = App; - - fn reload_library(mut self) -> Self::APP { - let previous = IdSelection::get( - self.inner.music_hoard.get_collection(), - &self.inner.selection, - ); - let result = self.inner.music_hoard.rescan_library(); - self.refresh(previous, result) - } - - fn reload_database(mut self) -> Self::APP { - let previous = IdSelection::get( - self.inner.music_hoard.get_collection(), - &self.inner.selection, - ); - let result = self.inner.music_hoard.load_from_database(); - self.refresh(previous, result) - } - - fn hide_reload_menu(self) -> Self::APP { - AppMachine::browse(self.inner).into() - } - - fn no_op(self) -> Self::APP { - self.into() - } -} - -trait IAppInteractReloadPrivate { - fn refresh(self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App; -} - -impl IAppInteractReloadPrivate for AppMachine { - fn refresh(mut self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App { - match result { - Ok(()) => { - self.inner - .selection - .select_by_id(self.inner.music_hoard.get_collection(), previous); - AppMachine::browse(self.inner).into() - } - Err(err) => AppMachine::error(self.inner, err.to_string()).into(), - } - } -} - -impl IAppInteractSearch for AppMachine { - type APP = App; - - fn append_character(mut self, ch: char) -> Self::APP { - let collection = self.inner.music_hoard.get_collection(); - self.state.string.push(ch); - let index = - self.inner - .selection - .incremental_artist_search(collection, &self.state.string, false); - self.state.memo.push(AppSearchMemo { index, char: true }); - self.into() - } - - fn search_next(mut self) -> Self::APP { - let collection = self.inner.music_hoard.get_collection(); - if !self.state.string.is_empty() { - let index = self.inner.selection.incremental_artist_search( - collection, - &self.state.string, - true, - ); - self.state.memo.push(AppSearchMemo { index, char: false }); - } - self.into() - } - - fn step_back(mut self) -> Self::APP { - let collection = self.inner.music_hoard.get_collection(); - if let Some(memo) = self.state.memo.pop() { - if memo.char { - self.state.string.pop(); - } - self.inner.selection.select_artist(collection, memo.index); - } - self.into() - } - - fn finish_search(self) -> Self::APP { - AppMachine::browse(self.inner).into() - } - - fn cancel_search(mut self) -> Self::APP { - self.inner.selection.select_by_list(self.state.orig); - AppMachine::browse(self.inner).into() - } - - fn no_op(self) -> Self::APP { - self.into() - } -} - -impl IAppInteractError for AppMachine { - type APP = App; - - fn dismiss_error(self) -> Self::APP { - AppMachine::browse(self.inner).into() - } -} - -impl IAppInteractCritical for AppMachine { - type APP = App; - - fn no_op(self) -> Self::APP { - self.into() - } -} - impl IAppAccess for App { fn get(&mut self) -> AppPublic { match self { - AppState::Browse(generic) => AppPublic { - inner: (&mut generic.inner).into(), - state: AppState::Browse(()), - }, - AppState::Info(generic) => AppPublic { - inner: (&mut generic.inner).into(), - state: AppState::Info(()), - }, - AppState::Reload(generic) => AppPublic { - inner: (&mut generic.inner).into(), - state: AppState::Reload(()), - }, - AppState::Search(search) => AppPublic { - inner: (&mut search.inner).into(), - state: AppState::Search(&search.state.string), - }, - AppState::Error(error) => AppPublic { - inner: (&mut error.inner).into(), - state: AppState::Error(&error.state.string), - }, - AppState::Critical(critical) => AppPublic { - inner: (&mut critical.inner).into(), - state: AppState::Error(&critical.state.string), - }, + 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<'app, MH: IMusicHoard> From<&'app mut AppInner> for AppPublicInner<'app> { - fn from(inner: &'app mut AppInner) -> Self { - AppPublicInner { - collection: inner.music_hoard.get_collection(), - selection: &mut inner.selection, - } - } -} - -#[cfg(test)] -mod tests { - use musichoard::collection::Collection; - - use crate::tui::{ - app::{selection::Category, AppPublicState}, - lib::MockIMusicHoard, - testmod::COLLECTION, - }; - - use super::*; - - impl AppState { - fn unwrap_browse(self) -> BS { - match self { - AppState::Browse(browse) => browse, - _ => panic!(), - } - } - - fn unwrap_info(self) -> IS { - match self { - AppState::Info(info) => info, - _ => panic!(), - } - } - - fn unwrap_reload(self) -> RS { - match self { - AppState::Reload(reload) => reload, - _ => panic!(), - } - } - - fn unwrap_search(self) -> SS { - match self { - AppState::Search(search) => search, - _ => panic!(), - } - } - - fn unwrap_error(self) -> ES { - match self { - AppState::Error(error) => error, - _ => panic!(), - } - } - - fn unwrap_critical(self) -> CS { - match self { - AppState::Critical(critical) => critical, - _ => panic!(), - } - } - } - - fn music_hoard(collection: Collection) -> MockIMusicHoard { - let mut music_hoard = MockIMusicHoard::new(); - - music_hoard - .expect_load_from_database() - .times(1) - .return_once(|| Ok(())); - music_hoard - .expect_rescan_library() - .times(1) - .return_once(|| Ok(())); - music_hoard.expect_get_collection().return_const(collection); - - music_hoard - } - - #[test] - fn app_is_state() { - let state = AppPublicState::Search("get rekt"); - assert!(state.is_search()); - } - - #[test] - fn running_quit() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); - - music_hoard - .expect_save_to_database() - .times(1) - .return_once(|| Ok(())); - - let app = App::new(music_hoard); - assert!(app.is_running()); - - let browse = app.unwrap_browse(); - - let app = browse.save_and_quit(); - assert!(!app.is_running()); - } - - #[test] - fn error_quit() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); - - music_hoard - .expect_save_to_database() - .times(1) - .return_once(|| Ok(())); - - let app = App::new(music_hoard); - assert!(app.is_running()); - - let app = App::Error(AppMachine::error( - app.unwrap_browse().inner, - String::from("get rekt"), - )); - - let error = app.unwrap_error(); - - let browse = error.dismiss_error().unwrap_browse(); - - let app = browse.save_and_quit(); - assert!(!app.is_running()); - } - - #[test] - fn running_force_quit() { - let app = App::new(music_hoard(COLLECTION.to_owned())); - assert!(app.is_running()); - - let app = app.force_quit(); - assert!(!app.is_running()); - } - - #[test] - fn error_force_quit() { - let app = App::new(music_hoard(COLLECTION.to_owned())); - assert!(app.is_running()); - - let app = App::Error(AppMachine::error( - app.unwrap_browse().inner, - String::from("get rekt"), - )); - - let app = app.force_quit(); - assert!(!app.is_running()); - } - - #[test] - fn save() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); - - music_hoard - .expect_save_to_database() - .times(1) - .return_once(|| Ok(())); - - let browse = App::new(music_hoard).unwrap_browse(); - - browse.save_and_quit().unwrap_browse(); - } - - #[test] - fn save_error() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); - - music_hoard - .expect_save_to_database() - .times(1) - .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); - - let browse = App::new(music_hoard).unwrap_browse(); - - browse.save_and_quit().unwrap_error(); - } - - #[test] - fn init_error() { - let mut music_hoard = MockIMusicHoard::new(); - - music_hoard - .expect_load_from_database() - .times(1) - .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); - music_hoard.expect_get_collection().return_const(vec![]); - - let app = App::new(music_hoard); - - assert!(app.is_running()); - app.unwrap_critical(); - } - - #[test] - fn modifiers() { - let app = App::new(music_hoard(COLLECTION.to_owned())); - assert!(app.is_running()); - - let browse = app.unwrap_browse(); - - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_category().unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_category().unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); - - let browse = browse.increment_category().unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.decrement_category().unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.decrement_category().unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_category().unwrap_browse(); - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.decrement_category().unwrap_browse(); - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let browse = browse.decrement_category().unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - } - - #[test] - fn no_tracks() { - let mut collection = COLLECTION.to_owned(); - collection[0].albums[0].tracks = vec![]; - - let app = App::new(music_hoard(collection)); - assert!(app.is_running()); - - let browse = app.unwrap_browse(); - - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.increment_category().unwrap_browse(); - let browse = browse.increment_category().unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - } - - #[test] - fn no_albums() { - let mut collection = COLLECTION.to_owned(); - collection[0].albums = vec![]; - - let app = App::new(music_hoard(collection)); - assert!(app.is_running()); - - let browse = app.unwrap_browse(); - - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.increment_category().unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.increment_category().unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - } - - #[test] - fn no_artists() { - let app = App::new(music_hoard(vec![])); - assert!(app.is_running()); - - let browse = app.unwrap_browse(); - - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.increment_category().unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.increment_category().unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - } - - #[test] - fn info_overlay() { - let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - - let info = browse.show_info_overlay().unwrap_info(); - - info.hide_info_overlay().unwrap_browse(); - } - - #[test] - fn reload_hide_menu() { - let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - - let reload = browse.show_reload_menu().unwrap_reload(); - - reload.hide_reload_menu().unwrap_browse(); - } - - #[test] - fn reload_database() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); - - music_hoard - .expect_load_from_database() - .times(1) - .return_once(|| Ok(())); - - let browse = App::new(music_hoard).unwrap_browse(); - - let reload = browse.show_reload_menu().unwrap_reload(); - - reload.reload_database().unwrap_browse(); - } - - #[test] - fn reload_library() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); - - music_hoard - .expect_rescan_library() - .times(1) - .return_once(|| Ok(())); - - let browse = App::new(music_hoard).unwrap_browse(); - - let reload = browse.show_reload_menu().unwrap_reload(); - - reload.reload_library().unwrap_browse(); - } - - #[test] - fn reload_error() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); - - music_hoard - .expect_load_from_database() - .times(1) - .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); - - let browse = App::new(music_hoard).unwrap_browse(); - - let reload = browse.show_reload_menu().unwrap_reload(); - - let error = reload.reload_database().unwrap_error(); - - error.dismiss_error().unwrap_browse(); - } - - #[test] - fn search() { - let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.increment_category().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); - - let search = browse.begin_search().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.append_character('a').unwrap_search(); - let search = search.append_character('l').unwrap_search(); - let search = search.append_character('b').unwrap_search(); - let search = search.append_character('u').unwrap_search(); - let search = search.append_character('m').unwrap_search(); - let search = search.append_character('_').unwrap_search(); - let search = search.append_character('a').unwrap_search(); - let search = search.append_character('r').unwrap_search(); - let search = search.append_character('t').unwrap_search(); - let search = search.append_character('i').unwrap_search(); - let search = search.append_character('s').unwrap_search(); - let search = search.append_character('t').unwrap_search(); - let search = search.append_character(' ').unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.append_character('\'').unwrap_search(); - let search = search.append_character('c').unwrap_search(); - let search = search.append_character('\'').unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - - let search = search.step_back().unwrap_search(); - let search = search.step_back().unwrap_search(); - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.append_character('\'').unwrap_search(); - let search = search.append_character('b').unwrap_search(); - let search = search.append_character('\'').unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); - - let browse = search.finish_search().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); - } - - #[test] - fn search_next() { - let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.increment_category().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); - - let search = browse.begin_search().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.append_character('a').unwrap_search(); - - let search = search.search_next().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); - - let search = search.search_next().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - - let search = search.search_next().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); - - let search = search.search_next().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - } - - #[test] - fn cancel_search() { - let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.increment_category().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); - - let search = browse.begin_search().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.append_character('a').unwrap_search(); - let search = search.append_character('l').unwrap_search(); - let search = search.append_character('b').unwrap_search(); - let search = search.append_character('u').unwrap_search(); - let search = search.append_character('m').unwrap_search(); - let search = search.append_character('_').unwrap_search(); - let search = search.append_character('a').unwrap_search(); - let search = search.append_character('r').unwrap_search(); - let search = search.append_character('t').unwrap_search(); - let search = search.append_character('i').unwrap_search(); - let search = search.append_character('s').unwrap_search(); - let search = search.append_character('t').unwrap_search(); - let search = search.append_character(' ').unwrap_search(); - let search = search.append_character('\'').unwrap_search(); - let search = search.append_character('c').unwrap_search(); - let search = search.append_character('\'').unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - - let browse = search.cancel_search().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); - } - - #[test] - fn empty_search() { - let browse = App::new(music_hoard(vec![])).unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.increment_category().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), None); - - let search = browse.begin_search().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), None); - - let search = search.append_character('a').unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), None); - - let search = search.search_next().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), None); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), None); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), None); - - let browse = search.cancel_search().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), None); - } -} diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index d0f4caf..53bff54 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -1,5 +1,6 @@ pub mod app; pub mod selection; +mod state; use musichoard::collection::Collection; diff --git a/src/tui/app/state/browse.rs b/src/tui/app/state/browse.rs new file mode 100644 index 0000000..5b572d5 --- /dev/null +++ b/src/tui/app/state/browse.rs @@ -0,0 +1,108 @@ +use crate::tui::{ + app::{ + app::App, + selection::{Delta, ListSelection}, + state::{critical::AppCritical, AppInner, AppMachine}, + AppPublic, AppState, IAppInteractBrowse, + }, + lib::IMusicHoard, +}; + +pub struct AppBrowse; + +impl AppMachine { + pub fn browse(inner: AppInner) -> Self { + AppMachine { + inner, + state: AppBrowse, + } + } + + pub fn new(mut music_hoard: MH) -> Result> { + let init_result = Self::init(&mut music_hoard); + let inner = AppInner::new(music_hoard); + match init_result { + Ok(()) => Ok(AppMachine::browse(inner)), + Err(err) => Err(AppMachine::critical(inner, err.to_string())), + } + } + + fn init(music_hoard: &mut MH) -> Result<(), musichoard::Error> { + music_hoard.load_from_database()?; + music_hoard.rescan_library()?; + Ok(()) + } +} + +impl From> for App { + fn from(machine: AppMachine) -> Self { + AppState::Browse(machine) + } +} + +impl<'a, MH: IMusicHoard> From<&'a mut AppMachine> for AppPublic<'a> { + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: AppState::Browse(()), + } + } +} + +impl IAppInteractBrowse for AppMachine { + type APP = App; + + fn save_and_quit(mut self) -> Self::APP { + match self.inner.music_hoard.save_to_database() { + Ok(_) => { + self.inner.running = false; + self.into() + } + Err(err) => AppMachine::error(self.inner, err.to_string()).into(), + } + } + + fn increment_category(mut self) -> Self::APP { + self.inner.selection.increment_category(); + self.into() + } + + fn decrement_category(mut self) -> Self::APP { + self.inner.selection.decrement_category(); + self.into() + } + + fn increment_selection(mut self, delta: Delta) -> Self::APP { + self.inner + .selection + .increment_selection(self.inner.music_hoard.get_collection(), delta); + self.into() + } + + fn decrement_selection(mut self, delta: Delta) -> Self::APP { + self.inner + .selection + .decrement_selection(self.inner.music_hoard.get_collection(), delta); + self.into() + } + + fn show_info_overlay(self) -> Self::APP { + AppMachine::info(self.inner).into() + } + + fn show_reload_menu(self) -> Self::APP { + AppMachine::reload(self.inner).into() + } + + fn begin_search(mut self) -> Self::APP { + let orig = ListSelection::get(&self.inner.selection); + self.inner + .selection + .reset_artist(self.inner.music_hoard.get_collection()); + AppMachine::search(self.inner, orig).into() + } + + fn no_op(self) -> Self::APP { + self.into() + } +} diff --git a/src/tui/app/state/critical.rs b/src/tui/app/state/critical.rs new file mode 100644 index 0000000..7149df6 --- /dev/null +++ b/src/tui/app/state/critical.rs @@ -0,0 +1,46 @@ +use crate::tui::{ + app::{ + app::App, + state::{AppInner, AppMachine}, + AppPublic, AppState, IAppInteractCritical, + }, + lib::IMusicHoard, +}; + +pub struct AppCritical { + string: String, +} + +impl AppMachine { + pub fn critical>(inner: AppInner, string: S) -> Self { + AppMachine { + inner, + state: AppCritical { + string: string.into(), + }, + } + } +} + +impl From> for App { + fn from(machine: AppMachine) -> Self { + AppState::Critical(machine) + } +} + +impl<'a, MH: IMusicHoard> From<&'a mut AppMachine> for AppPublic<'a> { + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: AppState::Critical(&machine.state.string), + } + } +} + +impl IAppInteractCritical for AppMachine { + type APP = App; + + fn no_op(self) -> Self::APP { + self.into() + } +} diff --git a/src/tui/app/state/error.rs b/src/tui/app/state/error.rs new file mode 100644 index 0000000..b0b0fdd --- /dev/null +++ b/src/tui/app/state/error.rs @@ -0,0 +1,46 @@ +use crate::tui::{ + app::{ + app::App, + state::{AppInner, AppMachine}, + AppPublic, AppState, IAppInteractError, + }, + lib::IMusicHoard, +}; + +pub struct AppError { + string: String, +} + +impl AppMachine { + pub fn error>(inner: AppInner, string: S) -> Self { + AppMachine { + inner, + state: AppError { + string: string.into(), + }, + } + } +} + +impl From> for App { + fn from(machine: AppMachine) -> Self { + AppState::Error(machine) + } +} + +impl<'a, MH: IMusicHoard> From<&'a mut AppMachine> for AppPublic<'a> { + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: AppState::Error(&machine.state.string), + } + } +} + +impl IAppInteractError for AppMachine { + type APP = App; + + fn dismiss_error(self) -> Self::APP { + AppMachine::browse(self.inner).into() + } +} diff --git a/src/tui/app/state/info.rs b/src/tui/app/state/info.rs new file mode 100644 index 0000000..2bd0e0c --- /dev/null +++ b/src/tui/app/state/info.rs @@ -0,0 +1,46 @@ +use crate::tui::{ + app::{ + app::App, + state::{AppInner, AppMachine}, + AppPublic, AppState, IAppInteractInfo, + }, + lib::IMusicHoard, +}; + +pub struct AppInfo; + +impl AppMachine { + pub fn info(inner: AppInner) -> Self { + AppMachine { + inner, + state: AppInfo, + } + } +} + +impl From> for App { + fn from(machine: AppMachine) -> Self { + AppState::Info(machine) + } +} + +impl<'a, MH: IMusicHoard> From<&'a mut AppMachine> for AppPublic<'a> { + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: AppState::Info(()), + } + } +} + +impl IAppInteractInfo for AppMachine { + type APP = App; + + fn hide_info_overlay(self) -> Self::APP { + AppMachine::browse(self.inner).into() + } + + fn no_op(self) -> Self::APP { + self.into() + } +} diff --git a/src/tui/app/state/mod.rs b/src/tui/app/state/mod.rs new file mode 100644 index 0000000..45eaf49 --- /dev/null +++ b/src/tui/app/state/mod.rs @@ -0,0 +1,778 @@ +pub mod browse; +pub mod critical; +pub mod error; +pub mod info; +pub mod reload; +pub mod search; + +use crate::tui::{ + app::{selection::Selection, AppPublicInner}, + lib::IMusicHoard, +}; + +pub struct AppMachine { + inner: AppInner, + state: STATE, +} + +impl AppMachine { + pub fn inner_ref(&self) -> &AppInner { + &self.inner + } + + pub fn inner_mut(&mut self) -> &mut AppInner { + &mut self.inner + } +} + +pub struct AppInner { + running: bool, + music_hoard: MH, + selection: Selection, +} + +impl AppInner { + pub fn new(music_hoard: MH) -> Self { + let selection = Selection::new(music_hoard.get_collection()); + AppInner { + running: true, + music_hoard, + 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> for AppPublicInner<'a> { + fn from(inner: &'a mut AppInner) -> Self { + AppPublicInner { + collection: inner.music_hoard.get_collection(), + selection: &mut inner.selection, + } + } +} + +#[cfg(test)] +mod tests { + use musichoard::collection::Collection; + + use crate::tui::{ + app::{ + app::App, + selection::{Category, Delta}, + AppPublicState, AppState, IAppInteract, IAppInteractBrowse, IAppInteractError, + IAppInteractInfo, IAppInteractReload, IAppInteractSearch, + }, + lib::MockIMusicHoard, + testmod::COLLECTION, + }; + + use super::*; + + impl AppState { + fn unwrap_browse(self) -> BS { + match self { + AppState::Browse(browse) => browse, + _ => panic!(), + } + } + + fn unwrap_info(self) -> IS { + match self { + AppState::Info(info) => info, + _ => panic!(), + } + } + + fn unwrap_reload(self) -> RS { + match self { + AppState::Reload(reload) => reload, + _ => panic!(), + } + } + + fn unwrap_search(self) -> SS { + match self { + AppState::Search(search) => search, + _ => panic!(), + } + } + + fn unwrap_error(self) -> ES { + match self { + AppState::Error(error) => error, + _ => panic!(), + } + } + + fn unwrap_critical(self) -> CS { + match self { + AppState::Critical(critical) => critical, + _ => panic!(), + } + } + } + + fn music_hoard(collection: Collection) -> MockIMusicHoard { + let mut music_hoard = MockIMusicHoard::new(); + + music_hoard + .expect_load_from_database() + .times(1) + .return_once(|| Ok(())); + music_hoard + .expect_rescan_library() + .times(1) + .return_once(|| Ok(())); + music_hoard.expect_get_collection().return_const(collection); + + music_hoard + } + + #[test] + fn app_is_state() { + let state = AppPublicState::Search("get rekt"); + assert!(state.is_search()); + } + + #[test] + fn running_quit() { + let mut music_hoard = music_hoard(COLLECTION.to_owned()); + + music_hoard + .expect_save_to_database() + .times(1) + .return_once(|| Ok(())); + + let app = App::new(music_hoard); + assert!(app.is_running()); + + let browse = app.unwrap_browse(); + + let app = browse.save_and_quit(); + assert!(!app.is_running()); + } + + #[test] + fn error_quit() { + let mut music_hoard = music_hoard(COLLECTION.to_owned()); + + music_hoard + .expect_save_to_database() + .times(1) + .return_once(|| Ok(())); + + let app = App::new(music_hoard); + assert!(app.is_running()); + + let app = App::Error(AppMachine::error( + app.unwrap_browse().inner, + String::from("get rekt"), + )); + + let error = app.unwrap_error(); + + let browse = error.dismiss_error().unwrap_browse(); + + let app = browse.save_and_quit(); + assert!(!app.is_running()); + } + + #[test] + fn running_force_quit() { + let app = App::new(music_hoard(COLLECTION.to_owned())); + assert!(app.is_running()); + + let app = app.force_quit(); + assert!(!app.is_running()); + } + + #[test] + fn error_force_quit() { + let app = App::new(music_hoard(COLLECTION.to_owned())); + assert!(app.is_running()); + + let app = App::Error(AppMachine::error( + app.unwrap_browse().inner, + String::from("get rekt"), + )); + + let app = app.force_quit(); + assert!(!app.is_running()); + } + + #[test] + fn save() { + let mut music_hoard = music_hoard(COLLECTION.to_owned()); + + music_hoard + .expect_save_to_database() + .times(1) + .return_once(|| Ok(())); + + let browse = App::new(music_hoard).unwrap_browse(); + + browse.save_and_quit().unwrap_browse(); + } + + #[test] + fn save_error() { + let mut music_hoard = music_hoard(COLLECTION.to_owned()); + + music_hoard + .expect_save_to_database() + .times(1) + .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); + + let browse = App::new(music_hoard).unwrap_browse(); + + browse.save_and_quit().unwrap_error(); + } + + #[test] + fn init_error() { + let mut music_hoard = MockIMusicHoard::new(); + + music_hoard + .expect_load_from_database() + .times(1) + .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); + music_hoard.expect_get_collection().return_const(vec![]); + + let app = App::new(music_hoard); + + assert!(app.is_running()); + app.unwrap_critical(); + } + + #[test] + fn modifiers() { + let app = App::new(music_hoard(COLLECTION.to_owned())); + assert!(app.is_running()); + + let browse = app.unwrap_browse(); + + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + let browse = browse.increment_category().unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + let browse = browse.increment_category().unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); + + let browse = browse.increment_category().unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.decrement_category().unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.decrement_category().unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + let browse = browse.increment_category().unwrap_browse(); + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.decrement_category().unwrap_browse(); + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let browse = browse.decrement_category().unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + } + + #[test] + fn no_tracks() { + let mut collection = COLLECTION.to_owned(); + collection[0].albums[0].tracks = vec![]; + + let app = App::new(music_hoard(collection)); + assert!(app.is_running()); + + let browse = app.unwrap_browse(); + + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + + let browse = browse.increment_category().unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + } + + #[test] + fn no_albums() { + let mut collection = COLLECTION.to_owned(); + collection[0].albums = vec![]; + + let app = App::new(music_hoard(collection)); + assert!(app.is_running()); + + let browse = app.unwrap_browse(); + + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + + let browse = browse.increment_category().unwrap_browse(); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + + let browse = browse.increment_category().unwrap_browse(); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + } + + #[test] + fn no_artists() { + let app = App::new(music_hoard(vec![])); + assert!(app.is_running()); + + let browse = app.unwrap_browse(); + + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + + let browse = browse.increment_category().unwrap_browse(); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + + let browse = browse.increment_category().unwrap_browse(); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + + let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let selection = &browse.inner.selection; + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), None); + assert_eq!(selection.artist.album.state.list.selected(), None); + assert_eq!(selection.artist.album.track.state.list.selected(), None); + } + + #[test] + fn info_overlay() { + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); + + let info = browse.show_info_overlay().unwrap_info(); + + info.hide_info_overlay().unwrap_browse(); + } + + #[test] + fn reload_hide_menu() { + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); + + let reload = browse.show_reload_menu().unwrap_reload(); + + reload.hide_reload_menu().unwrap_browse(); + } + + #[test] + fn reload_database() { + let mut music_hoard = music_hoard(COLLECTION.to_owned()); + + music_hoard + .expect_load_from_database() + .times(1) + .return_once(|| Ok(())); + + let browse = App::new(music_hoard).unwrap_browse(); + + let reload = browse.show_reload_menu().unwrap_reload(); + + reload.reload_database().unwrap_browse(); + } + + #[test] + fn reload_library() { + let mut music_hoard = music_hoard(COLLECTION.to_owned()); + + music_hoard + .expect_rescan_library() + .times(1) + .return_once(|| Ok(())); + + let browse = App::new(music_hoard).unwrap_browse(); + + let reload = browse.show_reload_menu().unwrap_reload(); + + reload.reload_library().unwrap_browse(); + } + + #[test] + fn reload_error() { + let mut music_hoard = music_hoard(COLLECTION.to_owned()); + + music_hoard + .expect_load_from_database() + .times(1) + .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); + + let browse = App::new(music_hoard).unwrap_browse(); + + let reload = browse.show_reload_menu().unwrap_reload(); + + let error = reload.reload_database().unwrap_error(); + + error.dismiss_error().unwrap_browse(); + } + + #[test] + fn search() { + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); + + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); + + let search = browse.begin_search().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('l').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + let search = search.append_character('u').unwrap_search(); + let search = search.append_character('m').unwrap_search(); + let search = search.append_character('_').unwrap_search(); + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('r').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character('i').unwrap_search(); + let search = search.append_character('s').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character(' ').unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.append_character('\'').unwrap_search(); + let search = search.append_character('c').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + + let search = search.step_back().unwrap_search(); + let search = search.step_back().unwrap_search(); + let search = search.step_back().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.append_character('\'').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); + + let browse = search.finish_search().unwrap_browse(); + + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); + } + + #[test] + fn search_next() { + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); + + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); + + let search = browse.begin_search().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.step_back().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.append_character('a').unwrap_search(); + + let search = search.search_next().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); + + let search = search.search_next().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + + let search = search.search_next().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); + + let search = search.search_next().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); + + let search = search.step_back().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); + + let search = search.step_back().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + + let search = search.step_back().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); + + let search = search.step_back().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.step_back().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + } + + #[test] + fn cancel_search() { + let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); + + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); + + let search = browse.begin_search().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('l').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + let search = search.append_character('u').unwrap_search(); + let search = search.append_character('m').unwrap_search(); + let search = search.append_character('_').unwrap_search(); + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('r').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character('i').unwrap_search(); + let search = search.append_character('s').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character(' ').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); + let search = search.append_character('c').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + + let browse = search.cancel_search().unwrap_browse(); + + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); + } + + #[test] + fn empty_search() { + let browse = App::new(music_hoard(vec![])).unwrap_browse(); + + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let browse = browse.increment_category().unwrap_browse(); + + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), None); + + let search = browse.begin_search().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); + + let search = search.append_character('a').unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); + + let search = search.search_next().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); + + let search = search.step_back().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); + + let search = search.step_back().unwrap_search(); + + assert_eq!(search.inner.selection.active, Category::Album); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); + + let browse = search.cancel_search().unwrap_browse(); + + assert_eq!(browse.inner.selection.active, Category::Album); + assert_eq!(browse.inner.selection.artist.state.list.selected(), None); + } +} diff --git a/src/tui/app/state/reload.rs b/src/tui/app/state/reload.rs new file mode 100644 index 0000000..e4f37d8 --- /dev/null +++ b/src/tui/app/state/reload.rs @@ -0,0 +1,82 @@ +use crate::tui::{ + app::{ + app::App, + selection::IdSelection, + state::{AppInner, AppMachine}, + AppPublic, AppState, IAppInteractReload, + }, + lib::IMusicHoard, +}; + +pub struct AppReload; + +impl AppMachine { + pub fn reload(inner: AppInner) -> Self { + AppMachine { + inner, + state: AppReload, + } + } +} + +impl From> for App { + fn from(machine: AppMachine) -> Self { + AppState::Reload(machine) + } +} +impl<'a, MH: IMusicHoard> From<&'a mut AppMachine> for AppPublic<'a> { + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: AppState::Reload(()), + } + } +} + +impl IAppInteractReload for AppMachine { + type APP = App; + + fn reload_library(mut self) -> Self::APP { + let previous = IdSelection::get( + self.inner.music_hoard.get_collection(), + &self.inner.selection, + ); + let result = self.inner.music_hoard.rescan_library(); + self.refresh(previous, result) + } + + fn reload_database(mut self) -> Self::APP { + let previous = IdSelection::get( + self.inner.music_hoard.get_collection(), + &self.inner.selection, + ); + let result = self.inner.music_hoard.load_from_database(); + self.refresh(previous, result) + } + + fn hide_reload_menu(self) -> Self::APP { + AppMachine::browse(self.inner).into() + } + + fn no_op(self) -> Self::APP { + self.into() + } +} + +trait IAppInteractReloadPrivate { + fn refresh(self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App; +} + +impl IAppInteractReloadPrivate for AppMachine { + fn refresh(mut self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App { + match result { + Ok(()) => { + self.inner + .selection + .select_by_id(self.inner.music_hoard.get_collection(), previous); + AppMachine::browse(self.inner).into() + } + Err(err) => AppMachine::error(self.inner, err.to_string()).into(), + } + } +} diff --git a/src/tui/app/state/search.rs b/src/tui/app/state/search.rs new file mode 100644 index 0000000..f3a783c --- /dev/null +++ b/src/tui/app/state/search.rs @@ -0,0 +1,100 @@ +use crate::tui::{ + app::{ + app::App, + selection::ListSelection, + state::{AppInner, AppMachine}, + AppState, IAppInteractSearch, AppPublic, + }, + lib::IMusicHoard, +}; + +pub struct AppSearch { + string: String, + orig: ListSelection, + memo: Vec, +} + +struct AppSearchMemo { + index: Option, + char: bool, +} + +impl AppMachine { + pub fn search(inner: AppInner, orig: ListSelection) -> Self { + AppMachine { + inner, + state: AppSearch { + string: String::new(), + orig, + memo: vec![], + }, + } + } +} + +impl From> for App { + fn from(machine: AppMachine) -> Self { + AppState::Search(machine) + } +} + +impl<'a, MH: IMusicHoard> From<&'a mut AppMachine> for AppPublic<'a> { + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: AppState::Search(&machine.state.string), + } + } +} + +impl IAppInteractSearch for AppMachine { + type APP = App; + + fn append_character(mut self, ch: char) -> Self::APP { + let collection = self.inner.music_hoard.get_collection(); + self.state.string.push(ch); + let index = + self.inner + .selection + .incremental_artist_search(collection, &self.state.string, false); + self.state.memo.push(AppSearchMemo { index, char: true }); + self.into() + } + + fn search_next(mut self) -> Self::APP { + let collection = self.inner.music_hoard.get_collection(); + if !self.state.string.is_empty() { + let index = self.inner.selection.incremental_artist_search( + collection, + &self.state.string, + true, + ); + self.state.memo.push(AppSearchMemo { index, char: false }); + } + self.into() + } + + fn step_back(mut self) -> Self::APP { + let collection = self.inner.music_hoard.get_collection(); + if let Some(memo) = self.state.memo.pop() { + if memo.char { + self.state.string.pop(); + } + self.inner.selection.select_artist(collection, memo.index); + } + self.into() + } + + fn finish_search(self) -> Self::APP { + AppMachine::browse(self.inner).into() + } + + fn cancel_search(mut self) -> Self::APP { + self.inner.selection.select_by_list(self.state.orig); + AppMachine::browse(self.inner).into() + } + + fn no_op(self) -> Self::APP { + self.into() + } +} -- 2.45.2 From 7ceb610bdeef9668064e2b3d809f107176b07844 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 18 Feb 2024 02:19:18 +0100 Subject: [PATCH 24/35] lint --- src/tui/app/state/search.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/app/state/search.rs b/src/tui/app/state/search.rs index f3a783c..213a326 100644 --- a/src/tui/app/state/search.rs +++ b/src/tui/app/state/search.rs @@ -3,7 +3,7 @@ use crate::tui::{ app::App, selection::ListSelection, state::{AppInner, AppMachine}, - AppState, IAppInteractSearch, AppPublic, + AppPublic, AppState, IAppInteractSearch, }, lib::IMusicHoard, }; -- 2.45.2 From 24daaff2a11f8366a3016166934e4e646dc5f386 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 18 Feb 2024 02:26:09 +0100 Subject: [PATCH 25/35] Fix a clippy lint --- src/tui/app/app.rs | 16 ++++++++++++---- src/tui/app/state/browse.rs | 17 +---------------- src/tui/app/state/mod.rs | 2 ++ 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index 5b0d8bb..a27145e 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -21,13 +21,21 @@ pub type App = AppState< >; impl App { - pub fn new(music_hoard: MH) -> Self { - match AppMachine::new(music_hoard) { - Ok(browse) => browse.into(), - Err(critical) => critical.into(), + 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 { match self { AppState::Browse(browse) => browse.inner_ref(), diff --git a/src/tui/app/state/browse.rs b/src/tui/app/state/browse.rs index 5b572d5..76fa079 100644 --- a/src/tui/app/state/browse.rs +++ b/src/tui/app/state/browse.rs @@ -2,7 +2,7 @@ use crate::tui::{ app::{ app::App, selection::{Delta, ListSelection}, - state::{critical::AppCritical, AppInner, AppMachine}, + state::{AppInner, AppMachine}, AppPublic, AppState, IAppInteractBrowse, }, lib::IMusicHoard, @@ -17,21 +17,6 @@ impl AppMachine { state: AppBrowse, } } - - pub fn new(mut music_hoard: MH) -> Result> { - let init_result = Self::init(&mut music_hoard); - let inner = AppInner::new(music_hoard); - match init_result { - Ok(()) => Ok(AppMachine::browse(inner)), - Err(err) => Err(AppMachine::critical(inner, err.to_string())), - } - } - - fn init(music_hoard: &mut MH) -> Result<(), musichoard::Error> { - music_hoard.load_from_database()?; - music_hoard.rescan_library()?; - Ok(()) - } } impl From> for App { diff --git a/src/tui/app/state/mod.rs b/src/tui/app/state/mod.rs index 45eaf49..1aae8b6 100644 --- a/src/tui/app/state/mod.rs +++ b/src/tui/app/state/mod.rs @@ -59,6 +59,8 @@ impl<'a, MH: IMusicHoard> From<&'a mut AppInner> 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; -- 2.45.2 From b5afa24b7ec36e3d94a8188ffdcee6b5881e3170 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 18 Feb 2024 12:29:02 +0100 Subject: [PATCH 26/35] Move some tests around --- src/tui/app/app.rs | 1 + src/tui/app/mod.rs | 11 + src/tui/app/selection.rs | 6 +- src/tui/app/state/browse.rs | 56 +++++ src/tui/app/state/critical.rs | 14 ++ src/tui/app/state/error.rs | 14 ++ src/tui/app/state/info.rs | 14 ++ src/tui/app/state/mod.rs | 389 ++-------------------------------- src/tui/app/state/reload.rs | 57 +++++ src/tui/app/state/search.rs | 148 +++++++++++++ 10 files changed, 339 insertions(+), 371 deletions(-) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index a27145e..4664fff 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -1,3 +1,4 @@ +// FIXME: combine with mod state into a mod machine #![allow(clippy::module_inception)] use crate::tui::{ diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 53bff54..87474d5 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -116,3 +116,14 @@ impl AppState { matches!(self, AppState::Search(_)) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn app_is_state() { + let state = AppPublicState::Search("get rekt"); + assert!(state.is_search()); + } +} diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index 82a3663..7f7b8df 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -496,9 +496,9 @@ impl TrackSelection { } pub struct ListSelection { - artist: ListState, - album: ListState, - track: ListState, + pub artist: ListState, + pub album: ListState, + pub track: ListState, } impl ListSelection { diff --git a/src/tui/app/state/browse.rs b/src/tui/app/state/browse.rs index 76fa079..e4c950d 100644 --- a/src/tui/app/state/browse.rs +++ b/src/tui/app/state/browse.rs @@ -91,3 +91,59 @@ impl IAppInteractBrowse for AppMachine { self.into() } } + +#[cfg(test)] +mod tests { + use crate::tui::app::{ + state::tests::{inner, music_hoard}, + IAppInteract, + }; + + use super::*; + + #[test] + fn save_and_quit() { + let mut music_hoard = music_hoard(vec![]); + + music_hoard + .expect_save_to_database() + .times(1) + .return_once(|| Ok(())); + + let browse = AppMachine::browse(inner(music_hoard)); + + let app = browse.save_and_quit(); + assert!(!app.is_running()); + app.unwrap_browse(); + } + + #[test] + fn save_and_quit_error() { + let mut music_hoard = music_hoard(vec![]); + + music_hoard + .expect_save_to_database() + .times(1) + .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); + + let browse = AppMachine::browse(inner(music_hoard)); + + let app = browse.save_and_quit(); + assert!(app.is_running()); + app.unwrap_error(); + } + + #[test] + fn show_info_overlay() { + let browse = AppMachine::browse(inner(music_hoard(vec![]))); + let app = browse.show_info_overlay(); + app.unwrap_info(); + } + + #[test] + fn show_reload_menu() { + let browse = AppMachine::browse(inner(music_hoard(vec![]))); + let app = browse.show_reload_menu(); + app.unwrap_reload(); + } +} diff --git a/src/tui/app/state/critical.rs b/src/tui/app/state/critical.rs index 7149df6..690584a 100644 --- a/src/tui/app/state/critical.rs +++ b/src/tui/app/state/critical.rs @@ -44,3 +44,17 @@ impl IAppInteractCritical for AppMachine { self.into() } } + +#[cfg(test)] +mod tests { + use crate::tui::app::state::tests::{music_hoard, inner}; + + use super::*; + + #[test] + fn no_op() { + let critical = AppMachine::critical(inner(music_hoard(vec![])), "get rekt"); + let app = critical.no_op(); + app.unwrap_critical(); + } +} diff --git a/src/tui/app/state/error.rs b/src/tui/app/state/error.rs index b0b0fdd..271f020 100644 --- a/src/tui/app/state/error.rs +++ b/src/tui/app/state/error.rs @@ -44,3 +44,17 @@ impl IAppInteractError for AppMachine { AppMachine::browse(self.inner).into() } } + +#[cfg(test)] +mod tests { + use crate::tui::app::state::tests::{inner, music_hoard}; + + use super::*; + + #[test] + fn dismiss_error() { + let error = AppMachine::error(inner(music_hoard(vec![])), "get rekt"); + let app = error.dismiss_error(); + app.unwrap_browse(); + } +} diff --git a/src/tui/app/state/info.rs b/src/tui/app/state/info.rs index 2bd0e0c..f9676be 100644 --- a/src/tui/app/state/info.rs +++ b/src/tui/app/state/info.rs @@ -44,3 +44,17 @@ impl IAppInteractInfo for AppMachine { self.into() } } + +#[cfg(test)] +mod tests { + use crate::tui::app::state::tests::{music_hoard, inner}; + + use super::*; + + #[test] + fn hide_info_overlay() { + let info = AppMachine::info(inner(music_hoard(vec![]))); + let app = info.hide_info_overlay(); + app.unwrap_browse(); + } +} diff --git a/src/tui/app/state/mod.rs b/src/tui/app/state/mod.rs index 1aae8b6..600a570 100644 --- a/src/tui/app/state/mod.rs +++ b/src/tui/app/state/mod.rs @@ -69,8 +69,7 @@ mod tests { app::{ app::App, selection::{Category, Delta}, - AppPublicState, AppState, IAppInteract, IAppInteractBrowse, IAppInteractError, - IAppInteractInfo, IAppInteractReload, IAppInteractSearch, + AppState, IAppInteract, IAppInteractBrowse, }, lib::MockIMusicHoard, testmod::COLLECTION, @@ -79,42 +78,42 @@ mod tests { use super::*; impl AppState { - fn unwrap_browse(self) -> BS { + pub fn unwrap_browse(self) -> BS { match self { AppState::Browse(browse) => browse, _ => panic!(), } } - fn unwrap_info(self) -> IS { + pub fn unwrap_info(self) -> IS { match self { AppState::Info(info) => info, _ => panic!(), } } - fn unwrap_reload(self) -> RS { + pub fn unwrap_reload(self) -> RS { match self { AppState::Reload(reload) => reload, _ => panic!(), } } - fn unwrap_search(self) -> SS { + pub fn unwrap_search(self) -> SS { match self { AppState::Search(search) => search, _ => panic!(), } } - fn unwrap_error(self) -> ES { + pub fn unwrap_error(self) -> ES { match self { AppState::Error(error) => error, _ => panic!(), } } - fn unwrap_critical(self) -> CS { + pub fn unwrap_critical(self) -> CS { match self { AppState::Critical(critical) => critical, _ => panic!(), @@ -122,7 +121,7 @@ mod tests { } } - fn music_hoard(collection: Collection) -> MockIMusicHoard { + fn music_hoard_app(collection: Collection) -> MockIMusicHoard { let mut music_hoard = MockIMusicHoard::new(); music_hoard @@ -138,58 +137,20 @@ mod tests { music_hoard } - #[test] - fn app_is_state() { - let state = AppPublicState::Search("get rekt"); - assert!(state.is_search()); - } - - #[test] - fn running_quit() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); + pub fn music_hoard(collection: Collection) -> MockIMusicHoard { + let mut music_hoard = MockIMusicHoard::new(); + music_hoard.expect_get_collection().return_const(collection); music_hoard - .expect_save_to_database() - .times(1) - .return_once(|| Ok(())); - - let app = App::new(music_hoard); - assert!(app.is_running()); - - let browse = app.unwrap_browse(); - - let app = browse.save_and_quit(); - assert!(!app.is_running()); } - #[test] - fn error_quit() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); - - music_hoard - .expect_save_to_database() - .times(1) - .return_once(|| Ok(())); - - let app = App::new(music_hoard); - assert!(app.is_running()); - - let app = App::Error(AppMachine::error( - app.unwrap_browse().inner, - String::from("get rekt"), - )); - - let error = app.unwrap_error(); - - let browse = error.dismiss_error().unwrap_browse(); - - let app = browse.save_and_quit(); - assert!(!app.is_running()); + pub fn inner(music_hoard: MockIMusicHoard) -> AppInner { + AppInner::new(music_hoard) } #[test] fn running_force_quit() { - let app = App::new(music_hoard(COLLECTION.to_owned())); + let app = App::new(music_hoard_app(COLLECTION.to_owned())); assert!(app.is_running()); let app = app.force_quit(); @@ -198,46 +159,15 @@ mod tests { #[test] fn error_force_quit() { - let app = App::new(music_hoard(COLLECTION.to_owned())); + let mut app = App::new(music_hoard_app(COLLECTION.to_owned())); assert!(app.is_running()); - let app = App::Error(AppMachine::error( - app.unwrap_browse().inner, - String::from("get rekt"), - )); + app = AppMachine::error(app.unwrap_browse().inner, "get rekt").into(); - let app = app.force_quit(); + app = app.force_quit(); assert!(!app.is_running()); } - #[test] - fn save() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); - - music_hoard - .expect_save_to_database() - .times(1) - .return_once(|| Ok(())); - - let browse = App::new(music_hoard).unwrap_browse(); - - browse.save_and_quit().unwrap_browse(); - } - - #[test] - fn save_error() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); - - music_hoard - .expect_save_to_database() - .times(1) - .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); - - let browse = App::new(music_hoard).unwrap_browse(); - - browse.save_and_quit().unwrap_error(); - } - #[test] fn init_error() { let mut music_hoard = MockIMusicHoard::new(); @@ -256,7 +186,7 @@ mod tests { #[test] fn modifiers() { - let app = App::new(music_hoard(COLLECTION.to_owned())); + let app = App::new(music_hoard_app(COLLECTION.to_owned())); assert!(app.is_running()); let browse = app.unwrap_browse(); @@ -363,7 +293,7 @@ mod tests { let mut collection = COLLECTION.to_owned(); collection[0].albums[0].tracks = vec![]; - let app = App::new(music_hoard(collection)); + let app = App::new(music_hoard_app(collection)); assert!(app.is_running()); let browse = app.unwrap_browse(); @@ -397,7 +327,7 @@ mod tests { let mut collection = COLLECTION.to_owned(); collection[0].albums = vec![]; - let app = App::new(music_hoard(collection)); + let app = App::new(music_hoard_app(collection)); assert!(app.is_running()); let browse = app.unwrap_browse(); @@ -443,7 +373,7 @@ mod tests { #[test] fn no_artists() { - let app = App::new(music_hoard(vec![])); + let app = App::new(music_hoard_app(vec![])); assert!(app.is_running()); let browse = app.unwrap_browse(); @@ -500,281 +430,4 @@ mod tests { assert_eq!(selection.artist.album.state.list.selected(), None); assert_eq!(selection.artist.album.track.state.list.selected(), None); } - - #[test] - fn info_overlay() { - let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - - let info = browse.show_info_overlay().unwrap_info(); - - info.hide_info_overlay().unwrap_browse(); - } - - #[test] - fn reload_hide_menu() { - let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - - let reload = browse.show_reload_menu().unwrap_reload(); - - reload.hide_reload_menu().unwrap_browse(); - } - - #[test] - fn reload_database() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); - - music_hoard - .expect_load_from_database() - .times(1) - .return_once(|| Ok(())); - - let browse = App::new(music_hoard).unwrap_browse(); - - let reload = browse.show_reload_menu().unwrap_reload(); - - reload.reload_database().unwrap_browse(); - } - - #[test] - fn reload_library() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); - - music_hoard - .expect_rescan_library() - .times(1) - .return_once(|| Ok(())); - - let browse = App::new(music_hoard).unwrap_browse(); - - let reload = browse.show_reload_menu().unwrap_reload(); - - reload.reload_library().unwrap_browse(); - } - - #[test] - fn reload_error() { - let mut music_hoard = music_hoard(COLLECTION.to_owned()); - - music_hoard - .expect_load_from_database() - .times(1) - .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); - - let browse = App::new(music_hoard).unwrap_browse(); - - let reload = browse.show_reload_menu().unwrap_reload(); - - let error = reload.reload_database().unwrap_error(); - - error.dismiss_error().unwrap_browse(); - } - - #[test] - fn search() { - let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.increment_category().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); - - let search = browse.begin_search().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.append_character('a').unwrap_search(); - let search = search.append_character('l').unwrap_search(); - let search = search.append_character('b').unwrap_search(); - let search = search.append_character('u').unwrap_search(); - let search = search.append_character('m').unwrap_search(); - let search = search.append_character('_').unwrap_search(); - let search = search.append_character('a').unwrap_search(); - let search = search.append_character('r').unwrap_search(); - let search = search.append_character('t').unwrap_search(); - let search = search.append_character('i').unwrap_search(); - let search = search.append_character('s').unwrap_search(); - let search = search.append_character('t').unwrap_search(); - let search = search.append_character(' ').unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.append_character('\'').unwrap_search(); - let search = search.append_character('c').unwrap_search(); - let search = search.append_character('\'').unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - - let search = search.step_back().unwrap_search(); - let search = search.step_back().unwrap_search(); - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.append_character('\'').unwrap_search(); - let search = search.append_character('b').unwrap_search(); - let search = search.append_character('\'').unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); - - let browse = search.finish_search().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); - } - - #[test] - fn search_next() { - let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.increment_category().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); - - let search = browse.begin_search().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.append_character('a').unwrap_search(); - - let search = search.search_next().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); - - let search = search.search_next().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - - let search = search.search_next().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); - - let search = search.search_next().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - } - - #[test] - fn cancel_search() { - let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.increment_category().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); - - let search = browse.begin_search().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); - - let search = search.append_character('a').unwrap_search(); - let search = search.append_character('l').unwrap_search(); - let search = search.append_character('b').unwrap_search(); - let search = search.append_character('u').unwrap_search(); - let search = search.append_character('m').unwrap_search(); - let search = search.append_character('_').unwrap_search(); - let search = search.append_character('a').unwrap_search(); - let search = search.append_character('r').unwrap_search(); - let search = search.append_character('t').unwrap_search(); - let search = search.append_character('i').unwrap_search(); - let search = search.append_character('s').unwrap_search(); - let search = search.append_character('t').unwrap_search(); - let search = search.append_character(' ').unwrap_search(); - let search = search.append_character('\'').unwrap_search(); - let search = search.append_character('c').unwrap_search(); - let search = search.append_character('\'').unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); - - let browse = search.cancel_search().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); - } - - #[test] - fn empty_search() { - let browse = App::new(music_hoard(vec![])).unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.increment_category().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), None); - - let search = browse.begin_search().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), None); - - let search = search.append_character('a').unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), None); - - let search = search.search_next().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), None); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), None); - - let search = search.step_back().unwrap_search(); - - assert_eq!(search.inner.selection.active, Category::Album); - assert_eq!(search.inner.selection.artist.state.list.selected(), None); - - let browse = search.cancel_search().unwrap_browse(); - - assert_eq!(browse.inner.selection.active, Category::Album); - assert_eq!(browse.inner.selection.artist.state.list.selected(), None); - } } diff --git a/src/tui/app/state/reload.rs b/src/tui/app/state/reload.rs index e4f37d8..5b17684 100644 --- a/src/tui/app/state/reload.rs +++ b/src/tui/app/state/reload.rs @@ -80,3 +80,60 @@ impl IAppInteractReloadPrivate for AppMachine IAppInteractSearch for AppMachine { self.into() } } + +#[cfg(test)] +mod tests { + use ratatui::widgets::ListState; + + use crate::tui::{ + app::state::tests::{inner, music_hoard}, + testmod::COLLECTION, + }; + + use super::*; + + fn orig(index: Option) -> ListSelection { + let mut artist = ListState::default(); + artist.select(index); + + ListSelection { + artist, + album: ListState::default(), + track: ListState::default(), + } + } + + #[test] + fn search() { + let search = AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2))); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('l').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + let search = search.append_character('u').unwrap_search(); + let search = search.append_character('m').unwrap_search(); + let search = search.append_character('_').unwrap_search(); + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('r').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character('i').unwrap_search(); + let search = search.append_character('s').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character(' ').unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.append_character('\'').unwrap_search(); + let search = search.append_character('c').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + + let search = search.step_back().unwrap_search(); + let search = search.step_back().unwrap_search(); + let search = search.step_back().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.append_character('\'').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); + + let app = search.finish_search(); + let browse = app.unwrap_browse(); + assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1)); + } + + #[test] + fn search_next_step_back() { + let search = AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2))); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.step_back().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.append_character('a').unwrap_search(); + let search = search.search_next().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); + + let search = search.search_next().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + + let search = search.search_next().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); + + let search = search.search_next().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); + + let search = search.step_back().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); + + let search = search.step_back().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + + let search = search.step_back().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); + + let search = search.step_back().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.step_back().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + } + + #[test] + fn cancel_search() { + let search = AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2))); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('l').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + let search = search.append_character('u').unwrap_search(); + let search = search.append_character('m').unwrap_search(); + let search = search.append_character('_').unwrap_search(); + let search = search.append_character('a').unwrap_search(); + let search = search.append_character('r').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character('i').unwrap_search(); + let search = search.append_character('s').unwrap_search(); + let search = search.append_character('t').unwrap_search(); + let search = search.append_character(' ').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); + let search = search.append_character('b').unwrap_search(); + let search = search.append_character('\'').unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); + + let browse = search.cancel_search().unwrap_browse(); + assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(2)); + } + + #[test] + fn empty_search() { + let search = AppMachine::search(inner(music_hoard(vec![])), orig(None)); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); + + let search = search.append_character('a').unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); + + let search = search.search_next().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); + + let search = search.step_back().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); + + let search = search.step_back().unwrap_search(); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); + + let browse = search.cancel_search().unwrap_browse(); + assert_eq!(browse.inner.selection.artist.state.list.selected(), None); + } +} -- 2.45.2 From 47bae4957d3ded9070b84e7480efc60c07175d4a Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 18 Feb 2024 13:24:39 +0100 Subject: [PATCH 27/35] Move search from selection to search state --- src/tui/app/selection.rs | 183 ++---------------------------- src/tui/app/state/search.rs | 215 ++++++++++++++++++++++++++++++++++-- 2 files changed, 212 insertions(+), 186 deletions(-) diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index 7f7b8df..ad7f05f 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -48,7 +48,6 @@ pub struct TrackSelection { pub state: WidgetState, } -// FIXME: should be with browse state (maybe?) pub enum Delta { Line, Page, @@ -85,6 +84,10 @@ impl Selection { self.artist.select(artists, index); } + pub fn selected_artist(&self) -> Option { + self.artist.selected() + } + pub fn reset_artist(&mut self, artists: &[Artist]) { if self.artist.state.list.selected() != Some(0) { self.select_by_id(artists, IdSelection { artist: None }); @@ -107,16 +110,6 @@ impl Selection { }; } - pub fn incremental_artist_search( - &mut self, - collection: &Collection, - artist_name: &str, - next: bool, - ) -> Option { - self.artist - .incremental_search(collection, artist_name, next) - } - pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) { match self.active { Category::Artist => self.increment_artist(collection, delta), @@ -200,6 +193,10 @@ impl ArtistSelection { } } + fn selected(&self) -> Option { + self.state.list.selected() + } + fn select(&mut self, artists: &[Artist], to: Option) { match to { Some(to) => self.select_to(artists, to), @@ -215,91 +212,6 @@ impl ArtistSelection { } } - // FIXME: use aho_corasick for normalization - AhoCorasick does not implement PartialEq. It - // makes more sense to be places in app.rs as it would make ArtistSelection non-trivial. - fn normalize_search(search: &str, lowercase: bool, asciify: bool) -> String { - let normalized = if lowercase { - search.to_lowercase() - } else { - search.to_owned() - }; - - // Unlikely that this covers all possible strings, but it should at least cover strings - // relevant for music (at least in English). The list of characters handled is based on - // https://wiki.musicbrainz.org/User:Yurim/Punctuation_and_Special_Characters. - if asciify { - normalized - // U+2010 hyphen, U+2012 figure dash, U+2013 en dash, U+2014 em dash, - // U+2015 horizontal bar - .replace(['‐', '‒', '–', '—', '―'], "-") - .replace(['‘', '’'], "'") // U+2018, U+2019 - .replace(['“', '”'], "\"") // U+201C, U+201D - .replace('…', "...") // U+2026 - .replace('−', "-") // U+2212 minus sign - } else { - normalized - } - } - - fn is_case_sensitive(artist_name: &str) -> bool { - artist_name - .chars() - .any(|ch| ch.is_alphabetic() && ch.is_uppercase()) - } - - fn is_char_sensitive(artist_name: &str) -> bool { - let special_chars: &[char] = &['‐', '‒', '–', '—', '―', '‘', '’', '“', '”', '…', '−']; - artist_name.chars().any(|ch| special_chars.contains(&ch)) - } - - fn incremental_search_predicate( - case_sensitive: bool, - char_sensitive: bool, - search_name: &String, - probe: &Artist, - ) -> bool { - let name = Self::normalize_search(&probe.id.name, !case_sensitive, !char_sensitive); - let mut result = name.starts_with(search_name); - - if let Some(ref probe_sort) = probe.sort { - let name = Self::normalize_search(&probe_sort.name, !case_sensitive, !char_sensitive); - result = result || name.starts_with(search_name); - } - - result - } - - // FIXME: search logic should be with the search state - fn incremental_search( - &mut self, - artists: &[Artist], - artist_name: &str, - next: bool, - ) -> Option { - let previous = self.state.list.selected(); - - if let Some(mut index) = self.state.list.selected() { - let case_sensitive = Self::is_case_sensitive(artist_name); - let char_sensitive = Self::is_char_sensitive(artist_name); - let search = Self::normalize_search(artist_name, !case_sensitive, !char_sensitive); - - if next && ((index + 1) < artists.len()) { - index += 1; - } - let slice = &artists[index..]; - - let result = slice.iter().position(|probe| { - Self::incremental_search_predicate(case_sensitive, char_sensitive, &search, probe) - }); - - if let Some(slice_index) = result { - self.select_to(artists, index + slice_index); - } - } - - previous - } - fn increment_by(&mut self, artists: &[Artist], by: usize) { if let Some(index) = self.state.list.selected() { let result = index.saturating_add(by); @@ -904,83 +816,4 @@ mod tests { sel.reinitialise(&[], active_artist); assert_eq!(sel, expected); } - - #[test] - fn artist_incremental_search() { - let artists = &COLLECTION; - - // Empty collection. - let mut sel = ArtistSelection::initialise(&[]); - assert_eq!(sel.state.list.selected(), None); - - sel.incremental_search(artists, "album_artist 'a'", false); - assert_eq!(sel.state.list.selected(), None); - - // Basic test, first element. - let mut sel = ArtistSelection::initialise(artists); - assert_eq!(sel.state.list.selected(), Some(0)); - - sel.incremental_search(artists, "", false); - assert_eq!(sel.state.list.selected(), Some(0)); - - sel.incremental_search(artists, "album_artist ", false); - assert_eq!(sel.state.list.selected(), Some(0)); - - sel.incremental_search(artists, "album_artist 'a'", false); - assert_eq!(sel.state.list.selected(), Some(0)); - - // Basic test, non-first element. - sel.reinitialise(artists, None); - - sel.incremental_search(artists, "album_artist ", false); - assert_eq!(sel.state.list.selected(), Some(0)); - - sel.incremental_search(artists, "album_artist 'c'", false); - assert_eq!(sel.state.list.selected(), Some(2)); - - // Non-lowercase. - sel.reinitialise(artists, None); - - sel.incremental_search(artists, "Album_Artist ", false); - assert_eq!(sel.state.list.selected(), Some(0)); - - sel.incremental_search(artists, "Album_Artist 'C'", false); - assert_eq!(sel.state.list.selected(), Some(2)); - - // Non-ascii. - sel.reinitialise(artists, None); - - sel.incremental_search(artists, "album_artist ", false); - assert_eq!(sel.state.list.selected(), Some(0)); - - sel.incremental_search(artists, "album_artist ‘c’", false); - assert_eq!(sel.state.list.selected(), Some(2)); - - // Stop at name, not sort name. - sel.reinitialise(artists, None); - - sel.incremental_search(artists, "the", false); - assert_eq!(sel.state.list.selected(), Some(2)); - - sel.incremental_search(artists, "the album_artist 'c'", false); - assert_eq!(sel.state.list.selected(), Some(2)); - - // Search next with common prefix. - sel.reinitialise(artists, None); - - sel.incremental_search(artists, "album_artist ", false); - assert_eq!(sel.state.list.selected(), Some(0)); - - sel.incremental_search(artists, "album_artist ", true); - assert_eq!(sel.state.list.selected(), Some(1)); - - sel.incremental_search(artists, "album_artist ", true); - assert_eq!(sel.state.list.selected(), Some(2)); - - sel.incremental_search(artists, "album_artist ", true); - assert_eq!(sel.state.list.selected(), Some(3)); - - sel.incremental_search(artists, "album_artist ", true); - assert_eq!(sel.state.list.selected(), Some(3)); - } } diff --git a/src/tui/app/state/search.rs b/src/tui/app/state/search.rs index f46d4a5..931f412 100644 --- a/src/tui/app/state/search.rs +++ b/src/tui/app/state/search.rs @@ -1,3 +1,5 @@ +use musichoard::collection::artist::Artist; + use crate::tui::{ app::{ app::App, @@ -51,25 +53,18 @@ impl IAppInteractSearch for AppMachine { type APP = App; fn append_character(mut self, ch: char) -> Self::APP { - let collection = self.inner.music_hoard.get_collection(); self.state.string.push(ch); - let index = - self.inner - .selection - .incremental_artist_search(collection, &self.state.string, false); + let index = self.inner.selection.artist.state.list.selected(); self.state.memo.push(AppSearchMemo { index, char: true }); + self.incremental_search(false); self.into() } fn search_next(mut self) -> Self::APP { - let collection = self.inner.music_hoard.get_collection(); if !self.state.string.is_empty() { - let index = self.inner.selection.incremental_artist_search( - collection, - &self.state.string, - true, - ); + let index = self.inner.selection.artist.state.list.selected(); self.state.memo.push(AppSearchMemo { index, char: false }); + self.incremental_search(true); } self.into() } @@ -99,6 +94,103 @@ impl IAppInteractSearch for AppMachine { } } +trait IAppInteractSearchPrivate { + fn incremental_search(&mut self, next: bool); + fn incremental_search_predicate( + case_sensitive: bool, + char_sensitive: bool, + search_name: &String, + probe: &Artist, + ) -> bool; + + fn is_case_sensitive(artist_name: &str) -> bool; + fn is_char_sensitive(artist_name: &str) -> bool; + fn normalize_search(search: &str, lowercase: bool, asciify: bool) -> String; +} + +impl IAppInteractSearchPrivate for AppMachine { + fn incremental_search(&mut self, next: bool) { + let artists = self.inner.music_hoard.get_collection(); + let artist_name = &self.state.string; + + let sel = &mut self.inner.selection; + if let Some(mut index) = sel.selected_artist() { + let case_sensitive = Self::is_case_sensitive(artist_name); + let char_sensitive = Self::is_char_sensitive(artist_name); + let search = Self::normalize_search(artist_name, !case_sensitive, !char_sensitive); + + if next && ((index + 1) < artists.len()) { + index += 1; + } + let slice = &artists[index..]; + + let result = slice.iter().position(|probe| { + Self::incremental_search_predicate(case_sensitive, char_sensitive, &search, probe) + }); + + if let Some(slice_index) = result { + sel.select_artist(artists, Some(index + slice_index)); + } + } + } + + fn incremental_search_predicate( + case_sensitive: bool, + char_sensitive: bool, + search_name: &String, + probe: &Artist, + ) -> bool { + let name = Self::normalize_search(&probe.id.name, !case_sensitive, !char_sensitive); + let mut result = name.starts_with(search_name); + + if let Some(ref probe_sort) = probe.sort { + if !result { + let name = + Self::normalize_search(&probe_sort.name, !case_sensitive, !char_sensitive); + result = name.starts_with(search_name); + } + } + + result + } + + fn is_case_sensitive(artist_name: &str) -> bool { + artist_name + .chars() + .any(|ch| ch.is_alphabetic() && ch.is_uppercase()) + } + + fn is_char_sensitive(artist_name: &str) -> bool { + let special_chars: &[char] = &['‐', '‒', '–', '—', '―', '−', '‘', '’', '“', '”', '…']; + artist_name.chars().any(|ch| special_chars.contains(&ch)) + } + + // FIXME: use aho_corasick for normalization - AhoCorasick does not implement PartialEq. It + // makes more sense to be places in app.rs as it would make ArtistSelection non-trivial. + fn normalize_search(search: &str, lowercase: bool, asciify: bool) -> String { + let normalized = if lowercase { + search.to_lowercase() + } else { + search.to_owned() + }; + + // Unlikely that this covers all possible strings, but it should at least cover strings + // relevant for music (at least in English). The list of characters handled is based on + // https://wiki.musicbrainz.org/User:Yurim/Punctuation_and_Special_Characters. + if asciify { + normalized + // U+2010 hyphen, U+2012 figure dash, U+2013 en dash, U+2014 em dash, + // U+2015 horizontal bar, U+2212 minus sign + .replace(['‐', '‒', '–', '—', '―', '−'], "-") + .replace(['‘', '’'], "'") // U+2018, U+2019 + .replace(['“', '”'], "\"") // U+201C, U+201D + .replace('…', "...") // U+2026 + } else { + normalized + } + } +} + #[cfg(test)] mod tests { use ratatui::widgets::ListState; @@ -121,6 +213,107 @@ mod tests { } } + #[test] + fn artist_incremental_search() { + // Empty collection. + let mut search = AppMachine::search(inner(music_hoard(vec![])), orig(None)); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); + + search.state.string = String::from("album_artist 'a'"); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), None); + + // Basic test, first element. + let mut search = + AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.state.string = String::from(""); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.state.string = String::from("album_artist "); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.state.string = String::from("album_artist 'a'"); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + // Basic test, non-first element. + let mut search = + AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.state.string = String::from("album_artist "); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.state.string = String::from("album_artist 'c'"); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + + // Non-lowercase. + let mut search = + AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.state.string = String::from("Album_Artist "); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.state.string = String::from("Album_Artist 'C'"); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + + // Non-ascii. + let mut search = + AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.state.string = String::from("album_artist "); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.state.string = String::from("album_artist ‘c’"); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + + // Stop at name, not sort name. + let mut search = + AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.state.string = String::from("the "); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + + search.state.string = String::from("the album_artist 'c'"); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + + // Search next with common prefix. + let mut search = + AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.state.string = String::from("album_artist"); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.incremental_search(true); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1)); + + search.incremental_search(true); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + + search.incremental_search(true); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); + + search.incremental_search(true); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3)); + } + #[test] fn search() { let search = AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2))); -- 2.45.2 From 127da2b180b414e4229f5956deda542a05767d35 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 18 Feb 2024 13:54:29 +0100 Subject: [PATCH 28/35] Simplify tests --- src/tui/app/selection.rs | 30 +++++++- src/tui/app/state/mod.rs | 143 --------------------------------------- 2 files changed, 27 insertions(+), 146 deletions(-) diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index ad7f05f..d0ac597 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -495,7 +495,13 @@ mod tests { let tracks = &COLLECTION[0].albums[0].tracks; assert!(tracks.len() > 1); - let empty = TrackSelection::initialise(&[]); + let mut empty = TrackSelection::initialise(&[]); + assert_eq!(empty.state.list.selected(), None); + + empty.increment(tracks, Delta::Line); + assert_eq!(empty.state.list.selected(), None); + + empty.decrement(tracks, Delta::Line); assert_eq!(empty.state.list.selected(), None); let mut sel = TrackSelection::initialise(tracks); @@ -578,8 +584,17 @@ mod tests { let albums = &COLLECTION[0].albums; assert!(albums.len() > 1); - let empty = AlbumSelection::initialise(&[]); + let mut empty = AlbumSelection::initialise(&[]); assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.track.state.list.selected(), None); + + empty.increment(albums, Delta::Line); + assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.track.state.list.selected(), None); + + empty.decrement(albums, Delta::Line); + assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.track.state.list.selected(), None); let mut sel = AlbumSelection::initialise(albums); assert_eq!(sel.state.list.selected(), Some(0)); @@ -700,8 +715,17 @@ mod tests { let artists = &COLLECTION; assert!(artists.len() > 1); - let empty = ArtistSelection::initialise(&[]); + let mut empty = ArtistSelection::initialise(&[]); assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.album.state.list.selected(), None); + + empty.increment(artists, Delta::Line); + assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.album.state.list.selected(), None); + + empty.decrement(artists, Delta::Line); + assert_eq!(empty.state.list.selected(), None); + assert_eq!(empty.album.state.list.selected(), None); let mut sel = ArtistSelection::initialise(artists); assert_eq!(sel.state.list.selected(), Some(0)); diff --git a/src/tui/app/state/mod.rs b/src/tui/app/state/mod.rs index 600a570..8dce8b6 100644 --- a/src/tui/app/state/mod.rs +++ b/src/tui/app/state/mod.rs @@ -287,147 +287,4 @@ mod tests { assert_eq!(selection.artist.album.state.list.selected(), Some(1)); assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); } - - #[test] - fn no_tracks() { - let mut collection = COLLECTION.to_owned(); - collection[0].albums[0].tracks = vec![]; - - let app = App::new(music_hoard_app(collection)); - assert!(app.is_running()); - - let browse = app.unwrap_browse(); - - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.increment_category().unwrap_browse(); - let browse = browse.increment_category().unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - } - - #[test] - fn no_albums() { - let mut collection = COLLECTION.to_owned(); - collection[0].albums = vec![]; - - let app = App::new(music_hoard_app(collection)); - assert!(app.is_running()); - - let browse = app.unwrap_browse(); - - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.increment_category().unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.increment_category().unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - } - - #[test] - fn no_artists() { - let app = App::new(music_hoard_app(vec![])); - assert!(app.is_running()); - - let browse = app.unwrap_browse(); - - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.increment_category().unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.increment_category().unwrap_browse(); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), None); - assert_eq!(selection.artist.album.state.list.selected(), None); - assert_eq!(selection.artist.album.track.state.list.selected(), None); - } } -- 2.45.2 From d427612bbf84aee5a2ad71cd75c097aa3f9964f9 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 18 Feb 2024 14:01:32 +0100 Subject: [PATCH 29/35] Move tests --- src/tui/app/selection.rs | 88 +++++++++++++++++++++++++++++++ src/tui/app/state/mod.rs | 110 +-------------------------------------- 2 files changed, 89 insertions(+), 109 deletions(-) diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs index d0ac597..6c4db7d 100644 --- a/src/tui/app/selection.rs +++ b/src/tui/app/selection.rs @@ -840,4 +840,92 @@ mod tests { sel.reinitialise(&[], active_artist); assert_eq!(sel, expected); } + + #[test] + fn selection() { + let mut selection = Selection::new(&COLLECTION); + + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + selection.increment_selection(&COLLECTION, Delta::Line); + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + selection.increment_category(); + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + selection.increment_selection(&COLLECTION, Delta::Line); + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + selection.increment_category(); + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + selection.increment_selection(&COLLECTION, Delta::Line); + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); + + selection.increment_category(); + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); + + selection.decrement_selection(&COLLECTION, Delta::Line); + assert_eq!(selection.active, Category::Track); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + selection.increment_selection(&COLLECTION, Delta::Line); + selection.decrement_category(); + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); + + selection.decrement_selection(&COLLECTION, Delta::Line); + assert_eq!(selection.active, Category::Album); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + selection.increment_selection(&COLLECTION, Delta::Line); + selection.decrement_category(); + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + selection.decrement_selection(&COLLECTION, Delta::Line); + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + + selection.increment_category(); + selection.increment_selection(&COLLECTION, Delta::Line); + selection.decrement_category(); + selection.decrement_selection(&COLLECTION, Delta::Line); + selection.decrement_category(); + assert_eq!(selection.active, Category::Artist); + assert_eq!(selection.artist.state.list.selected(), Some(0)); + assert_eq!(selection.artist.album.state.list.selected(), Some(1)); + assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); + } } diff --git a/src/tui/app/state/mod.rs b/src/tui/app/state/mod.rs index 8dce8b6..986f3a2 100644 --- a/src/tui/app/state/mod.rs +++ b/src/tui/app/state/mod.rs @@ -66,11 +66,7 @@ mod tests { use musichoard::collection::Collection; use crate::tui::{ - app::{ - app::App, - selection::{Category, Delta}, - AppState, IAppInteract, IAppInteractBrowse, - }, + app::{app::App, AppState, IAppInteract}, lib::MockIMusicHoard, testmod::COLLECTION, }; @@ -183,108 +179,4 @@ mod tests { assert!(app.is_running()); app.unwrap_critical(); } - - #[test] - fn modifiers() { - let app = App::new(music_hoard_app(COLLECTION.to_owned())); - assert!(app.is_running()); - - let browse = app.unwrap_browse(); - - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_category().unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_category().unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); - - let browse = browse.increment_category().unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Track); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.decrement_category().unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(1)); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Album); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.decrement_category().unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - - let browse = browse.increment_category().unwrap_browse(); - let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - let browse = browse.decrement_category().unwrap_browse(); - let browse = browse.decrement_selection(Delta::Line).unwrap_browse(); - let browse = browse.decrement_category().unwrap_browse(); - let selection = &browse.inner.selection; - assert_eq!(selection.active, Category::Artist); - assert_eq!(selection.artist.state.list.selected(), Some(0)); - assert_eq!(selection.artist.album.state.list.selected(), Some(1)); - assert_eq!(selection.artist.album.track.state.list.selected(), Some(0)); - } } -- 2.45.2 From 9a803c1ddef0db613ac44ad4782944cb5aa19323 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 18 Feb 2024 14:05:07 +0100 Subject: [PATCH 30/35] Rename state to machine --- src/tui/app/app.rs | 2 +- src/tui/app/{state => machine}/browse.rs | 4 ++-- src/tui/app/{state => machine}/critical.rs | 4 ++-- src/tui/app/{state => machine}/error.rs | 4 ++-- src/tui/app/{state => machine}/info.rs | 4 ++-- src/tui/app/{state => machine}/mod.rs | 0 src/tui/app/{state => machine}/reload.rs | 4 ++-- src/tui/app/{state => machine}/search.rs | 4 ++-- src/tui/app/mod.rs | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) rename src/tui/app/{state => machine}/browse.rs (97%) rename src/tui/app/{state => machine}/critical.rs (92%) rename src/tui/app/{state => machine}/error.rs (92%) rename src/tui/app/{state => machine}/info.rs (92%) rename src/tui/app/{state => machine}/mod.rs (100%) rename src/tui/app/{state => machine}/reload.rs (97%) rename src/tui/app/{state => machine}/search.rs (99%) diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index 4664fff..4d0c7ec 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -3,7 +3,7 @@ use crate::tui::{ app::{ - state::{ + machine::{ browse::AppBrowse, critical::AppCritical, error::AppError, info::AppInfo, reload::AppReload, search::AppSearch, AppInner, AppMachine, }, diff --git a/src/tui/app/state/browse.rs b/src/tui/app/machine/browse.rs similarity index 97% rename from src/tui/app/state/browse.rs rename to src/tui/app/machine/browse.rs index e4c950d..d7ad5c7 100644 --- a/src/tui/app/state/browse.rs +++ b/src/tui/app/machine/browse.rs @@ -1,8 +1,8 @@ use crate::tui::{ app::{ app::App, + machine::{AppInner, AppMachine}, selection::{Delta, ListSelection}, - state::{AppInner, AppMachine}, AppPublic, AppState, IAppInteractBrowse, }, lib::IMusicHoard, @@ -95,7 +95,7 @@ impl IAppInteractBrowse for AppMachine { #[cfg(test)] mod tests { use crate::tui::app::{ - state::tests::{inner, music_hoard}, + machine::tests::{inner, music_hoard}, IAppInteract, }; diff --git a/src/tui/app/state/critical.rs b/src/tui/app/machine/critical.rs similarity index 92% rename from src/tui/app/state/critical.rs rename to src/tui/app/machine/critical.rs index 690584a..a02d7f2 100644 --- a/src/tui/app/state/critical.rs +++ b/src/tui/app/machine/critical.rs @@ -1,7 +1,7 @@ use crate::tui::{ app::{ app::App, - state::{AppInner, AppMachine}, + machine::{AppInner, AppMachine}, AppPublic, AppState, IAppInteractCritical, }, lib::IMusicHoard, @@ -47,7 +47,7 @@ impl IAppInteractCritical for AppMachine { #[cfg(test)] mod tests { - use crate::tui::app::state::tests::{music_hoard, inner}; + use crate::tui::app::machine::tests::{music_hoard, inner}; use super::*; diff --git a/src/tui/app/state/error.rs b/src/tui/app/machine/error.rs similarity index 92% rename from src/tui/app/state/error.rs rename to src/tui/app/machine/error.rs index 271f020..006d4cf 100644 --- a/src/tui/app/state/error.rs +++ b/src/tui/app/machine/error.rs @@ -1,7 +1,7 @@ use crate::tui::{ app::{ app::App, - state::{AppInner, AppMachine}, + machine::{AppInner, AppMachine}, AppPublic, AppState, IAppInteractError, }, lib::IMusicHoard, @@ -47,7 +47,7 @@ impl IAppInteractError for AppMachine { #[cfg(test)] mod tests { - use crate::tui::app::state::tests::{inner, music_hoard}; + use crate::tui::app::machine::tests::{inner, music_hoard}; use super::*; diff --git a/src/tui/app/state/info.rs b/src/tui/app/machine/info.rs similarity index 92% rename from src/tui/app/state/info.rs rename to src/tui/app/machine/info.rs index f9676be..0b82292 100644 --- a/src/tui/app/state/info.rs +++ b/src/tui/app/machine/info.rs @@ -1,7 +1,7 @@ use crate::tui::{ app::{ app::App, - state::{AppInner, AppMachine}, + machine::{AppInner, AppMachine}, AppPublic, AppState, IAppInteractInfo, }, lib::IMusicHoard, @@ -47,7 +47,7 @@ impl IAppInteractInfo for AppMachine { #[cfg(test)] mod tests { - use crate::tui::app::state::tests::{music_hoard, inner}; + use crate::tui::app::machine::tests::{music_hoard, inner}; use super::*; diff --git a/src/tui/app/state/mod.rs b/src/tui/app/machine/mod.rs similarity index 100% rename from src/tui/app/state/mod.rs rename to src/tui/app/machine/mod.rs diff --git a/src/tui/app/state/reload.rs b/src/tui/app/machine/reload.rs similarity index 97% rename from src/tui/app/state/reload.rs rename to src/tui/app/machine/reload.rs index 5b17684..55d276d 100644 --- a/src/tui/app/state/reload.rs +++ b/src/tui/app/machine/reload.rs @@ -2,7 +2,7 @@ use crate::tui::{ app::{ app::App, selection::IdSelection, - state::{AppInner, AppMachine}, + machine::{AppInner, AppMachine}, AppPublic, AppState, IAppInteractReload, }, lib::IMusicHoard, @@ -83,7 +83,7 @@ impl IAppInteractReloadPrivate for AppMachine Date: Sun, 18 Feb 2024 14:24:54 +0100 Subject: [PATCH 31/35] Merge app with machine --- src/tui/app/app.rs | 96 -------------------- src/tui/app/machine/browse.rs | 3 +- src/tui/app/machine/critical.rs | 5 +- src/tui/app/machine/error.rs | 3 +- src/tui/app/machine/info.rs | 5 +- src/tui/app/machine/mod.rs | 149 +++++++++++++++++++++++--------- src/tui/app/machine/reload.rs | 4 +- src/tui/app/machine/search.rs | 7 +- src/tui/app/mod.rs | 8 +- src/tui/handler.rs | 4 +- src/tui/mod.rs | 6 +- src/tui/ui.rs | 7 +- 12 files changed, 130 insertions(+), 167 deletions(-) delete mode 100644 src/tui/app/app.rs diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs deleted file mode 100644 index 4d0c7ec..0000000 --- a/src/tui/app/app.rs +++ /dev/null @@ -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 = AppState< - AppMachine, - AppMachine, - AppMachine, - AppMachine, - AppMachine, - AppMachine, ->; - -impl App { - 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 { - 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 { - 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 IAppInteract for App { - type BS = AppMachine; - type IS = AppMachine; - type RS = AppMachine; - type SS = AppMachine; - type ES = AppMachine; - type CS = AppMachine; - - 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 - } -} - -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::Error(error) => error.into(), - AppState::Critical(critical) => critical.into(), - } - } -} diff --git a/src/tui/app/machine/browse.rs b/src/tui/app/machine/browse.rs index d7ad5c7..abc73d8 100644 --- a/src/tui/app/machine/browse.rs +++ b/src/tui/app/machine/browse.rs @@ -1,7 +1,6 @@ use crate::tui::{ app::{ - app::App, - machine::{AppInner, AppMachine}, + machine::{App, AppInner, AppMachine}, selection::{Delta, ListSelection}, AppPublic, AppState, IAppInteractBrowse, }, diff --git a/src/tui/app/machine/critical.rs b/src/tui/app/machine/critical.rs index a02d7f2..5fe911d 100644 --- a/src/tui/app/machine/critical.rs +++ b/src/tui/app/machine/critical.rs @@ -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 IAppInteractCritical for AppMachine { #[cfg(test)] mod tests { - use crate::tui::app::machine::tests::{music_hoard, inner}; + use crate::tui::app::machine::tests::{inner, music_hoard}; use super::*; diff --git a/src/tui/app/machine/error.rs b/src/tui/app/machine/error.rs index 006d4cf..63239d5 100644 --- a/src/tui/app/machine/error.rs +++ b/src/tui/app/machine/error.rs @@ -1,7 +1,6 @@ use crate::tui::{ app::{ - app::App, - machine::{AppInner, AppMachine}, + machine::{App, AppInner, AppMachine}, AppPublic, AppState, IAppInteractError, }, lib::IMusicHoard, diff --git a/src/tui/app/machine/info.rs b/src/tui/app/machine/info.rs index 0b82292..203bb74 100644 --- a/src/tui/app/machine/info.rs +++ b/src/tui/app/machine/info.rs @@ -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 IAppInteractInfo for AppMachine { #[cfg(test)] mod tests { - use crate::tui::app::machine::tests::{music_hoard, inner}; + use crate::tui::app::machine::tests::{inner, music_hoard}; use super::*; diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index 986f3a2..47c73a8 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -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 = AppState< + AppMachine, + AppMachine, + AppMachine, + AppMachine, + AppMachine, + AppMachine, +>; + pub struct AppMachine { inner: AppInner, state: STATE, } -impl AppMachine { - pub fn inner_ref(&self) -> &AppInner { - &self.inner - } - - pub fn inner_mut(&mut self) -> &mut AppInner { - &mut self.inner - } -} - pub struct AppInner { running: bool, music_hoard: MH, selection: Selection, } +impl App { + 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 { + 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 { + 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 IAppInteract for App { + type BS = AppMachine; + type IS = AppMachine; + type RS = AppMachine; + type SS = AppMachine; + type ES = AppMachine; + type CS = AppMachine; + + 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 + } +} + +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::Error(error) => error.into(), + AppState::Critical(critical) => critical.into(), + } + } +} + impl AppInner { pub fn new(music_hoard: MH) -> Self { let selection = Selection::new(music_hoard.get_collection()); @@ -40,14 +120,6 @@ impl AppInner { 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> for AppPublicInner<'a> { @@ -59,14 +131,12 @@ impl<'a, MH: IMusicHoard> From<&'a mut AppInner> 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(); diff --git a/src/tui/app/machine/reload.rs b/src/tui/app/machine/reload.rs index 55d276d..19a0164 100644 --- a/src/tui/app/machine/reload.rs +++ b/src/tui/app/machine/reload.rs @@ -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![]); diff --git a/src/tui/app/machine/search.rs b/src/tui/app/machine/search.rs index 6273799..783e816 100644 --- a/src/tui/app/machine/search.rs +++ b/src/tui/app/machine/search.rs @@ -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 IAppInteractSearchPrivate for AppMachine { 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); diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 59fc2bb..66f612a 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -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 { Browse(BS), Info(IS), diff --git a/src/tui/handler.rs b/src/tui/handler.rs index be88bb2..1f63d5d 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -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}, }; diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 629ecaa..e11acb6 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -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::*; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 80536a9..289035a 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -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: &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, }; -- 2.45.2 From 1cafca9048a77e350276fc8a636b5def5be5e763 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 18 Feb 2024 16:49:58 +0100 Subject: [PATCH 32/35] String operations with aho-corasick --- Cargo.lock | 10 ++++++++++ Cargo.toml | 4 +++- src/tui/app/machine/search.rs | 29 +++++++++++++++-------------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f662848..bc004e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.16" @@ -398,6 +407,7 @@ dependencies = [ name = "musichoard" version = "0.1.0" dependencies = [ + "aho-corasick", "crossterm", "mockall", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 3e83004..4065a28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +aho-corasick = { version = "1.1.2", 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} ratatui = { version = "0.26.0", optional = true} serde = { version = "1.0.196", features = ["derive"], optional = true } @@ -27,7 +29,7 @@ bin = ["structopt"] database-json = ["serde", "serde_json"] library-beets = [] ssh-library = ["openssh", "tokio"] -tui = ["crossterm", "ratatui"] +tui = ["aho-corasick", "crossterm", "once_cell", "ratatui"] [[bin]] name = "musichoard" diff --git a/src/tui/app/machine/search.rs b/src/tui/app/machine/search.rs index 783e816..92380a8 100644 --- a/src/tui/app/machine/search.rs +++ b/src/tui/app/machine/search.rs @@ -1,3 +1,6 @@ +use aho_corasick::AhoCorasick; +use once_cell::sync::Lazy; + use musichoard::collection::artist::Artist; use crate::tui::{ @@ -9,6 +12,16 @@ use crate::tui::{ lib::IMusicHoard, }; +// Unlikely that this covers all possible strings, but it should at least cover strings +// relevant for music (at least in English). The list of characters handled is based on +// https://wiki.musicbrainz.org/User:Yurim/Punctuation_and_Special_Characters. +// +// U+2010 hyphen, U+2012 figure dash, U+2013 en dash, U+2014 em dash, U+2015 horizontal bar, U+2018, +// U+2019, U+201C, U+201D, U+2026, U+2212 minus sign +static PATTERNS: [&'static str; 11] = ["‐", "‒", "–", "—", "―", "‘", "’", "“", "”", "…", "−"]; +static REPLACE: [&'static str; 11] = ["-", "-", "-", "-", "-", "'", "'", "\"", "\"", "...", "-"]; +static AC: Lazy = Lazy::new(|| AhoCorasick::new(&PATTERNS).unwrap()); + pub struct AppSearch { string: String, orig: ListSelection, @@ -160,12 +173,9 @@ impl IAppInteractSearchPrivate for AppMachine { } fn is_char_sensitive(artist_name: &str) -> bool { - let special_chars: &[char] = &['‐', '‒', '–', '—', '―', '−', '‘', '’', '“', '”', '…']; - artist_name.chars().any(|ch| special_chars.contains(&ch)) + AC.find(artist_name).is_some() } - // FIXME: use aho_corasick for normalization - AhoCorasick does not implement PartialEq. It - // makes more sense to be places in app.rs as it would make ArtistSelection non-trivial. fn normalize_search(search: &str, lowercase: bool, asciify: bool) -> String { let normalized = if lowercase { search.to_lowercase() @@ -173,17 +183,8 @@ impl IAppInteractSearchPrivate for AppMachine { search.to_owned() }; - // Unlikely that this covers all possible strings, but it should at least cover strings - // relevant for music (at least in English). The list of characters handled is based on - // https://wiki.musicbrainz.org/User:Yurim/Punctuation_and_Special_Characters. if asciify { - normalized - // U+2010 hyphen, U+2012 figure dash, U+2013 en dash, U+2014 em dash, - // U+2015 horizontal bar, U+2212 minus sign - .replace(['‐', '‒', '–', '—', '―', '−'], "-") - .replace(['‘', '’'], "'") // U+2018, U+2019 - .replace(['“', '”'], "\"") // U+201C, U+201D - .replace('…', "...") // U+2026 + AC.replace_all(&normalized, &REPLACE) } else { normalized } -- 2.45.2 From 49a96b5eb2a4ee2154750e21bb8135fb8ca01006 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 18 Feb 2024 20:54:55 +0100 Subject: [PATCH 33/35] Add benchmarks --- Cargo.lock | 49 +++++++++++++++++++ Cargo.toml | 4 ++ README.md | 15 ++++++ build.rs | 5 ++ src/main.rs | 4 ++ src/tui/app/machine/search.rs | 89 ++++++++++++++++++++++++++++++----- 6 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 build.rs diff --git a/Cargo.lock b/Cargo.lock index bc004e5..e240364 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,6 +235,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.1" @@ -412,6 +423,7 @@ dependencies = [ "mockall", "once_cell", "openssh", + "rand", "ratatui", "serde", "serde_json", @@ -420,6 +432,7 @@ dependencies = [ "tokio", "url", "uuid", + "version_check", ] [[package]] @@ -531,6 +544,12 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "predicates" version = "3.1.0" @@ -599,6 +618,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "ratatui" version = "0.26.0" diff --git a/Cargo.toml b/Cargo.toml index 4065a28..e188924 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,13 @@ tokio = { version = "1.36.0", features = ["rt"], optional = true} url = { version = "2.5.0" } uuid = { version = "1.7.0" } +[build-dependencies] +version_check = "0.9.4" + [dev-dependencies] mockall = "0.12.1" once_cell = "1.19.0" +rand = "0.8.5" tempfile = "3.10.0" [features] diff --git a/README.md b/README.md index 29444e9..17916e1 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,18 @@ Note that some changes may not be visible until `codecov/debug/coverage` is remo command is rerun. For most cases `cargo clean` can be replaced with `rm -rf ./codecov/debug/{coverage,profraw}`. + +## Benchmarks + +### Pre-requisites + +``` sh +rustup toolchain install nightly +``` + +### Running benchmarks + +``` sh +env BEETSDIR=./ rustup run nightly cargo bench --all-features --all-targets +``` + diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..9936968 --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +fn main() { + if let Some(true) = version_check::is_feature_flaggable() { + println!("cargo:rustc-cfg=nightly"); + } +} diff --git a/src/main.rs b/src/main.rs index 75423d0..63afadd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,7 @@ +#![cfg_attr(nightly, feature(test))] +#[cfg(nightly)] +extern crate test; + mod tui; use std::{ffi::OsString, fs::OpenOptions, io, path::PathBuf}; diff --git a/src/tui/app/machine/search.rs b/src/tui/app/machine/search.rs index 92380a8..4690067 100644 --- a/src/tui/app/machine/search.rs +++ b/src/tui/app/machine/search.rs @@ -18,9 +18,10 @@ use crate::tui::{ // // U+2010 hyphen, U+2012 figure dash, U+2013 en dash, U+2014 em dash, U+2015 horizontal bar, U+2018, // U+2019, U+201C, U+201D, U+2026, U+2212 minus sign -static PATTERNS: [&'static str; 11] = ["‐", "‒", "–", "—", "―", "‘", "’", "“", "”", "…", "−"]; -static REPLACE: [&'static str; 11] = ["-", "-", "-", "-", "-", "'", "'", "\"", "\"", "...", "-"]; -static AC: Lazy = Lazy::new(|| AhoCorasick::new(&PATTERNS).unwrap()); +static SPECIAL: [char; 11] = ['‐', '‒', '–', '—', '―', '‘', '’', '“', '”', '…', '−']; +static REPLACE: [&str; 11] = ["-", "-", "-", "-", "-", "'", "'", "\"", "\"", "...", "-"]; +static AC: Lazy = + Lazy::new(|| AhoCorasick::new(SPECIAL.map(|ch| ch.to_string())).unwrap()); pub struct AppSearch { string: String, @@ -173,20 +174,22 @@ impl IAppInteractSearchPrivate for AppMachine { } fn is_char_sensitive(artist_name: &str) -> bool { - AC.find(artist_name).is_some() + // Benchmarking reveals that using AhoCorasick is slower. At a guess, this is likely due to + // a high constant cost of AhoCorasick and the otherwise simple nature of the task. + artist_name.chars().any(|ch| SPECIAL.contains(&ch)) } fn normalize_search(search: &str, lowercase: bool, asciify: bool) -> String { - let normalized = if lowercase { - search.to_lowercase() + if lowercase { + if asciify { + AC.replace_all(&search.to_lowercase(), &REPLACE) + } else { + search.to_lowercase() + } + } else if asciify { + AC.replace_all(search, &REPLACE) } else { search.to_owned() - }; - - if asciify { - AC.replace_all(&normalized, &REPLACE) - } else { - normalized } } } @@ -439,3 +442,65 @@ mod tests { assert_eq!(browse.inner.selection.artist.state.list.selected(), None); } } + +#[cfg(nightly)] +#[cfg(test)] +mod benches { + // The purpose of these benches was to evaluate the benefit of AhoCorasick over std solutions. + + use rand::Rng; + use test::Bencher; + + use crate::tui::lib::MockIMusicHoard; + + use super::*; + + type Search = AppMachine; + + fn random_utf8_string(len: usize) -> String { + rand::thread_rng() + .sample_iter::(&rand::distributions::Standard) + .take(len) + .collect() + } + + fn random_alpanumeric_string(len: usize) -> String { + rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(len) + .map(char::from) + .collect() + } + + fn generate_sample(f: fn(usize) -> String) -> Vec { + (0..1000).map(|_| f(10)).collect() + } + + #[bench] + fn is_char_sensitive_alphanumeric(b: &mut Bencher) { + let strings = generate_sample(random_alpanumeric_string); + let mut iter = strings.iter().cycle(); + b.iter(|| test::black_box(Search::is_char_sensitive(&iter.next().unwrap()))) + } + + #[bench] + fn is_char_sensitive_utf8(b: &mut Bencher) { + let strings = generate_sample(random_utf8_string); + let mut iter = strings.iter().cycle(); + b.iter(|| test::black_box(Search::is_char_sensitive(&iter.next().unwrap()))) + } + + #[bench] + fn normalize_search_alphanumeric(b: &mut Bencher) { + let strings = generate_sample(random_alpanumeric_string); + let mut iter = strings.iter().cycle(); + b.iter(|| test::black_box(Search::normalize_search(&iter.next().unwrap(), false, true))) + } + + #[bench] + fn normalize_search_utf8(b: &mut Bencher) { + let strings = generate_sample(random_utf8_string); + let mut iter = strings.iter().cycle(); + b.iter(|| test::black_box(Search::normalize_search(&iter.next().unwrap(), false, true))) + } +} -- 2.45.2 From 68b70f6b040d1b292307358976196de99b22443a Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 18 Feb 2024 21:54:54 +0100 Subject: [PATCH 34/35] Complete unit tests --- .gitea/workflows/gitea-ci.yaml | 1 + README.md | 1 + src/tui/app/machine/browse.rs | 66 +++++++++++++++++++++-- src/tui/app/machine/info.rs | 7 +++ src/tui/app/machine/mod.rs | 98 +++++++++++++++++++++++++++++++--- src/tui/app/machine/reload.rs | 7 +++ src/tui/app/machine/search.rs | 30 +++++++++-- 7 files changed, 195 insertions(+), 15 deletions(-) diff --git a/.gitea/workflows/gitea-ci.yaml b/.gitea/workflows/gitea-ci.yaml index 9b25437..0ec458d 100644 --- a/.gitea/workflows/gitea-ci.yaml +++ b/.gitea/workflows/gitea-ci.yaml @@ -32,6 +32,7 @@ jobs: --output-types html --source-dir . --ignore-not-existing + --ignore "build.rs" --ignore "tests/*" --ignore "src/main.rs" --ignore "src/bin/musichoard-edit.rs" diff --git a/README.md b/README.md index 17916e1..cb4f20c 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ grcov codecov/debug/profraw \ --output-types html \ --source-dir . \ --ignore-not-existing \ + --ignore "build.rs" \ --ignore "tests/*" \ --ignore "src/main.rs" \ --ignore "src/bin/musichoard-edit.rs" \ diff --git a/src/tui/app/machine/browse.rs b/src/tui/app/machine/browse.rs index abc73d8..f327ee3 100644 --- a/src/tui/app/machine/browse.rs +++ b/src/tui/app/machine/browse.rs @@ -93,9 +93,12 @@ impl IAppInteractBrowse for AppMachine { #[cfg(test)] mod tests { - use crate::tui::app::{ - machine::tests::{inner, music_hoard}, - IAppInteract, + use crate::tui::{ + app::{ + machine::tests::{inner, music_hoard}, + Category, IAppInteract, + }, + testmod::COLLECTION, }; use super::*; @@ -132,6 +135,49 @@ mod tests { app.unwrap_error(); } + #[test] + fn increment_decrement() { + let mut browse = AppMachine::browse(inner(music_hoard(COLLECTION.to_owned()))); + let sel = &browse.inner.selection; + assert_eq!(sel.active, Category::Artist); + assert_eq!(sel.artist.state.list.selected(), Some(0)); + + browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let sel = &browse.inner.selection; + assert_eq!(sel.active, Category::Artist); + assert_eq!(sel.artist.state.list.selected(), Some(1)); + + browse = browse.increment_category().unwrap_browse(); + let sel = &browse.inner.selection; + assert_eq!(sel.active, Category::Album); + assert_eq!(sel.artist.state.list.selected(), Some(1)); + assert_eq!(sel.artist.album.state.list.selected(), Some(0)); + + browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let sel = &browse.inner.selection; + assert_eq!(sel.active, Category::Album); + assert_eq!(sel.artist.state.list.selected(), Some(1)); + assert_eq!(sel.artist.album.state.list.selected(), Some(1)); + + browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let sel = &browse.inner.selection; + assert_eq!(sel.active, Category::Album); + assert_eq!(sel.artist.state.list.selected(), Some(1)); + assert_eq!(sel.artist.album.state.list.selected(), Some(0)); + + browse = browse.decrement_category().unwrap_browse(); + let sel = &browse.inner.selection; + assert_eq!(sel.active, Category::Artist); + assert_eq!(sel.artist.state.list.selected(), Some(1)); + assert_eq!(sel.artist.album.state.list.selected(), Some(0)); + + browse = browse.decrement_selection(Delta::Line).unwrap_browse(); + let sel = &browse.inner.selection; + assert_eq!(sel.active, Category::Artist); + assert_eq!(sel.artist.state.list.selected(), Some(0)); + assert_eq!(sel.artist.album.state.list.selected(), Some(0)); + } + #[test] fn show_info_overlay() { let browse = AppMachine::browse(inner(music_hoard(vec![]))); @@ -145,4 +191,18 @@ mod tests { let app = browse.show_reload_menu(); app.unwrap_reload(); } + + #[test] + fn begin_search() { + let browse = AppMachine::browse(inner(music_hoard(vec![]))); + let app = browse.begin_search(); + app.unwrap_search(); + } + + #[test] + fn no_op() { + let browse = AppMachine::browse(inner(music_hoard(vec![]))); + let app = browse.no_op(); + app.unwrap_browse(); + } } diff --git a/src/tui/app/machine/info.rs b/src/tui/app/machine/info.rs index 203bb74..e6e005d 100644 --- a/src/tui/app/machine/info.rs +++ b/src/tui/app/machine/info.rs @@ -56,4 +56,11 @@ mod tests { let app = info.hide_info_overlay(); app.unwrap_browse(); } + + #[test] + fn no_op() { + let info = AppMachine::info(inner(music_hoard(vec![]))); + let app = info.no_op(); + app.unwrap_info(); + } } diff --git a/src/tui/app/machine/mod.rs b/src/tui/app/machine/mod.rs index 47c73a8..d5bea93 100644 --- a/src/tui/app/machine/mod.rs +++ b/src/tui/app/machine/mod.rs @@ -136,9 +136,8 @@ mod tests { use musichoard::collection::Collection; use crate::tui::{ - app::{AppState, IAppInteract}, + app::{AppState, IAppInteract, IAppInteractBrowse}, lib::MockIMusicHoard, - testmod::COLLECTION, }; use super::*; @@ -214,21 +213,107 @@ mod tests { } #[test] - fn force_quit() { - let app = App::new(music_hoard_init(COLLECTION.to_owned())); + fn state_browse() { + let mut app = App::new(music_hoard_init(vec![])); assert!(app.is_running()); + let state = app.state(); + matches!(state, AppState::Browse(_)); + app = state; + + let public = app.get(); + matches!(public.state, AppState::Browse(_)); + let app = app.force_quit(); assert!(!app.is_running()); } #[test] - fn error_force_quit() { - let mut app = App::new(music_hoard_init(COLLECTION.to_owned())); + fn state_info() { + let mut app = App::new(music_hoard_init(vec![])); + assert!(app.is_running()); + + app = app.unwrap_browse().show_info_overlay(); + + let state = app.state(); + matches!(state, AppState::Info(_)); + app = state; + + let public = app.get(); + matches!(public.state, AppState::Info(_)); + + let app = app.force_quit(); + assert!(!app.is_running()); + } + + #[test] + fn state_reload() { + let mut app = App::new(music_hoard_init(vec![])); + assert!(app.is_running()); + + app = app.unwrap_browse().show_reload_menu(); + + let state = app.state(); + matches!(state, AppState::Reload(_)); + app = state; + + let public = app.get(); + matches!(public.state, AppState::Reload(_)); + + let app = app.force_quit(); + assert!(!app.is_running()); + } + + #[test] + fn state_search() { + let mut app = App::new(music_hoard_init(vec![])); + assert!(app.is_running()); + + app = app.unwrap_browse().begin_search(); + + let state = app.state(); + matches!(state, AppState::Search(_)); + app = state; + + let public = app.get(); + matches!(public.state, AppState::Search("")); + + let app = app.force_quit(); + assert!(!app.is_running()); + } + + #[test] + fn state_error() { + let mut app = App::new(music_hoard_init(vec![])); assert!(app.is_running()); app = AppMachine::error(app.unwrap_browse().inner, "get rekt").into(); + let state = app.state(); + matches!(state, AppState::Error(_)); + app = state; + + let public = app.get(); + matches!(public.state, AppState::Error("get rekt")); + + app = app.force_quit(); + assert!(!app.is_running()); + } + + #[test] + fn state_critical() { + let mut app = App::new(music_hoard_init(vec![])); + assert!(app.is_running()); + + app = AppMachine::critical(app.unwrap_browse().inner, "get rekt").into(); + + let state = app.state(); + matches!(state, AppState::Critical(_)); + app = state; + + let public = app.get(); + matches!(public.state, AppState::Critical("get rekt")); + app = app.force_quit(); assert!(!app.is_running()); } @@ -244,7 +329,6 @@ mod tests { music_hoard.expect_get_collection().return_const(vec![]); let app = App::new(music_hoard); - assert!(app.is_running()); app.unwrap_critical(); } diff --git a/src/tui/app/machine/reload.rs b/src/tui/app/machine/reload.rs index 19a0164..79564f8 100644 --- a/src/tui/app/machine/reload.rs +++ b/src/tui/app/machine/reload.rs @@ -134,4 +134,11 @@ mod tests { let app = reload.reload_database(); app.unwrap_error(); } + + #[test] + fn no_op() { + let reload = AppMachine::reload(inner(music_hoard(vec![]))); + let app = reload.no_op(); + app.unwrap_reload(); + } } diff --git a/src/tui/app/machine/search.rs b/src/tui/app/machine/search.rs index 4690067..ebde2b8 100644 --- a/src/tui/app/machine/search.rs +++ b/src/tui/app/machine/search.rs @@ -180,14 +180,14 @@ impl IAppInteractSearchPrivate for AppMachine { } fn normalize_search(search: &str, lowercase: bool, asciify: bool) -> String { - if lowercase { - if asciify { + if asciify { + if lowercase { AC.replace_all(&search.to_lowercase(), &REPLACE) } else { - search.to_lowercase() + AC.replace_all(search, &REPLACE) } - } else if asciify { - AC.replace_all(search, &REPLACE) + } else if lowercase { + search.to_lowercase() } else { search.to_owned() } @@ -282,6 +282,19 @@ mod tests { search.incremental_search(false); assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + // Non-lowercase, non-ascii. + let mut search = + AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.state.string = String::from("Album_Artist "); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0)); + + search.state.string = String::from("Album_Artist ‘C’"); + search.incremental_search(false); + assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2)); + // Stop at name, not sort name. let mut search = AppMachine::search(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); @@ -441,6 +454,13 @@ mod tests { let browse = search.cancel_search().unwrap_browse(); assert_eq!(browse.inner.selection.artist.state.list.selected(), None); } + + #[test] + fn no_op() { + let search = AppMachine::search(inner(music_hoard(vec![])), orig(None)); + let app = search.no_op(); + app.unwrap_search(); + } } #[cfg(nightly)] -- 2.45.2 From 652dda30160a9f19c9bb50311bac31ae076f1577 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 18 Feb 2024 22:09:25 +0100 Subject: [PATCH 35/35] Typos --- README.md | 1 - src/tui/handler.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index cb4f20c..f254767 100644 --- a/README.md +++ b/README.md @@ -60,4 +60,3 @@ rustup toolchain install nightly ``` sh env BEETSDIR=./ rustup run nightly cargo bench --all-features --all-targets ``` - diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 1f63d5d..a9d73d6 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -94,7 +94,7 @@ impl IEventHandlerPrivate for EventHandler { KeyCode::PageDown => app.increment_selection(Delta::Page), // Toggle info overlay. KeyCode::Char('m') | KeyCode::Char('M') => app.show_info_overlay(), - // Toggle reload meny. + // Toggle reload menu. KeyCode::Char('g') | KeyCode::Char('G') => app.show_reload_menu(), // Toggle search. KeyCode::Char('s') | KeyCode::Char('S') => { -- 2.45.2