diff options
| author | salmonik <raisersoft@proton.me> | 2026-06-23 14:52:27 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-06-23 14:52:27 +0200 |
| commit | 1fd56ff980dd80a24a3d2c90b692dc7c9f346bdd (patch) | |
| tree | 575e28dc3da69ec8c1623b2bad542ec4a0c0f0ea /src/cli.rs | |
| parent | last autocompletion fixes (diff) | |
| download | rsplayer-1fd56ff980dd80a24a3d2c90b692dc7c9f346bdd.tar.gz rsplayer-1fd56ff980dd80a24a3d2c90b692dc7c9f346bdd.zip | |
Add shell path finding as a fallback.
Diffstat (limited to '')
| -rw-r--r-- | src/cli.rs | 138 |
1 files changed, 127 insertions, 11 deletions
| @@ -10,6 +10,7 @@ use crate::database::Database; | |||
| 10 | use clap::{Command, CommandFactory, Parser, Subcommand}; | 10 | use clap::{Command, CommandFactory, Parser, Subcommand}; |
| 11 | use clap_complete::{Shell, generate}; | 11 | use clap_complete::{Shell, generate}; |
| 12 | use std::path::PathBuf; | 12 | use std::path::PathBuf; |
| 13 | use 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. | ||
| 360 | fn 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. | ||
| 370 | fn 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. | ||
| 433 | fn 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 | } | ||
