Added TUI
This commit is contained in:
167
Cargo.lock
generated
167
Cargo.lock
generated
@@ -57,6 +57,31 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"rustix",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm_winapi"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
version = "2.10.0"
|
version = "2.10.0"
|
||||||
@@ -69,6 +94,16 @@ version = "0.15.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fallible-iterator"
|
name = "fallible-iterator"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -183,6 +218,21 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.4.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lofty"
|
name = "lofty"
|
||||||
version = "0.23.2"
|
version = "0.23.2"
|
||||||
@@ -231,6 +281,18 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ogg_pager"
|
name = "ogg_pager"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -246,6 +308,29 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@@ -268,6 +353,7 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
|||||||
name = "playlists"
|
name = "playlists"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"crossterm",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"lofty",
|
"lofty",
|
||||||
"rand",
|
"rand",
|
||||||
@@ -340,6 +426,15 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -368,6 +463,19 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "0.38.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.37"
|
version = "0.23.37"
|
||||||
@@ -412,6 +520,12 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -461,6 +575,37 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
@@ -577,6 +722,22 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.11"
|
version = "0.1.11"
|
||||||
@@ -586,6 +747,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ rusqlite = { version = "0.34", features = ["bundled"] }
|
|||||||
ureq = "3"
|
ureq = "3"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
walkdir = "2.5"
|
walkdir = "2.5"
|
||||||
|
crossterm = "0.28"
|
||||||
|
|||||||
@@ -61,6 +61,14 @@ pub fn get_local_tracks_for_artist(
|
|||||||
rows.collect()
|
rows.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_all_artists(conn: &Connection) -> Result<Vec<(String, String)>, rusqlite::Error> {
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT mbid, COALESCE(name, mbid) FROM artists ORDER BY name",
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
|
||||||
|
rows.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert_track(
|
pub fn insert_track(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
path: &str,
|
path: &str,
|
||||||
|
|||||||
72
src/main.rs
72
src/main.rs
@@ -2,6 +2,7 @@ mod db;
|
|||||||
mod filesystem;
|
mod filesystem;
|
||||||
mod lastfm;
|
mod lastfm;
|
||||||
mod metadata;
|
mod metadata;
|
||||||
|
mod tui;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
@@ -13,7 +14,7 @@ use rand::prelude::*;
|
|||||||
fn usage(program: &str) -> ! {
|
fn usage(program: &str) -> ! {
|
||||||
eprintln!("Usage:");
|
eprintln!("Usage:");
|
||||||
eprintln!(" {program} index [-v] <directory>");
|
eprintln!(" {program} index [-v] <directory>");
|
||||||
eprintln!(" {program} build [-v] [-n COUNT] <file>");
|
eprintln!(" {program} build [-v] [-n COUNT] [file]");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,24 +138,11 @@ fn cmd_build(args: &[String]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if rest.len() != 1 {
|
if rest.len() > 1 {
|
||||||
eprintln!("Usage: {} build [-v] [-n COUNT] <file>", args[0]);
|
eprintln!("Usage: {} build [-v] [-n COUNT] [file]", args[0]);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = Path::new(rest[0].as_str());
|
|
||||||
let artist_mbid = match metadata::read_artist_mbid(path) {
|
|
||||||
Ok(Some(mbid)) => mbid,
|
|
||||||
Ok(None) => {
|
|
||||||
eprintln!("{}: no artist MBID found", path.display());
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{}: could not read artist MBID: {e}", path.display());
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
dotenvy::dotenv().ok();
|
dotenvy::dotenv().ok();
|
||||||
let api_key = env::var("LASTFM_API_KEY").unwrap_or_default();
|
let api_key = env::var("LASTFM_API_KEY").unwrap_or_default();
|
||||||
if api_key.is_empty() {
|
if api_key.is_empty() {
|
||||||
@@ -165,12 +153,54 @@ fn cmd_build(args: &[String]) {
|
|||||||
let conn = db::open("playlists.db").expect("failed to open database");
|
let conn = db::open("playlists.db").expect("failed to open database");
|
||||||
let lastfm = lastfm::LastfmClient::new(api_key);
|
let lastfm = lastfm::LastfmClient::new(api_key);
|
||||||
|
|
||||||
let seed_name = metadata::read_artist_name(path)
|
let (artist_mbid, seed_name) = if let Some(file_arg) = rest.first() {
|
||||||
|
let path = Path::new(file_arg.as_str());
|
||||||
|
let mbid = match metadata::read_artist_mbid(path) {
|
||||||
|
Ok(Some(mbid)) => mbid,
|
||||||
|
Ok(None) => {
|
||||||
|
eprintln!("{}: no artist MBID found", path.display());
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}: could not read artist MBID: {e}", path.display());
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let name = metadata::read_artist_name(path)
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.unwrap_or_else(|| artist_mbid.clone());
|
.unwrap_or_else(|| mbid.clone());
|
||||||
|
(mbid, name)
|
||||||
|
} else {
|
||||||
|
let artists = match db::get_all_artists(&conn) {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("DB error: {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if artists.is_empty() {
|
||||||
|
eprintln!("No artists in database. Run 'index' first.");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
match tui::run_artist_picker(&artists) {
|
||||||
|
Some(selection) => selection,
|
||||||
|
None => std::process::exit(0),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let similar = match db::get_available_similar_artists(&conn, &artist_mbid) {
|
build_playlist(&conn, &lastfm, &artist_mbid, &seed_name, count, verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_playlist(
|
||||||
|
conn: &rusqlite::Connection,
|
||||||
|
lastfm: &lastfm::LastfmClient,
|
||||||
|
artist_mbid: &str,
|
||||||
|
seed_name: &str,
|
||||||
|
count: usize,
|
||||||
|
verbose: bool,
|
||||||
|
) {
|
||||||
|
let similar = match db::get_available_similar_artists(conn, artist_mbid) {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("DB error: {e}");
|
eprintln!("DB error: {e}");
|
||||||
@@ -180,7 +210,7 @@ fn cmd_build(args: &[String]) {
|
|||||||
|
|
||||||
// Seed artist + similar artists: (mbid, name, match_score)
|
// Seed artist + similar artists: (mbid, name, match_score)
|
||||||
let mut artists: Vec<(String, String, f64)> = vec![
|
let mut artists: Vec<(String, String, f64)> = vec![
|
||||||
(artist_mbid.clone(), seed_name, 1.0),
|
(artist_mbid.to_string(), seed_name.to_string(), 1.0),
|
||||||
];
|
];
|
||||||
artists.extend(similar);
|
artists.extend(similar);
|
||||||
|
|
||||||
@@ -188,7 +218,7 @@ fn cmd_build(args: &[String]) {
|
|||||||
let mut playlist: Vec<(f64, f64, f64, String, String)> = Vec::new();
|
let mut playlist: Vec<(f64, f64, f64, String, String)> = Vec::new();
|
||||||
|
|
||||||
for (mbid, name, match_score) in &artists {
|
for (mbid, name, match_score) in &artists {
|
||||||
let local_tracks = match db::get_local_tracks_for_artist(&conn, mbid) {
|
let local_tracks = match db::get_local_tracks_for_artist(conn, mbid) {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("DB error for {name}: {e}");
|
eprintln!("DB error for {name}: {e}");
|
||||||
|
|||||||
159
src/tui.rs
Normal file
159
src/tui.rs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
cursor,
|
||||||
|
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
|
||||||
|
execute,
|
||||||
|
style::{self, Stylize},
|
||||||
|
terminal::{self, ClearType},
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user