Provide search functionality through the TUI #134
@ -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) {
|
||||||
|
@ -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,6 +214,7 @@ 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.
|
||||||
|
if asciify {
|
||||||
normalized
|
normalized
|
||||||
.replace("‐", "-") // U+2010 hyphen
|
.replace("‐", "-") // U+2010 hyphen
|
||||||
.replace("‒", "-") // U+2012 figure dash
|
.replace("‒", "-") // U+2012 figure dash
|
||||||
@ -214,26 +227,106 @@ impl ArtistSelection {
|
|||||||
.replace("”", "\"") // U+201D
|
.replace("”", "\"") // U+201D
|
||||||
.replace("…", "...") // U+2026
|
.replace("…", "...") // U+2026
|
||||||
.replace("−", "-") // U+2212 minus sign
|
.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);
|
}
|
||||||
|
|
||||||
|
fn is_char_sensitive(artist_name: &str) -> bool {
|
||||||
|
let special_chars: &[char] = &['‐', '‒', '–', '—', '―', '‘', '’', '“', '”', '…', '−'];
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 slice = &artists[index..];
|
||||||
|
|
||||||
let result = slice.binary_search_by(|probe| {
|
let result = slice.iter().position(|probe| {
|
||||||
Self::normalize_search_string(&probe.get_sort_key().name, !case_sensitive)
|
Self::incremental_search_predicate(case_sensitive, char_sensitive, &search, probe)
|
||||||
.cmp(&search_name)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let new_index = match result {
|
let new_index = match result {
|
||||||
Ok(slice_index) | Err(slice_index) => index + slice_index,
|
Some(slice_index) => index + slice_index,
|
||||||
|
None => artists.len(),
|
||||||
};
|
};
|
||||||
self.select_to(artists, new_index);
|
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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user