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
3 changed files with 134 additions and 38 deletions
Showing only changes of commit 77790b8944 - Show all commits

View File

@ -7,6 +7,8 @@ use crate::tui::{
lib::IMusicHoard, lib::IMusicHoard,
}; };
use super::selection::IncSearch;
pub enum AppState<BS, IS, RS, SS, ES, CS> { pub enum AppState<BS, IS, RS, SS, ES, CS> {
Browse(BS), Browse(BS),
Info(IS), Info(IS),
@ -293,19 +295,19 @@ impl<MH: IMusicHoard> IAppInteractReloadPrivate for App<MH> {
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 s = self.state.as_mut().unwrap_search(); let collection = self.music_hoard.get_collection();
s.push(ch); let search = self.state.as_mut().unwrap_search();
search.push(ch);
self.selection self.selection
.incremental_artist_search(self.music_hoard.get_collection(), s); .incremental_artist_search(IncSearch::Forward, collection, search);
} }
fn remove_character(&mut self) { fn remove_character(&mut self) {
let s = self.state.as_mut().unwrap_search(); let collection = self.music_hoard.get_collection();
s.pop(); let search = self.state.as_mut().unwrap_search();
search.pop();
self.selection self.selection
.reset_artist(self.music_hoard.get_collection()); .incremental_artist_search(IncSearch::Reverse, collection, search);
self.selection
.incremental_artist_search(self.music_hoard.get_collection(), s);
} }
fn finish_search(&mut self) { fn finish_search(&mut self) {

View File

@ -61,6 +61,11 @@ impl Delta {
} }
} }
pub enum IncSearch {
Forward,
Reverse,
}
impl Selection { impl Selection {
pub fn new(artists: &[Artist]) -> Self { pub fn new(artists: &[Artist]) -> Self {
Selection { Selection {
@ -95,8 +100,14 @@ 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,
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) { 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 { let normalized = if lowercase {
search.to_lowercase() search.to_lowercase()
} else { } else {
@ -202,40 +214,121 @@ impl ArtistSelection {
// Unlikely that this covers all possible strings, but it should at least cover strings // 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 // 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. // https://wiki.musicbrainz.org/User:Yurim/Punctuation_and_Special_Characters.
normalized if asciify {
.replace("", "-") // U+2010 hyphen normalized
.replace("", "-") // U+2012 figure dash .replace("", "-") // U+2010 hyphen
.replace("", "-") // U+2013 en dash .replace("", "-") // U+2012 figure dash
.replace("", "-") // U+2014 em dash .replace("", "-") // U+2013 en dash
.replace("", "-") // U+2015 horizontal bar .replace("", "-") // U+2014 em dash
.replace("", "'") // U+2018 .replace("", "-") // U+2015 horizontal bar
.replace("", "'") // U+2019 .replace("", "'") // U+2018
.replace("", "\"") // U+201C .replace("", "'") // U+2019
.replace("", "\"") // U+201D .replace("", "\"") // U+201C
.replace("", "...") // U+2026 .replace("", "\"") // U+201D
.replace("", "-") // U+2212 minus sign .replace("", "...") // U+2026
.replace("", "-") // U+2212 minus sign
} else {
normalized
}
} }
fn incremental_search(&mut self, artists: &[Artist], artist_name: &str) { fn is_case_sensitive(artist_name: &str) -> bool {
if let Some(index) = self.state.list.selected() { artist_name
let case_sensitive = artist_name .chars()
.chars() .any(|ch| ch.is_alphabetic() && ch.is_uppercase())
.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 = slice.binary_search_by(|probe| { fn is_char_sensitive(artist_name: &str) -> bool {
Self::normalize_search_string(&probe.get_sort_key().name, !case_sensitive) let special_chars: &[char] = &['', '', '', '—', '―', '', '', '“', '”', '…', ''];
.cmp(&search_name) artist_name.chars().any(|ch| special_chars.contains(&ch))
}); }
let new_index = match result { // FIXME: compare against both sort key and actual name
Ok(slice_index) | Err(slice_index) => index + slice_index, fn incremental_search_predicate(
}; case_sensitive: bool,
self.select_to(artists, new_index); 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) { fn increment_by(&mut self, artists: &[Artist], by: usize) {
if let Some(index) = self.state.list.selected() { if let Some(index) = self.state.list.selected() {
let result = index.saturating_add(by); let result = index.saturating_add(by);

View File

@ -58,6 +58,7 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
// Exit application on `Ctrl-C`. // Exit application on `Ctrl-C`.
KeyCode::Char('c') | KeyCode::Char('C') => { KeyCode::Char('c') | KeyCode::Char('C') => {
app.force_quit(); app.force_quit();
return;
} }
_ => {} _ => {}
} }