monade-mprocs 0.3.0

A fork of the popular mprocs utility, includable via cargo as a library
Documentation
use anyhow::bail;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde::{Deserialize, Serialize};

static KEYS: phf::Map<&'static str, KeyCode> = phf::phf_map! {
  "bs" => KeyCode::Backspace,
  "enter" => KeyCode::Enter,
  "left" => KeyCode::Left,
  "right" => KeyCode::Right,
  "up" => KeyCode::Up,
  "down" => KeyCode::Down,
  "home" => KeyCode::Home,
  "end" => KeyCode::End,
  "pageup" => KeyCode::PageUp,
  "pagedown" => KeyCode::PageDown,
  "tab" => KeyCode::Tab,
  "del" => KeyCode::Delete,
  "insert" => KeyCode::Insert,
  "nul" => KeyCode::Null,
  "esc" => KeyCode::Esc,

  "lt" => KeyCode::Char('<'),
  "gt" => KeyCode::Char('>'),
  "minus" => KeyCode::Char('-'),

  "f1" => KeyCode::F(1),
  "f2" => KeyCode::F(2),
  "f3" => KeyCode::F(3),
  "f4" => KeyCode::F(4),
  "f5" => KeyCode::F(5),
  "f6" => KeyCode::F(6),
  "f7" => KeyCode::F(7),
  "f8" => KeyCode::F(8),
  "f9" => KeyCode::F(9),
  "f10" => KeyCode::F(10),
  "f11" => KeyCode::F(11),
  "f12" => KeyCode::F(12),
};

static SPECIAL_CHARS: phf::Map<char, &str> = phf::phf_map! {
  '<' => "LT",
  '>' => "GT",
  '-' => "Minus",
};

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Key(KeyEvent);

impl Key {
  pub fn new(code: KeyCode, mods: KeyModifiers) -> Key {
    Key::from(KeyEvent::new(code, mods))
  }

  pub fn parse(text: &str) -> anyhow::Result<Key> {
    KeyParser::parse(text)
  }

  pub fn code(&self) -> &KeyCode {
    &self.0.code
  }

  pub fn mods(&self) -> &KeyModifiers {
    &self.0.modifiers
  }

  pub fn set_mods(mut self, mods: KeyModifiers) -> Self {
    self.0.modifiers = mods;
    self
  }
}

impl From<KeyEvent> for Key {
  fn from(key_event: KeyEvent) -> Self {
    Key(key_event)
  }
}

impl From<KeyCode> for Key {
  fn from(code: KeyCode) -> Self {
    Key::new(code, KeyModifiers::NONE)
  }
}

impl ToString for Key {
  fn to_string(&self) -> String {
    let mut buf = String::new();

    buf.push('<');

    let mods = self.0.modifiers;
    if mods.intersects(KeyModifiers::CONTROL) {
      buf.push_str("C-");
    }
    if mods.intersects(KeyModifiers::SHIFT) {
      buf.push_str("S-");
    }
    if mods.intersects(KeyModifiers::ALT) {
      buf.push_str("M-");
    }

    match self.0.code {
      KeyCode::Backspace => buf.push_str("BS"),
      KeyCode::Enter => buf.push_str("Enter"),
      KeyCode::Left => buf.push_str("Left"),
      KeyCode::Right => buf.push_str("Right"),
      KeyCode::Up => buf.push_str("Up"),
      KeyCode::Down => buf.push_str("Down"),
      KeyCode::Home => buf.push_str("Home"),
      KeyCode::End => buf.push_str("End"),
      KeyCode::PageUp => buf.push_str("PageUp"),
      KeyCode::PageDown => buf.push_str("PageDown"),
      KeyCode::Tab => buf.push_str("Tab"),
      KeyCode::BackTab => buf.push_str("S-Tab"),
      KeyCode::Delete => buf.push_str("Del"),
      KeyCode::Insert => buf.push_str("Insert"),
      KeyCode::F(n) => {
        buf.push('F');
        buf.push_str(n.to_string().as_str());
      }
      KeyCode::Char(ch) => {
        if let Some(s) = SPECIAL_CHARS.get(&ch) {
          buf.push_str(s)
        } else {
          buf.push(ch)
        }
      }
      KeyCode::Null => buf.push_str("Nul"),
      KeyCode::Esc => buf.push_str("Esc"),
    }

    buf.push('>');

    buf
  }
}

impl Serialize for Key {
  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  where
    S: serde::Serializer,
  {
    serializer.serialize_str(self.to_string().as_str())
  }
}

