From 74f2927b8679c1b59f36984ab720f424b934c3c3 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 12 Jun 2025 18:14:34 +0300 Subject: [PATCH] mrc: refactor interactive mode into its own module --- src/cli.rs | 149 +--------------------------------------- src/interactive.rs | 168 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 25 ++++--- 3 files changed, 186 insertions(+), 156 deletions(-) create mode 100644 src/interactive.rs diff --git a/src/cli.rs b/src/cli.rs index 2634a6b9..8bd5e139 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,14 +1,12 @@ use clap::{Parser, Subcommand}; use mrc::SOCKET_PATH; +use mrc::interactive::InteractiveMode; use mrc::{ MrcError, Result, get_property, loadfile, playlist_clear, playlist_move, playlist_next, playlist_prev, playlist_remove, quit, seek, set_property, }; use serde_json::json; -use std::{ - io::{self, Write}, - path::PathBuf, -}; +use std::path::PathBuf; use tracing::{debug, error, info}; #[derive(Parser)] @@ -206,148 +204,7 @@ async fn main() -> Result<()> { } CommandOptions::Interactive => { - println!("Entering interactive mode. Type 'exit' to quit."); - let stdin = io::stdin(); - let mut stdout = io::stdout(); - - loop { - print!("mpv> "); - stdout.flush().map_err(MrcError::ConnectionError)?; - let mut input = String::new(); - stdin - .read_line(&mut input) - .map_err(MrcError::ConnectionError)?; - let trimmed = input.trim(); - - if trimmed.eq_ignore_ascii_case("exit") { - println!("Exiting interactive mode."); - break; - } - - // I don't like this either, but it looks cleaner than a multi-line - // print macro just cramped in here. - let commands = vec![ - ( - "play [index]", - "Play or unpause playback, optionally at the specified index", - ), - ("pause", "Pause playback"), - ("stop", "Stop playback and quit MPV"), - ("next", "Skip to the next item in the playlist"), - ("prev", "Skip to the previous item in the playlist"), - ("seek ", "Seek to the specified position"), - ("clear", "Clear the playlist"), - ("list", "List all items in the playlist"), - ("add ", "Add files to the playlist"), - ("get ", "Get the specified property"), - ( - "set ", - "Set the specified property to a value", - ), - ("help", "Show this help message"), - ("exit", "Quit interactive mode"), - ]; - - if trimmed.eq_ignore_ascii_case("help") { - println!("Valid commands:"); - for (command, description) in commands { - println!(" {} - {}", command, description); - } - continue; - } - - let parts: Vec<&str> = trimmed.split_whitespace().collect(); - match parts.as_slice() { - ["play"] => { - info!("Unpausing playback"); - set_property("pause", &json!(false), None).await?; - } - - ["play", index] => { - if let Ok(idx) = index.parse::() { - info!("Playing media at index: {}", idx); - set_property("playlist-pos", &json!(idx), None).await?; - set_property("pause", &json!(false), None).await?; - } else { - println!("Invalid index: {}", index); - } - } - - ["pause"] => { - info!("Pausing playback"); - set_property("pause", &json!(true), None).await?; - } - - ["stop"] => { - info!("Pausing playback"); - quit(None).await?; - } - - ["next"] => { - info!("Skipping to next item in the playlist"); - playlist_next(None).await?; - } - - ["prev"] => { - info!("Skipping to previous item in the playlist"); - playlist_prev(None).await?; - } - - ["seek", seconds] => { - if let Ok(sec) = seconds.parse::() { - info!("Seeking to {} seconds", sec); - seek(sec.into(), None).await?; - } else { - println!("Invalid seconds: {}", seconds); - } - } - - ["clear"] => { - info!("Clearing the playlist"); - playlist_clear(None).await?; - } - - ["list"] => { - info!("Listing playlist items"); - if let Some(data) = get_property("playlist", None).await? { - let pretty_json = serde_json::to_string_pretty(&data) - .map_err(MrcError::ParseError)?; - println!("{}", pretty_json); - } - } - - ["add", files @ ..] => { - if files.is_empty() { - println!("No files provided to add to the playlist"); - } else { - info!("Adding {} files to the playlist", files.len()); - for file in files { - loadfile(file, true, None).await?; - } - } - } - - ["get", property] => { - if let Some(data) = get_property(property, None).await? { - println!("{property}: {data}"); - } - } - - ["set", property, value] => { - let json_value = serde_json::from_str::(value) - .unwrap_or_else(|_| json!(value)); - set_property(property, &json_value, None).await?; - println!("Set {property} to {value}"); - } - - _ => { - println!("Unknown command: {}", trimmed); - println!( - "Valid commands: play , pause, stop, next, prev, seek , clear, list, add , get , set , help, exit" - ); - } - } - } + InteractiveMode::run().await?; } } diff --git a/src/interactive.rs b/src/interactive.rs new file mode 100644 index 00000000..9a2cf2b7 --- /dev/null +++ b/src/interactive.rs @@ -0,0 +1,168 @@ +use crate::{ + MrcError, Result, get_property, loadfile, playlist_clear, playlist_next, playlist_prev, quit, + seek, set_property, +}; +use serde_json::json; +use std::io::{self, Write}; +use tracing::info; + +pub struct InteractiveMode; + +impl InteractiveMode { + pub async fn run() -> Result<()> { + println!("Entering interactive mode. Type 'help' for commands or 'exit' to quit."); + let stdin = io::stdin(); + let mut stdout = io::stdout(); + + loop { + print!("mpv> "); + stdout.flush().map_err(MrcError::ConnectionError)?; + + let mut input = String::new(); + stdin + .read_line(&mut input) + .map_err(MrcError::ConnectionError)?; + let trimmed = input.trim(); + + if trimmed.eq_ignore_ascii_case("exit") { + println!("Exiting interactive mode."); + break; + } + + if trimmed.eq_ignore_ascii_case("help") { + Self::show_help(); + continue; + } + + if let Err(e) = Self::process_command(trimmed).await { + eprintln!("Error: {}", e); + } + } + + Ok(()) + } + + fn show_help() { + println!("Available commands:"); + let commands = [ + ( + "play [index]", + "Play or unpause playback, optionally at the specified index", + ), + ("pause", "Pause playback"), + ("stop", "Stop playback and quit MPV"), + ("next", "Skip to the next item in the playlist"), + ("prev", "Skip to the previous item in the playlist"), + ("seek ", "Seek to the specified position"), + ("clear", "Clear the playlist"), + ("list", "List all items in the playlist"), + ("add ", "Add files to the playlist"), + ("get ", "Get the specified property"), + ( + "set ", + "Set the specified property to a value", + ), + ("help", "Show this help message"), + ("exit", "Quit interactive mode"), + ]; + + for (command, description) in commands { + println!(" {} - {}", command, description); + } + } + + async fn process_command(input: &str) -> Result<()> { + let parts: Vec<&str> = input.split_whitespace().collect(); + + match parts.as_slice() { + ["play"] => { + info!("Unpausing playback"); + set_property("pause", &json!(false), None).await?; + } + + ["play", index] => { + if let Ok(idx) = index.parse::() { + info!("Playing media at index: {}", idx); + set_property("playlist-pos", &json!(idx), None).await?; + set_property("pause", &json!(false), None).await?; + } else { + println!("Invalid index: {}", index); + } + } + + ["pause"] => { + info!("Pausing playback"); + set_property("pause", &json!(true), None).await?; + } + + ["stop"] => { + info!("Stopping playback and quitting MPV"); + quit(None).await?; + } + + ["next"] => { + info!("Skipping to next item in the playlist"); + playlist_next(None).await?; + } + + ["prev"] => { + info!("Skipping to previous item in the playlist"); + playlist_prev(None).await?; + } + + ["seek", seconds] => { + if let Ok(sec) = seconds.parse::() { + info!("Seeking to {} seconds", sec); + seek(sec.into(), None).await?; + } else { + println!("Invalid seconds: {}", seconds); + } + } + + ["clear"] => { + info!("Clearing the playlist"); + playlist_clear(None).await?; + } + + ["list"] => { + info!("Listing playlist items"); + if let Some(data) = get_property("playlist", None).await? { + let pretty_json = + serde_json::to_string_pretty(&data).map_err(MrcError::ParseError)?; + println!("{}", pretty_json); + } + } + + ["add", files @ ..] => { + if files.is_empty() { + println!("No files provided to add to the playlist"); + } else { + info!("Adding {} files to the playlist", files.len()); + for file in files { + loadfile(file, true, None).await?; + } + } + } + + ["get", property] => { + if let Some(data) = get_property(property, None).await? { + println!("{}: {}", property, data); + } + } + + ["set", property, value] => { + let json_value = serde_json::from_str::(value) + .unwrap_or_else(|_| json!(value)); + set_property(property, &json_value, None).await?; + println!("Set {} to {}", property, value); + } + + _ => { + println!("Unknown command: {}", input); + println!("Type 'help' for a list of available commands."); + } + } + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 64d6eed5..8dbc4ae0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,8 @@ //! ### `SOCKET_PATH` //! Default path for the MPV IPC socket: `/tmp/mpvsocket` //! -//! ## Functions + +pub mod interactive; use serde_json::{Value, json}; use std::io; @@ -48,6 +49,7 @@ use tokio::net::UnixStream; use tracing::{debug, error}; pub const SOCKET_PATH: &str = "/tmp/mpvsocket"; +const SOCKET_TIMEOUT_SECS: u64 = 5; /// Errors that can occur when interacting with the MPV IPC interface. #[derive(Error, Debug)] @@ -105,11 +107,11 @@ async fn connect_to_socket(socket_path: &str) -> Result { debug!("Connecting to socket at {}", socket_path); tokio::time::timeout( - std::time::Duration::from_secs(5), + std::time::Duration::from_secs(SOCKET_TIMEOUT_SECS), UnixStream::connect(socket_path), ) .await - .map_err(|_| MrcError::SocketTimeout(5))? + .map_err(|_| MrcError::SocketTimeout(SOCKET_TIMEOUT_SECS))? .map_err(MrcError::ConnectionError) } @@ -124,16 +126,19 @@ async fn send_message(socket: &mut UnixStream, command: &str, args: &[Value]) -> // Write with timeout tokio::time::timeout( - std::time::Duration::from_secs(5), + std::time::Duration::from_secs(SOCKET_TIMEOUT_SECS), socket.write_all(message_str.as_bytes()), ) .await - .map_err(|_| MrcError::SocketTimeout(5))??; + .map_err(|_| MrcError::SocketTimeout(SOCKET_TIMEOUT_SECS))??; // Flush with timeout - tokio::time::timeout(std::time::Duration::from_secs(5), socket.flush()) - .await - .map_err(|_| MrcError::SocketTimeout(5))??; + tokio::time::timeout( + std::time::Duration::from_secs(SOCKET_TIMEOUT_SECS), + socket.flush(), + ) + .await + .map_err(|_| MrcError::SocketTimeout(SOCKET_TIMEOUT_SECS))??; debug!("Message sent and flushed"); Ok(()) @@ -145,11 +150,11 @@ async fn read_response(socket: &mut UnixStream) -> Result { // Read with timeout let n = tokio::time::timeout( - std::time::Duration::from_secs(5), + std::time::Duration::from_secs(SOCKET_TIMEOUT_SECS), socket.read(&mut response), ) .await - .map_err(|_| MrcError::SocketTimeout(5))??; + .map_err(|_| MrcError::SocketTimeout(SOCKET_TIMEOUT_SECS))??; if n == 0 { return Err(MrcError::ConnectionLost(