Skip to content

Commit

Permalink
feat: add transform subcommand
Browse files Browse the repository at this point in the history
Transforms raw text input for to be used by text editors.

Also refactored send as a subcommand.
  • Loading branch information
socsieng committed Jan 6, 2021
1 parent 1437aac commit 3893313
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 65 deletions.
18 changes: 18 additions & 0 deletions README.md
Expand Up @@ -185,6 +185,24 @@ sequence of character and inserting a new line for readability.
Insert a continuation using the character sequence `<\>`. The following instruction the sequence will be skipped over
(including another continuation).

## Transforming text for text editors

Some text editors like Visual Studio Code will automatically indent or insert closing brackets which can cause
duplication of whitespace and characters. The `transform` subcommand can help transform text files for better
compatibility with similar text editors.

Example:

```sh
sendkeys transform --input-file example.js
```

You can also pipe the output of the `transform` command directly to your editor of choice. Example:

```sh
sendkeys transform --input-file example.js | sendkeys --application-name "Code"
```

## Retrieving mouse position

The `mouse-position` sub command can be used to help determine which mouse coordinates to use in your scripts.
Expand Down
68 changes: 3 additions & 65 deletions Sources/SendKeysLib/SendKeysCli.swift
Expand Up @@ -5,73 +5,11 @@ import Foundation
public struct SendKeysCli: ParsableCommand {
public static let configuration = CommandConfiguration(
commandName: "sendkeys",
abstract: "Command line tool for automating keystrokes and mouse events",
abstract: "Command line tool for automating keystrokes and mouse events.",
version: "0.0.0", /* auto-updated */
subcommands: [MousePosition.self]
subcommands: [Sender.self, MousePosition.self, Transformer.self],
defaultSubcommand: Sender.self
)

@Option(name: .shortAndLong, help: "Name of a running application to send keys to.")
var applicationName: String?

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

@Option(name: .shortAndLong, help: "Initial delay before sending commands in seconds.")
var initialDelay: Double = 1

@Option(name: NameSpecification([.customShort("f"), .long ]), help: "File containing keystroke instructions.")
var inputFile: String?

@Option(name: .shortAndLong, help: "String of characters to send.")
var characters: String?

public init() { }

public mutating func run() throws {
let accessEnabled = AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary)

if !accessEnabled {
fputs("WARNING: Accessibility preferences must be enabled to use this tool. If running from the terminal, make sure that your terminal app has accessibility permissiions enabled.\n\n", stderr)
}

let commandProcessor = CommandsProcessor(defaultPause: delay)
var commandString: String?

if !(inputFile ?? "").isEmpty {
if let data = FileManager.default.contents(atPath: inputFile!) {
commandString = String(data: data, encoding: .utf8)
} else {
fatalError("Could not read file \(inputFile!)\n")
}
} else if !(characters ?? "").isEmpty {
commandString = characters
}

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

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

if !(commandString ?? "").isEmpty {
commandProcessor.process(commandString!)
} else if !isTty() {
var data: Data

repeat {
data = FileHandle.standardInput.availableData

commandString = String(data: data, encoding: .utf8)
commandProcessor.process(commandString!)
} while data.count > 0
} else {
print(SendKeysCli.helpMessage())
}
}

private func isTty() -> Bool {
return isatty(FileHandle.standardInput.fileDescriptor) == 1
}
}
73 changes: 73 additions & 0 deletions Sources/SendKeysLib/Sender.swift
@@ -0,0 +1,73 @@
import ArgumentParser
import Foundation

@available(OSX 10.11, *)
public struct Sender: ParsableCommand {
public static let configuration = CommandConfiguration(
commandName: "send",
abstract: "Sends keystroke and mouse event commands."
)

@Option(name: .shortAndLong, help: "Name of a running application to send keys to.")
var applicationName: String?

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

@Option(name: .shortAndLong, help: "Initial delay before sending commands in seconds.")
var initialDelay: Double = 1

@Option(name: NameSpecification([.customShort("f"), .long ]), help: "File containing keystroke instructions.")
var inputFile: String?

@Option(name: .shortAndLong, help: "String of characters to send.")
var characters: String?

public init() { }

public mutating func run() throws {
let accessEnabled = AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary)

if !accessEnabled {
fputs("WARNING: Accessibility preferences must be enabled to use this tool. If running from the terminal, make sure that your terminal app has accessibility permissiions enabled.\n\n", stderr)
}

let commandProcessor = CommandsProcessor(defaultPause: delay)
var commandString: String?

if !(inputFile ?? "").isEmpty {
if let data = FileManager.default.contents(atPath: inputFile!) {
commandString = String(data: data, encoding: .utf8)
} else {
fatalError("Could not read file \(inputFile!)\n")
}
} else if !(characters ?? "").isEmpty {
commandString = characters
}

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

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

if !(commandString ?? "").isEmpty {
commandProcessor.process(commandString!)
} else if !isTty() {
var data: Data

repeat {
data = FileHandle.standardInput.availableData

if data.count > 0 {
commandString = String(data: data, encoding: .utf8)
commandProcessor.process(commandString!)
}
} while data.count > 0
} else {
print(SendKeysCli.helpMessage(for: Self.self))
}
}
}
79 changes: 79 additions & 0 deletions Sources/SendKeysLib/Transformer.swift
@@ -0,0 +1,79 @@
import ArgumentParser
import Foundation

