Provide search functionality through the TUI #134

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

View File

@ -139,6 +139,9 @@ pub struct App<MH: IMusicHoard> {
music_hoard: MH, music_hoard: MH,
selection: Selection, selection: Selection,
state: AppState<(), (), (), String, String, String>, state: AppState<(), (), (), String, String, String>,
// FIXME: is it possible to use a wrapper struct? - when state() is called return a wrapper
// around App<MH> which will contain App.
memo: Vec<Option<usize>>,
} }
impl<MH: IMusicHoard> App<MH> { impl<MH: IMusicHoard> App<MH> {
@ -153,6 +156,7 @@ impl<MH: IMusicHoard> App<MH> {
music_hoard, music_hoard,
selection, selection,
state, state,
memo: vec![],
} }
} }
@ -242,6 +246,7 @@ impl<MH: IMusicHoard> IAppInteractBrowse for App<MH> {
fn begin_search(&mut self) { fn begin_search(&mut self) {
assert!(self.state.is_browse()); assert!(self.state.is_browse());
self.state = AppState::Search(String::new()); self.state = AppState::Search(String::new());
self.memo = vec![self.selection.selected_artist()];
self.selection self.selection
.reset_artist(self.music_hoard.get_collection()); .reset_artist(self.music_hoard.get_collection());
} }
@ -291,25 +296,27 @@ impl<MH: IMusicHoard> IAppInteractReloadPrivate for App<MH> {
} }
} }
// 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<MH: IMusicHoard> IAppInteractSearch for App<MH> { impl<MH: IMusicHoard> IAppInteractSearch for App<MH> {
fn append_character(&mut self, ch: char) { fn append_character(&mut self, ch: char) {
let collection = self.music_hoard.get_collection(); let collection = self.music_hoard.get_collection();
let search = self.state.as_mut().unwrap_search(); let search = self.state.as_mut().unwrap_search();
search.push(ch); 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) { fn remove_character(&mut self) {
let collection = self.music_hoard.get_collection(); let collection = self.music_hoard.get_collection();
let search = self.state.as_mut().unwrap_search(); let search = self.state.as_mut().unwrap_search();
search.pop(); if search.pop().is_some() {
self.selection.reset_artist(&collection); let prev = self.memo.pop().unwrap();
self.selection.incremental_artist_search(collection, search); self.selection.select_artist(collection, prev);
}
} }
fn finish_search(&mut self) { fn finish_search(&mut self) {

View File

@ -5,6 +5,7 @@ use musichoard::collection::{
Collection, Collection,
}; };
use ratatui::widgets::ListState; use ratatui::widgets::ListState;
use std::cmp;
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Category { 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) { pub fn select(&mut self, artists: &[Artist], selected: ActiveSelection) {
self.artist.reinitialise(artists, selected.artist); self.artist.reinitialise(artists, selected.artist);
} }
pub fn selected_artist(&mut self) -> Option<usize> {
self.artist.state.list.selected()
}
pub fn select_artist(&mut self, artists: &[Artist], index: Option<usize>) {
self.artist.select(artists, index);
}
pub fn reset_artist(&mut self, artists: &[Artist]) { pub fn reset_artist(&mut self, artists: &[Artist]) {
if self.artist.state.list.selected() != Some(0) { if self.artist.state.list.selected() != Some(0) {
self.select(artists, ActiveSelection { artist: None }); self.select(artists, ActiveSelection { artist: None });
@ -95,8 +105,12 @@ impl Selection {
}; };
} }
pub fn incremental_artist_search(&mut self, collection: &Collection, artist_name: &str) { pub fn incremental_artist_search(
self.artist.incremental_search(collection, artist_name); &mut self,
collection: &Collection,
artist_name: &str,
) -> Option<usize> {
self.artist.incremental_search(collection, artist_name)
} }
pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) { pub fn increment_selection(&mut self, collection: &Collection, delta: Delta) {
@ -182,10 +196,13 @@ impl ArtistSelection {
} }
} }
fn select_to(&mut self, artists: &[Artist], mut to: usize) { fn select(&mut self, artists: &[Artist], mut to: Option<usize>) {
if to >= artists.len() { to = to.map(|i| cmp::min(i, artists.len() - 1));
to = artists.len() - 1; self.state.list.select(to);
} }
fn select_to(&mut self, artists: &[Artist], mut to: usize) {
to = cmp::min(to, artists.len() - 1);
if self.state.list.selected() != Some(to) { if self.state.list.selected() != Some(to) {
self.state.list.select(Some(to)); self.state.list.select(Some(to));
self.album = AlbumSelection::initialise(&artists[to].albums); self.album = AlbumSelection::initialise(&artists[to].albums);
@ -249,8 +266,9 @@ impl ArtistSelection {
result result
} }
// FIXME: Use memoization - the only way to have correct behaviour fn incremental_search(&mut self, artists: &[Artist], artist_name: &str) -> Option<usize> {
fn incremental_search(&mut self, artists: &[Artist], artist_name: &str) { let previous = self.state.list.selected();
if let Some(index) = self.state.list.selected() { if let Some(index) = self.state.list.selected() {
let case_sensitive = Self::is_case_sensitive(artist_name); let case_sensitive = Self::is_case_sensitive(artist_name);
let char_sensitive = Self::is_char_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); self.select_to(artists, index + slice_index);
} }
} }
previous
} }
fn increment_by(&mut self, artists: &[Artist], by: usize) { fn increment_by(&mut self, artists: &[Artist], by: usize) {