cli: add --socket, --yes flags and shell completions

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I7cfd2149ff2c7599f9c6b192559ee4956a6a6964
This commit is contained in:
raf 2026-03-29 20:22:35 +03:00
commit ec229485d4
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
3 changed files with 669 additions and 194 deletions

746
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,10 +4,10 @@ description = "MPV Remote Control - CLI and server for controlling MPV via IPC"
version = "0.2.0"
edition = "2024"
default-run = "cli"
authors = ["NotAShelf <raf@notashelf.dev>"]
repository = "https://github.com/notashelf/mrc"
license = "MPL-2.*"
readme = "README.md"
license = "MPL-2.0"
rust-version = "1.92.0"
readme = true
keywords = ["mpv", "media", "player", "control", "ipc"]
categories = ["command-line-utilities", "multimedia"]
@ -16,14 +16,14 @@ categories = ["command-line-utilities", "multimedia"]
name = "cli"
path = "src/cli.rs"
# server implementation for remote usage
# Server implementation for remote usage
[[bin]]
name = "server"
path = "src/server.rs"
[dependencies]
anyhow = "1.0.102"
clap = { version = "4.6.0", features = ["derive"] }
clap = { version = "4.6.0", features = ["derive", "cargo"] }
clap_derive = "4.6.0"
ipc-channel = "0.21.0"
serde = { version = "1.0.228", features = ["derive"] }
@ -34,6 +34,10 @@ native-tls = "0.2.18"
tokio-native-tls = "0.3.1"
tracing = "0.1.44"
tracing-subscriber = "0.3.23"
rustyline = "18.0.0"
anstyle = "1.0.14"
dirs = "6.0.0"
clap_complete = "4.6.0"
[profile.dev]
opt-level = 1

View file

@ -1,15 +1,21 @@
use clap::{Parser, Subcommand};
use mrc::SOCKET_PATH;
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
use mrc::commands::Commands;
use mrc::interactive::InteractiveMode;
use mrc::{MrcError, Result};
use mrc::{MrcError, Result, SOCKET_PATH};
use std::io::{self, Write};
use std::path::PathBuf;
use tracing::{debug, error};
#[derive(Parser)]
#[command(author, version, about)]
struct Cli {
#[arg(short, long, global = true)]
#[arg(short, long, global = true, help = "Path to MPV socket")]
socket: Option<String>,
#[arg(short, long, global = true, help = "Skip confirmation prompts")]
yes: bool,
#[arg(short, long, global = true, help = "Enable debug output")]
debug: bool,
#[command(subcommand)]
@ -88,6 +94,68 @@ enum CommandOptions {
/// Enter interactive mode to send commands to MPV IPC
Interactive,
/// Generate shell completions
Completion {
#[arg(value_enum, default_value_t = Shell::Bash)]
shell: Shell,
},
}
#[expect(clippy::enum_variant_names)]
#[derive(Clone, ValueEnum)]
pub enum Shell {
Bash,
Elvish,
Fish,
PowerShell,
Zsh,
}
impl Shell {
pub fn generate(&self, app: &mut clap::Command) {
match self {
Shell::Bash => {
clap_complete::generate(clap_complete::Shell::Bash, app, "mrc", &mut io::stdout())
}
Shell::Elvish => {
clap_complete::generate(clap_complete::Shell::Elvish, app, "mrc", &mut io::stdout())
}
Shell::Fish => {
clap_complete::generate(clap_complete::Shell::Fish, app, "mrc", &mut io::stdout())
}
Shell::PowerShell => clap_complete::generate(
clap_complete::Shell::PowerShell,
app,
"mrc",
&mut io::stdout(),
),
Shell::Zsh => {
clap_complete::generate(clap_complete::Shell::Zsh, app, "mrc", &mut io::stdout())
}
}
}
}
fn get_socket_path(cli: &Cli) -> String {
cli.socket
.clone()
.unwrap_or_else(|| SOCKET_PATH.to_string())
}
fn confirm(prompt: &str, yes: bool) -> bool {
if yes {
return true;
}
print!("{} [y/N] ", prompt);
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
input.trim().eq_ignore_ascii_case("y")
}
#[tokio::main]
@ -95,9 +163,22 @@ async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let cli = Cli::parse();
if !PathBuf::from(SOCKET_PATH).exists() {
debug!(SOCKET_PATH);
error!("Error: MPV socket not found. Is MPV running?");
if matches!(cli.command, CommandOptions::Completion { .. }) {
let mut app = Cli::command();
if let CommandOptions::Completion { shell } = &cli.command {
shell.generate(&mut app);
}
return Ok(());
}
let socket_path = get_socket_path(&cli);
if !PathBuf::from(&socket_path).exists() {
debug!(socket_path);
error!(
"Error: MPV socket not found at {}. Is MPV running?",
socket_path
);
return Err(MrcError::ConnectionError(std::io::Error::new(
std::io::ErrorKind::NotFound,
"MPV socket not found",
@ -106,61 +187,79 @@ async fn main() -> Result<()> {
match cli.command {
CommandOptions::Play { index } => {
Commands::play(index).await?;
Commands::play(index, Some(&socket_path)).await?;
}
CommandOptions::Pause => {
Commands::pause().await?;
Commands::pause(Some(&socket_path)).await?;
}
CommandOptions::Stop => {
Commands::stop().await?;
if confirm("This will stop playback and quit MPV. Continue?", cli.yes) {
Commands::stop(Some(&socket_path)).await?;
} else {
println!("Cancelled.");
}
}
CommandOptions::Next => {
Commands::next().await?;
Commands::next(Some(&socket_path)).await?;
}
CommandOptions::Prev => {
Commands::prev().await?;
Commands::prev(Some(&socket_path)).await?;
}
CommandOptions::Seek { seconds } => {
Commands::seek_to(seconds.into()).await?;
Commands::seek_to(seconds.into(), Some(&socket_path)).await?;
}
CommandOptions::Move { index1, index2 } => {
Commands::move_item(index1, index2).await?;
Commands::move_item(index1, index2, Some(&socket_path)).await?;
}
CommandOptions::Remove { index } => {
Commands::remove_item(index).await?;
if confirm(
&format!("This will remove item at index {:?}. Continue?", index),
cli.yes,
) {
Commands::remove_item(index, Some(&socket_path)).await?;
} else {
println!("Cancelled.");
}
}
CommandOptions::Clear => {
Commands::clear_playlist().await?;
if confirm("This will clear the entire playlist. Continue?", cli.yes) {
Commands::clear_playlist(Some(&socket_path)).await?;
} else {
println!("Cancelled.");
}
}
CommandOptions::List => {
Commands::list_playlist().await?;
Commands::list_playlist(Some(&socket_path)).await?;
}
CommandOptions::Add { filenames } => {
Commands::add_files(&filenames).await?;
Commands::add_files(&filenames, Some(&socket_path)).await?;
}
CommandOptions::Replace { filenames } => {
Commands::replace_playlist(&filenames).await?;
Commands::replace_playlist(&filenames, Some(&socket_path)).await?;
}
CommandOptions::Prop { properties } => {
Commands::get_properties(&properties).await?;
Commands::get_properties(&properties, Some(&socket_path)).await?;
}
CommandOptions::Interactive => {
InteractiveMode::run().await?;
InteractiveMode::run(Some(&socket_path)).await?;
}
CommandOptions::Completion { .. } => unreachable!(),
}
Ok(())
}