@available(OSX 10.11, *)
class Transformer: ParsableCommand {
public static let configuration = CommandConfiguration(
commandName: "transform",
abstract: "Transforms raw text input into application friendly character sequences. Examples include accounting for applications that automatically indent source code and insert closing brackets."
)

@Option(name: .shortAndLong, help: "Determines if the application automatically inserts indentation.")
var indent = true

@Option(name: .shortAndLong, help: "Specifies which brackets are automatically closed by the application and don't need to be explicitly closed.")
var autoClose = "}])"

@Option(name: NameSpecification([.customShort("f"), .long ]), help: "File containing keystroke instructions to transform.")
var inputFile: String?

@Option(name: .shortAndLong, help: "String of characters to transform.")
var characters: String?

public init(indent: Bool, autoClose: String = "}])") {
self.indent = indent
self.autoClose = autoClose
}

required init() {
}

func run() {
var commandString: String?

if !(inputFile ?? "").isEmpty {
if let data = FileManager.default.contents(atPath: inputFile!) {
commandString = String(data: data, encoding: .utf8)
} else {
fatalError("Could not read file \(inputFile!)\n")
}
} else if !(characters ?? "").isEmpty {
commandString = characters
}

if !(commandString ?? "").isEmpty {
fputs(transform(commandString!), stdout)
} else if !isTty() {
var data: Data

repeat {
data = FileHandle.standardInput.availableData

if data.count > 0 {
commandString = String(data: data, encoding: .utf8)
fputs(transform(commandString!), stdout)
}
} while data.count > 0
} else {
print(SendKeysCli.helpMessage(for: Self.self))
}
}

func transform(_ input: String) -> String {
var output = input

if indent {
let removeIndentExpression = try! NSRegularExpression(pattern: "^[\\t ]+", options: .anchorsMatchLines)
let range = NSRange(location: 0, length: output.count)
output = removeIndentExpression.stringByReplacingMatches(in: output, options: [], range: range, withTemplate: "")
}

if !autoClose.isEmpty {
let removeBracketExpression = try! NSRegularExpression(pattern: "\\n[\\t ]*[\(NSRegularExpression.escapedPattern(for: autoClose).replacingOccurrences(of: "]", with: "\\]"))]+")
let range = NSRange(location: 0, length: output.count)
output = removeBracketExpression.stringByReplacingMatches(in: output, options: .withoutAnchoringBounds, range: range, withTemplate: "<\\\\>\n<c:down><p:0><c:right:command>")
}

return output
}
}
5 changes: 5 additions & 0 deletions Sources/SendKeysLib/Utilities.swift
@@ -0,0 +1,5 @@
import Foundation

func isTty() -> Bool {
return isatty(FileHandle.standardInput.fileDescriptor) == 1
}
40 changes: 40 additions & 0 deletions Tests/SendKeysTests/TransformerTests.swift
@@ -0,0 +1,40 @@
@testable import SendKeysLib

import XCTest

final class TransformerTests: XCTestCase {
func testShouldNoteTransformSingleLine() {
let transformer = Transformer(indent: true)
let result = transformer.transform("hello world")

XCTAssertEqual(result, "hello world")
}

func testShouldNotTransformCurlyBraceOnSameLine() {
let transformer = Transformer(indent: true)
let result = transformer.transform("{}")

XCTAssertEqual(result, "{}")
}

func testTransformCurlyBraceOnDifferentLine() {
let transformer = Transformer(indent: true)
let result = transformer.transform("{\n}")

XCTAssertEqual(result, "{<\\>\n<c:down><p:0><c:right:command>")
}

func testTransformCurlyBraceWithBasicContent() {
let transformer = Transformer(indent: true)
let result = transformer.transform("hello {\n world\n}")

XCTAssertEqual(result, "hello {\nworld<\\>\n<c:down><p:0><c:right:command>")
}

func testTransformBracketAndCurlyBraceWithBasicContent() {
let transformer = Transformer(indent: true)
let result = transformer.transform("hello ({\n world\n})")

XCTAssertEqual(result, "hello ({\nworld<\\>\n<c:down><p:0><c:right:command>")
}
}

0 comments on commit 3893313

Please sign in to comment.