impl<'de> Deserialize<'de> for Key {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: serde::Deserializer<'de>,
  {
    let text = String::deserialize(deserializer)?;
    Key::parse(text.as_str())
      .map_err(|err| serde::de::Error::custom(err.to_string()))
  }
}

struct KeyParser<'a> {
  text: &'a str,
  pos: usize,
}

impl KeyParser<'_> {
  fn parse(text: &str) -> anyhow::Result<Key> {
    let mut parser = KeyParser { text, pos: 0 };

    parser.expect("<")?;
    let mods = parser.take_mods()?;
    let code = {
      let word = parser.take_word()?;
      if let Some(code) = KEYS.get(word.to_ascii_lowercase().as_str()) {
        code.clone()
      } else if word.len() == 1 {
        KeyCode::Char(word.chars().next().unwrap())
      } else {
        bail!("Wrong key code: \"{}\"", word);
      }
    };
    parser.expect(">")?;

    Ok(Key(KeyEvent::new(code, mods)))
  }

  fn expect(&mut self, s: &str) -> anyhow::Result<()> {
    let next_pos = self.pos + s.len();
    if next_pos > self.text.len() {
      bail!("Expected \"{}\"", s);
    }
    let subtext = &self.text[self.pos..next_pos];
    if subtext != s {
      bail!("Expected \"{}\"", s);
    }

    self.pos = next_pos;
    Ok(())
  }

  fn take_word(&mut self) -> anyhow::Result<&str> {
    let mut next_pos = self.pos;
    let mut chars = self.text[self.pos..].chars();
    while let Some(ch) = chars.next() {
      if ch.is_alphanumeric() {
        next_pos += ch.len_utf8();
      } else {
        break;
      }
    }
    let start = self.pos;
    self.pos = next_pos;
    Ok(&self.text[start..next_pos])
  }

  fn take_mods(&mut self) -> anyhow::Result<KeyModifiers> {
    let mut mods = KeyModifiers::NONE;
    let mut pos = self.pos;
    while pos + 1 < self.text.len() && &self.text[pos + 1..pos + 2] == "-" {
      match &self.text[pos..pos + 1] {
        "c" | "C" => mods = mods.union(KeyModifiers::CONTROL),
        "s" | "S" => mods = mods.union(KeyModifiers::SHIFT),
        "m" | "M" => mods = mods.union(KeyModifiers::ALT),
        ch => bail!("Wrong key modifier: \"{}\"", ch),
      }
      pos += 2;
    }
    self.pos = pos;
    Ok(mods)
  }
}

#[cfg(test)]
mod tests {
  use assert_matches::assert_matches;

  use super::*;

  #[test]
  fn parse() {
    assert_eq!(
      Key::parse("<Tab>").unwrap(),
      Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE))
    );
    assert_eq!(
      Key::parse("<C-Enter>").unwrap(),
      Key(KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL))
    );
    assert_eq!(
      Key::parse("<C-Esc>").unwrap(),
      Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::CONTROL))
    );

    assert_eq!(
      Key::parse("<F1>").unwrap(),
      Key(KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE))
    );
    assert_eq!(
      Key::parse("<f12>").unwrap(),
      Key(KeyEvent::new(KeyCode::F(12), KeyModifiers::NONE))
    );
    assert_matches!(Key::parse("<F13>"), Err(_));

    assert_eq!(
      Key::parse("<a>").unwrap(),
      Key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE))
    );
    assert_eq!(
      Key::parse("<C-a>").unwrap(),
      Key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL))
    );
    assert_eq!(
      Key::parse("<C-M-a>").unwrap(),
      Key(KeyEvent::new(
        KeyCode::Char('a'),
        KeyModifiers::CONTROL | KeyModifiers::ALT
      ))
    );
  }

  #[test]
  fn parse_and_print() {
    fn in_out(key: &str) {
      assert_eq!(Key::parse(key).unwrap().to_string(), key);
    }

    in_out("<BS>");
    in_out("<Enter>");
    in_out("<Left>");
    in_out("<Right>");
    in_out("<Up>");
    in_out("<Down>");
    in_out("<Home>");
    in_out("<End>");
    in_out("<PageUp>");
    in_out("<PageDown>");
    in_out("<Tab>");
    in_out("<Del>");
    in_out("<Insert>");
    in_out("<Nul>");
    in_out("<Esc>");

    in_out("<a>");
    in_out("<A>");

    in_out("<C-a>");
    in_out("<C-M-a>");
    in_out("<C-Enter>");

    in_out("<Minus>");
    in_out("<LT>");
    in_out("<GT>");
  }
}