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