Skip to content

Commit

Permalink
feat: add support for key down and up commands
Browse files Browse the repository at this point in the history
  • Loading branch information
socsieng committed Jan 1, 2021
1 parent 7c5e9d9 commit 2c5cefb
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 51 deletions.
6 changes: 6 additions & 0 deletions .editorconfig
@@ -0,0 +1,6 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
12 changes: 12 additions & 0 deletions README.md
Expand Up @@ -77,6 +77,18 @@ Example key combinations:
- `command` + `a`: `<c:a:command>`
- `option` + `shift` + `left arrow`: `<c:left:option,shift>`

#### 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 `<kd:key[:modifiers]>` and key up `<ku:key[:modifiers]>`.

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:
`<kd:option><m:left:option><ku:option>`.

### Mouse commands

#### Move mouse cursor
Expand Down
6 changes: 4 additions & 2 deletions Sources/SendKeysLib/Commands/Command.swift
@@ -1,5 +1,7 @@
public enum CommandType {
case keyPress
case keyDown
case keyUp
case pause
case stickyPause
case mouseMove
Expand All @@ -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, [])
}
Expand Down
37 changes: 24 additions & 13 deletions Sources/SendKeysLib/Commands/CommandExecutor.swift
Expand Up @@ -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)
Expand All @@ -22,41 +22,52 @@ 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]!)!
let x2 = Double(command.arguments[2]!)!
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),
duration: duration,
flags: modifiers != nil ? try! KeyPresser.getModifierFlags(modifiers!.components(separatedBy: ",")) : []
)
}

private func executeMouseClick(_ command: Command) {
let button = command.arguments[0]!
let modifiers = command.arguments[1]
Expand All @@ -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
Expand Down Expand Up @@ -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":
Expand Down
16 changes: 9 additions & 7 deletions Sources/SendKeysLib/Commands/CommandsIterator.swift
Expand Up @@ -2,9 +2,11 @@ import Foundation

public class CommandsIterator: IteratorProtocol {
public typealias Element = Command

private let commandMatchers: [CommandMatcher] = [
KeyPressCommandMatcher(),
KeyDownCommandMatcher(),
KeyUpCommandMatcher(),
StickyPauseCommandMatcher(),
PauseCommandMatcher(),
ContinuationCommandMatcher(),
Expand All @@ -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 {
Expand All @@ -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")
Expand All @@ -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..<numberOfRanges {
let range = Range(matchResult.range(at: i), in: commandString)
let arg = range == nil ? nil : String(commandString[range!])
args.append(arg)
}

return args
}
}
15 changes: 15 additions & 0 deletions Sources/SendKeysLib/Commands/KeyDownCommandMatcher.swift
@@ -0,0 +1,15 @@
import Foundation

public class KeyDownCommandMatcher: CommandMatcher {
public init() {
super.init(try! NSRegularExpression(pattern: "\\<kd:(.|[\\w]+)(:([,\\w⌘^⌥⇧]+))?\\>"))
}

override public func createCommand(_ arguments: [String?]) -> Command {
var args = [arguments[1]!]
if arguments[3] != nil {
args.append(arguments[3]!)
}
return Command(.keyDown, args)
}
}
4 changes: 2 additions & 2 deletions Sources/SendKeysLib/Commands/KeyPressCommandMatcher.swift
Expand Up @@ -2,9 +2,9 @@ import Foundation

public class KeyPressCommandMatcher: CommandMatcher {
public init() {
super.init(try! NSRegularExpression(pattern: "\\<c:(.|[\\w]+)(:([,\\w⌘^⌥⇧]+))?\\>"))
super.init(try! NSRegularExpression(pattern: "\\<[ck]:(.|[\\w]+)(:([,\\w⌘^⌥⇧]+))?\\>"))
}

override public func createCommand(_ arguments: [String?]) -> Command {
var args = [arguments[1]!]
if arguments[3] != nil {
Expand Down
15 changes: 15 additions & 0 deletions Sources/SendKeysLib/Commands/KeyUpCommandMatcher.swift
@@ -0,0 +1,15 @@
import Foundation

public class KeyUpCommandMatcher: CommandMatcher {
public init() {
super.init(try! NSRegularExpression(pattern: "\\<ku:(.|[\\w]+)(:([,\\w⌘^⌥⇧]+))?\\>"))
}

override public func createCommand(_ arguments: [String?]) -> Command {
var args = [arguments[1]!]
if arguments[3] != nil {
args.append(arguments[3]!)
}
return Command(.keyUp, args)
}
}
52 changes: 34 additions & 18 deletions Sources/SendKeysLib/KeyPresser.swift
Expand Up @@ -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 "",
Expand Down

0 comments on commit 2c5cefb

Please sign in to comment.