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; } _ => {} } } }