Skip to content

Commit

Permalink
feat: add support for sending keys to an application without activation
Browse files Browse the repository at this point in the history
For #67
  • Loading branch information
socsieng committed Oct 6, 2023
1 parent 1fa0f95 commit b42d4bc
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 83 deletions.
21 changes: 17 additions & 4 deletions README.md
Expand Up @@ -38,10 +38,23 @@ cat example.txt | sendkeys --application-name "Notes"

_Activates the Notes application and sends keystrokes piped from `stdout` of the preceding command._

Note that a list of applications that can be used in `--application-name` can be found using the
[`apps` sub command](#list-of-applications-names).

Applications can also be activated using the running process id (`--pid` or `-p` option).
### Arguments

- `--application-name <application-name>`: The application name to activate or target when sending commands. Note that a
list of applications that can be used in `--application-name` can be found using the
[`apps` sub command](#list-of-applications-names).
- `--pid <process-id>`: The process id of the application to target when sending commands. Note that this if this
argument is supplied with `--application-name`, `--pid` takes precedence.
- `--targeted`: If supplied, the application keystrokes will only be sent to the targeted application.
- `--no-activate`: If supplied, the specified application will not be activated before sending commands.
- `--input-file <file-name>`: The path to a file containing the commands to send to the application.
- `--characters <characters>`: The characters to send to the application. Note that this argument is ignored if
`--input-file` is supplied.
- `--delay <delay>`: The delay between keystrokes and instructions. Defaults to `0.1` seconds.
- `--initial-delay <initial-delay>`: The initial delay before sending the first keystroke or instruction. Defaults to
`1` second.
- `--animation-interval <interval-in-seconds>`: The time between mouse movements when animating mouse commands. Lower
values results in smoother animations. Defaults to `0.01` seconds.

## Installation

Expand Down
18 changes: 12 additions & 6 deletions Sources/SendKeysLib/AppActivator.swift
Expand Up @@ -10,7 +10,7 @@ class AppActivator: NSObject {
self.processId = processId
}

func activate() throws {
func find() throws -> NSRunningApplication? {
let apps = NSWorkspace.shared.runningApplications.filter({ a in
return a.activationPolicy == .regular
})
Expand Down Expand Up @@ -61,12 +61,18 @@ class AppActivator: NSObject {
return bundleMatch != nil
}).first
}
}

if app == nil {
throw RuntimeError(
"Application \(appName!) cannot be activated. Run `sendkeys apps` to see a list of applications that can be activated."
)
}
return app
}

