/* * This file is a part of RSPlayer * Licensed under the GPL-3.0 License * Copyright (C) 2026 Salmonik @ salmonik@tuta.io * https://codeberg.org/salmonik/RSPlayer */ use crate::config::Config; use crate::database::Database; use clap::{Command, CommandFactory, Parser, Subcommand}; use clap_complete::{Shell, generate}; use std::path::PathBuf; #[derive(Parser)] #[command(name = "rsp", infer_long_args = true)] #[command(about = "Rust music player daemon", long_about = None)] pub struct Cli { #[command(subcommand)] pub command: Commands, } #[derive(Subcommand)] pub enum Commands { /// Start the daemon StartDaemon, /// Stop the daemon StopDaemon, /// Restart the daemon (kill existing and start new) Restart, /// Play a track, album, or artist Play { /// Add after current track and skip to it #[arg(short = 'i', long = "now")] now: bool, /// Add to queue after current track (don't skip) #[arg(short = 'n', long = "next")] next: bool, /// Queue rest of album after this track (requires a track query) #[arg(short = 'a', long = "album")] album: bool, #[arg(value_name = "QUERY")] query: Option, }, /// Pause playback Pause, /// Resume playback Resume, /// Toggle play/pause Toggle, /// Stop playback Stop, /// Skip to next track Next, /// Skip to previous track Previous, /// Set volume (0-100) Volume { #[arg(value_name = "LEVEL")] level: Option, }, /// Shuffle mode Shuffle { #[arg(value_name = "MODE")] mode: Option, }, /// Queue management Queue { #[arg(value_name = "POSITION")] position: Option, #[arg(value_name = "QUERY")] query: Option, }, /// Show player status Status, /// Scan music paths to populate database Scan { #[arg(long)] force: bool, #[arg(long, value_name = "PATHS")] paths: Option>, }, /// Settings management Settings { #[command(subcommand)] command: SettingsCommands, }, #[clap(hide = true)] /// Generate shell completions (developer only) Completions { /// Shell to generate (bash, zsh, fish, elvish, powershell) /// If not specified, generates all completion files #[arg(value_name = "SHELL")] shell: Option, }, #[clap(hide = true)] /// Complete artists, albums, or tracks from database DbComplete { /// Query string (e.g. \"J\", \"Artist/\", \"Artist/Album/\") #[arg(value_name = "QUERY")] query: String, }, /// display lyrics Lyrics, } #[derive(Subcommand)] pub enum SettingsCommands { /// Get a setting value Get { #[arg(value_name = "KEY")] key: String, }, /// Set a setting value Set { #[arg(value_name = "KEY")] key: String, #[arg(value_name = "VALUE")] value: String, }, /// List all settings List, /// Unset a setting Unset { #[arg(value_name = "KEY")] key: String, }, /// Reload config from disk (re-reads config file, re-validates scrobbling tokens) Reload, } impl Cli { pub fn run(&self) -> Result<(), Box> { match &self.command { Commands::StartDaemon => { crate::daemon::start_daemon()?; } Commands::StopDaemon => { crate::daemon::stop_daemon()?; } Commands::Restart => { crate::daemon::restart_daemon()?; } Commands::Play { now, next, album, query, .. } => { if let Some(q) = query { let pos = if *now { "now" } else if *next { "next" } else { "last" }; if *album { crate::daemon::send_command(format!("play-album {} {}", pos, q))?; } else { crate::daemon::send_command(format!("play {} {}", pos, q))?; } } else { crate::daemon::send_command("play last".to_string())?; } } Commands::Pause => { crate::daemon::send_command("pause".to_string())?; } Commands::Resume => { crate::daemon::send_command("resume".to_string())?; } Commands::Toggle => { crate::daemon::send_command("toggle".to_string())?; } Commands::Stop => { crate::daemon::send_command("stop".to_string())?; } Commands::Next => { crate::daemon::send_command("next".to_string())?; } Commands::Previous => { crate::daemon::send_command("previous".to_string())?; } Commands::Volume { level } => { if let Some(l) = level { crate::daemon::send_command(format!("volume {}", l))?; } else { crate::daemon::send_command("volume".to_string())?; } } Commands::Shuffle { mode } => { if let Some(m) = mode { crate::daemon::send_command(format!("shuffle {}", m))?; } else { crate::daemon::send_command("shuffle".to_string())?; } } Commands::Queue { position, query } => match (position, query) { (None, None) => { crate::daemon::send_command("queue".to_string())?; } (pos, Some(q)) => { let pos = pos.as_deref().unwrap_or("last"); crate::daemon::send_command(format!("queue {} {}", pos, q))?; } _ => {} }, Commands::Status => { crate::daemon::send_command("status".to_string())?; } Commands::Scan { force, paths } => { let config = Config::load()?; let db = Database::open(&config)?; if let Some(custom_paths) = paths { db.scan_paths(custom_paths, *force)?; } else { db.scan_paths(&config.music_paths, *force)?; } println!("Scan complete"); } Commands::Settings { command } => { let mut config = Config::load()?; match command { SettingsCommands::Get { key } => { println!("{}", config.get(key)?); } SettingsCommands::Set { key, value } => { config.set(key, value)?; config.save()?; if key == "listenbrainz_token" { let _ = crate::daemon::send_command("validate".to_string()); } } SettingsCommands::List => { config.list(); } SettingsCommands::Unset { key } => { config.unset(key)?; config.save()?; } SettingsCommands::Reload => { let _ = crate::daemon::send_command("reload".to_string()); } } } Commands::DbComplete { query } => { // Unescape shell escapes in the input query let query = { let mut unescaped = String::with_capacity(query.len()); let mut chars = query.chars(); while let Some(c) = chars.next() { if c == '\\' { if let Some(next) = chars.next() { unescaped.push(next); } } else { unescaped.push(c); } } unescaped }; let config = match Config::load() { Ok(c) => c, Err(_) => return Ok(()), }; let db = match Database::open(&config) { Ok(d) => d, Err(_) => return Ok(()), }; let results = db.complete(&query).unwrap_or_default(); for r in &results { println!("{r}"); } } Commands::Completions { shell } => { let full = Cli::command(); let subcommands: Vec = full .get_subcommands() .filter(|s| s.get_name() != "completions" && s.get_name() != "db-complete") .cloned() .collect(); let cmd_name = full.get_name().to_string(); let about = full.get_about().cloned(); let mut cmd = Command::new(cmd_name).infer_long_args(true); if let Some(a) = about { cmd = cmd.about(a); } for sc in subcommands { cmd = cmd.subcommand(sc); } if let Some(s) = shell { let shell = match s.as_str() { "bash" => Shell::Bash, "zsh" => Shell::Zsh, "fish" => Shell::Fish, "elvish" => Shell::Elvish, "powershell" => Shell::PowerShell, _ => { eprintln!("Unknown shell: {s}"); return Ok(()); } }; generate(shell, &mut cmd, "rsp", &mut std::io::stdout()); } else { std::fs::create_dir_all("completions/")?; let shells = [ (Shell::Bash, "bash"), (Shell::Zsh, "zsh"), (Shell::Fish, "fish"), (Shell::Elvish, "elvish"), (Shell::PowerShell, "powershell"), ]; for (shell, ext) in &shells { let path = format!("completions/rsp.{ext}"); let mut file = std::fs::File::create(&path)?; generate(*shell, &mut cmd, "rsp", &mut file); println!("Generated {path}"); } } } Commands::Lyrics => { let _ = crate::daemon::send_command("lyrics".to_string()); } } Ok(()) } }