use crate::model::{parse_clock, RepeatMode}; #[derive(Debug, Clone, Copy, PartialEq)] pub enum Amount { Abs(f64), Delta(f64), } #[derive(Debug, Clone, PartialEq)] pub enum Command { Search(String), Play(Option), Pause, Resume, Toggle, Stop, Next, Prev, Quit, Add(Option), AddAll, PlayAll, Remove(Option), Clear, ShowResults, ShowQueue, ShowPlaylists, Volume(Amount), Seek(Amount), Shuffle, Repeat(Option), Save(String), SaveForce(String), SaveAlbum(Option), SaveResults(Option), Load(String), Append(String), DeletePlaylist(String), AddTo(String), Import(String), Artist, Similar, Album(Option), PlaylistSearch(String), Lyrics, Help, } pub const COMMAND_NAMES: &[&str] = &[ "search", "play", "pause", "resume", "toggle", "stop", "next", "prev", "quit", "add", "addall", "playall", "remove", "clear", "results", "queue", "playlists", "volume", "seek", "shuffle", "repeat", "save", "savealbum", "saveresults", "load", "append", "delpl", "addto", "import", "artist", "similar", "album", "playlist", "lyrics", "help", ]; fn unquote(s: &str) -> &str { let s = s.trim(); let b = s.as_bytes(); if b.len() >= 2 && (b[0] == b'"' || b[0] == b'\'') && b[b.len() - 1] == b[0] { &s[1..s.len() - 1] } else { s } } fn index_arg(arg: &str, cmd: &str) -> Result, String> { if arg.is_empty() { return Ok(None); } match arg.parse::() { Ok(n) if n >= 1 => Ok(Some(n)), _ => Err(format!( ":{cmd} expects a row number (1, 2, 3 …), got '{arg}'" )), } } fn name_arg(arg: &str, cmd: &str) -> Result { let a = unquote(arg); if a.is_empty() { Err(format!("usage: :{cmd} ")) } else { Ok(a.to_string()) } } fn parse_amount(arg: &str, cmd: &str) -> Result { let a = arg.trim().trim_end_matches('%'); if a.is_empty() { return Err(format!("usage: :{cmd} (e.g. 50, +10, -10)")); } let err = || format!(":{cmd}: cannot understand '{arg}'"); if let Some(rest) = a.strip_prefix('+') { Ok(Amount::Delta(parse_clock(rest).ok_or_else(err)?)) } else if let Some(rest) = a.strip_prefix('-') { Ok(Amount::Delta(-parse_clock(rest).ok_or_else(err)?)) } else { Ok(Amount::Abs(parse_clock(a).ok_or_else(err)?)) } } pub fn parse(line: &str) -> Result, String> { let line = line.trim().trim_start_matches(':').trim(); if line.is_empty() { return Ok(None); } let end = line .find(|c: char| !(c.is_ascii_alphabetic() || c == '!')) .unwrap_or(line.len()); let (name, rest) = line.split_at(end); let rest = rest.trim(); let arg = unquote(rest); use Command::*; let cmd = match name.to_ascii_lowercase().as_str() { "search" | "s" | "find" => { if arg.is_empty() { return Err("usage: :search \"your query\"".into()); } Search(arg.to_string()) } "play" => Play(index_arg(arg, "play")?), "pause" => Pause, "resume" | "unpause" => Resume, "toggle" => Toggle, "stop" => Stop, "next" | "n" => Next, "prev" | "previous" => Prev, "quit" | "q" | "exit" | "q!" | "qa" | "wq" => Quit, "add" | "a" => Add(index_arg(arg, "add")?), "addall" => AddAll, "playall" => PlayAll, "remove" | "rm" | "del" => Remove(index_arg(arg, "remove")?), "clear" => Clear, "results" | "res" => ShowResults, "queue" => ShowQueue, "playlists" | "pls" => ShowPlaylists, "volume" | "vol" | "v" => Volume(parse_amount(arg, "volume")?), "seek" => Seek(parse_amount(arg, "seek")?), "shuffle" => Shuffle, "repeat" => match arg.to_ascii_lowercase().as_str() { "" => Repeat(None), "off" | "none" | "no" => Repeat(Some(RepeatMode::Off)), "all" | "queue" => Repeat(Some(RepeatMode::All)), "one" | "track" | "1" => Repeat(Some(RepeatMode::One)), other => return Err(format!(":repeat expects off|all|one, got '{other}'")), }, "save" | "w" => Save(name_arg(arg, "save")?), "save!" | "w!" => SaveForce(name_arg(arg, "save!")?), "savealbum" | "sa" => SaveAlbum((!arg.is_empty()).then(|| arg.to_string())), "saveresults" | "sr" => SaveResults((!arg.is_empty()).then(|| arg.to_string())), "load" | "e" => Load(name_arg(arg, "load")?), "append" => Append(name_arg(arg, "append")?), "delpl" | "deletepl" | "delete" => DeletePlaylist(name_arg(arg, "delpl")?), "addto" => AddTo(name_arg(arg, "addto")?), "import" => { if arg.is_empty() { return Err("usage: :import ".into()); } Import(arg.to_string()) } "artist" | "ar" => Artist, "similar" | "radio" | "sim" => Similar, "album" | "albums" | "al" => Album((!arg.is_empty()).then(|| arg.to_string())), "playlist" => { if arg.is_empty() { return Err("usage: :playlist \"query\"".into()); } PlaylistSearch(arg.to_string()) } "lyrics" | "ly" => Lyrics, "help" | "h" => Help, other => return Err(format!("unknown command: :{other} (try :help)")), }; Ok(Some(cmd)) } #[cfg(test)] mod tests { use super::*; #[test] fn search_variants() { assert_eq!( parse(r#"search "daft punk""#), Ok(Some(Command::Search("daft punk".into()))) ); assert_eq!( parse(":search daft punk"), Ok(Some(Command::Search("daft punk".into()))) ); assert_eq!( parse(r#"s"lofi beats""#), Ok(Some(Command::Search("lofi beats".into()))) ); assert!(parse("search").is_err()); } #[test] fn simple_commands() { assert_eq!(parse("play"), Ok(Some(Command::Play(None)))); assert_eq!(parse("play 3"), Ok(Some(Command::Play(Some(3))))); assert!(parse("play 0").is_err()); assert_eq!(parse("q!"), Ok(Some(Command::Quit))); assert_eq!(parse("pause"), Ok(Some(Command::Pause))); assert_eq!(parse(" "), Ok(None)); assert!(parse("bogus").is_err()); } #[test] fn amounts() { assert_eq!( parse("vol 50"), Ok(Some(Command::Volume(Amount::Abs(50.0)))) ); assert_eq!( parse("vol -10"), Ok(Some(Command::Volume(Amount::Delta(-10.0)))) ); assert_eq!( parse("seek 1:30"), Ok(Some(Command::Seek(Amount::Abs(90.0)))) ); assert_eq!( parse("seek +15"), Ok(Some(Command::Seek(Amount::Delta(15.0)))) ); assert!(parse("vol loud").is_err()); } #[test] fn artist_command() { assert_eq!(parse("artist"), Ok(Some(Command::Artist))); assert_eq!(parse(":ar"), Ok(Some(Command::Artist))); assert_eq!(parse("lyrics"), Ok(Some(Command::Lyrics))); assert_eq!(parse("similar"), Ok(Some(Command::Similar))); assert_eq!(parse(":radio"), Ok(Some(Command::Similar))); assert_eq!(parse("album"), Ok(Some(Command::Album(None)))); assert_eq!( parse("album daft punk"), Ok(Some(Command::Album(Some("daft punk".into())))) ); assert_eq!( parse("playlist lofi"), Ok(Some(Command::PlaylistSearch("lofi".into()))) ); assert!(parse("playlist").is_err()); assert_eq!(parse("playlists"), Ok(Some(Command::ShowPlaylists))); } #[test] fn playlists() { assert_eq!( parse(r#"save "my mix""#), Ok(Some(Command::Save("my mix".into()))) ); assert!(parse("save").is_err()); assert_eq!( parse("save! my mix"), Ok(Some(Command::SaveForce("my mix".into()))) ); assert_eq!(parse("savealbum"), Ok(Some(Command::SaveAlbum(None)))); assert_eq!( parse(r#"savealbum "Abbey Road""#), Ok(Some(Command::SaveAlbum(Some("Abbey Road".into())))) ); assert_eq!( parse("saveresults"), Ok(Some(Command::SaveResults(None))) ); assert_eq!( parse(r#"saveresults "My Hits""#), Ok(Some(Command::SaveResults(Some("My Hits".into())))) ); } }