func activate() throws {
let app = try self.find()

if app == nil {
throw RuntimeError(
"Application \(appName!) cannot be activated. Run `sendkeys apps` to see a list of applications that can be activated."
)
}

if app != nil {
Expand Down
6 changes: 4 additions & 2 deletions Sources/SendKeysLib/Commands/CommandFactory.swift
Expand Up @@ -26,8 +26,10 @@ public class CommandFactory {
self.mouseController = mouseController
}

convenience public init() {
self.init(keyPresser: KeyPresser(), mouseController: MouseController(animationRefreshInterval: 0.01))
convenience public init(keyPresser: KeyPresser) {
self.init(
keyPresser: keyPresser,
mouseController: MouseController(animationRefreshInterval: 0.01, keyPresser: keyPresser))
}

public func create(_ commandType: Command.Type, arguments: [String?]) -> Command {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SendKeysLib/Commands/CommandsIterator.swift
Expand Up @@ -8,7 +8,7 @@ public class CommandsIterator: IteratorProtocol {

var index = 0

public init(_ commandString: String, commandFactory: CommandFactory = CommandFactory()) {
public init(_ commandString: String, commandFactory: CommandFactory) {
self.commandString = commandString
self.commandFactory = commandFactory
}
Expand Down
9 changes: 6 additions & 3 deletions Sources/SendKeysLib/Commands/CommandsProcessor.swift
Expand Up @@ -22,10 +22,13 @@ public class CommandsProcessor {
numberFormatter.maximumSignificantDigits = 3
}

convenience public init(defaultPause: Double, commandExecutor: CommandExecutorProtocol? = nil) {
convenience public init(
defaultPause: Double, keyPresser: KeyPresser, commandExecutor: CommandExecutorProtocol? = nil
) {
self.init(
defaultPause: defaultPause, keyPresser: KeyPresser(),
mouseController: MouseController(animationRefreshInterval: 0.01), commandExecutor: commandExecutor)
defaultPause: defaultPause, keyPresser: keyPresser,
mouseController: MouseController(animationRefreshInterval: 0.01, keyPresser: keyPresser),
commandExecutor: commandExecutor)
}

private func getDefaultPauseCommand() -> Command {
Expand Down
39 changes: 35 additions & 4 deletions Sources/SendKeysLib/KeyPresser.swift
@@ -1,7 +1,13 @@
import Cocoa
import Foundation

class KeyPresser {
public class KeyPresser {
private var application: NSRunningApplication?

init(app: NSRunningApplication?) {
self.application = app
}

func keyPress(key: String, modifiers: [String]) throws {
if let keyDownEvent = try! keyDown(key: key, modifiers: modifiers) {
let _ = keyUp(event: keyDownEvent)
Expand All @@ -11,23 +17,48 @@ class KeyPresser {
func keyDown(key: String, modifiers: [String]) throws -> CGEvent? {
let keyDownEvent = try! createKeyEvent(key: key, modifiers: modifiers, keyDown: true)

keyDownEvent?.post(tap: CGEventTapLocation.cghidEventTap)
if self.application == nil {
keyDownEvent?.post(tap: CGEventTapLocation.cghidEventTap)
} else {
if #available(OSX 10.11, *) {
keyDownEvent?.postToPid(self.application!.processIdentifier)
} else {
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)
if self.application == nil {
keyUpEvent?.post(tap: CGEventTapLocation.cghidEventTap)
} else {
if #available(OSX 10.11, *) {
keyUpEvent?.postToPid(self.application!.processIdentifier)
} else {
keyUpEvent?.post(tap: CGEventTapLocation.cghidEventTap)
}
}

return keyUpEvent
}

func keyUp(event: CGEvent) -> CGEvent? {
let keyCode = UInt16(event.getIntegerValueField(.keyboardEventKeycode))
let keyUpEvent = CGEvent(keyboardEventSource: CGEventSource(event: event), virtualKey: keyCode, keyDown: false)
keyUpEvent?.post(tap: CGEventTapLocation.cghidEventTap)

if self.application == nil {
keyUpEvent?.post(tap: CGEventTapLocation.cghidEventTap)
} else {
if #available(OSX 10.11, *) {
keyUpEvent?.postToPid(self.application!.processIdentifier)
} else {
keyUpEvent?.post(tap: CGEventTapLocation.cghidEventTap)
}
}

return keyUpEvent
}
Expand Down
5 changes: 3 additions & 2 deletions Sources/SendKeysLib/MouseController.swift
Expand Up @@ -15,11 +15,12 @@ class MouseController {
}

let animationRefreshInterval: TimeInterval
let keyPresser = KeyPresser()
let keyPresser: KeyPresser
var downButtons = Set<CGMouseButton>()

init(animationRefreshInterval: TimeInterval) {
init(animationRefreshInterval: TimeInterval, keyPresser: KeyPresser) {
self.animationRefreshInterval = animationRefreshInterval
self.keyPresser = keyPresser
}

func move(start: CGPoint?, end: CGPoint, duration: TimeInterval, flags: CGEventFlags) {
Expand Down
3 changes: 2 additions & 1 deletion Sources/SendKeysLib/MousePosition.swift
Expand Up @@ -50,7 +50,8 @@ class MousePosition: ParsableCommand {

func printMousePosition(_ position: CGPoint?) {
let numberFormatter = Self.createNumberFormatter()
let location = position ?? MouseController(animationRefreshInterval: 0.01).getLocation()!
let location =
position ?? MouseController(animationRefreshInterval: 0.01, keyPresser: KeyPresser(app: nil)).getLocation()!

printAndFlush("\(numberFormatter.string(for: location.x)!),\(numberFormatter.string(for: location.y)!)")
}
Expand Down
28 changes: 25 additions & 3 deletions Sources/SendKeysLib/Sender.swift
Expand Up @@ -17,6 +17,14 @@ public struct Sender: ParsableCommand {
help: "Process id of a running application to send keys to.")
var processId: Int?

@Flag(
name: .long, inversion: FlagInversion.prefixedNo,
help: "Activate the specified app or process before sending commands.")
var activate: Bool = true

@Flag(name: .long, help: "Only send keystrokes to the targeted app or process.")
var targeted: Bool = false

@Option(name: .shortAndLong, help: "Default delay between keystrokes in seconds.")
var delay: Double = 0.1

Expand Down Expand Up @@ -44,8 +52,20 @@ public struct Sender: ParsableCommand {
stderr)
}

let keyPresser = KeyPresser()
let mouseController = MouseController(animationRefreshInterval: animationInterval)
let activator = AppActivator(appName: applicationName, processId: processId)
let app: NSRunningApplication? = try activator.find()
let keyPresser: KeyPresser

if targeted {
if app == nil {
throw RuntimeError("Application could not be found.")
}
keyPresser = KeyPresser(app: app)
} else {
keyPresser = KeyPresser(app: nil)
}

let mouseController = MouseController(animationRefreshInterval: animationInterval, keyPresser: keyPresser)
let commandProcessor = CommandsProcessor(
defaultPause: delay, keyPresser: keyPresser, mouseController: mouseController)
var commandString: String?
Expand All @@ -60,7 +80,9 @@ public struct Sender: ParsableCommand {
commandString = characters
}

try AppActivator(appName: applicationName, processId: processId).activate()
if activate {
try activator.activate()
}

if initialDelay > 0 {
Sleeper.sleep(seconds: initialDelay)
Expand Down

0 comments on commit b42d4bc

Please sign in to comment.