Skip to content

Commit

Permalink
feat: implement keyboard and mouse commands
Browse files Browse the repository at this point in the history
  • Loading branch information
socsieng committed Dec 30, 2020
1 parent d038cc8 commit 6618d21
Show file tree
Hide file tree
Showing 30 changed files with 1,313 additions and 29 deletions.
11 changes: 7 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import PackageDescription

let package = Package(
name: "sendkeys",
name: "SendKeys",
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/apple/swift-argument-parser", from: "0.3.1")
Expand All @@ -13,10 +13,13 @@ let package = Package(
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "sendkeys",
name: "SendKeys",
dependencies: ["SendKeysLib", "ArgumentParser"]),
.target(
name: "SendKeysLib",
dependencies: ["ArgumentParser"]),
.testTarget(
name: "sendkeysTests",
dependencies: ["sendkeys"]),
name: "SendKeysTests",
dependencies: ["SendKeys", "SendKeysLib"]),
]
)
7 changes: 7 additions & 0 deletions Sources/SendKeys/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import SendKeysLib

if #available(OSX 10.11, *) {
SendKeysCli.main()
} else {
print("OS version 10.11 or higher is required.")
}
33 changes: 33 additions & 0 deletions Sources/SendKeysLib/Animator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Foundation

class Animator {
typealias AnimationCallback = (_ progress: Double) -> Void

let duration: TimeInterval
let frequency: TimeInterval
let animateFn: AnimationCallback

init(_ duration: TimeInterval, _ frequency: TimeInterval, _ animateFn: @escaping AnimationCallback) {
self.duration = duration
self.frequency = frequency
self.animateFn = animateFn
}

func animate() {
let startDate = Date()

while -startDate.timeIntervalSinceNow < duration {
let progress = min(-startDate.timeIntervalSinceNow as Double / duration as Double, 1)
let easedValue = easeInOut(progress)

Sleeper.sleep(seconds: frequency)
animateFn(easedValue)
}

animateFn(1)
}

func easeInOut(_ x: Double) -> Double {
return x < 0.5 ? 2 * x * x : 1 - pow(-2 * x + 2, 2) / 2;
}
}
50 changes: 50 additions & 0 deletions Sources/SendKeysLib/AppActivator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Cocoa

class AppActivator: NSObject {
private var application: NSRunningApplication!
private let filterName: String

init(appName: String) {
filterName = appName
}

func activate() throws {
guard let app = NSWorkspace.shared.runningApplications.filter ({
return $0.localizedName == self.filterName || $0.bundleIdentifier?.contains(self.filterName) ?? false
}).first else {
throw RuntimeError("Application \(self.filterName) not found")
}

guard app.activationPolicy != .prohibited else {
throw RuntimeError("Application \(self.filterName) prohibits activation")
}

self.application = app

self.unhideAppIfNeeded()
self.activateAppIfNeeded()
}

private func unhideAppIfNeeded() {
if application.isHidden {
application.addObserver(self, forKeyPath: "isHidden", options: .new, context: nil)
application.unhide()
}
}

private func activateAppIfNeeded() {
if !application.isHidden && !application.isActive {
application.addObserver(self, forKeyPath: "isActive", options: .new, context: nil)
application.activate(options: .activateIgnoringOtherApps)
}
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "isHidden" {
application.removeObserver(self, forKeyPath: "isHidden")
activateAppIfNeeded()
} else if keyPath == "isActive" {
application.removeObserver(self, forKeyPath: "isActive")
}
}
}
27 changes: 27 additions & 0 deletions Sources/SendKeysLib/Commands/Command.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
public enum CommandType {
case keyPress
case pause
case stickyPause
case mouseMove
case mouseClick
case mouseDrag
case continuation
}

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, [])
}

public static func == (lhs: Command, rhs: Command) -> Bool {
return lhs.type == rhs.type && lhs.arguments == rhs.arguments;
}
}
95 changes: 95 additions & 0 deletions Sources/SendKeysLib/Commands/CommandExecutor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import Foundation

public protocol CommandExecutorProtocol {
func execute(_ command: Command)
}

public class CommandExecutor: CommandExecutorProtocol {
private let keyPresser = KeyPresser()
private let mouseController = MouseController()

public func execute(_ command: Command) {
switch command.type {
case .keyPress:
executeKeyPress(command)
case .pause, .stickyPause:
executePause(command)
case .mouseMove:
executeMouseMove(command)
case .mouseClick:
executeMouseClick(command)
case .mouseDrag:
executeMouseDrag(command)
default:
fatalError("Unrecognized command type \(command.type)")
}
}

private func executeKeyPress(_ command: Command) {
var modifiers: [String] = []

if command.arguments.count > 1 {
modifiers = command.arguments[1].components(separatedBy: ",")
}

try! keyPresser.pressKey(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])!

mouseController.move(
start: CGPoint(x: x1, y: y1),
end: CGPoint(x: x2, y: y2),
duration: duration
)
}

