aboutsummaryrefslogtreecommitdiffstats
path: root/src/cli.rs
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-06-23 14:52:27 +0200
committersalmonik <raisersoft@proton.me>2026-06-23 14:52:27 +0200
commit1fd56ff980dd80a24a3d2c90b692dc7c9f346bdd (patch)
tree575e28dc3da69ec8c1623b2bad542ec4a0c0f0ea /src/cli.rs
parentlast autocompletion fixes (diff)
downloadrsplayer-1fd56ff980dd80a24a3d2c90b692dc7c9f346bdd.tar.gz
rsplayer-1fd56ff980dd80a24a3d2c90b692dc7c9f346bdd.zip
Add shell path finding as a fallback.
Diffstat (limited to 'src/cli.rs')
-rw-r--r--src/cli.rs138
1 files changed, 127 insertions, 11 deletions
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;
10use clap::{Command, CommandFactory, Parser, Subcommand}; 10use clap::{Command, CommandFactory, Parser, Subcommand};
11use clap_complete::{Shell, generate}; 11use clap_complete::{Shell, generate};
12use std::path::PathBuf; 12use std::path::PathBuf;
13use std::time::Duration;
13 14
14#[derive(Parser)] 15#[derive(Parser)]
15#[command(name = "rsp", infer_long_args = true)] 16#[command(name = "rsp", infer_long_args = true)]
@@ -261,19 +262,44 @@ impl Cli {
261 unescaped 262 unescaped
262 }; 263 };
263 264
264 let config = match Config::load() { 265 // Empty query: synchronously query the DB directly (no timeout thread)
265 Ok(c) => c, 266 // and never fall back to a directory listing, which would be confusing.
266 Err(_) => return Ok(()), 267 if query.is_empty() {
267 }; 268 if let Ok(config) = Config::load()
268 let db = match Database::open(&config) { 269 && let Ok(db) = Database::open(&config)
269 Ok(d) => d, 270 {
270 Err(_) => return Ok(()), 271 let results = db.complete("").unwrap_or_default();
271 }; 272 for r in &results {
273 println!("{r}");
274 }
275 }
276 return Ok(());
277 }
278
279 // If query looks like a filesystem path, skip DB entirely
280 if looks_like_path(&query) {
281 for c in complete_path(&query) {
282 println!("{c}");
283 }
284 return Ok(());
285 }
286
287 // Try DB completion with a timeout; if nothing returned, fall back
288 // to filesystem path completion (e.g. empty library, no match).
289 let db_results = db_complete_with_timeout(&query, Duration::from_millis(100));
272 290
273 let results = db.complete(&query).unwrap_or_default(); 291 if let Some(ref results) = db_results
292 && !results.is_empty()
293 {
294 for r in results {
295 println!("{r}");
296 }
297 return Ok(());
298 }
274 299
275 for r in &results { 300 // Fallback: filesystem path completion
276 println!("{r}"); 301 for c in complete_path(&query) {
302 println!("{c}");
277 } 303 }
278 } 304 }
279 Commands::Completions { shell } => { 305 Commands::Completions { shell } => {
@@ -329,3 +355,93 @@ impl Cli {
329 Ok(()) 355 Ok(())
330 } 356 }
331} 357}
358
359/// Returns true if the query looks like a filesystem path.
360fn looks_like_path(query: &str) -> bool {
361 query.starts_with("./")
362 || query.starts_with("../")
363 || query.starts_with("~/")
364 || query.starts_with('/')
365}
366
367/// List files/directories matching `query`, formatted as completion candidates.
368///
369/// Directories get a trailing `/` so the shell knows they can be further expanded.
370fn complete_path(query: &str) -> Vec<String> {
371 if query.is_empty() {
372 return Vec::new();
373 }
374
375 // Split at the last '/' to separate directory and filename prefix
376 let (dir_part, file_prefix) = match query.rfind('/') {
377 Some(pos) => (&query[..=pos], &query[pos + 1..]),
378 None => ("", query),
379 };
380
381 // Resolve the directory to read. For ~/… we need the expanded path;
382 // everything else works as-is from the CWD.
383 let read_dir = if let Some(rest) = dir_part.strip_prefix("~/") {
384 if let Some(home) = dirs::home_dir() {
385 if rest.is_empty() {
386 home
387 } else {
388 home.join(rest.trim_end_matches('/'))
389 }
390 } else {
391 PathBuf::from(dir_part)
392 }
393 } else if dir_part.is_empty() {
394 PathBuf::from(".")
395 } else {
396 PathBuf::from(dir_part)
397 };
398
399 let mut results: Vec<String> = Vec::new();
400 if let Ok(entries) = std::fs::read_dir(&read_dir) {
401 for entry in entries.flatten() {
402 let name = entry.file_name();
403 let name_str = name.to_string_lossy().to_string();
404
405 // Skip hidden entries unless the query explicitly asks for them
406 if name_str.starts_with('.') && !file_prefix.starts_with('.') {
407 continue;
408 }
409 if name_str == "." || name_str == ".." {
410 continue;
411 }
412
413 if !name_str.starts_with(file_prefix) {
414 continue;
415 }
416
417 let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
418 if is_dir {
419 results.push(format!("{dir_part}{name_str}/"));
420 } else {
421 results.push(format!("{dir_part}{name_str}"));
422 }
423 }
424 }
425 results.sort();
426 results
427}
428
429/// Try the DB completion query in a separate thread with a timeout.
430///
431/// Opens its own database connection so the caller doesn't need to keep one
432/// alive. Returns `None` on timeout or when the DB can't be opened.
433fn db_complete_with_timeout(query: &str, timeout: Duration) -> Option<Vec<String>> {
434 let query = query.to_string();
435 let (tx, rx) = std::sync::mpsc::channel();
436
437 std::thread::spawn(move || {
438 if let Ok(config) = Config::load()
439 && let Ok(db) = Database::open(&config)
440 {
441 let results = db.complete(&query).unwrap_or_default();
442 let _ = tx.send(results);
443 }
444 });
445
446 rx.recv_timeout(timeout).ok()
447}