Browse Source

Add Command struct to formalize command organization

Use this to implement a !help command to list available commands.
Frans Bergman 4 years ago
parent
commit
1d4a90da1f
2 changed files with 205 additions and 90 deletions
  1. 202 90
      src/commands.rs
  2. 3 0
      src/main.rs

+ 202 - 90
src/commands.rs

@@ -3,7 +3,185 @@ use mpd::search::Term;
 use mpd::search::Window;
 use mpd::Client;
 use mpd::Query;
+use std::cmp::Ordering;
 use std::collections::BTreeMap;
+use std::collections::BTreeSet;
+
+#[derive(Clone)]
+pub struct Command {
+    names: &'static [&'static str],
+    description: &'static str,
+    run: &'static dyn Fn(CommandContext) -> Vec<String>,
+}
+
+// Used for sorting the command list shown by the !help command
+impl Ord for Command {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.names.cmp(&other.names)
+    }
+}
+
+impl PartialOrd for Command {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Eq for Command {}
+
+impl PartialEq for Command {
+    fn eq(&self, other: &Self) -> bool {
+        self.names == other.names && self.description == other.description
+    }
+}
+
+struct CommandContext<'a> {
+    mpd: &'a mut Client,
+    pages: &'a mut BTreeMap<u32, Vec<String>>,
+    actor: u32,
+    args: &'a mut dyn Iterator<Item = &'a str>,
+    commands: &'a BTreeMap<String, Command>,
+}
+
+pub fn create_commands() -> BTreeMap<String, Command> {
+    let commands = [
+        Command {
+            names: &["more", "m"],
+            description: "Displays next page of command output.",
+            run: &|ctx| {
+                if let Some(lines) = ctx.pages.get(&ctx.actor) {
+                    lines.clone()
+                } else {
+                    vec!["No more pages :(".to_string()]
+                }
+            },
+        },
+        Command {
+            names: &["next", "skip", "s"],
+            description: "Skips the currently playing song.",
+            run: &|ctx| {
+                ctx.mpd.next().unwrap();
+                vec!["Skipping".to_string()]
+            },
+        },
+        Command {
+            names: &["pause"],
+            description: "Pauses playback.",
+            run: &|ctx| {
+                ctx.mpd.pause(true).unwrap();
+                vec!["Pausing".to_string()]
+            },
+        },
+        Command {
+            names: &["play"],
+            description: "Resumes playback.",
+            run: &|ctx| {
+                ctx.mpd.play().unwrap();
+                vec!["Playing".to_string()]
+            },
+        },
+        Command {
+            names: &["toggle", "t"],
+            description: "Toggles playback.",
+            run: &|ctx| {
+                ctx.mpd.toggle_pause().unwrap();
+                vec!["Toggling".to_string()]
+            },
+        },
+        Command {
+            names: &["search", "se"],
+            description: "Searches the database for tracks matching specified query strings.",
+            run: &|ctx| {
+                let mut query = Query::new();
+                let query = ctx.args.fold(&mut query, |a, b| a.and(Term::Any, b));
+
+                if let Ok(result) = ctx.mpd.search(&query, None) {
+                    let display = result.iter().filter_map(display_song);
+                    vec![format!("{} results:", result.len())]
+                        .into_iter()
+                        .chain(display)
+                        .collect()
+                } else {
+                    vec!["Search failed".to_string()]
+                }
+            },
+        },
+        Command {
+            names: &["searchplay", "sp"],
+            description: "Searches the database for tracks matching specified query strings and plays the first track found.",
+            run: &|ctx| {
+                let mut query = Query::new();
+                let query = ctx.args.fold(&mut query, |a, b| a.and(Term::Any, b));
+
+                if let Ok(result) = ctx.mpd.search(&query, Window::from((0, 1))) {
+                    let target_song = result.first().unwrap();
+                    let position =
+                        ctx.mpd.queue()
+                        .unwrap()
+                        .iter()
+                        .enumerate()
+                        .find_map(|(i, song)| {
+                            if song.file == target_song.file {
+                                Some(i)
+                            } else {
+                                None
+                            }
+                        });
+                    if let Some(pos) = position {
+                        ctx.mpd.switch(pos as u32).unwrap();
+                        vec![format!("Playing {}", display_song(&target_song).unwrap())]
+                    } else {
+                        vec![format!(
+                            "Song not found in queue: {}",
+                            display_song(&target_song).unwrap()
+                            )]
+                    }
+                } else {
+                    vec!["Search failed".to_string()]
+                }
+            },
+        },
+        Command {
+            names: &["queue", "q"],
+            description: "Shows the queue.",
+            run: &|ctx| {
+                ctx.mpd
+                    .queue()
+                    .unwrap()
+                    .iter()
+                    .filter_map(display_song)
+                    .collect()
+            },
+        },
+        Command {
+            names: &["help", "h"],
+            description: "Display this command listing.",
+            run: &|c| {
+                let command_set: BTreeSet<Command> = c.commands
+                    .values()
+                    .cloned()
+                    .into_iter()
+                    .collect();
+                let text: Vec<String> =
+                    command_set
+                    .into_iter()
+                    .map(|c| format!("<b>{}</b>: {}", c.names.join(", "), c.description))
+                    .collect();
+                text
+            },
+        }
+    ];
+
+    let mut command_map = BTreeMap::new();
+
+    for command in &commands {
+        for name in command.names.iter() {
+            command_map.insert(name.to_string(), command.clone());
+        }
+    }
+
+    command_map
+}
 
 fn paginate<K: Ord, V: Clone>(
     pages: &mut BTreeMap<K, Vec<V>>,
@@ -26,101 +204,35 @@ pub fn parse_command(
     pages: &mut BTreeMap<u32, Vec<String>>,
     actor: u32,
     msg: &str,
+    commands: &BTreeMap<String, Command>,
 ) -> Option<String> {
-    let reply = run_command(conn, pages, actor, msg);
-    if reply.len() > 50 {
-        Some(format!(
-            "{} <br> Output has been paginated, use !more to view the rest.",
-            paginate(pages, 50, actor, reply).join("<br>")
-        ))
-    } else {
-        pages.remove(&actor);
-        Some(reply.join("<br>"))
-    }
-}
-
-fn run_command(
-    conn: &mut Client,
-    pages: &mut BTreeMap<u32, Vec<String>>,
-    actor: u32,
-    msg: &str,
-) -> Vec<String> {
-    let mut command = msg.split_whitespace();
-    match command.nth(0).unwrap() {
-        "!more" => {
-            if let Some(lines) = pages.get(&actor) {
-                lines.clone()
-            } else {
-                vec!["No more pages :(".to_string()]
-            }
-        }
-        "!next" | "!skip" => {
-            conn.next().unwrap();
-            vec!["Skipping".to_string()]
-        }
-        "!pause" => {
-            conn.pause(true).unwrap();
-            vec!["Pausing".to_string()]
-        }
-        "!play" => {
-            conn.play().unwrap();
-            vec!["Playing".to_string()]
-        }
-        "!toggle" => {
-            conn.toggle_pause().unwrap();
-            vec!["Toggling".to_string()]
-        }
-        "!search" | "!se" => {
-            let mut query = Query::new();
-            let query = command.fold(&mut query, |a, b| a.and(Term::Any, b));
-
-            if let Ok(result) = conn.search(&query, None) {
-                let display = result.iter().filter_map(display_song);
-                vec![format!("{} results:", result.len())]
-                    .into_iter()
-                    .chain(display)
-                    .collect()
-            } else {
-                vec!["Search failed".to_string()]
-            }
-        }
-        "!searchplay" | "!sp" => {
-            let mut query = Query::new();
-            let query = command.fold(&mut query, |a, b| a.and(Term::Any, b));
-
-            if let Ok(result) = conn.search(&query, Window::from((0, 1))) {
-                let target_song = result.first().unwrap();
-                let position = conn
-                    .queue()
-                    .unwrap()
-                    .iter()
-                    .enumerate()
-                    .find_map(|(i, song)| {
-                        if song.file == target_song.file {
-                            Some(i)
-                        } else {
-                            None
-                        }
-                    });
-                if let Some(pos) = position {
-                    conn.switch(pos as u32).unwrap();
-                    vec![format!("Playing {}", display_song(&target_song).unwrap())]
+    let mut words = msg.split_whitespace();
+    if let Some(command_name) = words.nth(0) {
+        if command_name.chars().nth(0).unwrap() == '!' {
+            if let Some(command) = commands.get(&command_name.chars().skip(1).collect::<String>()) {
+                let reply = (*command.run)(CommandContext {
+                    mpd: conn,
+                    pages,
+                    actor,
+                    args: &mut words,
+                    commands,
+                });
+                if reply.len() > 50 {
+                    Some(format!(
+                        "{} <br> Output has been paginated, use !more to view the rest.",
+                        paginate(pages, 50, actor, reply).join("<br>")
+                    ))
                 } else {
-                    vec![format!(
-                        "Song not found in queue: {}",
-                        display_song(&target_song).unwrap()
-                    )]
+                    pages.remove(&actor);
+                    Some(reply.join("<br>"))
                 }
             } else {
-                vec!["Search failed".to_string()]
+                Some("Command not found, use !help to list commands.".to_string())
             }
+        } else {
+            None
         }
-        "!queue" => conn
-            .queue()
-            .unwrap()
-            .iter()
-            .filter_map(display_song)
-            .collect(),
-        _ => vec![],
+    } else {
+        None
     }
 }

+ 3 - 0
src/main.rs

@@ -12,6 +12,7 @@ use argparse::ArgumentParser;
 use argparse::Store;
 use argparse::StoreTrue;
 use bytes::Bytes;
+use commands::create_commands;
 use commands::parse_command;
 use futures::channel::oneshot;
 use futures::join;
@@ -118,6 +119,7 @@ async fn connect(
     });
 
     let mut paged_results = BTreeMap::new();
+    let commands = create_commands();
 
     while let Some(i) = rx.recv().await {
         match i {
@@ -134,6 +136,7 @@ async fn connect(
                         &mut paged_results,
                         msg.get_actor(),
                         &msg.take_message(),
+                        &commands,
                     ) {
                         let mut response = msgs::TextMessage::new();
                         response.mut_session().push(msg.get_actor());