160 lines
4.6 KiB
Rust
160 lines
4.6 KiB
Rust
use std::io::{self, Write};
|
|
|
|
use crossterm::{
|
|
cursor,
|
|
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
|
|
execute,
|
|
style::{self, Stylize},
|
|
terminal::{self, ClearType},
|
|
};
|
|
|
|
pub fn fuzzy_match(query: &str, name: &str) -> bool {
|
|
let name_lower = name.to_lowercase();
|
|
let mut chars = name_lower.chars();
|
|
for qch in query.chars() {
|
|
loop {
|
|
match chars.next() {
|
|
Some(ch) if ch == qch => break,
|
|
Some(_) => continue,
|
|
None => return false,
|
|
}
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
pub fn run_artist_picker(artists: &[(String, String)]) -> Option<(String, String)> {
|
|
let mut stdout = io::stdout();
|
|
terminal::enable_raw_mode().ok()?;
|
|
execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide).ok()?;
|
|
|
|
let result = picker_loop(&mut stdout, artists);
|
|
|
|
execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen).ok();
|
|
terminal::disable_raw_mode().ok();
|
|
|
|
result
|
|
}
|
|
|
|
fn picker_loop(stdout: &mut io::Stdout, artists: &[(String, String)]) -> Option<(String, String)> {
|
|
let mut query = String::new();
|
|
let mut selected: usize = 0;
|
|
let mut scroll: usize = 0;
|
|
|
|
loop {
|
|
let (w, h) = terminal::size().ok()?;
|
|
let w = w as usize;
|
|
let h = h as usize;
|
|
if h < 2 {
|
|
continue;
|
|
}
|
|
|
|
let q = query.to_lowercase();
|
|
let filtered: Vec<&(String, String)> = if q.is_empty() {
|
|
artists.iter().collect()
|
|
} else {
|
|
artists.iter().filter(|(_, name)| fuzzy_match(&q, name)).collect()
|
|
};
|
|
|
|
if filtered.is_empty() {
|
|
selected = 0;
|
|
} else {
|
|
selected = selected.min(filtered.len() - 1);
|
|
}
|
|
|
|
// Scroll bounds
|
|
let list_h = h - 1;
|
|
if selected < scroll {
|
|
scroll = selected;
|
|
}
|
|
if selected >= scroll + list_h {
|
|
scroll = selected - list_h + 1;
|
|
}
|
|
|
|
// Draw
|
|
execute!(stdout, terminal::Clear(ClearType::All)).ok();
|
|
|
|
// Prompt line
|
|
let prompt = format!(" > {query}");
|
|
let count_str = format!(" {}/{}", filtered.len(), artists.len());
|
|
execute!(
|
|
stdout,
|
|
cursor::MoveTo(0, 0),
|
|
style::PrintStyledContent(prompt.as_str().cyan().bold()),
|
|
style::PrintStyledContent(count_str.as_str().dark_grey()),
|
|
)
|
|
.ok();
|
|
|
|
// Artist list
|
|
for i in 0..list_h {
|
|
let idx = scroll + i;
|
|
if idx >= filtered.len() {
|
|
break;
|
|
}
|
|
let (_, name) = filtered[idx];
|
|
let display: String = if name.len() >= w {
|
|
name[..w].to_string()
|
|
} else {
|
|
format!(" {name}")
|
|
};
|
|
execute!(stdout, cursor::MoveTo(0, (i + 1) as u16)).ok();
|
|
if idx == selected {
|
|
execute!(
|
|
stdout,
|
|
style::PrintStyledContent(display.as_str().black().on_cyan()),
|
|
)
|
|
.ok();
|
|
} else {
|
|
execute!(stdout, style::Print(&display)).ok();
|
|
}
|
|
}
|
|
|
|
stdout.flush().ok();
|
|
|
|
// Input
|
|
let Event::Key(KeyEvent { code, modifiers, .. }) = event::read().ok()? else {
|
|
continue;
|
|
};
|
|
|
|
match code {
|
|
KeyCode::Esc => return None,
|
|
KeyCode::Enter => {
|
|
if !filtered.is_empty() {
|
|
let (mbid, name) = filtered[selected];
|
|
return Some((mbid.clone(), name.clone()));
|
|
}
|
|
}
|
|
KeyCode::Up => {
|
|
selected = selected.saturating_sub(1);
|
|
}
|
|
KeyCode::Down => {
|
|
if !filtered.is_empty() {
|
|
selected = selected.min(filtered.len().saturating_sub(1));
|
|
if selected + 1 < filtered.len() {
|
|
selected += 1;
|
|
}
|
|
}
|
|
}
|
|
KeyCode::Char('p') if modifiers.contains(KeyModifiers::CONTROL) => {
|
|
selected = selected.saturating_sub(1);
|
|
}
|
|
KeyCode::Char('n') if modifiers.contains(KeyModifiers::CONTROL) => {
|
|
if !filtered.is_empty() && selected + 1 < filtered.len() {
|
|
selected += 1;
|
|
}
|
|
}
|
|
KeyCode::Backspace => {
|
|
query.pop();
|
|
selected = 0;
|
|
scroll = 0;
|
|
}
|
|
KeyCode::Char(ch) => {
|
|
query.push(ch);
|
|
selected = 0;
|
|
scroll = 0;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|