1 use crate::queue::RepeatSetting;
2 use std::collections::HashMap;
3 use std::fmt;
4 
5 use strum_macros::Display;
6 
7 #[derive(Clone, Serialize, Deserialize, Debug)]
8 pub enum SeekInterval {
9     Forward,
10     Backwards,
11     Custom(usize),
12 }
13 
14 #[derive(Display, Clone, Serialize, Deserialize, Debug)]
15 #[strum(serialize_all = "lowercase")]
16 pub enum TargetMode {
17     Current,
18     Selected,
19 }
20 
21 #[derive(Display, Clone, Serialize, Deserialize, Debug)]
22 #[strum(serialize_all = "lowercase")]
23 pub enum MoveMode {
24     Up,
25     Down,
26     Left,
27     Right,
28     Playing,
29 }
30 
31 #[derive(Display, Clone, Serialize, Deserialize, Debug)]
32 #[strum(serialize_all = "lowercase")]
33 pub enum MoveAmount {
34     Integer(i32),
35     Extreme,
36 }
37 
38 impl Default for MoveAmount {
default() -> Self39     fn default() -> Self {
40         MoveAmount::Integer(1)
41     }
42 }
43 
44 #[derive(Display, Clone, Serialize, Deserialize, Debug)]
45 #[strum(serialize_all = "lowercase")]
46 pub enum SortKey {
47     Title,
48     Duration,
49     Artist,
50     Album,
51     Added,
52 }
53 
54 #[derive(Display, Clone, Serialize, Deserialize, Debug)]
55 #[strum(serialize_all = "lowercase")]
56 pub enum SortDirection {
57     Ascending,
58     Descending,
59 }
60 
61 #[derive(Display, Clone, Serialize, Deserialize, Debug)]
62 #[strum(serialize_all = "lowercase")]
63 pub enum JumpMode {
64     Previous,
65     Next,
66     Query(String),
67 }
68 
69 #[derive(Display, Clone, Serialize, Deserialize, Debug)]
70 #[strum(serialize_all = "lowercase")]
71 pub enum ShiftMode {
72     Up,
73     Down,
74 }
75 
76 #[derive(Display, Clone, Serialize, Deserialize, Debug)]
77 #[strum(serialize_all = "lowercase")]
78 pub enum GotoMode {
79     Album,
80     Artist,
81 }
82 
83 #[derive(Clone, Serialize, Deserialize, Debug)]
84 pub enum SeekDirection {
85     Relative(i32),
86     Absolute(u32),
87 }
88 
89 impl fmt::Display for SeekDirection {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result90     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91         let repr = match self {
92             SeekDirection::Absolute(pos) => format!("{}", pos),
93             SeekDirection::Relative(delta) => {
94                 format!("{}{}", if delta > &0 { "+" } else { "" }, delta)
95             }
96         };
97         write!(f, "{}", repr)
98     }
99 }
100 
101 #[derive(Clone, Serialize, Deserialize, Debug)]
102 pub enum Command {
103     Quit,
104     TogglePlay,
105     Stop,
106     Previous,
107     Next,
108     Clear,
109     Queue,
110     PlayNext,
111     Play,
112     UpdateLibrary,
113     Save,
114     SaveQueue,
115     Delete,
116     Focus(String),
117     Seek(SeekDirection),
118     VolumeUp(u16),
119     VolumeDown(u16),
120     Repeat(Option<RepeatSetting>),
121     Shuffle(Option<bool>),
122     Share(TargetMode),
123     Back,
124     Open(TargetMode),
125     Goto(GotoMode),
126     Move(MoveMode, MoveAmount),
127     Shift(ShiftMode, Option<i32>),
128     Search(String),
129     Jump(JumpMode),
130     Help,
131     ReloadConfig,
132     Noop,
133     Insert(Option<String>),
134     NewPlaylist(String),
135     Sort(SortKey, SortDirection),
136     Logout,
137     ShowRecommendations(TargetMode),
138     Redraw,
139 }
140 
141 impl fmt::Display for Command {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result142     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143         let repr = match self {
144             Command::Noop => "noop".to_string(),
145             Command::Quit => "quit".to_string(),
146             Command::TogglePlay => "playpause".to_string(),
147             Command::Stop => "stop".to_string(),
148             Command::Previous => "previous".to_string(),
149             Command::Next => "next".to_string(),
150             Command::Clear => "clear".to_string(),
151             Command::Queue => "queue".to_string(),
152             Command::PlayNext => "playnext".to_string(),
153             Command::Play => "play".to_string(),
154             Command::UpdateLibrary => "update".to_string(),
155             Command::Save => "save".to_string(),
156             Command::SaveQueue => "save queue".to_string(),
157             Command::Delete => "delete".to_string(),
158             Command::Focus(tab) => format!("focus {}", tab),
159             Command::Seek(direction) => format!("seek {}", direction),
160             Command::VolumeUp(amount) => format!("volup {}", amount),
161             Command::VolumeDown(amount) => format!("voldown {}", amount),
162             Command::Repeat(mode) => {
163                 let param = match mode {
164                     Some(mode) => format!("{}", mode),
165                     None => "".to_string(),
166                 };
167                 format!("repeat {}", param)
168             }
169             Command::Shuffle(on) => {
170                 let param = on.map(|x| if x { "on" } else { "off" });
171                 format!("shuffle {}", param.unwrap_or(""))
172             }
173             Command::Share(mode) => format!("share {}", mode),
174             Command::Back => "back".to_string(),
175             Command::Open(mode) => format!("open {}", mode),
176             Command::Goto(mode) => format!("goto {}", mode),
177             Command::Move(mode, MoveAmount::Extreme) => format!(
178                 "move {}",
179                 match mode {
180                     MoveMode::Up => "top",
181                     MoveMode::Down => "bottom",
182                     MoveMode::Left => "leftmost",
183                     MoveMode::Right => "rightmost",
184                     _ => "",
185                 }
186             ),
187             Command::Move(MoveMode::Playing, _) => "move playing".to_string(),
188             Command::Move(mode, MoveAmount::Integer(amount)) => format!("move {} {}", mode, amount),
189             Command::Shift(mode, amount) => format!("shift {} {}", mode, amount.unwrap_or(1)),
190             Command::Search(term) => format!("search {}", term),
191             Command::Jump(mode) => match mode {
192                 JumpMode::Previous => "jumpprevious".to_string(),
193                 JumpMode::Next => "jumpnext".to_string(),
194                 JumpMode::Query(term) => format!("jump {}", term).to_string(),
195             },
196             Command::Help => "help".to_string(),
197             Command::ReloadConfig => "reload".to_string(),
198             Command::Insert(_) => "insert".to_string(),
199             Command::NewPlaylist(name) => format!("new playlist {}", name),
200             Command::Sort(key, direction) => format!("sort {} {}", key, direction),
201             Command::Logout => "logout".to_string(),
202             Command::ShowRecommendations(mode) => format!("similar {}", mode),
203             Command::Redraw => "redraw".to_string(),
204         };
205         // escape the command separator
206         let repr = repr.replace(";", ";;");
207         write!(f, "{}", repr)
208     }
209 }
210 
register_aliases(map: &mut HashMap<&str, &str>, cmd: &'static str, names: Vec<&'static str>)211 fn register_aliases(map: &mut HashMap<&str, &str>, cmd: &'static str, names: Vec<&'static str>) {
212     for a in names {
213         map.insert(a, cmd);
214     }
215 }
216 
217 lazy_static! {
218     static ref ALIASES: HashMap<&'static str, &'static str> = {
219         let mut m = HashMap::new();
220 
221         register_aliases(&mut m, "quit", vec!["q", "x"]);
222         register_aliases(
223             &mut m,
224             "playpause",
225             vec!["pause", "toggleplay", "toggleplayback"],
226         );
227         register_aliases(&mut m, "repeat", vec!["loop"]);
228 
229         m.insert("1", "foo");
230         m.insert("2", "bar");
231         m.insert("3", "baz");
232         m
233     };
234 }
235 
handle_aliases(input: &str) -> &str236 fn handle_aliases(input: &str) -> &str {
237     if let Some(cmd) = ALIASES.get(input) {
238         handle_aliases(cmd)
239     } else {
240         input
241     }
242 }
243 
parse(input: &str) -> Option<Vec<Command>>244 pub fn parse(input: &str) -> Option<Vec<Command>> {
245     let mut command_inputs = vec!["".to_string()];
246     let mut command_idx = 0;
247     enum ParseState {
248         Normal,
249         SeparatorEncountered,
250     }
251     let mut parse_state = ParseState::Normal;
252     for c in input.chars() {
253         let is_separator = c == ';';
254         match parse_state {
255             ParseState::Normal if is_separator => parse_state = ParseState::SeparatorEncountered,
256             ParseState::Normal => command_inputs[command_idx].push(c),
257             // ";" is escaped using ";;", so if the previous char already was a ';' push a ';'.
258             ParseState::SeparatorEncountered if is_separator => {
259                 command_inputs[command_idx].push(c);
260                 parse_state = ParseState::Normal;
261             }
262             ParseState::SeparatorEncountered => {
263                 command_idx += 1;
264                 command_inputs.push(c.to_string());
265                 parse_state = ParseState::Normal;
266             }
267         }
268     }
269 
270     let mut commands = vec![];
271     for command_input in command_inputs {
272         let components: Vec<_> = command_input.trim().split(' ').collect();
273 
274         let command = handle_aliases(components[0]);
275         let args = components[1..].to_vec();
276 
277         let command = match command {
278             "quit" => Some(Command::Quit),
279             "playpause" => Some(Command::TogglePlay),
280             "stop" => Some(Command::Stop),
281             "previous" => Some(Command::Previous),
282             "next" => Some(Command::Next),
283             "clear" => Some(Command::Clear),
284             "playnext" => Some(Command::PlayNext),
285             "queue" => Some(Command::Queue),
286             "play" => Some(Command::Play),
287             "update" => Some(Command::UpdateLibrary),
288             "delete" => Some(Command::Delete),
289             "back" => Some(Command::Back),
290             "open" => args
291                 .get(0)
292                 .and_then(|target| match *target {
293                     "selected" => Some(TargetMode::Selected),
294                     "current" => Some(TargetMode::Current),
295                     _ => None,
296                 })
297                 .map(Command::Open),
298             "jump" => Some(Command::Jump(JumpMode::Query(args.join(" ")))),
299             "jumpnext" => Some(Command::Jump(JumpMode::Next)),
300             "jumpprevious" => Some(Command::Jump(JumpMode::Previous)),
301             "search" => Some(Command::Search(args.join(" "))),
302             "shift" => {
303                 let amount = args.get(1).and_then(|amount| amount.parse().ok());
304 
305                 args.get(0)
306                     .and_then(|direction| match *direction {
307                         "up" => Some(ShiftMode::Up),
308                         "down" => Some(ShiftMode::Down),
309                         _ => None,
310                     })
311                     .map(|mode| Command::Shift(mode, amount))
312             }
313             "move" => {
314                 let cmd: Option<Command> = {
315                     args.get(0).and_then(|extreme| match *extreme {
316                         "top" => Some(Command::Move(MoveMode::Up, MoveAmount::Extreme)),
317                         "bottom" => Some(Command::Move(MoveMode::Down, MoveAmount::Extreme)),
318                         "leftmost" => Some(Command::Move(MoveMode::Left, MoveAmount::Extreme)),
319                         "rightmost" => Some(Command::Move(MoveMode::Right, MoveAmount::Extreme)),
320                         "playing" => Some(Command::Move(MoveMode::Playing, MoveAmount::default())),
321                         _ => None,
322                     })
323                 };
324 
325                 cmd.or({
326                     let amount = args
327                         .get(1)
328                         .and_then(|amount| amount.parse().ok())
329                         .map(MoveAmount::Integer)
330                         .unwrap_or_default();
331 
332                     args.get(0)
333                         .and_then(|direction| match *direction {
334                             "up" => Some(MoveMode::Up),
335                             "down" => Some(MoveMode::Down),
336                             "left" => Some(MoveMode::Left),
337                             "right" => Some(MoveMode::Right),
338                             _ => None,
339                         })
340                         .map(|mode| Command::Move(mode, amount))
341                 })
342             }
343             "goto" => args
344                 .get(0)
345                 .and_then(|mode| match *mode {
346                     "album" => Some(GotoMode::Album),
347                     "artist" => Some(GotoMode::Artist),
348                     _ => None,
349                 })
350                 .map(Command::Goto),
351             "share" => args
352                 .get(0)
353                 .and_then(|target| match *target {
354                     "selected" => Some(TargetMode::Selected),
355                     "current" => Some(TargetMode::Current),
356                     _ => None,
357                 })
358                 .map(Command::Share),
359             "shuffle" => {
360                 let shuffle = args.get(0).and_then(|mode| match *mode {
361                     "on" => Some(true),
362                     "off" => Some(false),
363                     _ => None,
364                 });
365 
366                 Some(Command::Shuffle(shuffle))
367             }
368             "repeat" => {
369                 let mode = args.get(0).and_then(|mode| match *mode {
370                     "list" | "playlist" | "queue" => Some(RepeatSetting::RepeatPlaylist),
371                     "track" | "once" => Some(RepeatSetting::RepeatTrack),
372                     "none" | "off" => Some(RepeatSetting::None),
373                     _ => None,
374                 });
375 
376                 Some(Command::Repeat(mode))
377             }
378             "seek" => args.get(0).and_then(|arg| match arg.chars().next() {
379                 Some(x) if x == '-' || x == '+' => arg
380                     .chars()
381                     .skip(1)
382                     .collect::<String>()
383                     .parse::<i32>()
384                     .ok()
385                     .map(|amount| {
386                         Command::Seek(SeekDirection::Relative(
387                             amount
388                                 * match x {
389                                     '-' => -1,
390                                     _ => 1,
391                                 },
392                         ))
393                     }),
394                 _ => arg
395                     .chars()
396                     .collect::<String>()
397                     .parse()
398                     .ok()
399                     .map(|amount| Command::Seek(SeekDirection::Absolute(amount))),
400             }),
401             "focus" => args
402                 .get(0)
403                 .map(|target| Command::Focus((*target).to_string())),
404             "save" => args
405                 .get(0)
406                 .map(|target| match *target {
407                     "queue" => Command::SaveQueue,
408                     _ => Command::Save,
409                 })
410                 .or(Some(Command::Save)),
411             "volup" => Some(Command::VolumeUp(
412                 args.get(0).and_then(|v| v.parse::<u16>().ok()).unwrap_or(1),
413             )),
414             "voldown" => Some(Command::VolumeDown(
415                 args.get(0).and_then(|v| v.parse::<u16>().ok()).unwrap_or(1),
416             )),
417             "help" => Some(Command::Help),
418             "reload" => Some(Command::ReloadConfig),
419             "insert" => {
420                 if args.is_empty() {
421                     Some(Command::Insert(None))
422                 } else {
423                     args.get(0)
424                         .map(|url| Command::Insert(Some((*url).to_string())))
425                 }
426             }
427             "newplaylist" => {
428                 if !args.is_empty() {
429                     Some(Command::NewPlaylist(args.join(" ")))
430                 } else {
431                     None
432                 }
433             }
434             "sort" => {
435                 if !args.is_empty() {
436                     let sort_key = args.get(0).and_then(|key| match *key {
437                         "title" => Some(SortKey::Title),
438                         "duration" => Some(SortKey::Duration),
439                         "album" => Some(SortKey::Album),
440                         "added" => Some(SortKey::Added),
441                         "artist" => Some(SortKey::Artist),
442                         _ => None,
443                     })?;
444 
445                     let sort_direction = args
446                         .get(1)
447                         .map(|direction| match *direction {
448                             "a" => SortDirection::Ascending,
449                             "asc" => SortDirection::Ascending,
450                             "ascending" => SortDirection::Ascending,
451                             "d" => SortDirection::Descending,
452                             "desc" => SortDirection::Descending,
453                             "descending" => SortDirection::Descending,
454                             _ => SortDirection::Ascending,
455                         })
456                         .unwrap_or(SortDirection::Ascending);
457 
458                     Some(Command::Sort(sort_key, sort_direction))
459                 } else {
460                     None
461                 }
462             }
463             "logout" => Some(Command::Logout),
464             "similar" => args
465                 .get(0)
466                 .and_then(|target| match *target {
467                     "selected" => Some(TargetMode::Selected),
468                     "current" => Some(TargetMode::Current),
469                     _ => None,
470                 })
471                 .map(Command::ShowRecommendations),
472             "noop" => Some(Command::Noop),
473             "redraw" => Some(Command::Redraw),
474             _ => None,
475         };
476         commands.push(command?);
477     }
478     Some(commands)
479 }
480