interactive: improve general usage with vi mode & history; add shortcuts

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I5bf85f6ad656ddc3c8246f07630658806a6a6964
This commit is contained in:
raf 2026-03-29 20:29:31 +03:00
commit 1768c14a33
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF

View file

@ -1,41 +1,72 @@
use crate::commands::Commands; use crate::commands::Commands;
use crate::{MrcError, Result}; use crate::{MrcError, Result};
use rustyline::config::EditMode;
use rustyline::{Cmd, Config, Editor, KeyEvent};
use serde_json::json; use serde_json::json;
use std::io::{self, Write}; use std::io::Error;
use std::path::PathBuf;
pub struct InteractiveMode; pub struct InteractiveMode;
impl 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."); println!("Entering interactive mode. Type 'help' for commands or 'exit' to quit.");
let stdin = io::stdin(); println!("Socket: {}", socket_path.unwrap_or("/tmp/mpvsocket"));
let mut stdout = io::stdout();
loop { loop {
print!("mpv> "); let readline = rl.readline("mpv> ");
stdout.flush().map_err(MrcError::ConnectionError)?; match readline {
Ok(input) => {
let trimmed = input.trim();
if trimmed.is_empty() {
continue;
}
rl.add_history_entry(trimmed).ok();
let mut input = String::new(); if trimmed.eq_ignore_ascii_case("exit") {
stdin println!("Exiting interactive mode.");
.read_line(&mut input) break;
.map_err(MrcError::ConnectionError)?; }
let trimmed = input.trim();
if trimmed.eq_ignore_ascii_case("exit") { if trimmed.eq_ignore_ascii_case("help") {
println!("Exiting interactive mode."); Self::show_help();
break; continue;
} }
if trimmed.eq_ignore_ascii_case("help") { if let Err(e) = Self::process_command(trimmed, socket_path).await {
Self::show_help(); eprintln!("Error: {}", e);
continue; }
} }
Err(rustyline::error::ReadlineError::Interrupted) => {
if let Err(e) = Self::process_command(trimmed).await { println!("(interrupted)");
eprintln!("Error: {}", e); continue;
}
Err(rustyline::error::ReadlineError::Eof) => {
println!("Exiting interactive mode.");
break;
}
Err(err) => {
eprintln!("Error: {:?}", err);
break;
}
} }
} }
rl.save_history(&hist_path).ok();
Ok(()) Ok(())
} }
@ -53,7 +84,7 @@ impl InteractiveMode {
("seek <seconds>", "Seek to the specified position"), ("seek <seconds>", "Seek to the specified position"),
("clear", "Clear the playlist"), ("clear", "Clear the playlist"),
("list", "List all items in the playlist"), ("list", "List all items in the playlist"),
("add <files>", "Add files to the playlist"), ("add <files...>", "Add files to the playlist"),
("get <property>", "Get the specified property"), ("get <property>", "Get the specified property"),
( (
"set <property> <value>", "set <property> <value>",
@ -64,56 +95,61 @@ impl InteractiveMode {
]; ];
for (command, description) in commands { 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(); let parts: Vec<&str> = input.split_whitespace().collect();
match parts.as_slice() { match parts.as_slice() {
["play"] => { ["play"] => {
Commands::play(None).await?; Commands::play(None, socket_path).await?;
} }
["play", index] => { ["play", index] => {
if let Ok(idx) = index.parse::<usize>() { if let Ok(idx) = index.parse::<usize>() {
Commands::play(Some(idx)).await?; Commands::play(Some(idx), socket_path).await?;
} else { } else {
println!("Invalid index: {}", index); println!("Invalid index: {}", index);
} }
} }
["pause"] => { ["pause"] => {
Commands::pause().await?; Commands::pause(socket_path).await?;
} }
["stop"] => { ["stop"] => {
Commands::stop().await?; Commands::stop(socket_path).await?;
} }
["next"] => { ["next"] => {
Commands::next().await?; Commands::next(socket_path).await?;
} }
["prev"] => { ["prev"] => {
Commands::prev().await?; Commands::prev(socket_path).await?;
} }
["seek", seconds] => { ["seek", seconds] => {
if let Ok(sec) = seconds.parse::<i32>() { if let Ok(sec) = seconds.parse::<i32>() {
Commands::seek_to(sec.into()).await?; Commands::seek_to(sec.into(), socket_path).await?;
} else { } else {
println!("Invalid seconds: {}", seconds); println!("Invalid seconds: {}", seconds);
} }
} }
["clear"] => { ["clear"] => {
Commands::clear_playlist().await?; Commands::clear_playlist(socket_path).await?;
} }
["list"] => { ["list"] => {
Commands::list_playlist().await?; Commands::list_playlist(socket_path).await?;
} }
["add", files @ ..] => { ["add", files @ ..] => {
@ -121,18 +157,18 @@ impl InteractiveMode {
if file_strings.is_empty() { if file_strings.is_empty() {
println!("No files provided to add to the playlist"); println!("No files provided to add to the playlist");
} else { } else {
Commands::add_files(&file_strings).await?; Commands::add_files(&file_strings, socket_path).await?;
} }
} }
["get", property] => { ["get", property] => {
Commands::get_single_property(property).await?; Commands::get_single_property(property, socket_path).await?;
} }
["set", property, value] => { ["set", property, value] => {
let json_value = serde_json::from_str::<serde_json::Value>(value) let json_value = serde_json::from_str::<serde_json::Value>(value)
.unwrap_or_else(|_| json!(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(()) Ok(())
} }
} }