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, };