Skip to content

Commit

Permalink
feat: add support for activating application by process id
Browse files Browse the repository at this point in the history
closes: #63
  • Loading branch information
socsieng committed Apr 15, 2023
1 parent 07f6c50 commit 0a44470
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 38 deletions.
8 changes: 5 additions & 3 deletions README.md
Expand Up @@ -36,10 +36,12 @@ Input can also be piped to `stdin`:
cat example.txt | sendkeys --application-name "Notes"
```

_Activates the Notes application and sends keystrokes piped from `stdout` of the preceeding command._
_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)
[`apps` sub command](#list-of-applications-names).

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

## Installation

Expand Down Expand Up @@ -302,7 +304,7 @@ sendkeys apps

Sample output:

```
```text
Code id:com.microsoft.VSCode
Finder id:com.apple.finder
Google Chrome id:com.google.Chrome
Expand Down
85 changes: 53 additions & 32 deletions Sources/SendKeysLib/AppActivator.swift
Expand Up @@ -2,58 +2,79 @@ import Cocoa

class AppActivator: NSObject {
private var application: NSRunningApplication!
private let filterName: String
private let appName: String?
private let processId: Int?

init(appName: String) {
filterName = appName.lowercased()
init(appName: String?, processId: Int?) {
self.appName = appName?.lowercased()
self.processId = processId
}

func activate() throws {
let apps = NSWorkspace.shared.runningApplications.filter({ a in
return a.activationPolicy == .regular
})

// exact match (case insensitive)
var app = apps.filter({ a in
return a.localizedName?.lowercased() == self.filterName
|| a.bundleIdentifier?.lowercased() == self.filterName
}).first
var app: NSRunningApplication?

let expression = try! NSRegularExpression(
pattern: "\\b\(NSRegularExpression.escapedPattern(for: self.filterName))\\b", options: .caseInsensitive)

// partial name match
if app == nil {
if processId != nil {
app =
apps.filter({ a in
let nameMatch = expression.firstMatch(
in: a.localizedName ?? "", options: [], range: NSMakeRange(0, a.localizedName?.utf16.count ?? 0)
)
return nameMatch != nil
return a.processIdentifier == pid_t(processId!)
}).first
}

// patial bundle id match
if app == nil {
if app == nil {
throw RuntimeError(
"Application with process id \(processId!) could not be found."
)
}
} else if appName != nil {
// exact match (case insensitive)
app =
apps.filter({ a in
let bundleMatch = expression.firstMatch(
in: a.bundleIdentifier ?? "", options: [],
range: NSMakeRange(0, a.bundleIdentifier?.utf16.count ?? 0))
return bundleMatch != nil
return a.localizedName?.lowercased() == appName
|| a.bundleIdentifier?.lowercased() == appName
}).first
}

if app == nil {
throw RuntimeError(
"Application \(self.filterName) could not be activated. Run `sendkeys apps` to see a list of applications that can be activated."
)
let expression = try! NSRegularExpression(
pattern: "\\b\(NSRegularExpression.escapedPattern(for: appName!))\\b", options: .caseInsensitive)

// partial name match
if app == nil {
app =
apps.filter({ a in
let nameMatch = expression.firstMatch(
in: a.localizedName ?? "", options: [],
range: NSMakeRange(0, a.localizedName?.utf16.count ?? 0)
)
return nameMatch != nil
}).first
}

// patial bundle id match
if app == nil {
app =
apps.filter({ a in
let bundleMatch = expression.firstMatch(
in: a.bundleIdentifier ?? "", options: [],
range: NSMakeRange(0, a.bundleIdentifier?.utf16.count ?? 0))
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."
)
}
}

self.application = app
if app != nil {
self.application = app

self.unhideAppIfNeeded()
self.activateAppIfNeeded()
self.unhideAppIfNeeded()
self.activateAppIfNeeded()
}
}

private func unhideAppIfNeeded() {
Expand Down
9 changes: 6 additions & 3 deletions Sources/SendKeysLib/Sender.swift
Expand Up @@ -12,6 +12,11 @@ public struct Sender: ParsableCommand {
@Option(name: .shortAndLong, help: "Name of a running application to send keys to.")
var applicationName: String?

@Option(
name: NameSpecification([.short, .customLong("pid")]),
help: "Process id of a running application to send keys to.")
var processId: Int?

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

Expand Down Expand Up @@ -55,9 +60,7 @@ public struct Sender: ParsableCommand {
commandString = characters
}

if !(applicationName ?? "").isEmpty {
try AppActivator(appName: applicationName!).activate()
}
try AppActivator(appName: applicationName, processId: processId).activate()

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

0 comments on commit 0a44470

Please sign in to comment.