private func executeMouseClick(_ command: Command) {
let button = command.arguments[0]
let clicks = Int(command.arguments[1])!

try! mouseController.click(
CGPoint(x: -1, y: -1),
button: getMouseButton(button: button),
clickCount: clicks
)
}

private func executeMouseDrag(_ 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 button = command.arguments[5]

try! mouseController.drag(
start: CGPoint(x: x1, y: y1),
end: CGPoint(x: x2, y: y2),
duration: duration,
button: getMouseButton(button: button)
)
}

private func getMouseButton(button: String) throws -> CGMouseButton {
switch button {
case "left":
return CGMouseButton.left
case "center":
return CGMouseButton.center
case "right":
return CGMouseButton.right
default:
throw RuntimeError("Unknown mouse button: \(button)")
}
}
}
13 changes: 13 additions & 0 deletions Sources/SendKeysLib/Commands/CommandMatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

public class CommandMatcher {
let expression: NSRegularExpression

public func createCommand(_ arguments: [String?]) -> Command {
fatalError("Not implemented")
}

public init(_ expression: NSRegularExpression) {
self.expression = expression
}
}
63 changes: 63 additions & 0 deletions Sources/SendKeysLib/Commands/CommandsIterator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Foundation

public class CommandsIterator: IteratorProtocol {
public typealias Element = Command

private let commandMatchers: [CommandMatcher] = [
KeyPressCommandMatcher(),
StickyPauseCommandMatcher(),
PauseCommandMatcher(),
ContinuationCommandMatcher(),
NewlineCommandMatcher(),
MouseMoveCommandMatcher(),
MouseClickCommandMatcher(),
MouseDragCommandMatcher(),
DefaultCommandMatcher()
]

let commandString: String
var index = 0;

public init(_ commandString: String) {
self.commandString = commandString
}

public func next() -> Element? {
let length = commandString.utf16.count
if index < length {
var matchResult: NSTextCheckingResult?;
let matcher = commandMatchers.first { (matcher: CommandMatcher) -> Bool in
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.")
}
}
return nil
}

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
}
}
56 changes: 56 additions & 0 deletions Sources/SendKeysLib/Commands/CommandsProcessor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Foundation

public class CommandsProcessor {
var defaultPause: Double
let commandExecutor: CommandExecutorProtocol
let numberFormatter = NumberFormatter()

public init(defaultPause: Double, commandExecutor: CommandExecutorProtocol? = nil) {
self.defaultPause = defaultPause
self.commandExecutor = commandExecutor ?? CommandExecutor()

numberFormatter.usesSignificantDigits = true
numberFormatter.minimumSignificantDigits = 1
numberFormatter.maximumSignificantDigits = 3
}

private func getDefaultPauseCommand() -> Command {
return Command(.pause, [numberFormatter.string(from: NSNumber(value: defaultPause))!])
}

public func process(_ commandString: String) {
let commands = IteratorSequence(CommandsIterator(commandString))
var shouldDefaultPause = false
var shouldIgnoreNextCommand = false

for command in commands {
if shouldIgnoreNextCommand {
shouldIgnoreNextCommand = false
continue
}

if command.type == .continuation {
shouldIgnoreNextCommand = true
continue
}

if command.type == .pause {
shouldDefaultPause = false
} else if command.type == .stickyPause {
shouldDefaultPause = false
defaultPause = Double(command.arguments[0])!
} else if shouldDefaultPause {
executeCommand(getDefaultPauseCommand())
shouldDefaultPause = true
} else {
shouldDefaultPause = true
}

executeCommand(command)
}
}

func executeCommand(_ command: Command) {
commandExecutor.execute(command)
}
}
11 changes: 11 additions & 0 deletions Sources/SendKeysLib/Commands/ContinuationCommandMatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

public class ContinuationCommandMatcher: CommandMatcher {
public init() {
super.init(try! NSRegularExpression(pattern: "\\<\\\\\\>"))
}

override public func createCommand(_ arguments: [String?]) -> Command {
return Command(.continuation, [])
}
}
Loading

0 comments on commit 6618d21

Please sign in to comment.