diff --git a/src/interactive.rs b/src/interactive.rs index 4308eb4a..7459f3d1 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -1,41 +1,72 @@ use crate::commands::Commands; use crate::{MrcError, Result}; +use rustyline::config::EditMode; +use rustyline::{Cmd, Config, Editor, KeyEvent}; use serde_json::json; -use std::io::{self, Write}; +use std::io::Error; +use std::path::PathBuf; pub struct InteractiveMode; impl InteractiveMode { - pub async fn run() -> Result<()> { + pub async fn run(socket_path: Option<&str>) -> Result<()> { + let hist_path = dirs::data_local_dir() + .map(|p| p.join("mrc").join("history.txt")) + .unwrap_or_else(|| PathBuf::from("history.txt")); + + let config = Config::builder().edit_mode(EditMode::Vi).build(); + + let mut rl: Editor<(), _> = Editor::with_config(config) + .map_err(|e| MrcError::ConnectionError(Error::other(e.to_string())))?; + + rl.bind_sequence(KeyEvent::ctrl('c'), Cmd::Interrupt); + rl.bind_sequence(KeyEvent::ctrl('l'), Cmd::ClearScreen); + + let _ = rl.load_history(&hist_path); + println!("Entering interactive mode. Type 'help' for commands or 'exit' to quit."); - let stdin = io::stdin(); - let mut stdout = io::stdout(); + println!("Socket: {}", socket_path.unwrap_or("/tmp/mpvsocket")); loop { - print!("mpv> "); - stdout.flush().map_err(MrcError::ConnectionError)?; + let readline = rl.readline("mpv> "); + match readline { + Ok(input) => { + let trimmed = input.trim(); + if trimmed.is_empty() { + continue; + } + rl.add_history_entry(trimmed).ok(); - 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("exit") { - println!("Exiting interactive mode."); - break; - } + if trimmed.eq_ignore_ascii_case("help") { + Self::show_help(); + continue; + } - if trimmed.eq_ignore_ascii_case("help") { - Self::show_help(); - continue; - } - - if let Err(e) = Self::process_command(trimmed).await { - eprintln!("Error: {}", e); + if let Err(e) = Self::process_command(trimmed, socket_path).await { + eprintln!("Error: {}", e); + } + } + Err(rustyline::error::ReadlineError::Interrupted) => { + println!("(interrupted)"); + continue; + } + Err(rustyline::error::ReadlineError::Eof) => { + println!("Exiting interactive mode."); + break; + } + Err(err) => { + eprintln!("Error: {:?}", err); + break; + } } } + rl.save_history(&hist_path).ok(); Ok(()) } @@ -53,7 +84,7 @@ impl InteractiveMode { ("seek ", "Seek to the specified position"), ("clear", "Clear the playlist"), ("list", "List all items in the playlist"), - ("add ", "Add files to the playlist"), + ("add ", "Add files to the playlist"), ("get ", "Get the specified property"), ( "set ", @@ -64,56 +95,61 @@ impl InteractiveMode { ]; for (command, description) in commands { - println!(" {} - {}", command, description); + println!(" {:<22} - {}", command, description); } + + println!("\nKeyboard shortcuts:"); + println!(" Ctrl+C - Interrupt current command"); + println!(" Ctrl+L - Clear screen"); + println!(" Ctrl+D - Exit"); } - async fn process_command(input: &str) -> Result<()> { + async fn process_command(input: &str, socket_path: Option<&str>) -> Result<()> { let parts: Vec<&str> = input.split_whitespace().collect(); match parts.as_slice() { ["play"] => { - Commands::play(None).await?; + Commands::play(None, socket_path).await?; } ["play", index] => { if let Ok(idx) = index.parse::() { - Commands::play(Some(idx)).await?; + Commands::play(Some(idx), socket_path).await?; } else { println!("Invalid index: {}", index); } } ["pause"] => { - Commands::pause().await?; + Commands::pause(socket_path).await?; } ["stop"] => { - Commands::stop().await?; + Commands::stop(socket_path).await?; } ["next"] => { - Commands::next().await?; + Commands::next(socket_path).await?; } ["prev"] => { - Commands::prev().await?; + Commands::prev(socket_path).await?; } ["seek", seconds] => { if let Ok(sec) = seconds.parse::() { - Commands::seek_to(sec.into()).await?; + Commands::seek_to(sec.into(), socket_path).await?; } else { println!("Invalid seconds: {}", seconds); } } ["clear"] => { - Commands::clear_playlist().await?; + Commands::clear_playlist(socket_path).await?; } ["list"] => { - Commands::list_playlist().await?; + Commands::list_playlist(socket_path).await?; } ["add", files @ ..] => { @@ -121,18 +157,18 @@ impl InteractiveMode { if file_strings.is_empty() { println!("No files provided to add to the playlist"); } else { - Commands::add_files(&file_strings).await?; + Commands::add_files(&file_strings, socket_path).await?; } } ["get", property] => { - Commands::get_single_property(property).await?; + Commands::get_single_property(property, socket_path).await?; } ["set", property, value] => { let json_value = serde_json::from_str::(value) .unwrap_or_else(|_| json!(value)); - Commands::set_single_property(property, &json_value).await?; + Commands::set_single_property(property, &json_value, socket_path).await?; } _ => { @@ -144,3 +180,4 @@ impl InteractiveMode { Ok(()) } } +