From 2c5cefb28c802dea94d55920c4092b232f50e62e Mon Sep 17 00:00:00 2001 From: Soc Sieng Date: Fri, 1 Jan 2021 15:48:01 -0800 Subject: [PATCH] feat: add support for key down and up commands --- .editorconfig | 6 ++ README.md | 12 +++ Sources/SendKeysLib/Commands/Command.swift | 6 +- .../Commands/CommandExecutor.swift | 37 ++++++--- .../Commands/CommandsIterator.swift | 16 ++-- .../Commands/KeyDownCommandMatcher.swift | 15 ++++ .../Commands/KeyPressCommandMatcher.swift | 4 +- .../Commands/KeyUpCommandMatcher.swift | 15 ++++ Sources/SendKeysLib/KeyPresser.swift | 52 +++++++----- .../SendKeysTests/CommandIteratorTests.swift | 81 ++++++++++++++++--- 10 files changed, 193 insertions(+), 51 deletions(-) create mode 100644 .editorconfig create mode 100644 Sources/SendKeysLib/Commands/KeyDownCommandMatcher.swift create mode 100644 Sources/SendKeysLib/Commands/KeyUpCommandMatcher.swift diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..589f816 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/README.md b/README.md index f8c83f7..f7ff5f1 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,18 @@ Example key combinations: - `command` + `a`: `` - `option` + `shift` + `left arrow`: `` +#### Key up and down + +Some applications expect modifier keys to be pressed explicitly before invoking actions like mouse click. An example of +this is Pixelmator which expect the `option` key to be pressed before executing the alternate click action. This can be +achieved with key down `` and key up ``. + +Note that these command shoulds only be used in these special cases when the mouse action and modifier keys are not +supported natively. + +An example of how to trigger alternate click behavior in Pixelmator as described above: +``. + ### Mouse commands #### Move mouse cursor diff --git a/Sources/SendKeysLib/Commands/Command.swift b/Sources/SendKeysLib/Commands/Command.swift index f3b869a..bcaa675 100644 --- a/Sources/SendKeysLib/Commands/Command.swift +++ b/Sources/SendKeysLib/Commands/Command.swift @@ -1,5 +1,7 @@ public enum CommandType { case keyPress + case keyDown + case keyUp case pause case stickyPause case mouseMove @@ -12,12 +14,12 @@ public enum CommandType { public struct Command: Equatable { let type: CommandType let arguments: [String?] - + public init(_ type: CommandType, _ arguments: [String?]) { self.type = type self.arguments = arguments } - + public init(_ type: CommandType) { self.init(type, []) } diff --git a/Sources/SendKeysLib/Commands/CommandExecutor.swift b/Sources/SendKeysLib/Commands/CommandExecutor.swift index ed78ca3..d3668ce 100644 --- a/Sources/SendKeysLib/Commands/CommandExecutor.swift +++ b/Sources/SendKeysLib/Commands/CommandExecutor.swift @@ -7,10 +7,10 @@ public protocol CommandExecutorProtocol { public class CommandExecutor: CommandExecutorProtocol { private let keyPresser = KeyPresser() private let mouseController = MouseController() - + public func execute(_ command: Command) { switch command.type { - case .keyPress: + case .keyPress, .keyDown, .keyUp: executeKeyPress(command) case .pause, .stickyPause: executePause(command) @@ -22,25 +22,36 @@ public class CommandExecutor: CommandExecutorProtocol { executeMouseDrag(command) case .mouseScroll: executeMouseScroll(command) - default: - fatalError("Unrecognized command type \(command.type)\n") + case .continuation: + return } } - + private func executeKeyPress(_ command: Command) { var modifiers: [String] = [] - + if command.arguments.count > 1 { modifiers = command.arguments[1]!.components(separatedBy: ",") } - + + switch command.type { + case .keyPress: + try! keyPresser.keyPress(key: command.arguments[0]!, modifiers: modifiers) + case .keyDown: + let _ = try! keyPresser.keyDown(key: command.arguments[0]!, modifiers: modifiers) + case .keyUp: + let _ = try! keyPresser.keyUp(key: command.arguments[0]!, modifiers: modifiers) + default: + return + } + try! keyPresser.keyPress(key: command.arguments[0]!, modifiers: modifiers) } - + private func executePause(_ command: Command) { Sleeper.sleep(seconds: Double(command.arguments[0]!)!) } - + private func executeMouseMove(_ command: Command) { let x1 = Double(command.arguments[0]!)! let y1 = Double(command.arguments[1]!)! @@ -48,7 +59,7 @@ public class CommandExecutor: CommandExecutorProtocol { let y2 = Double(command.arguments[3]!)! let duration: TimeInterval = Double(command.arguments[4]!)! let modifiers = command.arguments[5] - + mouseController.move( start: CGPoint(x: x1, y: y1), end: CGPoint(x: x2, y: y2), @@ -56,7 +67,7 @@ public class CommandExecutor: CommandExecutorProtocol { flags: modifiers != nil ? try! KeyPresser.getModifierFlags(modifiers!.components(separatedBy: ",")) : [] ) } - + private func executeMouseClick(_ command: Command) { let button = command.arguments[0]! let modifiers = command.arguments[1] @@ -69,7 +80,7 @@ public class CommandExecutor: CommandExecutorProtocol { clickCount: clicks ) } - + private func executeMouseScroll(_ command: Command) { let x = Int(command.arguments[0]!) ?? 0 let y = Int(command.arguments[1]!) ?? 0 @@ -100,7 +111,7 @@ public class CommandExecutor: CommandExecutorProtocol { flags: modifiers != nil ? try! KeyPresser.getModifierFlags(modifiers!.components(separatedBy: ",")) : [] ) } - + private func getMouseButton(button: String) throws -> CGMouseButton { switch button { case "left": diff --git a/Sources/SendKeysLib/Commands/CommandsIterator.swift b/Sources/SendKeysLib/Commands/CommandsIterator.swift index b6b42cf..5281b1b 100644 --- a/Sources/SendKeysLib/Commands/CommandsIterator.swift +++ b/Sources/SendKeysLib/Commands/CommandsIterator.swift @@ -2,9 +2,11 @@ import Foundation public class CommandsIterator: IteratorProtocol { public typealias Element = Command - + private let commandMatchers: [CommandMatcher] = [ KeyPressCommandMatcher(), + KeyDownCommandMatcher(), + KeyUpCommandMatcher(), StickyPauseCommandMatcher(), PauseCommandMatcher(), ContinuationCommandMatcher(), @@ -22,7 +24,7 @@ public class CommandsIterator: IteratorProtocol { public init(_ commandString: String) { self.commandString = commandString } - + public func next() -> Element? { let length = commandString.utf16.count if index < length { @@ -31,16 +33,16 @@ public class CommandsIterator: IteratorProtocol { matchResult = matcher.expression.firstMatch(in: commandString, options: .anchored, range: NSMakeRange(index, length - index)) return matchResult != nil } - + if matcher != nil { let args = getArguments(commandString, matchResult!) let command = matcher!.createCommand(args) - + if matchResult != nil { let range = Range(matchResult!.range, in: commandString) index = range!.upperBound.utf16Offset(in: commandString) } - + return command } else { fatalError("Unmatched sequence.\n") @@ -52,13 +54,13 @@ public class CommandsIterator: IteratorProtocol { private func getArguments(_ commandString: String, _ matchResult: NSTextCheckingResult) -> [String?] { var args: [String?] = []; let numberOfRanges = matchResult.numberOfRanges - + for i in 0..")) + } + + override public func createCommand(_ arguments: [String?]) -> Command { + var args = [arguments[1]!] + if arguments[3] != nil { + args.append(arguments[3]!) + } + return Command(.keyDown, args) + } +} diff --git a/Sources/SendKeysLib/Commands/KeyPressCommandMatcher.swift b/Sources/SendKeysLib/Commands/KeyPressCommandMatcher.swift index c2b6be9..7987a9f 100644 --- a/Sources/SendKeysLib/Commands/KeyPressCommandMatcher.swift +++ b/Sources/SendKeysLib/Commands/KeyPressCommandMatcher.swift @@ -2,9 +2,9 @@ import Foundation public class KeyPressCommandMatcher: CommandMatcher { public init() { - super.init(try! NSRegularExpression(pattern: "\\")) + super.init(try! NSRegularExpression(pattern: "\\<[ck]:(.|[\\w]+)(:([,\\w⌘^⌥⇧]+))?\\>")) } - + override public func createCommand(_ arguments: [String?]) -> Command { var args = [arguments[1]!] if arguments[3] != nil { diff --git a/Sources/SendKeysLib/Commands/KeyUpCommandMatcher.swift b/Sources/SendKeysLib/Commands/KeyUpCommandMatcher.swift new file mode 100644 index 0000000..4a796db --- /dev/null +++ b/Sources/SendKeysLib/Commands/KeyUpCommandMatcher.swift @@ -0,0 +1,15 @@ +import Foundation + +public class KeyUpCommandMatcher: CommandMatcher { + public init() { + super.init(try! NSRegularExpression(pattern: "\\")) + } + + override public func createCommand(_ arguments: [String?]) -> Command { + var args = [arguments[1]!] + if arguments[3] != nil { + args.append(arguments[3]!) + } + return Command(.keyUp, args) + } +} diff --git a/Sources/SendKeysLib/KeyPresser.swift b/Sources/SendKeysLib/KeyPresser.swift index c38df99..be04bf8 100644 --- a/Sources/SendKeysLib/KeyPresser.swift +++ b/Sources/SendKeysLib/KeyPresser.swift @@ -3,50 +3,66 @@ import Foundation class KeyPresser { func keyPress(key: String, modifiers: [String]) throws { if let keyDownEvent = try! keyDown(key: key, modifiers: modifiers) { - keyUp(event: keyDownEvent) + let _ = keyUp(event: keyDownEvent) } } func keyDown(key: String, modifiers: [String]) throws -> CGEvent? { + let keyDownEvent = try! createKeyEvent(key: key, modifiers: modifiers, keyDown: true) + + keyDownEvent?.post(tap: CGEventTapLocation.cghidEventTap) + + return keyDownEvent + } + + func keyUp(key: String, modifiers: [String]) throws -> CGEvent? { + let keyUpEvent = try! createKeyEvent(key: key, modifiers: modifiers, keyDown: false) + + keyUpEvent?.post(tap: CGEventTapLocation.cghidEventTap) + + return keyUpEvent + } + + func keyUp(event: CGEvent) -> CGEvent? { + let keyUpEvent = CGEvent(keyboardEventSource: CGEventSource(event: event), virtualKey: 0, keyDown: false) + keyUpEvent?.flags = event.flags + keyUpEvent?.post(tap: CGEventTapLocation.cghidEventTap) + + return keyUpEvent + } + + private func createKeyEvent(key: String, modifiers: [String], keyDown: Bool) throws -> CGEvent? { let keycode = KeyCodes.getKeyCode(key) ?? 0 let flags = try! KeyPresser.getModifierFlags(modifiers) - let keyDownEvent = CGEvent(keyboardEventSource: nil, virtualKey: keycode, keyDown: true) - + let keyEvent = CGEvent(keyboardEventSource: nil, virtualKey: keycode, keyDown: keyDown) + if keycode == 0 { if key.count == 1 { let utf16Chars = Array(key.utf16) - keyDownEvent!.keyboardSetUnicodeString(stringLength: utf16Chars.count, unicodeString: utf16Chars) + keyEvent!.keyboardSetUnicodeString(stringLength: utf16Chars.count, unicodeString: utf16Chars) } else { throw RuntimeError("Unrecognized key: \(key)") } } - + if !flags.isEmpty { - keyDownEvent?.flags = flags + keyEvent?.flags = flags } - keyDownEvent?.post(tap: CGEventTapLocation.cghidEventTap) - - return keyDownEvent + return keyEvent } - func keyUp(event: CGEvent) { - let keyUpEvent = CGEvent(keyboardEventSource: CGEventSource(event: event), virtualKey: 0, keyDown: false) - keyUpEvent?.flags = event.flags - keyUpEvent?.post(tap: CGEventTapLocation.cghidEventTap) - } - static func getModifierFlags(_ modifiers: [String]) throws -> CGEventFlags { var flags: CGEventFlags = [] - + for modifier in modifiers.filter({ !$0.isEmpty }) { let flag = try getModifierFlag(modifier) flags.insert(flag) } - + return flags } - + private static func getModifierFlag(_ modifier: String) throws -> CGEventFlags { switch modifier { case "⌘", diff --git a/Tests/SendKeysTests/CommandIteratorTests.swift b/Tests/SendKeysTests/CommandIteratorTests.swift index b7509ed..019f0b2 100644 --- a/Tests/SendKeysTests/CommandIteratorTests.swift +++ b/Tests/SendKeysTests/CommandIteratorTests.swift @@ -13,8 +13,8 @@ final class CommandIteratorTests: XCTestCase { Command(CommandType.keyPress, ["c"]) ]) } - - func testParsesKeyPresses() throws { + + func testParsesKeyPress() throws { let commands = getCommands(CommandsIterator("")) XCTAssertEqual( commands, @@ -22,7 +22,7 @@ final class CommandIteratorTests: XCTestCase { Command(CommandType.keyPress, ["a"]) ]) } - + func testParsesKeyPressesWithModifierKey() throws { let commands = getCommands(CommandsIterator("")) XCTAssertEqual( @@ -31,7 +31,7 @@ final class CommandIteratorTests: XCTestCase { Command(CommandType.keyPress, ["a", "command"]) ]) } - + func testParsesKeyPressesWithModifierKeys() throws { let commands = getCommands(CommandsIterator("")) XCTAssertEqual( @@ -41,6 +41,69 @@ final class CommandIteratorTests: XCTestCase { ]) } + func testParsesKeyPressAlias() throws { + let commands = getCommands(CommandsIterator("")) + XCTAssertEqual( + commands, + [ + Command(CommandType.keyPress, ["a"]) + ]) + } + + func testParsesKeyDown() throws { + let commands = getCommands(CommandsIterator("")) + XCTAssertEqual( + commands, + [ + Command(CommandType.keyDown, ["a"]) + ]) + } + + func testParsesKeyDownWithModifierKey() throws { + let commands = getCommands(CommandsIterator("")) + XCTAssertEqual( + commands, + [ + Command(CommandType.keyDown, ["a", "shift"]) + ]) + } + + func testParsesKeyDownAsModifierKey() throws { + let commands = getCommands(CommandsIterator("")) + XCTAssertEqual( + commands, + [ + Command(CommandType.keyDown, ["shift"]) + ]) + } + + func testParsesKeyUp() throws { + let commands = getCommands(CommandsIterator("")) + XCTAssertEqual( + commands, + [ + Command(CommandType.keyUp, ["a"]) + ]) + } + + func testParsesKeyUpWithModifierKey() throws { + let commands = getCommands(CommandsIterator("")) + XCTAssertEqual( + commands, + [ + Command(CommandType.keyUp, ["a", "shift"]) + ]) + } + + func testParsesKeyUpAsModifierKey() throws { + let commands = getCommands(CommandsIterator("")) + XCTAssertEqual( + commands, + [ + Command(CommandType.keyUp, ["shift"]) + ]) + } + func testParsesNewLines() throws { let commands = getCommands(CommandsIterator("\n\n\n")) XCTAssertEqual( @@ -51,7 +114,7 @@ final class CommandIteratorTests: XCTestCase { Command(CommandType.keyPress, ["return"]) ]) } - + func testParsesNewLinesWithCarriageReturns() throws { let commands = getCommands(CommandsIterator("\r\n\r\n\n")) XCTAssertEqual( @@ -318,17 +381,17 @@ final class CommandIteratorTests: XCTestCase { private func getCommands(_ iterator: CommandsIterator) -> [Command] { var commands: [Command] = [] - + while true { let command = iterator.next() - + if command == nil { break } - + commands.append(command!) } - + return commands } }