185 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			185 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use crate::{Key, KeyboardControllable};
 | |
| use std::error::Error;
 | |
| use std::fmt;
 | |
| 
 | |
| /// An error that can occur when parsing DSL
 | |
| #[derive(Debug, PartialEq, Eq)]
 | |
| pub enum ParseError {
 | |
|     /// When a tag doesn't exist.
 | |
|     /// Example: {+TEST}{-TEST}
 | |
|     ///            ^^^^   ^^^^
 | |
|     UnknownTag(String),
 | |
| 
 | |
|     /// When a { is encountered inside a {TAG}.
 | |
|     /// Example: {+HELLO{WORLD}
 | |
|     ///                 ^
 | |
|     UnexpectedOpen,
 | |
| 
 | |
|     /// When a { is never matched with a }.
 | |
|     /// Example: {+SHIFT}Hello{-SHIFT
 | |
|     ///                              ^
 | |
|     UnmatchedOpen,
 | |
| 
 | |
|     /// Opposite of UnmatchedOpen.
 | |
|     /// Example: +SHIFT}Hello{-SHIFT}
 | |
|     ///         ^
 | |
|     UnmatchedClose,
 | |
| }
 | |
| impl Error for ParseError {
 | |
|     fn description(&self) -> &str {
 | |
|         match *self {
 | |
|             ParseError::UnknownTag(_) => "Unknown tag",
 | |
|             ParseError::UnexpectedOpen => "Unescaped open bracket ({) found inside tag name",
 | |
|             ParseError::UnmatchedOpen => "Unmatched open bracket ({). No matching close (})",
 | |
|             ParseError::UnmatchedClose => "Unmatched close bracket (}). No previous open ({)",
 | |
|         }
 | |
|     }
 | |
| }
 | |
