Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for sending keys to an application without activation #68

Merged
merged 3 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 10 additions & 76 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,6 @@ on:
- main

jobs:
macos_catalina:
runs-on: macos-10.15

strategy:
matrix:
xcode:
- '12'

name: macOS Catalina (Xcode ${{ matrix.xcode }})

steps:
- name: checkout
uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: .build
key: ${{ runner.os }}-xcode-${{ matrix.xcode }}-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-xcode-${{ matrix.xcode }}
- name: build
run: |
ls -n /Applications/ | grep Xcode*
make build
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
- name: test
run: |
swift test
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer

macos_big_sur:
runs-on: macos-11.0

Expand All @@ -52,8 +21,8 @@ jobs:

steps:
- name: checkout
uses: actions/checkout@v2
- uses: actions/cache@v2
uses: actions/checkout@v4
- uses: actions/cache@v3
with:
path: .build
key: ${{ runner.os }}-xcode-${{ matrix.xcode }}-${{ hashFiles('**/Package.resolved') }}
Expand All @@ -78,8 +47,8 @@ jobs:

steps:
- name: checkout
uses: actions/checkout@v2
- uses: actions/cache@v2
uses: actions/checkout@v4
- uses: actions/cache@v3
with:
path: .build
key: ${{ runner.os }}-xcode-${{ hashFiles('**/Package.resolved') }}
Expand All @@ -98,7 +67,6 @@ jobs:

create_release:
needs:
- macos_catalina
- macos_big_sur
- macos_arm64

Expand Down Expand Up @@ -128,39 +96,6 @@ jobs:
{"type":"misc","section":"Miscellaneous","hidden":false}
]

create_bottle_catalina:
needs:
- create_release

runs-on: macos-10.15
if: ${{ needs.create_release.outputs.release_created }}

name: Create bottle Catalina

outputs:
file: ${{ steps.bottle.outputs.file }}
sha: ${{ steps.bottle.outputs.sha }}
root_url: ${{ steps.bottle.outputs.root_url }}

steps:
- uses: actions/checkout@v2

- id: bottle
name: Create bottle
run: |
./scripts/update-version.sh ${{ needs.create_release.outputs.tag_name }}
./scripts/bottle.sh ${{ needs.create_release.outputs.tag_name }} catalina

- name: Upload bottle
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create_release.outputs.upload_url }}
asset_path: ./${{ steps.bottle.outputs.file }}
asset_name: ${{ steps.bottle.outputs.file }}
asset_content_type: application/gzip

create_bottle_big_sur:
needs:
- create_release
Expand All @@ -173,9 +108,10 @@ jobs:
outputs:
file: ${{ steps.bottle.outputs.file }}
sha: ${{ steps.bottle.outputs.sha }}
root_url: ${{ steps.bottle.outputs.root_url }}

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- id: bottle
name: Create bottle
Expand Down Expand Up @@ -206,9 +142,10 @@ jobs:
outputs:
file: ${{ steps.bottle.outputs.file }}
sha: ${{ steps.bottle.outputs.sha }}
root_url: ${{ steps.bottle.outputs.root_url }}

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- id: bottle
name: Create bottle
Expand All @@ -229,7 +166,6 @@ jobs:
homebrew:
needs:
- create_release
- create_bottle_catalina
- create_bottle_big_sur
- create_bottle_big_sur_arm64

Expand All @@ -239,7 +175,7 @@ jobs:
name: Update homebrew formula

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Update homebrew formula
run: |
git config user.name github-actions[bot]
Expand All @@ -251,8 +187,7 @@ jobs:
formula='Formula/sendkeys.rb'
version=`echo '${{ needs.create_release.outputs.tag_name }}' | sed -E 's/^v//g'`
revision='${{ needs.create_release.outputs.sha }}'
sed_root_url=`echo '${{ needs.create_bottle_catalina.outputs.root_url }}' | sed 's/\\//\\\\\//g'`
sha_catalina='${{ needs.create_bottle_catalina.outputs.sha }}'
sed_root_url=`echo '${{ needs.create_bottle_big_sur.outputs.root_url }}' | sed 's/\\//\\\\\//g'`
sha_big_sur='${{ needs.create_bottle_big_sur.outputs.sha }}'
sha_big_sur_arm64='${{ needs.create_bottle_big_sur_arm64.outputs.sha }}'

Expand All @@ -261,7 +196,6 @@ jobs:
sed -E -i "" "s/version \"[^\"]+\"/version \"$version\"/g" $formula
sed -E -i "" "s/root_url \"[^\"]+\"/root_url \"$sed_root_url\"/g" $formula
sed -E -i "" "s/sha256 cellar: :any_skip_relocation, arm64_big_sur: \"[^\"]+\"/sha256 cellar: :any_skip_relocation, arm64_big_sur: \"$sha_big_sur_arm64\"/g" $formula
sed -E -i "" "s/sha256 cellar: :any_skip_relocation, catalina: \"[^\"]+\"/sha256 cellar: :any_skip_relocation, catalina: \"$sha_catalina\"/g" $formula
sed -E -i "" "s/sha256 cellar: :any_skip_relocation, big_sur: \"[^\"]+\"/sha256 cellar: :any_skip_relocation, big_sur: \"$sha_big_sur\"/g" $formula

git commit -am "chore: update sendkeys to ${{ needs.create_release.outputs.tag_name }}"
Expand Down
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading