Provide search functionality through the TUI #134
@ -7,28 +7,33 @@ use crate::tui::{
|
|||||||
lib::IMusicHoard,
|
lib::IMusicHoard,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum AppState<BS, IS, RS, ES, CS> {
|
pub enum AppState<BS, IS, RS, SS, ES, CS> {
|
||||||
Browse(BS),
|
Browse(BS),
|
||||||
Info(IS),
|
Info(IS),
|
||||||
Reload(RS),
|
Reload(RS),
|
||||||
|
Search(SS),
|
||||||
Error(ES),
|
Error(ES),
|
||||||
Critical(CS),
|
Critical(CS),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<BS, IS, RS, ES, CS> AppState<BS, IS, RS, ES, CS> {
|
impl<BS, IS, RS, SS, ES, CS> AppState<BS, IS, RS, SS, ES, CS> {
|
||||||
fn is_browse(&self) -> bool {
|
pub fn is_browse(&self) -> bool {
|
||||||
matches!(self, AppState::Browse(_))
|
matches!(self, AppState::Browse(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_info(&self) -> bool {
|
pub fn is_info(&self) -> bool {
|
||||||
matches!(self, AppState::Info(_))
|
matches!(self, AppState::Info(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_reload(&self) -> bool {
|
pub fn is_reload(&self) -> bool {
|
||||||
matches!(self, AppState::Reload(_))
|
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(_))
|
matches!(self, AppState::Error(_))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,6 +42,7 @@ pub trait IAppInteract {
|
|||||||
type BS: IAppInteractBrowse;
|
type BS: IAppInteractBrowse;
|
||||||
type IS: IAppInteractInfo;
|
type IS: IAppInteractInfo;
|
||||||
type RS: IAppInteractReload;
|
type RS: IAppInteractReload;
|
||||||
|
type SS: IAppInteractSearch;
|
||||||
type ES: IAppInteractError;
|
type ES: IAppInteractError;
|
||||||
type CS: IAppInteractCritical;
|
type CS: IAppInteractCritical;
|
||||||
|
|
||||||
@ -46,7 +52,14 @@ pub trait IAppInteract {
|
|||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn state(
|
fn state(
|
||||||
&mut self,
|
&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 {
|
pub trait IAppInteractBrowse {
|
||||||
@ -61,6 +74,8 @@ pub trait IAppInteractBrowse {
|
|||||||
fn show_info_overlay(&mut self);
|
fn show_info_overlay(&mut self);
|
||||||
|
|
||||||
fn show_reload_menu(&mut self);
|
fn show_reload_menu(&mut self);
|
||||||
|
|
||||||
|
fn begin_search(&mut self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait IAppInteractInfo {
|
pub trait IAppInteractInfo {
|
||||||
@ -73,6 +88,12 @@ pub trait IAppInteractReload {
|
|||||||
fn hide_reload_menu(&mut self);
|
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 {
|
pub trait IAppInteractError {
|
||||||
fn dismiss_error(&mut self);
|
fn dismiss_error(&mut self);
|
||||||
}
|
}
|
||||||
@ -87,7 +108,7 @@ pub trait IAppAccess {
|
|||||||
fn get(&mut self) -> AppPublic;
|
fn get(&mut self) -> AppPublic;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type AppPublicState = AppState<(), (), (), String, String>;
|
pub type AppPublicState = AppState<(), (), (), String, String, String>;
|
||||||
|
|
||||||
pub struct AppPublic<'app> {
|
pub struct AppPublic<'app> {
|
||||||
pub collection: &'app Collection,
|
pub collection: &'app Collection,
|
||||||
@ -99,7 +120,7 @@ pub struct App<MH: IMusicHoard> {
|
|||||||
running: bool,
|
running: bool,
|
||||||
music_hoard: MH,
|
music_hoard: MH,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
state: AppState<(), (), (), String, String>,
|
state: AppState<(), (), (), String, String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> App<MH> {
|
impl<MH: IMusicHoard> App<MH> {
|
||||||
@ -128,6 +149,7 @@ impl<MH: IMusicHoard> IAppInteract for App<MH> {
|
|||||||
type BS = Self;
|
type BS = Self;
|
||||||
type IS = Self;
|
type IS = Self;
|
||||||
type RS = Self;
|
type RS = Self;
|
||||||
|
type SS = Self;
|
||||||
type ES = Self;
|
type ES = Self;
|
||||||
type CS = Self;
|
type CS = Self;
|
||||||
|
|
||||||
@ -141,11 +163,19 @@ impl<MH: IMusicHoard> IAppInteract for App<MH> {
|
|||||||
|
|
||||||
fn state(
|
fn state(
|
||||||
&mut self,
|
&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 {
|
match self.state {
|
||||||
AppState::Browse(_) => AppState::Browse(self),
|
AppState::Browse(_) => AppState::Browse(self),
|
||||||
AppState::Info(_) => AppState::Info(self),
|
AppState::Info(_) => AppState::Info(self),
|
||||||
AppState::Reload(_) => AppState::Reload(self),
|
AppState::Reload(_) => AppState::Reload(self),
|
||||||
|
AppState::Search(_) => AppState::Search(self),
|
||||||
AppState::Error(_) => AppState::Error(self),
|
AppState::Error(_) => AppState::Error(self),
|
||||||
AppState::Critical(_) => AppState::Critical(self),
|
AppState::Critical(_) => AppState::Critical(self),
|
||||||
}
|
}
|
||||||
@ -190,6 +220,11 @@ impl<MH: IMusicHoard> IAppInteractBrowse for App<MH> {
|
|||||||
assert!(self.state.is_browse());
|
assert!(self.state.is_browse());
|
||||||
self.state = AppState::Reload(());
|
self.state = AppState::Reload(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn begin_search(&mut self) {
|
||||||
|
assert!(self.state.is_browse());
|
||||||
|
self.state = AppState::Search(String::new());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> IAppInteractInfo for App<MH> {
|
impl<MH: IMusicHoard> IAppInteractInfo for App<MH> {
|
||||||
@ -236,6 +271,27 @@ impl<MH: IMusicHoard> IAppInteractReloadPrivate for App<MH> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<MH: IMusicHoard> IAppInteractSearch for App<MH> {
|
||||||
|
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<MH: IMusicHoard> IAppInteractError for App<MH> {
|
impl<MH: IMusicHoard> IAppInteractError for App<MH> {
|
||||||
fn dismiss_error(&mut self) {
|
fn dismiss_error(&mut self) {
|
||||||
assert!(self.state.is_error());
|
assert!(self.state.is_error());
|
||||||
|
@ -7,7 +7,7 @@ use crate::tui::{
|
|||||||
app::{
|
app::{
|
||||||
app::{
|
app::{
|
||||||
AppState, IAppInteract, IAppInteractBrowse, IAppInteractError, IAppInteractInfo,
|
AppState, IAppInteract, IAppInteractBrowse, IAppInteractError, IAppInteractInfo,
|
||||||
IAppInteractReload,
|
IAppInteractReload, IAppInteractSearch,
|
||||||
},
|
},
|
||||||
selection::Delta,
|
selection::Delta,
|
||||||
},
|
},
|
||||||
@ -24,6 +24,7 @@ trait IEventHandlerPrivate<APP: IAppInteract> {
|
|||||||
fn handle_browse_key_event(app: &mut <APP as IAppInteract>::BS, key_event: KeyEvent);
|
fn handle_browse_key_event(app: &mut <APP as IAppInteract>::BS, key_event: KeyEvent);
|
||||||
fn handle_info_key_event(app: &mut <APP as IAppInteract>::IS, key_event: KeyEvent);
|
fn handle_info_key_event(app: &mut <APP as IAppInteract>::IS, key_event: KeyEvent);
|
||||||
fn handle_reload_key_event(app: &mut <APP as IAppInteract>::RS, key_event: KeyEvent);
|
fn handle_reload_key_event(app: &mut <APP as IAppInteract>::RS, key_event: KeyEvent);
|
||||||
|
fn handle_search_key_event(app: &mut <APP as IAppInteract>::SS, key_event: KeyEvent);
|
||||||
fn handle_error_key_event(app: &mut <APP as IAppInteract>::ES, key_event: KeyEvent);
|
fn handle_error_key_event(app: &mut <APP as IAppInteract>::ES, key_event: KeyEvent);
|
||||||
fn handle_critical_key_event(app: &mut <APP as IAppInteract>::CS, key_event: KeyEvent);
|
fn handle_critical_key_event(app: &mut <APP as IAppInteract>::CS, key_event: KeyEvent);
|
||||||
}
|
}
|
||||||
@ -69,6 +70,9 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
|
|||||||
AppState::Reload(reload) => {
|
AppState::Reload(reload) => {
|
||||||
<Self as IEventHandlerPrivate<APP>>::handle_reload_key_event(reload, key_event);
|
<Self as IEventHandlerPrivate<APP>>::handle_reload_key_event(reload, key_event);
|
||||||
}
|
}
|
||||||
|
AppState::Search(search) => {
|
||||||
|
<Self as IEventHandlerPrivate<APP>>::handle_search_key_event(search, key_event);
|
||||||
|
}
|
||||||
AppState::Error(error) => {
|
AppState::Error(error) => {
|
||||||
<Self as IEventHandlerPrivate<APP>>::handle_error_key_event(error, key_event);
|
<Self as IEventHandlerPrivate<APP>>::handle_error_key_event(error, key_event);
|
||||||
}
|
}
|
||||||
@ -96,10 +100,16 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
|
|||||||
KeyCode::Down => app.increment_selection(Delta::Line),
|
KeyCode::Down => app.increment_selection(Delta::Line),
|
||||||
KeyCode::PageUp => app.decrement_selection(Delta::Page),
|
KeyCode::PageUp => app.decrement_selection(Delta::Page),
|
||||||
KeyCode::PageDown => app.increment_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(),
|
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(),
|
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.
|
// Othey keys.
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -134,6 +144,18 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_search_key_event(app: &mut <APP as IAppInteract>::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 <APP as IAppInteract>::ES, _key_event: KeyEvent) {
|
fn handle_error_key_event(app: &mut <APP as IAppInteract>::ES, _key_event: KeyEvent) {
|
||||||
// Any key dismisses the error.
|
// Any key dismisses the error.
|
||||||
app.dismiss_error();
|
app.dismiss_error();
|
||||||
|
@ -353,12 +353,13 @@ struct Minibuffer<'a> {
|
|||||||
|
|
||||||
impl Minibuffer<'_> {
|
impl Minibuffer<'_> {
|
||||||
fn paragraphs(state: &AppPublicState) -> Self {
|
fn paragraphs(state: &AppPublicState) -> Self {
|
||||||
let columns = 2;
|
let columns = 3;
|
||||||
match state {
|
let mut mb = match state {
|
||||||
AppState::Browse(_) => Minibuffer {
|
AppState::Browse(_) => Minibuffer {
|
||||||
paragraphs: vec![
|
paragraphs: vec![
|
||||||
Paragraph::new("m: show info overlay"),
|
Paragraph::new("m: show info overlay"),
|
||||||
Paragraph::new("g: show reload menu"),
|
Paragraph::new("g: show reload menu"),
|
||||||
|
Paragraph::new("ctrl+s: search artist"),
|
||||||
],
|
],
|
||||||
columns,
|
columns,
|
||||||
},
|
},
|
||||||
@ -374,6 +375,10 @@ impl Minibuffer<'_> {
|
|||||||
],
|
],
|
||||||
columns,
|
columns,
|
||||||
},
|
},
|
||||||
|
AppState::Search(ref s) => Minibuffer {
|
||||||
|
paragraphs: vec![Paragraph::new(format!("I-search: {s}"))],
|
||||||
|
columns: 1,
|
||||||
|
},
|
||||||
AppState::Error(_) => Minibuffer {
|
AppState::Error(_) => Minibuffer {
|
||||||
paragraphs: vec![Paragraph::new(
|
paragraphs: vec![Paragraph::new(
|
||||||
"Press any key to dismiss the error message...",
|
"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...")],
|
paragraphs: vec![Paragraph::new("Press ctrl+c to terminate the program...")],
|
||||||
columns: 1,
|
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) {
|
fn render_minibuffer(state: &AppPublicState, ar: Rect, fr: &mut Frame) {
|
||||||
let mut mb = Minibuffer::paragraphs(state);
|
let mb = Minibuffer::paragraphs(state);
|
||||||
mb.paragraphs = mb
|
|
||||||
.paragraphs
|
|
||||||
.into_iter()
|
|
||||||
.map(|p| p.alignment(Alignment::Center))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
|
let space = 3;
|
||||||
let area = Rect {
|
let area = Rect {
|
||||||
x: ar.x + 1,
|
x: ar.x + 1 + space,
|
||||||
y: ar.y + 1,
|
y: ar.y + 1,
|
||||||
width: ar.width.saturating_sub(2),
|
width: ar.width.saturating_sub(2 + 2 * space),
|
||||||
height: 1,
|
height: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user