From 1fd56ff980dd80a24a3d2c90b692dc7c9f346bdd Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 23 Jun 2026 14:52:27 +0200 Subject: Add shell path finding as a fallback. --- src/cli.rs | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- src/daemon.rs | 2 +- 2 files changed, 128 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/cli.rs b/src/cli.rs index 37b0825..de342e4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,6 +10,7 @@ use crate::database::Database; use clap::{Command, CommandFactory, Parser, Subcommand}; use clap_complete::{Shell, generate}; use std::path::PathBuf; +use std::time::Duration; #[derive(Parser)] #[command(name = "rsp", infer_long_args = true)] @@ -261,19 +262,44 @@ impl Cli { unescaped }; - let config = match Config::load() { - Ok(c) => c, - Err(_) => return Ok(()), - }; - let db = match Database::open(&config) { - Ok(d) => d, - Err(_) => return Ok(()), - }; + // Empty query: synchronously query the DB directly (no timeout thread) + // and never fall back to a directory listing, which would be confusing. + if query.is_empty() { + if let Ok(config) = Config::load() + && let Ok(db) = Database::open(&config) + { + let results = db.complete("").unwrap_or_default(); + for r in &results { + println!("{r}"); + } + } + return Ok(()); + } + + // If query looks like a filesystem path, skip DB entirely + if looks_like_path(&query) { + for c in complete_path(&query) { + println!("{c}"); + } + return Ok(()); + } + + // Try DB completion with a timeout; if nothing returned, fall back + // to filesystem path completion (e.g. empty library, no match). + let db_results = db_complete_with_timeout(&query, Duration::from_millis(100)); - let results = db.complete(&query).unwrap_or_default(); + if let Some(ref results) = db_results + && !results.is_empty() + { + for r in results { + println!("{r}"); + } + return Ok(()); + } - for r in &results { - println!("{r}"); + // Fallback: filesystem path completion + for c in complete_path(&query) { + println!("{c}"); } } Commands::Completions { shell } => { @@ -329,3 +355,93 @@ impl Cli { Ok(()) } } + +/// Returns true if the query looks like a filesystem path. +fn looks_like_path(query: &str) -> bool { + query.starts_with("./") + || query.starts_with("../") + || query.starts_with("~/") + || query.starts_with('/') +} + +/// List files/directories matching `query`, formatted as completion candidates. +/// +/// Directories get a trailing `/` so the shell knows they can be further expanded. +fn complete_path(query: &str) -> Vec { + if query.is_empty() { + return Vec::new(); + } + + // Split at the last '/' to separate directory and filename prefix + let (dir_part, file_prefix) = match query.rfind('/') { + Some(pos) => (&query[..=pos], &query[pos + 1..]), + None => ("", query), + }; + + // Resolve the directory to read. For ~/… we need the expanded path; + // everything else works as-is from the CWD. + let read_dir = if let Some(rest) = dir_part.strip_prefix("~/") { + if let Some(home) = dirs::home_dir() { + if rest.is_empty() { + home + } else { + home.join(rest.trim_end_matches('/')) + } + } else { + PathBuf::from(dir_part) + } + } else if dir_part.is_empty() { + PathBuf::from(".") + } else { + PathBuf::from(dir_part) + }; + + let mut results: Vec = Vec::new(); + if let Ok(entries) = std::fs::read_dir(&read_dir) { + for entry in entries.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy().to_string(); + + // Skip hidden entries unless the query explicitly asks for them + if name_str.starts_with('.') && !file_prefix.starts_with('.') { + continue; + } + if name_str == "." || name_str == ".." { + continue; + } + + if !name_str.starts_with(file_prefix) { + continue; + } + + let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false); + if is_dir { + results.push(format!("{dir_part}{name_str}/")); + } else { + results.push(format!("{dir_part}{name_str}")); + } + } + } + results.sort(); + results +} + +/// Try the DB completion query in a separate thread with a timeout. +/// +/// Opens its own database connection so the caller doesn't need to keep one +/// alive. Returns `None` on timeout or when the DB can't be opened. +fn db_complete_with_timeout(query: &str, timeout: Duration) -> Option> { + let query = query.to_string(); + let (tx, rx) = std::sync::mpsc::channel(); + + std::thread::spawn(move || { + if let Ok(config) = Config::load() + && let Ok(db) = Database::open(&config) + { + let results = db.complete(&query).unwrap_or_default(); + let _ = tx.send(results); + } + }); + + rx.recv_timeout(timeout).ok() +} diff --git a/src/daemon.rs b/src/daemon.rs index a0de698..1434555 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -550,7 +550,7 @@ fn handle_lyrics_stream(stream: &mut UnixStream, audio: &Arc) { if prev_line_count > 0 { frame.push_str(&format!("\x1b[{}A\x1b[1B", prev_line_count)); } else { - frame.push_str(&format!("\x1b[2K")); + frame.push_str("\x1b[2K"); } frame.push_str(&output); if stream.write_all(frame.as_bytes()).is_err() { -- cgit v1.2.3