| impl fmt::Display for ParseError {
 | |
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | |
|         f.write_str(&self.to_string())
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// Evaluate the DSL. This tokenizes the input and presses the keys.
 | |
| pub fn eval<K>(enigo: &mut K, input: &str) -> Result<(), ParseError>
 | |
| where
 | |
|     K: KeyboardControllable,
 | |
| {
 | |
|     for token in tokenize(input)? {
 | |
|         match token {
 | |
|             Token::Sequence(buffer) => {
 | |
|                 for key in buffer.chars() {
 | |
|                     enigo.key_click(Key::Layout(key));
 | |
|                 }
 | |
|             }
 | |
|             Token::Unicode(buffer) => enigo.key_sequence(&buffer),
 | |
|             Token::KeyUp(key) => enigo.key_up(key),
 | |
|             Token::KeyDown(key) => enigo.key_down(key).unwrap_or(()),
 | |
|         }
 | |
|     }
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[derive(Debug, PartialEq, Eq)]
 | |
| enum Token {
 | |
|     Sequence(String),
 | |
|     Unicode(String),
 | |
|     KeyUp(Key),
 | |
|     KeyDown(Key),
 | |
| }
 | |
| 
 | |
| fn tokenize(input: &str) -> Result<Vec<Token>, ParseError> {
 | |
|     let mut unicode = false;
 | |
| 
 | |
|     let mut tokens = Vec::new();
 | |
|     let mut buffer = String::new();
 | |
|     let mut iter = input.chars().peekable();
 | |
| 
 | |
|     fn flush(tokens: &mut Vec<Token>, buffer: String, unicode: bool) {
 | |
|         if !buffer.is_empty() {
 | |
|             if unicode {
 | |
|                 tokens.push(Token::Unicode(buffer));
 | |
|             } else {
 | |
|                 tokens.push(Token::Sequence(buffer));
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     while let Some(c) = iter.next() {
 | |
|         if c == '{' {
 | |
|             match iter.next() {
 | |
|                 Some('{') => buffer.push('{'),
 | |
|                 Some(mut c) => {
 | |
|                     flush(&mut tokens, buffer, unicode);
 | |
|                     buffer = String::new();
 | |
| 
 | |
|                     let mut tag = String::new();
 | |
|                     loop {
 | |
|                         tag.push(c);
 | |
|                         match iter.next() {
 | |
|                             Some('{') => match iter.peek() {
 | |
|                                 Some(&'{') => {
 | |
|                                     iter.next();
 | |
|                                     c = '{'
 | |
|                                 }
 | |
|                                 _ => return Err(ParseError::UnexpectedOpen),
 | |
|                             },
 | |
|                             Some('}') => match iter.peek() {
 | |
|                                 Some(&'}') => {
 | |
|                                     iter.next();
 | |
|                                     c = '}'
 | |
|                                 }
 | |
|                                 _ => break,
 | |
|                             },
 | |
|                             Some(new) => c = new,
 | |
|                             None => return Err(ParseError::UnmatchedOpen),
 | |
|                         }
 | |
|                     }
 | |
|                     match &*tag {
 | |
|                         "+UNICODE" => unicode = true,
 | |
|                         "-UNICODE" => unicode = false,
 | |
|                         "+SHIFT" => tokens.push(Token::KeyDown(Key::Shift)),
 | |
|                         "-SHIFT" => tokens.push(Token::KeyUp(Key::Shift)),
 | |
|                         "+CTRL" => tokens.push(Token::KeyDown(Key::Control)),
 | |
|                         "-CTRL" => tokens.push(Token::KeyUp(Key::Control)),
 | |
|                         "+META" => tokens.push(Token::KeyDown(Key::Meta)),
 | |
|                         "-META" => tokens.push(Token::KeyUp(Key::Meta)),
 | |
|                         "+ALT" => tokens.push(Token::KeyDown(Key::Alt)),
 | |
|                         "-ALT" => tokens.push(Token::KeyUp(Key::Alt)),
 | |
|                         _ => return Err(ParseError::UnknownTag(tag)),
 | |
|                     }
 | |
|                 }
 | |
|                 None => return Err(ParseError::UnmatchedOpen),
 | |
|             }
 | |
|         } else if c == '}' {
 | |
|             match iter.next() {
 | |
|                 Some('}') => buffer.push('}'),
 | |
|                 _ => return Err(ParseError::UnmatchedClose),
 | |
|             }
 | |
|         } else {
 | |
|             buffer.push(c);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     flush(&mut tokens, buffer, unicode);
 | |
| 
 | |
|     Ok(tokens)
 | |
| }
 | |
| 
 | |
| #[cfg(test)]
 | |
| mod tests {
 | |
|     use super::*;
 | |
| 
 | |
|     #[test]
 | |
|     fn success() {
 | |
|         assert_eq!(
 | |
|             tokenize("{{Hello World!}} {+CTRL}hi{-CTRL}"),
 | |
|             Ok(vec![
 | |
|                 Token::Sequence("{Hello World!} ".into()),
 | |
|                 Token::KeyDown(Key::Control),
 | |
|                 Token::Sequence("hi".into()),
 | |
|                 Token::KeyUp(Key::Control)
 | |
|             ])
 | |
|         );
 | |
|     }
 | |
|     #[test]
 | |
|     fn unexpected_open() {
 | |
|         assert_eq!(tokenize("{hello{}world}"), Err(ParseError::UnexpectedOpen));
 | |
|     }
 | |
|     #[test]
 | |
|     fn unmatched_open() {
 | |
|         assert_eq!(
 | |
|             tokenize("{this is going to fail"),
 | |
|             Err(ParseError::UnmatchedOpen)
 | |
|         );
 | |
|     }
 | |
|     #[test]
 | |
|     fn unmatched_close() {
 | |
|         assert_eq!(
 | |
|             tokenize("{+CTRL}{{this}} is going to fail}"),
 | |
|             Err(ParseError::UnmatchedClose)
 | |
|         );
 | |
|     }
 | |
| }
 |