Showing preview only (235K chars total). Download the full file or copy to clipboard to get everything.
Repository: socsieng/sendkeys
Branch: main
Commit: c5c8dd98ee8d
Files: 87
Total size: 213.7 KB
Directory structure:
gitextract_cwwrie3z/
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yaml
│ │ └── feature_request.yaml
│ └── workflows/
│ └── build.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.yml
├── .swift-format
├── .vscode/
│ ├── extensions.json
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── CHANGELOG.md
├── Formula/
│ └── sendkeys_template.rb
├── LICENSE
├── Makefile
├── Package.resolved
├── Package.swift
├── README.md
├── Sources/
│ ├── SendKeysLib/
│ │ ├── Animator.swift
│ │ ├── AppActivator.swift
│ │ ├── AppLister.swift
│ │ ├── Bridge.swift
│ │ ├── Commands/
│ │ │ ├── Command.swift
│ │ │ ├── CommandExecutor.swift
│ │ │ ├── CommandFactory.swift
│ │ │ ├── CommandsIterator.swift
│ │ │ ├── CommandsProcessor.swift
│ │ │ ├── ContinuationCommand.swift
│ │ │ ├── DefaultCommand.swift
│ │ │ ├── KeyDownCommand.swift
│ │ │ ├── KeyPressCommand.swift
│ │ │ ├── KeyUpCommand.swift
│ │ │ ├── MouseClickCommand.swift
│ │ │ ├── MouseDownCommand.swift
│ │ │ ├── MouseDragCommand.swift
│ │ │ ├── MouseFocusCommand.swift
│ │ │ ├── MouseMoveCommand.swift
│ │ │ ├── MousePathCommand.swift
│ │ │ ├── MouseScrollCommand.swift
│ │ │ ├── MouseUpCommand.swift
│ │ │ ├── NewlineCommand.swift
│ │ │ ├── PauseCommand.swift
│ │ │ └── StickyPauseCommand.swift
│ │ ├── Configuration/
│ │ │ ├── AllConfiguration.swift
│ │ │ ├── ConfigLoader.swift
│ │ │ ├── MousePositionConfig.swift
│ │ │ ├── SendConfig.swift
│ │ │ └── TransformerConfig.swift
│ │ ├── KeyCodes.swift
│ │ ├── KeyMappings.swift
│ │ ├── KeyPresser.swift
│ │ ├── MouseController.swift
│ │ ├── MouseEventProcessor.swift
│ │ ├── MousePosition.swift
│ │ ├── Path/
│ │ │ ├── Extensions.swift
│ │ │ ├── PathCommands.swift
│ │ │ ├── PathData.swift
│ │ │ └── PathParser.swift
│ │ ├── RuntimeError.swift
│ │ ├── SendKeysCli.swift
│ │ ├── Sender.swift
│ │ ├── Sleeper.swift
│ │ ├── TerminationListener.swift
│ │ ├── Transformer.swift
│ │ └── Utilities.swift
│ └── sendkeys/
│ └── main.swift
├── Tests/
│ ├── LinuxMain.swift
│ ├── SendKeysTests/
│ │ ├── CommandIteratorTests.swift
│ │ ├── CommandsProcessorTests.swift
│ │ ├── KeyPresserTests.swift
│ │ ├── PathDataTests.swift
│ │ ├── PathParserTests.swift
│ │ ├── TransformerTests.swift
│ │ ├── XCTestManifests.swift
│ │ └── sendkeysTests.swift
│ └── keys.txt
├── examples/
│ ├── .sendkeysrc.yml
│ └── node.js
├── scripts/
│ ├── bottle.sh
│ ├── code-coverage.sh
│ ├── format.sh
│ ├── install-pre-commit.sh
│ ├── pre-commit.sh
│ ├── update-version.sh
│ └── verify-output.sh
└── version.txt
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.swift]
indent_style = space
indent_size = 4
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yaml
================================================
name: Bug Report
description: Report a bug you've found
title: "[bug]: "
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to file a bug.
- type: textarea
attributes:
label: What went wrong?
description: Describe the issue including reproduction steps.
placeholder: |
Executing `sendkeys ...`
Fails with error: ...
validations:
required: true
- type: textarea
attributes:
label: "Expected result:"
description: Describe what you expected to happen.
- type: textarea
attributes:
label: "Actual result:"
description: Describe what actually happened.
- type: textarea
attributes:
label: "Other information:"
description: |
Include any other useful information that would help to reproduce/fix the issue.
value: |
- Sendkeys version: ...
- Operating system: ...
- Processor (e.g. Intel, M1): ...
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yaml
================================================
name: Feature Request
description: Suggest an idea for this project
title: "[feature]: "
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Thank you for using sendkeys.
- type: textarea
attributes:
label: What feature would you like to see in sendkeys?
description: Describe the feature and the problem that you would like to see solved.
placeholder: |
I would like to be able to ...
So that I can ...
validations:
required: true
- type: textarea
attributes:
label: "Other information:"
description: |
Include any other information that may be useful for prioritizing the feature request. Examples may include your own proposed solution, other alternatives, references to similar features in other tools, or screenshots.
================================================
FILE: .github/workflows/build.yml
================================================
name: build
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: macos-14
env:
DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer
name: Build
steps:
- name: checkout
uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-xcode-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-xcode-
- name: build
run: |
ls -n /Applications/ | grep Xcode*
make build
- name: test
run: |
./scripts/code-coverage.sh
create_release:
needs:
- build
runs-on: macos-14
if: github.ref == 'refs/heads/main'
name: Create release
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
sha: ${{ steps.release.outputs.sha }}
upload_url: ${{ steps.release.outputs.upload_url }}
steps:
- id: release
uses: google-github-actions/release-please-action@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
release-type: simple
package-name: sendkeys
changelog-types: |
[
{"type":"feat","section":"Features","hidden":false},
{"type":"fix","section":"Bug Fixes","hidden":false},
{"type":"docs","section":"Documentation","hidden":false},
{"type":"misc","section":"Miscellaneous","hidden":false}
]
create_bottle:
needs:
- create_release
runs-on: macos-14
if: ${{ needs.create_release.outputs.release_created }}
name: Create bottle
outputs:
sha: ${{ steps.bottle.outputs.sha }}
root_url: ${{ steps.bottle.outputs.root_url }}
steps:
- uses: actions/checkout@v4
- 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 }}
- name: Upload bottle big_sur
if: ${{ needs.create_release.outputs.release_created }}
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.big_sur }}
asset_name: ${{ steps.bottle.outputs.big_sur }}
asset_content_type: application/gzip
- name: Upload bottle arm64_big_sur
if: ${{ needs.create_release.outputs.release_created }}
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.arm64_big_sur }}
asset_name: ${{ steps.bottle.outputs.arm64_big_sur }}
asset_content_type: application/gzip
homebrew:
needs:
- create_release
- create_bottle
runs-on: macos-14
if: ${{ needs.create_release.outputs.release_created }}
name: Update homebrew formula
steps:
- uses: actions/checkout@v4
- name: Update homebrew formula
run: |
git config user.name github-actions[bot]
git config user.email socsieng-github-actions[bot]@users.noreply.github.com
git clone "https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/socsieng/homebrew-tap.git"
cd homebrew-tap
git checkout -B main
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.outputs.root_url }}' | sed 's/\\//\\\\\//g'`
sha='${{ needs.create_bottle.outputs.sha }}'
sed -E -i "" "s/tag: \"[^\"]+\"/tag: \"v$version\"/g" $formula
sed -E -i "" "s/revision: \"[^\"]+\"/revision: \"$revision\"/g" $formula
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\"/g" $formula
sed -E -i "" "s/sha256 cellar: :any_skip_relocation, big_sur: \"[^\"]+\"/sha256 cellar: :any_skip_relocation, big_sur: \"$sha\"/g" $formula
git commit -am "chore: update sendkeys to ${{ needs.create_release.outputs.tag_name }}"
git push origin main
================================================
FILE: .gitignore
================================================
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
.swiftpm/
*.bak
Formula/sendkeys.rb
*.tar.gz
/.output
================================================
FILE: .prettierignore
================================================
node_modules/
.build/
CHANGELOG.md
================================================
FILE: .prettierrc.yml
================================================
arrowParens: avoid
printWidth: 120
quoteProps: consistent
semi: true
singleQuote: true
tabWidth: 2
trailingComma: all
overrides:
- files: '*.md'
options:
parser: markdown
proseWrap: always
================================================
FILE: .swift-format
================================================
{
"fileScopedDeclarationPrivacy": {
"accessLevel": "private"
},
"indentation": {
"spaces": 4
},
"indentConditionalCompilationBlocks": true,
"indentSwitchCaseLabels": false,
"lineBreakAroundMultilineExpressionChainComponents": false,
"lineBreakBeforeControlFlowKeywords": false,
"lineBreakBeforeEachArgument": false,
"lineBreakBeforeEachGenericRequirement": false,
"lineLength": 120,
"maximumBlankLines": 1,
"prioritizeKeepingFunctionOutputTogether": false,
"respectsExistingLineBreaks": true,
"rules": {
"AllPublicDeclarationsHaveDocumentation": false,
"AlwaysUseLowerCamelCase": true,
"AmbiguousTrailingClosureOverload": true,
"BeginDocumentationCommentWithOneLineSummary": true,
"DoNotUseSemicolons": true,
"DontRepeatTypeInStaticProperties": true,
"FileScopedDeclarationPrivacy": true,
"FullyIndirectEnum": true,
"GroupNumericLiterals": true,
"IdentifiersMustBeASCII": true,
"NeverForceUnwrap": false,
"NeverUseForceTry": false,
"NeverUseImplicitlyUnwrappedOptionals": false,
"NoAccessLevelOnExtensionDeclaration": true,
"NoBlockComments": true,
"NoCasesWithOnlyFallthrough": true,
"NoEmptyTrailingClosureParentheses": true,
"NoLabelsInCasePatterns": true,
"NoLeadingUnderscores": false,
"NoParensAroundConditions": true,
"NoVoidReturnOnFunctionSignature": true,
"OneCasePerLine": true,
"OneVariableDeclarationPerLine": true,
"OnlyOneTrailingClosureArgument": true,
"OrderedImports": true,
"ReturnVoidInsteadOfEmptyTuple": true,
"UseLetInEveryBoundCaseVariable": true,
"UseShorthandTypeNames": true,
"UseSingleLinePropertyGetter": true,
"UseSynthesizedInitializer": true,
"UseTripleSlashForDocumentationComments": true,
"ValidateDocumentationComments": false
},
"tabWidth": 8,
"version": 1
}
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": ["vknabel.vscode-apple-swift-format", "sswg.swift-lang", "editorconfig.editorconfig"]
}
================================================
FILE: .vscode/launch.json
================================================
{
"configurations": [
{
"type": "lldb",
"request": "launch",
"sourceLanguages": ["swift"],
"name": "Debug sendkeys",
"program": "${workspaceFolder:sendkeys}/.build/debug/sendkeys",
"args": [],
"cwd": "${workspaceFolder:sendkeys}",
"preLaunchTask": "swift: Build Debug sendkeys"
},
{
"type": "lldb",
"request": "launch",
"sourceLanguages": ["swift"],
"name": "Release sendkeys",
"program": "${workspaceFolder:sendkeys}/.build/release/sendkeys",
"args": [],
"cwd": "${workspaceFolder:sendkeys}",
"preLaunchTask": "swift: Build Release sendkeys"
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"editor.formatOnSave": true
}
================================================
FILE: .vscode/tasks.json
================================================
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "swift build"
}
]
}
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## [4.3.1](https://github.com/socsieng/sendkeys/compare/v4.3.0...v4.3.1) (2024-12-14)
### Bug Fixes
* add esc alias for escape key ([ff88df1](https://github.com/socsieng/sendkeys/commit/ff88df1022633e3ab4f7e85a05801ac607024872)), closes [#93](https://github.com/socsieng/sendkeys/issues/93)
## [4.3.0](https://github.com/socsieng/sendkeys/compare/v4.2.0...v4.3.0) (2024-10-28)
### Features
* add option to specify arbitrary configuration file ([61133bf](https://github.com/socsieng/sendkeys/commit/61133bf7ec8089d6f3e631a69f35a198cc8c644b))
* add support for custom key mappings in configuration file ([cc21cce](https://github.com/socsieng/sendkeys/commit/cc21ccee8b10109c74735a8458f8f7161515992d))
* add support for reading configuration from home directory ([01908f1](https://github.com/socsieng/sendkeys/commit/01908f11f3b85a551f643b2f679f1e595afbf8e3))
## [4.2.0](https://github.com/socsieng/sendkeys/compare/v4.1.2...v4.2.0) (2024-10-26)
### Features
* add support for alternate keyboard layouts ([7db41a8](https://github.com/socsieng/sendkeys/commit/7db41a829d1f403dc77206f6c737db11c2bb064e))
## [4.1.2](https://github.com/socsieng/sendkeys/compare/v4.1.1...v4.1.2) (2024-10-24)
### Bug Fixes
* add support for function modifier key ([db36c92](https://github.com/socsieng/sendkeys/commit/db36c9200f2bb982c2e0077df42436b3f7a1086e))
* **build:** restore arm64 binary in homebrew formula ([e38451a](https://github.com/socsieng/sendkeys/commit/e38451aad1f9a2b8c8d4ab5afb25c9de26147d8f))
## [4.1.1](https://github.com/socsieng/sendkeys/compare/v4.1.0...v4.1.1) (2024-10-22)
### Bug Fixes
* **build:** fix broken build ([234b441](https://github.com/socsieng/sendkeys/commit/234b441a210bf038b6cfcd60e9ac691d9dcc0d6c))
## [4.1.0](https://github.com/socsieng/sendkeys/compare/v4.0.4...v4.1.0) (2024-10-22)
### Features
* add argument to terminate execution ([1b8c1f1](https://github.com/socsieng/sendkeys/commit/1b8c1f1aa2ceb9ae666aaa74142df150d7a85b67)), closes [#79](https://github.com/socsieng/sendkeys/issues/79)
### Bug Fixes
* **build:** update macos runners ([2af43cd](https://github.com/socsieng/sendkeys/commit/2af43cda3362757dc1fb2404bd456ae34292e955))
* update package versions ([b1e1319](https://github.com/socsieng/sendkeys/commit/b1e1319259cef524e5c83632eabecdbcd353b0bb))
## [4.0.4](https://github.com/socsieng/sendkeys/compare/v4.0.3...v4.0.4) (2023-10-07)
### Bug Fixes
* **build:** add cache busting parameter ([c2a4f59](https://github.com/socsieng/sendkeys/commit/c2a4f598f0677a0dd490339eddbfaf22083e5b30))
## [4.0.3](https://github.com/socsieng/sendkeys/compare/v4.0.2...v4.0.3) (2023-10-07)
### Bug Fixes
* **build:** ignore errors if archive does not exist ([7f2fc4b](https://github.com/socsieng/sendkeys/commit/7f2fc4b5cd5c865228f5455892d7f1cabdad2612))
## [4.0.2](https://github.com/socsieng/sendkeys/compare/v4.0.1...v4.0.2) (2023-10-07)
### Bug Fixes
* **build:** clean up archives before bottling ([408db73](https://github.com/socsieng/sendkeys/commit/408db7323ab29733552fe7140405804bb4207e51))
## [4.0.1](https://github.com/socsieng/sendkeys/compare/v4.0.0...v4.0.1) (2023-10-07)
### Bug Fixes
* **build:** clean up archives after bottling ([1ef78ed](https://github.com/socsieng/sendkeys/commit/1ef78ed4297d0c69ab1327ed6796fdddda1d2543))
## [4.0.0](https://github.com/socsieng/sendkeys/compare/v3.0.2...v4.0.0) (2023-10-07)
### ⚠ BREAKING CHANGES
* fix handling of key strokes to use shift key when appropriate
### Bug Fixes
* fix handling of key strokes to use shift key when appropriate ([beb535b](https://github.com/socsieng/sendkeys/commit/beb535b2693085cf50bc479276a4d806e55d288c)), closes [#62](https://github.com/socsieng/sendkeys/issues/62)
* fix issue with unspecified application name ([440153d](https://github.com/socsieng/sendkeys/commit/440153d073c10888fd33990197434d9565b430d1))
## [3.0.2](https://github.com/socsieng/sendkeys/compare/v3.0.1...v3.0.2) (2023-10-06)
### Bug Fixes
* introduce a small delay to allow commands to be processed before terminating ([9ffc2b2](https://github.com/socsieng/sendkeys/commit/9ffc2b2a012314d983c7e2a94385747abce8ef24)), closes [#60](https://github.com/socsieng/sendkeys/issues/60)
## [3.0.1](https://github.com/socsieng/sendkeys/compare/v3.0.0...v3.0.1) (2023-10-06)
### Bug Fixes
* update key handling to use keyboardEventSource ([c26bbfa](https://github.com/socsieng/sendkeys/commit/c26bbfaaab700a6063b8fbb967622241595eb5a0))
## [3.0.0](https://www.github.com/socsieng/sendkeys/compare/v2.9.1...v3.0.0) (2023-10-06)
### ⚠ BREAKING CHANGES
* drop support for building on macOS catalina
### Features
* add support for sending keys to an application without activation ([b42d4bc](https://www.github.com/socsieng/sendkeys/commit/b42d4bc347d1800068e8753bc6daa13b255319b2)), closes [#67](https://www.github.com/socsieng/sendkeys/issues/67)
### Bug Fixes
* remove dependency on macos-10.15 ([14aa5a8](https://www.github.com/socsieng/sendkeys/commit/14aa5a82ba2fd34e2fa49d5b749b0fb941d1f902))
### Miscellaneous
* drop support for building on macOS catalina ([8c027cb](https://www.github.com/socsieng/sendkeys/commit/8c027cb14e4ca9bfe1515a41191c1ecfa4499112))
### [2.9.1](https://www.github.com/socsieng/sendkeys/compare/v2.9.0...v2.9.1) (2023-04-15)
### Bug Fixes
* fix homebrew update script ([dac393f](https://www.github.com/socsieng/sendkeys/commit/dac393fe982b7b5ce39a9450277a36b0ea46d58e))
## [2.9.0](https://www.github.com/socsieng/sendkeys/compare/v2.8.0...v2.9.0) (2023-04-15)
### Features
* add support for activating application by process id ([0a44470](https://www.github.com/socsieng/sendkeys/commit/0a4447044d2acddba176db74d9698cbef4dfc872)), closes [#63](https://www.github.com/socsieng/sendkeys/issues/63)
## [2.8.0](https://www.github.com/socsieng/sendkeys/compare/v2.7.1...v2.8.0) (2021-09-01)
### Features
* convert mouse coordinates from integers to doubles ([bbd4534](https://www.github.com/socsieng/sendkeys/commit/bbd45342cf0f1974ed2a2365d386b44b3225841d))
* make scaleY option in path command ([f1fe840](https://www.github.com/socsieng/sendkeys/commit/f1fe8406a13c7abee8844123d0d5aeda903b6620))
* output mouse positions as decimals ([d723082](https://www.github.com/socsieng/sendkeys/commit/d723082a068f4806bad9363098727af287c067d7))
### Documentation
* add sample command for mouse path command ([9c9e6cb](https://www.github.com/socsieng/sendkeys/commit/9c9e6cbe0b4c7ca313116eb4b7966824c681ed17))
### [2.7.1](https://www.github.com/socsieng/sendkeys/compare/v2.7.0...v2.7.1) (2021-08-31)
### Bug Fixes
* remove debug print statements ([5109a12](https://www.github.com/socsieng/sendkeys/commit/5109a12045bb65d18fe570db53c9ce88e6fa4d3d))
## [2.7.0](https://www.github.com/socsieng/sendkeys/compare/v2.6.2...v2.7.0) (2021-08-31)
### Features
* add mouse path command ([6c5c0b9](https://www.github.com/socsieng/sendkeys/commit/6c5c0b9ab5e25d795ee470884a25de2b28d5988d))
### Documentation
* add example animation for mouse path command ([2c49b8d](https://www.github.com/socsieng/sendkeys/commit/2c49b8d16735d60186c1b1c842ff31607ec265bb))
### [2.6.2](https://www.github.com/socsieng/sendkeys/compare/v2.6.1...v2.6.2) (2021-08-22)
### Features
* improve application name matching algorithm ([456586a](https://www.github.com/socsieng/sendkeys/commit/456586a746bea2294fd64db238cf595d9f7bf417)), closes [#50](https://www.github.com/socsieng/sendkeys/issues/50)
### Documentation
* update README to include details on apps sub command ([40f49be](https://www.github.com/socsieng/sendkeys/commit/40f49bec4ce9574ee7e7ecafafd7facba067f64c))
### [2.6.1](https://www.github.com/socsieng/sendkeys/compare/v2.6.0...v2.6.1) (2021-08-21)
### chore
* bump version ([ee86888](https://www.github.com/socsieng/sendkeys/commit/ee86888368363598d51fbd5b1b49ca59a68ef541))
## [2.6.0](https://www.github.com/socsieng/sendkeys/compare/v2.5.2...v2.6.0) (2021-08-21)
### Features
* display a list applications that can be used with sendkeys ([94626fa](https://www.github.com/socsieng/sendkeys/commit/94626fa39a30b9b51f03548aa0b8dbb62bd5cfb6)), closes [#46](https://www.github.com/socsieng/sendkeys/issues/46)
### [2.5.2](https://www.github.com/socsieng/sendkeys/compare/v2.5.1...v2.5.2) (2021-06-19)
### Bug Fixes
* **build:** remove usage of realpath ([c9019d7](https://www.github.com/socsieng/sendkeys/commit/c9019d7d3ac9a3776b15a50708365de86d6f3b72))
### [2.5.1](https://www.github.com/socsieng/sendkeys/compare/v2.5.0...v2.5.1) (2021-06-19)
### Bug Fixes
* **build:** apply changes to address broken homebrew build ([536603a](https://www.github.com/socsieng/sendkeys/commit/536603a0e4f224ffe87487954318f4b617264e87))
## [2.5.0](https://www.github.com/socsieng/sendkeys/compare/v2.4.0...v2.5.0) (2021-06-18)
### Features
* add option to output mouse position commands with predefined duration ([f662869](https://www.github.com/socsieng/sendkeys/commit/f662869cf207bca9978407758eadef244c9d026f))
## [2.4.0](https://www.github.com/socsieng/sendkeys/compare/v2.3.10...v2.4.0) (2021-03-27)
### Features
* add support for apple m1 processors ([6706f85](https://www.github.com/socsieng/sendkeys/commit/6706f85b340168783ecb5e4fe56bde545c2bf86a))
### [2.3.10](https://www.github.com/socsieng/sendkeys/compare/v2.3.0...v2.3.10) (2021-03-27)
### Documentation
* add example file for using transform ([1a2c8e8](https://www.github.com/socsieng/sendkeys/commit/1a2c8e82cd6b04321aadb78979c73a120d340bf4))
* update readme to include installation instructions for alternate versions ([72030ab](https://www.github.com/socsieng/sendkeys/commit/72030abc018a7bd81e31509ac7085e77620d6528))
## [2.3.0](https://www.github.com/socsieng/sendkeys/compare/v2.2.0...v2.3.0) (2021-01-20)
### Features
* add mouse focus command ([1bbab2d](https://www.github.com/socsieng/sendkeys/commit/1bbab2d0a9377260d892a78cfbc41713064ac736))
## [2.2.0](https://www.github.com/socsieng/sendkeys/compare/v2.1.1...v2.2.0) (2021-01-16)
### Features
* add support for triggering mouse up and down events independently ([bee0fbe](https://www.github.com/socsieng/sendkeys/commit/bee0fbe5032a97fe63090fedf9cc7c9d537f60f6))
### [2.1.1](https://www.github.com/socsieng/sendkeys/compare/v2.1.0...v2.1.1) (2021-01-07)
### Bug Fixes
* fix typo in key mappings ([c4a4997](https://www.github.com/socsieng/sendkeys/commit/c4a4997375c55cc4217c1a381df39ff94d9ff751))
* handle `a` key correctly as keycode 0 ([29209a3](https://www.github.com/socsieng/sendkeys/commit/29209a34a495afdc0028e353ccf1b8375b32f005))
## [2.1.0](https://www.github.com/socsieng/sendkeys/compare/v2.0.0...v2.1.0) (2021-01-06)
### Features
* add option to change animation refresh rate ([73a2c29](https://www.github.com/socsieng/sendkeys/commit/73a2c294a878f9e8e64c14a4b6664ddb1586e13b)), closes [#21](https://www.github.com/socsieng/sendkeys/issues/21)
* add transform subcommand ([3893313](https://www.github.com/socsieng/sendkeys/commit/38933135a3746343d501cd720d4b66f7ee1ac552))
### Bug Fixes
* start mouse timing when mouse-position command is executed ([1437aac](https://www.github.com/socsieng/sendkeys/commit/1437aac909e289a782d16677601a81c49d443d85))
* support negative values for mouse click and drag events ([f50209a](https://www.github.com/socsieng/sendkeys/commit/f50209ae1e8924dbd189a11a6ecad388082c1d17)), closes [#23](https://www.github.com/socsieng/sendkeys/issues/23)
### Documentation
* update documentation to state that the application should be running ([3e0d973](https://www.github.com/socsieng/sendkeys/commit/3e0d9736559d48e45c3287c48830bd743926e3ef))
## [2.0.0](https://www.github.com/socsieng/sendkeys/compare/v1.3.0...v2.0.0) (2021-01-04)
### Features
* use click timings when producing mouse commands ([970e1df](https://www.github.com/socsieng/sendkeys/commit/970e1df52a903c93da62a572edc99caea1ffc1b5)), closes [#18](https://www.github.com/socsieng/sendkeys/issues/18)
### Bug Fixes
* check file exists before activating application ([94cda37](https://www.github.com/socsieng/sendkeys/commit/94cda379c54175e1b59a87633512456b327e63fa)), closes [#17](https://www.github.com/socsieng/sendkeys/issues/17)
### Documentation
* include example recording and replaying mouse commands ([5019461](https://www.github.com/socsieng/sendkeys/commit/501946176dab36189e05b49c6e09800ec3bd77ad)), closes [#19](https://www.github.com/socsieng/sendkeys/issues/19)
## [1.3.0](https://www.github.com/socsieng/sendkeys/compare/v1.2.0...v1.3.0) (2021-01-04)
### Features
* add option to listen to mouse clicks ([d1d129f](https://www.github.com/socsieng/sendkeys/commit/d1d129fed23f969b10bf0475aeb77f9752a4a5bd))
### Documentation
* use expanded argument names in examples ([f4839a3](https://www.github.com/socsieng/sendkeys/commit/f4839a3c027bc78fb13e6f717147f206c3c043d8))
## [1.2.0](https://www.github.com/socsieng/sendkeys/compare/v1.1.1...v1.2.0) (2021-01-03)
### Features
* defer accesibility check to execution of the command ([670d091](https://www.github.com/socsieng/sendkeys/commit/670d091d17100fd9b9cdb74ba0eebf69cae6af51))
### [1.1.1](https://www.github.com/socsieng/sendkeys/compare/v1.1.0...v1.1.1) (2021-01-02)
### Bug Fixes
* address modifier key issue on key up ([0bfa58a](https://www.github.com/socsieng/sendkeys/commit/0bfa58ad87ebcff46b905900584877544a533463))
* double key entry issue ([26ad67e](https://www.github.com/socsieng/sendkeys/commit/26ad67e80aaa30fcd9c2a1bb20f5987288760bbc))
## [1.1.0](https://www.github.com/socsieng/sendkeys/compare/v1.0.0...v1.1.0) (2021-01-02)
### Features
* add support for key down and up commands ([2c5cefb](https://www.github.com/socsieng/sendkeys/commit/2c5cefb28c802dea94d55920c4092b232f50e62e))
* add support for mouse clicks with modifier keys ([1654fd7](https://www.github.com/socsieng/sendkeys/commit/1654fd7217c7588c16c22ff779d26564aee634f9))
* add support for mouse drag with modifier keys ([32964a7](https://www.github.com/socsieng/sendkeys/commit/32964a725312bfa04d203c4b56d3cf20cf237b52))
* add support for mouse move with modifier keys ([7f4c891](https://www.github.com/socsieng/sendkeys/commit/7f4c891be9aed26f37c38d6275247830d9f04b5c))
* add support for mouse scroll with modifier keys ([27afc5b](https://www.github.com/socsieng/sendkeys/commit/27afc5bb7c50b762d4d1333f3c6eeef6b92a8d8d))
### Bug Fixes
* add event source attribution to related events ([d443fdf](https://www.github.com/socsieng/sendkeys/commit/d443fdf6c2e64b8258e76c7a2ea23a94de62a695))
* make right click work consistently across applications ([464a401](https://www.github.com/socsieng/sendkeys/commit/464a401d532ef2afc1cfa6a206d84479aa92888a))
* use click count when triggering mouse click ([f824a98](https://www.github.com/socsieng/sendkeys/commit/f824a98823551c451fcb4e8f6c9196d9fe26b8e6))
### Documentation
* add example of mouse move command ([2470c7b](https://www.github.com/socsieng/sendkeys/commit/2470c7bc56feb207fda2b1f2e39b1377ccf4ffd2))
* **modifier keys:** add documentation for mouse modifier keys ([7c5e9d9](https://www.github.com/socsieng/sendkeys/commit/7c5e9d917d5eb18b003095a8a361e9ac99078826))
## [1.0.0](https://www.github.com/socsieng/sendkeys/compare/v0.5.0...v1.0.0) (2020-12-31)
### Features
* stable release 1.0.0 ([27bcfc6](https://www.github.com/socsieng/sendkeys/commit/27bcfc68bc183b7a0d6e466b32ea619d7eee7aed))
### Bug Fixes
* read file relative to current directory ([49e1253](https://www.github.com/socsieng/sendkeys/commit/49e12537b288a34e7eb3bc060bc513ae36a86a82))
### Miscellaneous
* append newline when raising a fatal error ([b33f495](https://www.github.com/socsieng/sendkeys/commit/b33f495d7c60810cc82a540b920467dd61f8b869))
## [0.5.0](https://www.github.com/socsieng/sendkeys/compare/v0.4.0...v0.5.0) (2020-12-31)
### Features
* add support for scrolling ([b438e00](https://www.github.com/socsieng/sendkeys/commit/b438e0089b947339c935a21ba39c66375dea4b5d))
## [0.4.0](https://www.github.com/socsieng/sendkeys/compare/v0.3.0...v0.4.0) (2020-12-31)
### Features
* add check to see if accessibility permissions have been enabled ([409e0fb](https://www.github.com/socsieng/sendkeys/commit/409e0fbe935113329b1d61cdbe3f2cd186781117))
* add sub command to display the current mouse position ([507f8b8](https://www.github.com/socsieng/sendkeys/commit/507f8b8aa1ccedbee2936295c2d192f1a1c33ede))
* add wait option for mouse-position ([2c0aa8f](https://www.github.com/socsieng/sendkeys/commit/2c0aa8fab484de46b835cc0ec52afc22699f9033))
### Bug Fixes
* display help when no commands supplied ([c3175ba](https://www.github.com/socsieng/sendkeys/commit/c3175ba182ab98831dfbb29afcd2241f6e22cdfc))
* only perform accessibility check if stdin is tty ([6043a78](https://www.github.com/socsieng/sendkeys/commit/6043a784a9f2245bd826b30787d494a687899428))
### Documentation
* add documentation on how to use mouse-position command ([42e9955](https://www.github.com/socsieng/sendkeys/commit/42e9955b4f3efbc2d56c062f85134d17deea68b7))
## [0.3.0](https://www.github.com/socsieng/sendkeys/compare/v0.2.4...v0.3.0) (2020-12-31)
### ⚠ BREAKING CHANGES
* update build step to update homebrew formula
### build
* update build step to update homebrew formula ([5fd8722](https://www.github.com/socsieng/sendkeys/commit/5fd8722409c6b536ec2388db7e99e0dfa6a134ea))
### [0.2.4](https://www.github.com/socsieng/sendkeys/compare/v0.2.3...v0.2.4) (2020-12-31)
### Documentation
* add brew install instructions ([95023b1](https://www.github.com/socsieng/sendkeys/commit/95023b1a8533ab7f2fb6e7ec4f650a312e0ab828))
### [0.2.3](https://www.github.com/socsieng/sendkeys/compare/v0.2.2...v0.2.3) (2020-12-31)
### Bug Fixes
* update bottle name to work with brew ([259c140](https://www.github.com/socsieng/sendkeys/commit/259c14051895c5672f290e2f8f2bf299395e31a4))
### [0.2.2](https://www.github.com/socsieng/sendkeys/compare/v0.2.1...v0.2.2) (2020-12-30)
### Bug Fixes
* **build:** fix bad substitution in sed ([254161e](https://www.github.com/socsieng/sendkeys/commit/254161e567bfb8a8063d0dd8c8b976ec0d2c09da))
### [0.2.1](https://www.github.com/socsieng/sendkeys/compare/v0.2.0...v0.2.1) (2020-12-30)
### Bug Fixes
* **build:** update scripts to handle differences in tag_name ([13fbf6b](https://www.github.com/socsieng/sendkeys/commit/13fbf6bb377ada5becd25399b75e9367cbbc4302))
## [0.2.0](https://www.github.com/socsieng/sendkeys/compare/v0.1.0...v0.2.0) (2020-12-30)
### ⚠ BREAKING CHANGES
* update build step names
### Build System
* update build step names ([4d6a0f4](https://www.github.com/socsieng/sendkeys/commit/4d6a0f476d5331a1d99bfd150dcf88281d0f1dd3))
## [0.1.0](https://www.github.com/socsieng/sendkeys/compare/v0.0.3...v0.1.0) (2020-12-30)
### Features
* add support for reading from stdin ([463d777](https://www.github.com/socsieng/sendkeys/commit/463d7775e1bc11a293917c43e7c00335d2e77404))
================================================
FILE: Formula/sendkeys_template.rb
================================================
# Documentation: https://docs.brew.sh/Formula-Cookbook
# https://rubydoc.brew.sh/Formula
class Sendkeys < Formula
desc "Command line tool for automating keystrokes and mouse events"
homepage "https://github.com/socsieng/sendkeys"
url ""
version "0.0.0"
license "Apache-2.0"
depends_on :xcode => ["12.0", :build]
def install
system "make", "install", "prefix=#{prefix}"
end
test do
system "sendkeys" "--help"
end
end
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: Makefile
================================================
prefix ?= /usr/local
bindir ?= $(prefix)/bin
.PHONY: build
build:
@scripts/update-version.sh
@swift build -c release --disable-sandbox --arch arm64 --arch x86_64
.PHONY: verify
verify:
@swift test
@scripts/verify-output.sh
.PHONY: install
install: build
@install -d "$(bindir)"
@install ".build/apple/Products/Release/sendkeys" "$(bindir)/sendkeys"
.PHONY: uninstall
uninstall:
@rm -rf "$(bindir)/sendkeys"
.PHONY: clean
clean:
@rm -rf .build
================================================
FILE: Package.resolved
================================================
{
"object": {
"pins": [
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "41982a3656a71c768319979febd796c6fd111d5c",
"version": "1.5.0"
}
},
{
"package": "cmark-gfm",
"repositoryURL": "https://github.com/apple/swift-cmark.git",
"state": {
"branch": null,
"revision": "3ccff77b2dc5b96b77db3da0d68d28068593fa53",
"version": "0.5.0"
}
},
{
"package": "swift-format",
"repositoryURL": "https://github.com/apple/swift-format.git",
"state": {
"branch": null,
"revision": "65f9da9aad84adb7e2028eb32ca95164aa590e3b",
"version": "600.0.0"
}
},
{
"package": "swift-markdown",
"repositoryURL": "https://github.com/apple/swift-markdown.git",
"state": {
"branch": null,
"revision": "8f79cb175981458a0a27e76cb42fee8e17b1a993",
"version": "0.5.0"
}
},
{
"package": "swift-syntax",
"repositoryURL": "https://github.com/swiftlang/swift-syntax.git",
"state": {
"branch": null,
"revision": "0687f71944021d616d34d922343dcef086855920",
"version": "600.0.1"
}
},
{
"package": "Yams",
"repositoryURL": "https://github.com/jpsim/Yams.git",
"state": {
"branch": null,
"revision": "3036ba9d69cf1fd04d433527bc339dc0dc75433d",
"version": "5.1.3"
}
}
]
},
"version": 1
}
================================================
FILE: Package.swift
================================================
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "sendkeys",
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"),
.package(url: "https://github.com/apple/swift-format", from: "600.0.0"),
.package(url: "https://github.com/jpsim/Yams.git", from: "5.1.3"),
],
targets: [
// 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.
.executableTarget(
name: "sendkeys",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"SendKeysLib",
]),
.target(
name: "SendKeysLib",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Yams", package: "Yams"),
]),
.testTarget(
name: "SendKeysTests",
dependencies: ["sendkeys", "SendKeysLib"]),
]
)
================================================
FILE: README.md
================================================
# SendKeys


SendKeys is a macOS command line application used to automate the keystrokes and mouse events.
It is a great tool for automating input and mouse events for screen recordings.
This is a Swift rewrite of [`sendkeys-macos`](https://github.com/socsieng/sendkeys-macos).
## Usage
Basic usage:
```sh
sendkeys --application-name "Notes" --characters "Hello<p:1> world<c:left:option,shift><c:i:command>"
```

_Activates the Notes application (assuming Notes is already running) and types `Hello` (followed by a 1 second pause)
and `world`, and then selects the word `world` and changes the font to italics with `command` + `i`._
Input can be read from a file:
```sh
sendkeys --application-name "Code" --input-file example.txt
```
_Activates Visual Studio Code and sends keystrokes loaded from `example.txt`._
Input can also be piped to `stdin`:
```sh
cat example.txt | sendkeys --application-name "Notes"
```
_Activates the Notes application and sends keystrokes piped from `stdout` of the preceding command._
### 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.
- `--terminate-command <command>`: The command that should be used to terminate the application. Not set by default.
Follows a similar convention to `--characters`. (e.g. `f12:command,shift`).
- `--keyboard-layout <layout>`: Use alternate keyboard layout. Defaults to `qwerty`. `colemak` and `dvorak` are also
supported, pull requests for other common keyboard layouts may be considered. If a specific keyboard layout is not
supported, a custom layout can be defined in using the `--config` option or using the
[`.sendkeysrc.yml`](./examples/.sendkeysrc.yml) configuration file (`send.remap`).
- `--config <yaml-file>`: Configuration file to load settings from.
## Installation
### Homebrew (recommended)
Install using [homebrew](https://brew.sh/):
```sh
brew install socsieng/tap/sendkeys
```
### Manual installation
Alternatively, install from source:
```sh
git clone https://github.com/socsieng/sendkeys.git
cd sendkeys
make install
```
## Markup
Most printable characters will be sent as keystrokes to the active application. Support for additional instructions is
provided by some basic markup which is unlikely to be used in other markup languages to avoid conflicts.
### Key codes and modifier keys
Support for special key codes and modifier keys is provided with the following markup structure: `<c:key[:modifiers]>`
- `key` can include any printable character or, one of the following key names: `f1`, `f2`, `f3`, `f4`, `f5`, `f6`,
`f7`, `f8`, `f9`, `f10`, `f11`, `f12`, `esc`, `return`, `enter`, `delete`, `space`, `tab`, `up`, `down`, `left`,
`right`, `home`, `end`, `pgup`, and `pgdown`. See list of
[mapped keys](https://github.com/socsieng/sendkeys/blob/main/Sources/SendKeysLib/KeyCodes.swift#L127) for a full list.
- `modifiers` is an optional list of comma separated values that can include `command`, `shift`, `control`, `option`,
and `function`.
Example key combinations:
- `tab`: `<c:tab>`
- `command` + `a`: `<c:a:command>`
- `option` + `shift` + `left arrow`: `<c:left:option,shift>`
#### Key down and up
Some applications expect modifier keys to be pressed explicitly before invoking actions like mouse click. An example of
this is Pixelmator which expect the `option` key to be pressed before executing the alternate click action. This can be
achieved with key down `<kd:key[:modifiers]>` and key up `<ku:key[:modifiers]>`.
Note that these command shoulds only be used in these special cases when the mouse action and modifier keys are not
supported natively.
An example of how to trigger alternate click behavior in Pixelmator as described above:
`<kd:option><m:left:option><ku:option>`.
### Mouse commands
#### Move mouse cursor
The mouse cursor can be moved using the following markup: `<m:[x1,y1,]x2,y2[:duration][:modifiers]>`
- `x1` and `y1` are optional x and y coordinates to move the mouse from. Defaults to the current mouse position.
- `x2` and `y2` are x and y coordinates to move the mouse to. These values are required.
- `duration` is optional and determines the number of seconds (supports partial seconds) that should be used to move the
mouse cursor (larger number means slower movement). Defaults to `0`.
- `modifiers` is an optional list of comma separated values that can include `command`, `shift`, `control`, and
`option`.
Example usage:
- `<m:400,400:0.5>`: Move mouse cursor from current position to 400, 400 over 0.5 seconds.
- `<m:400,400,0,0:2>`: Move mouse cursor from 400, 400 position to 0, 0 over 2 seconds.
- `<m:400,400>`: Move mouse cursor to 400, 400 instantly.
 <br>_Sample command:
`sendkeys -c "<m:100,300,300,300:0.5><p:0.5><m:100,300:0.5>"`_
#### Mouse click
A mouse click can be activated using the following markup: `<m:button[:modifiers][:clicks]>`
- `button` is required and refers to the mouse button to click. Supported values include `left`, `center`, and `right`.
- `modifiers` is an optional list of comma separated values that can include `command`, `shift`, `control`, and
`option`.
- `clicks` is optional and specifies the number of times the button should be clicked. Defaults to `1`.
Example usage:
- `<m:right>`: Right mouse click at the current mouse location.
- `<m:left:2>`: Double click the left button at the current mouse location.
#### Mouse drag
A mouse drag be initiated with: `<d:[x1,y1,]x2,y2[:duration][:button[:modifiers]]>`
The argument structure is similar to moving the mouse cursor.
- `x1` and `y1` are optional x and y coordinates to start the drage. Defaults to the current mouse position.
- `x2` and `y2` are x and y coordinates to end the drag. These values are required.
- `duration` is optional and determines the number of seconds (supports partial seconds) that should be used to drag the
mouse (larger number means slower movement). Defaults to `0`.
- `button` is optional and refers to the mouse button to use when initiating the mouse drag. Supported values include
`left`, `center`, and `right`. Defaults to `left`.
- `modifiers` is an optional list of comma separated values that can include `command`, `shift`, `control`, and
`option`. Note that modifiers can only be used if `button` is explicitly set.
Example usage:
- `<d:400,400:0.5>`: Drag the mouse using the left mouse button from current position to 400, 400 over 0.5 seconds.
- `<d:400,400,0,0:2:right>`: Drag the mouse using the right mouse button from 400, 400 position to 0, 0 over 2 seconds.
- `<d:400,400:2:left:shift>`: Drag the mouse using the left mouse button to 400, 400 over 2 seconds with the `shift` key
down.

#### Mouse scrolling
A mouse scroll can be initiated with: `<s:x,y[:duration][:modifiers]>`
- `x` is required and controls horizontal scrolling. Positive values scroll to the right, while negative values scroll
to the left.
- `y` is required and controls vertical scrolling. Positive values scroll down, while negative values scroll up.
- `duration` is optional and determines the number of seconds (supports partial seconds) that should be used to drag the
mouse (larger number means slower movement). Defaults to `0`.
- `modifiers` is an optional list of comma separated values that can include `command`, `shift`, `control`, and
`option`.
Example usage:
- `<s:0,400:0.5>`: Scrolls down 400 pixels over 0.5 seconds.
- `<s:0,-100:0.2>`: Scrolls up 400 pixels over 0.2 seconds.
- `<s:100,0>`: Scrolls 100 pixel to the right instantly.
#### Mouse focus
The mouse focus command can be used to draw attention to an area of the screen by moving the cursor in a circular
pattern. The mouse focus command uses the following markup:
`<mf:centerX,centerY:radiusX[,radiusY]:angleFrom,angleTo:duration>`
- `centerX` is required and represents the center x coordinate of the circular path.
- `centerY` is required and represents the center y coordinate of the circular path.
- `radiusX` is required and represents the size of the radius along the x axis of the circular path.
- `radiusY` is optional and represents the size of the radius along the y axis of the circular path. If omitted,
`radiusX` will be used indicating that the circular path will be a regular circle. An elipse can be achieved by having
different values for `radiusX` and `radiusY`.
- `angleFrom` is required and represents the start angle/position of the circular path. Angle is defined using degrees
where `0` represents 12 o'clock on an analog clock, and positive are applied in a clockwize direction. (e.g. 90
degrees is 3 o'clock).
- `angleTo` is required and represents the end angle/position of the circular path.
- `duration` is required and determines the number of seconds (supports partial seconds) used to complete the animation
between `angleFrom` to `angleTo`.
Example usage:
- `<mf:1000,200:50,20:180,900:2>`: Draws attention to position 1000, 200 by moving the mouse along an eliptical 50
pixels wide by 20 pixels high starting at the bottom (180 degrees) to 900 degrees (delta of 720 degrees) over a period
of 2 seconds.

#### Mouse path
The mouse path command can be used move the mouse cursor along a path. The mouse path command uses the following markup:
`<mpath:path[:ofssetX,offsetY[,scaleX[,scaleY]]]:duration>`
- `path` is required and defines path for the mouse cursor to follow. The path is described using
[SVG Path data](https://www.w3.org/TR/SVG/paths.html#PathData)
- `ofssetX` and `offsetY` are optional and can be used to offset path coordinates by their respective `x` and `y`
values. Defaults to `0,0`.
- `scaleX` and `scaleY` are also optional and can be used to scale path coordinates by their respective `x` and `y`
values. Defaults to `1,1`. If `scaleY` is omitted while `scaleX` is provided, a uniform scale will be assumed. i.e.
`x` = `y`.
- `duration` is required and determines the number of seconds (supports partial seconds) used to complete the animation
along the `path`.
Example usage:
- `<mpath:c0,40 200,40 200,0:2>`: Moves the mouse from its current position along a cubic bezier path with control
points `0,40` and `200,40` to the final position of `200,1`.
 <br>_Sample command:
`sendkeys -c "<mpath:M100,100 h 100 l5,30 10,-60 5,30 h 100:2><mpath:c0,40 -220,40 -220,0:1.5>"`_
#### Mouse down and up
Mouse down and up events can be used to manually initiate a drag event or multiple mouse move commands while the mouse
button is down. This can be achieved with mouse down `<md:button[:modifiers]>` and mouse up `<mu:button[:modifiers]>`.
Note that the drag command is recommended for basic drag functionality..
An example of how include multiple mouse movements while the mouse button is down:
`<md:left><m:0,0,100,0:1><m:100,100:1><mu:left>`.
### Pauses
The default time between keystrokes and instructions is determined by the `--delay`/`-d` argument (default value is
`0.1`). Pauses can be customized with: `<p:duration>`
- `duration` is required and controls the amount of time to pause before the next keystroke/instruction is executed.
`<P:seconds>` (note upper case `P`) can be used to modify the default delay between subsequent keystrokes.
### Continuation
A continuation can be used to ignore the next keystroke or instruction. This is useful to help with formatting a long
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 examples/node.js
```
You can also pipe the output of the `transform` command directly to your editor of choice. Example:
```sh
sendkeys transform --input-file examples/node.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.
For a one off read, move your mouse to the desired position, switch to your terminal app using `command` + `tab` and
execute the following command:
```sh
sendkeys mouse-position
```
Use the `--watch` option to capture the location of mouse clicks, and combine it with `--output commands` to output
approximate mouse commands that can be used to _replay_ mouse actions.
```sh
# capture mouse commands
sendkeys mouse-position --watch --output commands > mouse_commands.txt
# replay mouse commands
sendkeys --input-file mouse_commands.txt
```
## List of applications names
A list of the current applications that can be activated by SendKeys (`--application-name`) can be displayed using the
`apps` command.
```sh
# list apps that can be activated with --application-name
sendkeys apps
```
Sample output:
```text
Code id:com.microsoft.VSCode
Finder id:com.apple.finder
Google Chrome id:com.google.Chrome
Safari id:com.apple.Safari
```
The first column includes the application name and the second column includes the application's bundle ID.
SendKeys will use `--application-name` to activate the first application instance that matches either the application
name or bundle id (case insensitive). If there are no exact matches, it will attempt to match on whole words for the
application name, followed by the bundle id.
## Configuration
Common arguments can be stored in a [`.sendkeysrc.yml`](./examples/.senkeysrc.yml) configuration file. Configuration
values are applied and merged in the following priority order:
1. Command line arguments
2. Configuration file defined with `--config` option
3. Configuration file defined in `~/.sendkeysrc.yml`
4. Default values
## Prerequisites
This application will only run on macOS 10.11 or later.
When running from the terminal, ensure that the terminal has permission to use accessibility features. This can be done
by navigating to System Preferences > Security & Privacy > Privacy > Accessibility and adding your terminal application
there.

## Installing previous versions
A specific version of the package can be installed by targeting the appropriate release artifact. Here's an example of
the command:
```sh
brew install --force-bottle https://github.com/socsieng/sendkeys/releases/download/v2.3.0/sendkeys-2.3.0.catalina.bottle.tar.gz
```
================================================
FILE: Sources/SendKeysLib/Animator.swift
================================================
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
}
}
================================================
FILE: Sources/SendKeysLib/AppActivator.swift
================================================
import Cocoa
class AppActivator: NSObject {
private var application: NSRunningApplication!
private let appName: String?
private let processId: Int?
init(appName: String?, processId: Int?) {
self.appName = appName?.lowercased()
self.processId = processId
}
func find() throws -> NSRunningApplication? {
let apps = NSWorkspace.shared.runningApplications.filter({ a in
return a.activationPolicy == .regular
})
var app: NSRunningApplication?
if processId != nil {
app =
apps.filter({ a in
return a.processIdentifier == pid_t(processId!)
}).first
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
return a.localizedName?.lowercased() == appName
|| a.bundleIdentifier?.lowercased() == appName
}).first
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
}
}
return app
}
func activate() throws {
let app = try! self.find()
if app == nil && appName != nil {
throw RuntimeError(
"Application \(appName!) cannot be activated. Run `sendkeys apps` to see a list of applications that can be activated."
)
}
if app != nil {
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")
}
}
}
================================================
FILE: Sources/SendKeysLib/AppLister.swift
================================================
import ArgumentParser
import Cocoa
import Foundation
class AppLister: ParsableCommand {
public static let configuration = CommandConfiguration(
commandName: "apps",
abstract:
"Lists apps that can be used with the send command."
)
struct AppInfo: Hashable {
let name: String?
let id: String?
init(name: String?, id: String?) {
self.name = name
self.id = id
}
static func == (lhs: AppInfo, rhs: AppInfo) -> Bool {
return lhs.name == rhs.name && lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.name)
hasher.combine(self.id)
}
}
required init() {
}
func run() {
let apps = Set(
NSWorkspace.shared.runningApplications.filter { app in
return app.activationPolicy == .regular
}
.map { app in
return AppInfo(name: app.localizedName, id: app.bundleIdentifier)
}
)
.sorted { a, b in
return a.name?.lowercased() ?? "" < b.name?.lowercased() ?? ""
}
let maxLength = apps.reduce(
0,
{ max, info in
return info.name?.count ?? 0 > max ? info.name!.count : max
})
apps.forEach { info in
print(
"\((info.name ?? "-").padding(toLength: maxLength + 4, withPad: " ", startingAt: 0))id:\(info.id ?? "-")"
)
}
}
}
================================================
FILE: Sources/SendKeysLib/Bridge.swift
================================================
func bridge<T: AnyObject>(obj: T) -> UnsafeRawPointer {
return UnsafeRawPointer(Unmanaged.passUnretained(obj).toOpaque())
}
func bridge<T: AnyObject>(ptr: UnsafeRawPointer) -> T {
return Unmanaged<T>.fromOpaque(ptr).takeUnretainedValue()
}
func bridgeRetained<T: AnyObject>(obj: T) -> UnsafeRawPointer {
return UnsafeRawPointer(Unmanaged.passRetained(obj).toOpaque())
}
func bridgeTransfer<T: AnyObject>(ptr: UnsafeRawPointer) -> T {
return Unmanaged<T>.fromOpaque(ptr).takeRetainedValue()
}
================================================
FILE: Sources/SendKeysLib/Commands/Command.swift
================================================
import Foundation
public enum CommandType {
case undefined
case keyPress
case keyDown
case keyUp
case pause
case stickyPause
case mouseMove
case mousePath
case mouseClick
case mouseDrag
case mouseScroll
case mouseDown
case mouseUp
case continuation
}
public protocol CommandProtocol {
static var commandType: CommandType { get }
static var expression: NSRegularExpression { get }
init(arguments: [String?])
func execute() throws
func equals(_ comparison: Command) -> Bool
}
protocol RequiresKeyPresser {
var keyPresser: KeyPresser? { get set }
}
protocol RequiresMouseController {
var mouseController: MouseController? { get set }
}
public class Command: Equatable, CustomStringConvertible {
public class var commandType: CommandType { return .undefined }
private static let _expression = try! NSRegularExpression(pattern: ".")
public class var expression: NSRegularExpression { return _expression }
init() {}
required public init(arguments: [String?]) {
}
public func execute() throws {
}
public func equals(_ comparison: Command) -> Bool {
return type(of: self) == type(of: comparison)
}
public static func == (lhs: Command, rhs: Command) -> Bool {
return lhs.equals(rhs) && rhs.equals(lhs)
}
public var description: String {
let output = "\(type(of: self)): \(type(of: self).commandType)"
let members = describeMembers()
if !members.isEmpty {
return "\(output) (\(members))"
}
return output
}
func describeMembers() -> String {
return ""
}
}
================================================
FILE: Sources/SendKeysLib/Commands/CommandExecutor.swift
================================================
import Foundation
public protocol CommandExecutorProtocol {
func execute(_ command: Command)
}
public class CommandExecutor: CommandExecutorProtocol {
public func execute(_ command: Command) {
try! command.execute()
}
}
================================================
FILE: Sources/SendKeysLib/Commands/CommandFactory.swift
================================================
public class CommandFactory {
public static let commands: [Command.Type] = [
KeyPressCommand.self,
KeyDownCommand.self,
KeyUpCommand.self,
StickyPauseCommand.self,
PauseCommand.self,
ContinuationCommand.self,
NewlineCommand.self,
MouseMoveCommand.self,
MousePathCommand.self,
MouseClickCommand.self,
MouseDragCommand.self,
MouseScrollCommand.self,
MouseDownCommand.self,
MouseUpCommand.self,
MouseFocusCommand.self,
DefaultCommand.self,
]
let keyPresser: KeyPresser
let mouseController: MouseController
init(keyPresser: KeyPresser, mouseController: MouseController) {
self.keyPresser = keyPresser
self.mouseController = mouseController
}
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 {
let command = commandType.init(arguments: arguments)
if var keyCommand = command as? RequiresKeyPresser {
keyCommand.keyPresser = keyPresser
}
if var mouseCommand = command as? RequiresMouseController {
mouseCommand.mouseController = mouseController
}
return command
}
}
================================================
FILE: Sources/SendKeysLib/Commands/CommandsIterator.swift
================================================
import Foundation
public class CommandsIterator: IteratorProtocol {
public typealias Element = Command
let commandString: String
let commandFactory: CommandFactory
var index = 0
public init(_ commandString: String, commandFactory: CommandFactory) {
self.commandString = commandString
self.commandFactory = commandFactory
}
public func next() -> Element? {
let length = commandString.utf16.count
if index < length {
var matchResult: NSTextCheckingResult?
if let commandType = CommandFactory.commands.first(where: { (commandType: Command.Type) -> Bool in
matchResult = commandType.expression.firstMatch(
in: commandString, options: .anchored, range: NSMakeRange(index, length - index))
return matchResult != nil
}
) {
let args = getArguments(commandString, matchResult!)
let command = commandFactory.create(commandType, arguments: args)
if matchResult != nil {
let range = Range(matchResult!.range, in: commandString)
index = range!.upperBound.utf16Offset(in: commandString)
}
return command
} else {
fatalError("Unmatched sequence.\n")
}
}
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
}
}
================================================
FILE: Sources/SendKeysLib/Commands/CommandsProcessor.swift
================================================
import Foundation
public class CommandsProcessor {
var defaultPause: TimeInterval
let numberFormatter = NumberFormatter()
let commandExecutor: CommandExecutorProtocol
let keyPresser: KeyPresser
let mouseController: MouseController
init(
defaultPause: Double, keyPresser: KeyPresser, mouseController: MouseController,
commandExecutor: CommandExecutorProtocol? = nil
) {
self.defaultPause = defaultPause
self.commandExecutor = commandExecutor ?? CommandExecutor()
self.keyPresser = keyPresser
self.mouseController = mouseController
numberFormatter.usesSignificantDigits = true
numberFormatter.minimumSignificantDigits = 1
numberFormatter.maximumSignificantDigits = 3
}
convenience public init(
defaultPause: Double, keyPresser: KeyPresser, commandExecutor: CommandExecutorProtocol? = nil
) {
self.init(
defaultPause: defaultPause, keyPresser: keyPresser,
mouseController: MouseController(animationRefreshInterval: 0.01, keyPresser: keyPresser),
commandExecutor: commandExecutor)
}
private func getDefaultPauseCommand() -> Command {
return PauseCommand(duration: defaultPause)
}
public func process(_ commandString: String) {
let commandFactory = CommandFactory(keyPresser: keyPresser, mouseController: mouseController)
let commands = IteratorSequence(CommandsIterator(commandString, commandFactory: commandFactory))
var shouldDefaultPause = false
var shouldIgnoreNextCommand = false
for command in commands {
if shouldIgnoreNextCommand {
shouldIgnoreNextCommand = false
continue
}
if command is ContinuationCommand {
shouldIgnoreNextCommand = true
continue
}
if command is StickyPauseCommand {
shouldDefaultPause = false
defaultPause = (command as! StickyPauseCommand).duration
} else if command is PauseCommand {
shouldDefaultPause = false
} else if shouldDefaultPause {
commandExecutor.execute(getDefaultPauseCommand())
shouldDefaultPause = true
} else {
shouldDefaultPause = true
}
commandExecutor.execute(command)
}
}
}
================================================
FILE: Sources/SendKeysLib/Commands/ContinuationCommand.swift
================================================
import Foundation
public class ContinuationCommand: Command {
public override class var commandType: CommandType { return .continuation }
private static let _expression = try! NSRegularExpression(pattern: "\\<\\\\\\>")
public override class var expression: NSRegularExpression { return _expression }
public override init() {
super.init()
}
required public init(arguments: [String?]) {
super.init(arguments: arguments)
}
}
================================================
FILE: Sources/SendKeysLib/Commands/DefaultCommand.swift
================================================
import Foundation
public class DefaultCommand: KeyPressCommand {
private static let _expression = try! NSRegularExpression(pattern: ".")
public override class var expression: NSRegularExpression { return _expression }
public init(key: String) {
super.init()
self.key = key
}
required public init(arguments: [String?]) {
super.init()
self.key = arguments[0]!
}
}
================================================
FILE: Sources/SendKeysLib/Commands/KeyDownCommand.swift
================================================
import Foundation
public class KeyDownCommand: KeyPressCommand {
public override class var commandType: CommandType { return .keyDown }
private static let _expression = try! NSRegularExpression(pattern: "\\<kd:(.|[\\w]+)(:([,\\w⌘^⌥⇧]+))?\\>")
public override class var expression: NSRegularExpression { return _expression }
public override init(key: String, modifiers: [String]) {
super.init(key: key, modifiers: modifiers)
}
required public init(arguments: [String?]) {
super.init(arguments: arguments)
}
public override func execute() throws {
let _ = try! keyPresser!.keyDown(key: key!, modifiers: modifiers)
}
}
================================================
FILE: Sources/SendKeysLib/Commands/KeyPressCommand.swift
================================================
import Foundation
public class KeyPressCommand: Command, RequiresKeyPresser {
public override class var commandType: CommandType { return .keyPress }
private static let _expression = try! NSRegularExpression(pattern: "\\<[ck]:(.|[\\w]+)(:([,\\w⌘^⌥⇧]+))?\\>")
public override class var expression: NSRegularExpression { return _expression }
var key: String?
var modifiers: [String] = []
var keyPresser: KeyPresser?
override init() {
super.init()
}
public init(key: String, modifiers: [String]) {
super.init()
self.key = key
self.modifiers = modifiers
}
required public init(arguments: [String?]) {
super.init()
self.key = arguments[1]!
self.modifiers = arguments[3]?.components(separatedBy: ",").filter({ !$0.isEmpty }) ?? []
}
public override func execute() throws {
let _ = try! keyPresser!.keyPress(key: key!, modifiers: modifiers)
}
public override func equals(_ comparison: Command) -> Bool {
return super.equals(comparison)
&& {
if let command = comparison as? KeyPressCommand {
return key == command.key
&& modifiers == command.modifiers
}
return false
}()
}
public override func describeMembers() -> String {
return "key: \(key ?? "''")), modifiers: \(modifiers)"
}
}
================================================
FILE: Sources/SendKeysLib/Commands/KeyUpCommand.swift
================================================
import Foundation
public class KeyUpCommand: KeyPressCommand {
public override class var commandType: CommandType { return .keyUp }
private static let _expression = try! NSRegularExpression(pattern: "\\<ku:(.|[\\w]+)(:([,\\w⌘^⌥⇧]+))?\\>")
public override class var expression: NSRegularExpression { return _expression }
public override init(key: String, modifiers: [String]) {
super.init(key: key, modifiers: modifiers)
}
required public init(arguments: [String?]) {
super.init(arguments: arguments)
}
public override func execute() throws {
let _ = try! keyPresser!.keyUp(key: key!, modifiers: modifiers)
}
}
================================================
FILE: Sources/SendKeysLib/Commands/MouseClickCommand.swift
================================================
import Cocoa
import Foundation
public class MouseClickCommand: Command, RequiresMouseController {
public override class var commandType: CommandType { return .mouseClick }
private static let _expression = try! NSRegularExpression(pattern: "\\<m:([a-z]+)(:([a-z,]+))?(:(\\d+))?\\>")
public override class var expression: NSRegularExpression { return _expression }
var button: String?
var modifiers: [String] = []
var clicks: Int = 1
var mouseController: MouseController?
override init() {
super.init()
}
public init(button: String?, modifiers: [String], clicks: Int) {
super.init()
self.button = button
self.modifiers = modifiers
self.clicks = clicks
}
required public init(arguments: [String?]) {
super.init()
self.button = arguments[1]!
self.modifiers = arguments[3]?.components(separatedBy: ",").filter({ !$0.isEmpty }) ?? []
self.clicks = Int(arguments[5] ?? "1")!
}
public override func execute() throws {
try! mouseController!.click(
nil,
button: getMouseButton(button: button!),
flags: try! KeyPresser.getModifierFlags(modifiers),
clickCount: clicks
)
}
public override func equals(_ comparison: Command) -> Bool {
return super.equals(comparison)
&& {
if let command = comparison as? MouseClickCommand {
return button == command.button
&& modifiers == command.modifiers
&& clicks == command.clicks
}
return false
}()
}
public override func describeMembers() -> String {
return "button: \(button ?? "''")), modifiers: \(modifiers), clicks: \(clicks)"
}
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)")
}
}
}
================================================
FILE: Sources/SendKeysLib/Commands/MouseDownCommand.swift
================================================
import Foundation
public class MouseDownCommand: MouseClickCommand {
public override class var commandType: CommandType { return .mouseDown }
private static let _expression = try! NSRegularExpression(pattern: "\\<md:([a-z]+)(:([a-z,]+))?\\>")
public override class var expression: NSRegularExpression { return _expression }
override init() {
super.init()
}
public init(button: String?, modifiers: [String]) {
super.init()
self.button = button
self.modifiers = modifiers
}
required public init(arguments: [String?]) {
super.init()
self.button = arguments[1]!
self.modifiers = arguments[3]?.components(separatedBy: ",").filter({ !$0.isEmpty }) ?? []
}
public override func execute() throws {
try! mouseController!.down(
nil,
button: getMouseButton(button: button!),
flags: try! KeyPresser.getModifierFlags(modifiers)
)
}
}
================================================
FILE: Sources/SendKeysLib/Commands/MouseDragCommand.swift
================================================
import Foundation
public class MouseDragCommand: MouseMoveCommand {
public override class var commandType: CommandType { return .mouseDrag }
private static let _expression = try! NSRegularExpression(
pattern: "\\<d:((-?[.\\d]+),(-?[.\\d]+),)?(-?[.\\d]+),(-?[.\\d]+)(:([\\d.]+))?(:([a-z]+))?(:([a-z,]+))?\\>")
public override class var expression: NSRegularExpression { return _expression }
public init(
x1: Double?, y1: Double?, x2: Double, y2: Double, duration: TimeInterval, button: String?, modifiers: [String]
) {
super.init()
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.duration = duration
self.button = button
self.modifiers = modifiers
}
required public init(arguments: [String?]) {
super.init()
self.x1 = Double(arguments[2] ?? "")
self.y1 = Double(arguments[3] ?? "")
self.x2 = Double(arguments[4]!)!
self.y2 = Double(arguments[5]!)!
self.duration = TimeInterval(arguments[7] ?? "0")!
self.button = arguments[9] ?? "left"
self.modifiers = arguments[11]?.components(separatedBy: ",").filter({ !$0.isEmpty }) ?? []
}
public override func describeMembers() -> String {
return
"x1: \(x1?.description ?? "nil")), y1: \(y1?.description ?? "nil"), x2: \(x2), y2: \(y2), duration: \(duration), button: \(button ?? "''")), modifiers: \(modifiers)"
}
public override func execute() throws {
try! mouseController!.drag(
start: x1 == nil || y1 == nil ? nil : CGPoint(x: x1!, y: y1!),
end: CGPoint(x: x2, y: y2),
duration: duration,
button: getMouseButton(button: button!),
flags: try! KeyPresser.getModifierFlags(modifiers)
)
}
}
================================================
FILE: Sources/SendKeysLib/Commands/MouseFocusCommand.swift
================================================
import Foundation
public class MouseFocusCommand: MouseClickCommand {
public override class var commandType: CommandType { return .mouseScroll }
// <mf:centerX,centerY:radiusX[,radiusY]:angleFrom,angleTo:duration>
private static let _expression = try! NSRegularExpression(
pattern: "\\<mf:(-?[.\\d]+),(-?[.\\d]+):([.\\d]+)(,([.\\d]+))?:(-?[.\\d]+),(-?[.\\d]+):([.\\d]+)\\>")
public override class var expression: NSRegularExpression { return _expression }
var x: Double
var y: Double
var rx: Double
var ry: Double
var from: Double
var to: Double
var duration: TimeInterval
public init(
x: Double, y: Double, rx: Double, ry: Double, angleFrom: Double, angleTo: Double, duration: TimeInterval
) {
self.x = x
self.y = y
self.rx = rx
self.ry = ry
self.from = angleFrom
self.to = angleTo
self.duration = duration
super.init()
}
required public init(arguments: [String?]) {
self.x = Double(arguments[1]!)!
self.y = Double(arguments[2]!)!
self.rx = Double(arguments[3]!)!
self.ry = Double(arguments[5] ?? arguments[3]!)!
self.from = Double(arguments[6]!)!
self.to = Double(arguments[7]!)!
self.duration = TimeInterval(arguments[8]!)!
super.init()
}
public override func execute() throws {
mouseController!.circle(CGPoint(x: x, y: y), CGPoint(x: rx, y: ry), from, to, duration)
}
public override func describeMembers() -> String {
return "x: \(x), y: \(y), rx: \(rx), ry: \(ry), from: \(from), to: \(to), duration: \(duration)"
}
public override func equals(_ comparison: Command) -> Bool {
return super.equals(comparison)
&& {
if let command = comparison as? MouseFocusCommand {
return x == command.x
&& y == command.y
&& rx == command.rx
&& ry == command.ry
&& from == command.from
&& to == command.to
&& duration == command.duration
}
return false
}()
}
}
================================================
FILE: Sources/SendKeysLib/Commands/MouseMoveCommand.swift
================================================
import Foundation
public class MouseMoveCommand: MouseClickCommand {
public override class var commandType: CommandType { return .mouseMove }
private static let _expression = try! NSRegularExpression(
pattern: "\\<m:((-?[.\\d]+),(-?[.\\d]+),)?(-?[.\\d]+),(-?[.\\d]+)(:([\\d.]+))?(:([a-z,]+))?\\>")
public override class var expression: NSRegularExpression { return _expression }
var x1: Double?
var y1: Double?
var x2: Double = 0
var y2: Double = 0
var duration: TimeInterval = 0
override init() {
super.init()
}
public init(x1: Double?, y1: Double?, x2: Double, y2: Double, duration: TimeInterval, modifiers: [String]) {
super.init()
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.duration = duration
self.modifiers = modifiers
}
required public init(arguments: [String?]) {
super.init()
self.x1 = Double(arguments[2] ?? "")
self.y1 = Double(arguments[3] ?? "")
self.x2 = Double(arguments[4]!)!
self.y2 = Double(arguments[5]!)!
self.duration = TimeInterval(arguments[7] ?? "0")!
self.modifiers = arguments[9]?.components(separatedBy: ",").filter({ !$0.isEmpty }) ?? []
}
public override func execute() throws {
mouseController!.move(
start: x1 == nil || y1 == nil ? nil : CGPoint(x: x1!, y: y1!),
end: CGPoint(x: x2, y: y2),
duration: duration,
flags: try! KeyPresser.getModifierFlags(modifiers)
)
}
public override func equals(_ comparison: Command) -> Bool {
return super.equals(comparison)
&& {
if let command = comparison as? MouseMoveCommand {
return x1 == command.x1
&& y1 == command.y1
&& x2 == command.x2
&& y2 == command.y2
&& duration == command.duration
}
return false
}()
}
public override func describeMembers() -> String {
return
"x1: \(x1?.description ?? "nil")), y1: \(y1?.description ?? "nil"), x2: \(x2), y2: \(y2), duration: \(duration)"
}
}
================================================
FILE: Sources/SendKeysLib/Commands/MousePathCommand.swift
================================================
import Foundation
public class MousePathCommand: MouseClickCommand {
public override class var commandType: CommandType { return .mousePath }
private static let _expression = try! NSRegularExpression(
pattern:
"\\<mpath:([^:\\>]+)(:(-?[\\d.]+),(-?[\\d.]+)(,(-?[\\d.]+)(,(-?[\\d.]+))?)?)?:([\\d.]+)(:([a-z,]+))?\\>")
public override class var expression: NSRegularExpression { return _expression }
var path: String
var offsetX: Double = 0
var offsetY: Double = 0
var scaleX: Double = 1
var scaleY: Double = 1
var duration: TimeInterval = 0
public init(
path: String, offsetX: Double, offsetY: Double, scaleX: Double, scaleY: Double, duration: TimeInterval,
modifiers: [String]
) {
self.path = path
self.offsetX = offsetX
self.offsetY = offsetY
self.scaleX = scaleX
self.scaleY = scaleY
self.duration = duration
super.init()
self.modifiers = modifiers
}
required public init(arguments: [String?]) {
self.path = arguments[1]!
self.offsetX = Double(arguments[3] ?? "0")!
self.offsetY = Double(arguments[4] ?? "0")!
self.scaleX = Double(arguments[6] ?? "1")!
self.scaleY = Double(arguments[8] ?? arguments[6] ?? "1")!
self.duration = TimeInterval(arguments[9] ?? "0")!
super.init()
self.modifiers = arguments[10]?.components(separatedBy: ",").filter({ !$0.isEmpty }) ?? []
}
public override func execute() throws {
mouseController!.move(
start: nil,
path: path,
offset: CGPoint(x: offsetX, y: offsetY),
scale: CGPoint(x: scaleX, y: scaleY),
duration: duration,
flags: try! KeyPresser.getModifierFlags(modifiers)
)
}
public override func equals(_ comparison: Command) -> Bool {
return super.equals(comparison)
&& {
if let command = comparison as? MousePathCommand {
return path == command.path
&& offsetX == command.offsetX
&& offsetY == command.offsetY
&& scaleX == command.scaleX
&& scaleY == command.scaleY
&& duration == command.duration
}
return false
}()
}
public override func describeMembers() -> String {
return
"path: \(path), offsetX: \(offsetX), offsetY: \(offsetY), scaleX: \(scaleX), scaleY: \(scaleY), duration: \(duration)"
}
}
================================================
FILE: Sources/SendKeysLib/Commands/MouseScrollCommand.swift
================================================
import Foundation
public class MouseScrollCommand: MouseClickCommand {
public override class var commandType: CommandType { return .mouseScroll }
private static let _expression = try! NSRegularExpression(
pattern: "\\<s:(-?[.\\d]+),(-?[.\\d]+)(:([.\\d]+))?(:([a-z,]+))?\\>")
public override class var expression: NSRegularExpression { return _expression }
var x: Double
var y: Double
var duration: TimeInterval
public init(x: Double, y: Double, duration: TimeInterval, modifiers: [String]) {
self.x = x
self.y = y
self.duration = duration
super.init()
self.modifiers = modifiers
}
required public init(arguments: [String?]) {
self.x = Double(arguments[1]!)!
self.y = Double(arguments[2]!)!
self.duration = TimeInterval(arguments[4] ?? "0")!
super.init()
self.modifiers = arguments[6]?.components(separatedBy: ",").filter({ !$0.isEmpty }) ?? []
}
public override func execute() throws {
mouseController!.scroll(
CGPoint(x: x, y: y),
duration,
flags: try! KeyPresser.getModifierFlags(modifiers)
)
}
public override func describeMembers() -> String {
return "x: \(x), y: \(y), duration: \(duration), modifiers: \(modifiers)"
}
public override func equals(_ comparison: Command) -> Bool {
return super.equals(comparison)
&& {
if let command = comparison as? MouseScrollCommand {
return x == command.x
&& y == command.y
&& duration == command.duration
}
return false
}()
}
}
================================================
FILE: Sources/SendKeysLib/Commands/MouseUpCommand.swift
================================================
import Foundation
public class MouseUpCommand: MouseClickCommand {
public override class var commandType: CommandType { return .mouseUp }
private static let _expression = try! NSRegularExpression(pattern: "\\<mu:([a-z]+)(:([a-z,]+))?\\>")
public override class var expression: NSRegularExpression { return _expression }
override init() {
super.init()
}
public init(button: String?, modifiers: [String]) {
super.init()
self.button = button
self.modifiers = modifiers
}
required public init(arguments: [String?]) {
super.init()
self.button = arguments[1]!
self.modifiers = arguments[3]?.components(separatedBy: ",").filter({ !$0.isEmpty }) ?? []
}
public override func execute() throws {
try! mouseController!.up(
nil,
button: getMouseButton(button: button!),
flags: try! KeyPresser.getModifierFlags(modifiers)
)
}
}
================================================
FILE: Sources/SendKeysLib/Commands/NewlineCommand.swift
================================================
import Foundation
public class NewlineCommand: KeyPressCommand {
private static let _expression = try! NSRegularExpression(pattern: "\\r?\\n")
public override class var expression: NSRegularExpression { return _expression }
public override init() {
super.init()
self.key = "return"
}
required public convenience init(arguments: [String?]) {
self.init()
}
}
================================================
FILE: Sources/SendKeysLib/Commands/PauseCommand.swift
================================================
import Foundation
public class PauseCommand: Command {
public override class var commandType: CommandType { return .pause }
private static let _expression = try! NSRegularExpression(pattern: "\\<p:([\\d.]+)\\>")
public override class var expression: NSRegularExpression { return _expression }
var duration: TimeInterval = 0
init(duration: TimeInterval) {
super.init()
self.duration = duration
}
required public init(arguments: [String?]) {
super.init()
self.duration = TimeInterval(arguments[1]!)!
}
public override func execute() throws {
Sleeper.sleep(seconds: duration)
}
public override func equals(_ comparison: Command) -> Bool {
return super.equals(comparison)
&& {
if let command = comparison as? PauseCommand {
return duration == command.duration
}
return false
}()
}
public override func describeMembers() -> String {
return "duration: \(duration)"
}
}
================================================
FILE: Sources/SendKeysLib/Commands/StickyPauseCommand.swift
================================================
import Foundation
public class StickyPauseCommand: PauseCommand {
public override class var commandType: CommandType { return .stickyPause }
private static let _expression = try! NSRegularExpression(pattern: "\\<P:([\\d.]+)\\>")
public override class var expression: NSRegularExpression { return _expression }
}
================================================
FILE: Sources/SendKeysLib/Configuration/AllConfiguration.swift
================================================
struct AllConfiguration: Codable {
var send: SendConfig?
var mousePosition: MousePositionConfig?
var transformer: TransformerConfig?
init(send: SendConfig? = nil, mousePosition: MousePositionConfig? = nil, transformer: TransformerConfig? = nil) {
self.send = send
self.mousePosition = mousePosition
self.transformer = transformer
}
func merge(with other: AllConfiguration?) -> AllConfiguration {
return AllConfiguration(
send: other?.send?.merge(with: self.send) ?? self.send,
mousePosition: other?.mousePosition?.merge(with: self.mousePosition) ?? self.mousePosition,
transformer: other?.transformer?.merge(with: self.transformer) ?? self.transformer
)
}
}
================================================
FILE: Sources/SendKeysLib/Configuration/ConfigLoader.swift
================================================
import Foundation
import Yams
let defaultConfigFiles = [
NSString("~/.sendkeysrc.yml").expandingTildeInPath,
NSString("~/.sendkeysrc.yaml").expandingTildeInPath,
]
struct ConfigLoader {
static func loadConfig(_ file: String? = nil) -> AllConfiguration {
var config = AllConfiguration()
let configFiles =
if file != nil {
[file!]
} else {
defaultConfigFiles
}
for configFile in configFiles {
if !configFile.isEmpty && FileManager.default.fileExists(atPath: configFile) {
if let contents = FileManager.default.contents(atPath: configFile) {
do {
let decoder = YAMLDecoder()
config = config.merge(with: try decoder.decode(AllConfiguration.self, from: contents))
} catch {
print("Unable to read \(configFile): \(error)")
}
}
}
}
return config
}
}
================================================
FILE: Sources/SendKeysLib/Configuration/MousePositionConfig.swift
================================================
struct MousePositionConfig: Codable {
var watch: Bool?
var output: OutputMode?
var duration: Double?
init(watch: Bool? = nil, output: OutputMode? = nil, duration: Double? = nil) {
self.watch = watch
self.output = output
self.duration = duration
}
func merge(with other: MousePositionConfig?) -> MousePositionConfig {
return MousePositionConfig(
watch: other?.watch ?? self.watch,
output: other?.output ?? self.output,
duration: other?.duration ?? self.duration
)
}
}
================================================
FILE: Sources/SendKeysLib/Configuration/SendConfig.swift
================================================
struct SendConfig: Codable {
var activate: Bool?
var animationInterval: Double?
var delay: Double?
var initialDelay: Double?
var keyboardLayout: KeyMappings.Layouts?
var remap: [String: String]?
var targeted: Bool?
var terminateCommand: String?
init(
activate: Bool? = nil, animationInterval: Double? = nil, delay: Double? = nil, initialDelay: Double? = nil,
keyboardLayout: KeyMappings.Layouts? = nil, remap: [String: String]? = nil, targeted: Bool? = nil,
terminateCommand: String? = nil
) {
self.activate = activate
self.animationInterval = animationInterval
self.delay = delay
self.initialDelay = initialDelay
self.keyboardLayout = keyboardLayout
self.remap = remap
self.targeted = targeted
self.terminateCommand = terminateCommand
}
func merge(with other: SendConfig?) -> SendConfig {
return SendConfig(
activate: other?.activate ?? self.activate,
animationInterval: other?.animationInterval ?? self.animationInterval,
delay: other?.delay ?? self.delay,
initialDelay: other?.initialDelay ?? self.initialDelay,
keyboardLayout: other?.keyboardLayout ?? self.keyboardLayout,
remap: other?.remap ?? self.remap,
targeted: other?.targeted ?? self.targeted,
terminateCommand: other?.terminateCommand ?? self.terminateCommand
)
}
}
================================================
FILE: Sources/SendKeysLib/Configuration/TransformerConfig.swift
================================================
struct TransformerConfig: Codable {
var indent: Bool?
var autoClose: String?
init(indent: Bool? = nil, autoClose: String? = nil) {
self.indent = indent
self.autoClose = autoClose
}
func merge(with other: TransformerConfig?) -> TransformerConfig {
return TransformerConfig(
indent: other?.indent ?? self.indent,
autoClose: other?.autoClose ?? self.autoClose
)
}
}
================================================
FILE: Sources/SendKeysLib/KeyCodes.swift
================================================
import Cocoa
// From: https://gist.github.com/swillits/df648e87016772c7f7e5dbed2b345066
struct KeyCodes {
// Layout-independent Keys
// eg.These key codes are always the same key on all layouts.
static let returnKey: UInt16 = 0x24
static let enter: UInt16 = 0x4C
static let tab: UInt16 = 0x30
static let space: UInt16 = 0x31
static let delete: UInt16 = 0x33
static let escape: UInt16 = 0x35
static let command: UInt16 = 0x37
static let shift: UInt16 = 0x38
static let capsLock: UInt16 = 0x39
static let option: UInt16 = 0x3A
static let control: UInt16 = 0x3B
static let rightShift: UInt16 = 0x3C
static let rightOption: UInt16 = 0x3D
static let rightControl: UInt16 = 0x3E
static let leftArrow: UInt16 = 0x7B
static let rightArrow: UInt16 = 0x7C
static let downArrow: UInt16 = 0x7D
static let upArrow: UInt16 = 0x7E
static let volumeUp: UInt16 = 0x48
static let volumeDown: UInt16 = 0x49
static let mute: UInt16 = 0x4A
static let help: UInt16 = 0x72
static let home: UInt16 = 0x73
static let pageUp: UInt16 = 0x74
static let forwardDelete: UInt16 = 0x75
static let end: UInt16 = 0x77
static let pageDown: UInt16 = 0x79
static let function: UInt16 = 0x3F
static let f1: UInt16 = 0x7A
static let f2: UInt16 = 0x78
static let f4: UInt16 = 0x76
static let f5: UInt16 = 0x60
static let f6: UInt16 = 0x61
static let f7: UInt16 = 0x62
static let f3: UInt16 = 0x63
static let f8: UInt16 = 0x64
static let f9: UInt16 = 0x65
static let f10: UInt16 = 0x6D
static let f11: UInt16 = 0x67
static let f12: UInt16 = 0x6F
static let f13: UInt16 = 0x69
static let f14: UInt16 = 0x6B
static let f15: UInt16 = 0x71
static let f16: UInt16 = 0x6A
static let f17: UInt16 = 0x40
static let f18: UInt16 = 0x4F
static let f19: UInt16 = 0x50
static let f20: UInt16 = 0x5A
// US-ANSI Keyboard Positions
// eg. These key codes are for the physical key (in any keyboard layout)
// at the location of the named key in the US-ANSI layout.
static let a: UInt16 = 0x00
static let b: UInt16 = 0x0B
static let c: UInt16 = 0x08
static let d: UInt16 = 0x02
static let e: UInt16 = 0x0E
static let f: UInt16 = 0x03
static let g: UInt16 = 0x05
static let h: UInt16 = 0x04
static let i: UInt16 = 0x22
static let j: UInt16 = 0x26
static let k: UInt16 = 0x28
static let l: UInt16 = 0x25
static let m: UInt16 = 0x2E
static let n: UInt16 = 0x2D
static let o: UInt16 = 0x1F
static let p: UInt16 = 0x23
static let q: UInt16 = 0x0C
static let r: UInt16 = 0x0F
static let s: UInt16 = 0x01
static let t: UInt16 = 0x11
static let u: UInt16 = 0x20
static let v: UInt16 = 0x09
static let w: UInt16 = 0x0D
static let x: UInt16 = 0x07
static let y: UInt16 = 0x10
static let z: UInt16 = 0x06
static let zero: UInt16 = 0x1D
static let one: UInt16 = 0x12
static let two: UInt16 = 0x13
static let three: UInt16 = 0x14
static let four: UInt16 = 0x15
static let five: UInt16 = 0x17
static let six: UInt16 = 0x16
static let seven: UInt16 = 0x1A
static let eight: UInt16 = 0x1C
static let nine: UInt16 = 0x19
static let equals: UInt16 = 0x18
static let minus: UInt16 = 0x1B
static let semicolon: UInt16 = 0x29
static let apostrophe: UInt16 = 0x27
static let comma: UInt16 = 0x2B
static let period: UInt16 = 0x2F
static let forwardSlash: UInt16 = 0x2C
static let backslash: UInt16 = 0x2A
static let grave: UInt16 = 0x32
static let leftBracket: UInt16 = 0x21
static let rightBracket: UInt16 = 0x1E
static let keypadDecimal: UInt16 = 0x41
static let keypadMultiply: UInt16 = 0x43
static let keypadPlus: UInt16 = 0x45
static let keypadClear: UInt16 = 0x47
static let keypadDivide: UInt16 = 0x4B
static let keypadEnter: UInt16 = 0x4C
static let keypadMinus: UInt16 = 0x4E
static let keypadEquals: UInt16 = 0x51
static let keypad0: UInt16 = 0x52
static let keypad1: UInt16 = 0x53
static let keypad2: UInt16 = 0x54
static let keypad3: UInt16 = 0x55
static let keypad4: UInt16 = 0x56
static let keypad5: UInt16 = 0x57
static let keypad6: UInt16 = 0x58
static let keypad7: UInt16 = 0x59
static let keypad8: UInt16 = 0x5B
static let keypad9: UInt16 = 0x5C
struct KeyCodeWithFlags {
let keyCode: UInt16
let flags: [CGEventFlags]
init(_ keyCode: UInt16, _ flags: [CGEventFlags] = []) {
self.keyCode = keyCode
self.flags = flags
}
}
struct KeyWithFlags {
let key: String
let flags: [CGEventFlags]
init(_ key: String, _ flags: [CGEventFlags] = []) {
self.key = key
self.flags = flags
}
}
// map
private static let keyDictionary = [
"return": KeyCodeWithFlags(returnKey),
"enter": KeyCodeWithFlags(enter),
"tab": KeyCodeWithFlags(tab),
"space": KeyCodeWithFlags(space),
"delete": KeyCodeWithFlags(delete),
"escape": KeyCodeWithFlags(escape),
"esc": KeyCodeWithFlags(escape),
"⌘": KeyCodeWithFlags(command),
"cmd": KeyCodeWithFlags(command),
"command": KeyCodeWithFlags(command),
"shift": KeyCodeWithFlags(shift),
"capslock": KeyCodeWithFlags(capsLock),
"⌥": KeyCodeWithFlags(option),
"alt": KeyCodeWithFlags(option),
"option": KeyCodeWithFlags(option),
"ctrl": KeyCodeWithFlags(control),
"control": KeyCodeWithFlags(control),
"rightshift": KeyCodeWithFlags(rightShift),
"rightoption": KeyCodeWithFlags(rightOption),
"rightControl": KeyCodeWithFlags(rightControl),
"left": KeyCodeWithFlags(leftArrow),
"right": KeyCodeWithFlags(rightArrow),
"down": KeyCodeWithFlags(downArrow),
"up": KeyCodeWithFlags(upArrow),
"volumeup": KeyCodeWithFlags(volumeUp),
"volumedown": KeyCodeWithFlags(volumeDown),
"mute": KeyCodeWithFlags(mute),
"help": KeyCodeWithFlags(help),
"home": KeyCodeWithFlags(home),
"pgup": KeyCodeWithFlags(pageUp),
"forwarddelete": KeyCodeWithFlags(forwardDelete),
"end": KeyCodeWithFlags(end),
"pgdown": KeyCodeWithFlags(pageDown),
"fn": KeyCodeWithFlags(function),
"function": KeyCodeWithFlags(function),
"f1": KeyCodeWithFlags(f1),
"f2": KeyCodeWithFlags(f2),
"f4": KeyCodeWithFlags(f4),
"f5": KeyCodeWithFlags(f5),
"f6": KeyCodeWithFlags(f6),
"f7": KeyCodeWithFlags(f7),
"f3": KeyCodeWithFlags(f3),
"f8": KeyCodeWithFlags(f8),
"f9": KeyCodeWithFlags(f9),
"f10": KeyCodeWithFlags(f10),
"f11": KeyCodeWithFlags(f11),
"f12": KeyCodeWithFlags(f12),
"f13": KeyCodeWithFlags(f13),
"f14": KeyCodeWithFlags(f14),
"f15": KeyCodeWithFlags(f15),
"f16": KeyCodeWithFlags(f16),
"f17": KeyCodeWithFlags(f17),
"f18": KeyCodeWithFlags(f18),
"f19": KeyCodeWithFlags(f19),
"f20": KeyCodeWithFlags(f20),
"a": KeyCodeWithFlags(a),
"b": KeyCodeWithFlags(b),
"c": KeyCodeWithFlags(c),
"d": KeyCodeWithFlags(d),
"e": KeyCodeWithFlags(e),
"f": KeyCodeWithFlags(f),
"g": KeyCodeWithFlags(g),
"h": KeyCodeWithFlags(h),
"i": KeyCodeWithFlags(i),
"j": KeyCodeWithFlags(j),
"k": KeyCodeWithFlags(k),
"l": KeyCodeWithFlags(l),
"m": KeyCodeWithFlags(m),
"n": KeyCodeWithFlags(n),
"o": KeyCodeWithFlags(o),
"p": KeyCodeWithFlags(p),
"q": KeyCodeWithFlags(q),
"r": KeyCodeWithFlags(r),
"s": KeyCodeWithFlags(s),
"t": KeyCodeWithFlags(t),
"u": KeyCodeWithFlags(u),
"v": KeyCodeWithFlags(v),
"w": KeyCodeWithFlags(w),
"x": KeyCodeWithFlags(x),
"y": KeyCodeWithFlags(y),
"z": KeyCodeWithFlags(z),
"0": KeyCodeWithFlags(zero),
"1": KeyCodeWithFlags(one),
"2": KeyCodeWithFlags(two),
"3": KeyCodeWithFlags(three),
"4": KeyCodeWithFlags(four),
"5": KeyCodeWithFlags(five),
"6": KeyCodeWithFlags(six),
"7": KeyCodeWithFlags(seven),
"8": KeyCodeWithFlags(eight),
"9": KeyCodeWithFlags(nine),
"=": KeyCodeWithFlags(equals),
"-": KeyCodeWithFlags(minus),
";": KeyCodeWithFlags(semicolon),
"'": KeyCodeWithFlags(apostrophe),
",": KeyCodeWithFlags(comma),
".": KeyCodeWithFlags(period),
"/": KeyCodeWithFlags(forwardSlash),
"\\": KeyCodeWithFlags(backslash),
"`": KeyCodeWithFlags(grave),
"[": KeyCodeWithFlags(leftBracket),
"]": KeyCodeWithFlags(rightBracket),
"keypaddecimal": KeyCodeWithFlags(keypadDecimal),
"keypadmultiply": KeyCodeWithFlags(keypadMultiply),
"keypadplus": KeyCodeWithFlags(keypadPlus),
"keypadclear": KeyCodeWithFlags(keypadClear),
"keypaddivide": KeyCodeWithFlags(keypadDivide),
"keypadenter": KeyCodeWithFlags(keypadEnter),
"keypadminus": KeyCodeWithFlags(keypadMinus),
"keypadequals": KeyCodeWithFlags(keypadEquals),
"keypad0": KeyCodeWithFlags(keypad0),
"keypad1": KeyCodeWithFlags(keypad1),
"keypad2": KeyCodeWithFlags(keypad2),
"keypad3": KeyCodeWithFlags(keypad3),
"keypad4": KeyCodeWithFlags(keypad4),
"keypad5": KeyCodeWithFlags(keypad5),
"keypad6": KeyCodeWithFlags(keypad6),
"keypad7": KeyCodeWithFlags(keypad7),
"keypad8": KeyCodeWithFlags(keypad8),
"keypad9": KeyCodeWithFlags(keypad9),
]
private static let modifierKeyDictionary = [
"A": KeyWithFlags("a", [.maskShift]),
"B": KeyWithFlags("b", [.maskShift]),
"C": KeyWithFlags("c", [.maskShift]),
"D": KeyWithFlags("d", [.maskShift]),
"E": KeyWithFlags("e", [.maskShift]),
"F": KeyWithFlags("f", [.maskShift]),
"G": KeyWithFlags("g", [.maskShift]),
"H": KeyWithFlags("h", [.maskShift]),
"I": KeyWithFlags("i", [.maskShift]),
"J": KeyWithFlags("j", [.maskShift]),
"K": KeyWithFlags("k", [.maskShift]),
"L": KeyWithFlags("l", [.maskShift]),
"M": KeyWithFlags("m", [.maskShift]),
"N": KeyWithFlags("n", [.maskShift]),
"O": KeyWithFlags("o", [.maskShift]),
"P": KeyWithFlags("p", [.maskShift]),
"Q": KeyWithFlags("q", [.maskShift]),
"R": KeyWithFlags("r", [.maskShift]),
"S": KeyWithFlags("s", [.maskShift]),
"T": KeyWithFlags("t", [.maskShift]),
"U": KeyWithFlags("u", [.maskShift]),
"V": KeyWithFlags("v", [.maskShift]),
"W": KeyWithFlags("w", [.maskShift]),
"X": KeyWithFlags("x", [.maskShift]),
"Y": KeyWithFlags("y", [.maskShift]),
"Z": KeyWithFlags("z", [.maskShift]),
")": KeyWithFlags("0", [.maskShift]),
"!": KeyWithFlags("1", [.maskShift]),
"@": KeyWithFlags("2", [.maskShift]),
"#": KeyWithFlags("3", [.maskShift]),
"$": KeyWithFlags("4", [.maskShift]),
"%": KeyWithFlags("5", [.maskShift]),
"^": KeyWithFlags("6", [.maskShift]),
"&": KeyWithFlags("7", [.maskShift]),
"*": KeyWithFlags("8", [.maskShift]),
"(": KeyWithFlags("9", [.maskShift]),
"+": KeyWithFlags("=", [.maskShift]),
"_": KeyWithFlags("-", [.maskShift]),
":": KeyWithFlags(";", [.maskShift]),
"\"": KeyWithFlags("'", [.maskShift]),
"<": KeyWithFlags(",", [.maskShift]),
">": KeyWithFlags(".", [.maskShift]),
"?": KeyWithFlags("/", [.maskShift]),
"|": KeyWithFlags("\\", [.maskShift]),
"~": KeyWithFlags("`", [.maskShift]),
"{": KeyWithFlags("[", [.maskShift]),
"}": KeyWithFlags("]", [.maskShift]),
]
private static var remappingDictionary: [String: String] = [:]
static func updateMapping(_ newOldMapping: [String: String]) {
// Merge the new mapping with the existing one
remappingDictionary.merge(newOldMapping) { current, new in new }
}
private static func getRemappedKey(_ key: String) -> String {
return remappingDictionary[key] ?? key
}
static func getKeyInfo(_ name: String) -> KeyCodeWithFlags? {
let keys = keyDictionary[getRemappedKey(name)]
if keys != nil {
return keys
}
let modifierKeys = modifierKeyDictionary[name]
if modifierKeys != nil {
let key = keyDictionary[getRemappedKey(modifierKeys!.key)]
if key != nil {
return KeyCodeWithFlags(key!.keyCode, modifierKeys!.flags)
}
}
return nil
}
}
================================================
FILE: Sources/SendKeysLib/KeyMappings.swift
================================================
import ArgumentParser
struct KeyMappings {
enum Layouts: String, Codable, ExpressibleByArgument {
case qwerty
case colemak
case dvorak
}
static let Mappings: [Layouts: [String: String]] = [
.qwerty: Qwerty,
.colemak: Colemak,
.dvorak: Dvorak,
]
static let Qwerty: [String: String] = [:]
static let Colemak: [String: String] = [
"q": "q",
"w": "w",
"f": "e",
"p": "r",
"g": "t",
"j": "y",
"l": "u",
"u": "i",
"y": "o",
";": "p",
"a": "a",
"r": "s",
"s": "d",
"t": "f",
"d": "g",
"h": "h",
"n": "j",
"e": "k",
"i": "l",
"o": ";",
"z": "z",
"x": "x",
"c": "c",
"b": "b",
"k": "n",
"m": "m",
]
static let Dvorak: [String: String] = [
"[": "-",
"]": "=",
"'": "q",
",": "w",
".": "e",
"p": "r",
"y": "t",
"f": "y",
"g": "u",
"c": "i",
"r": "o",
"l": "p",
"/": "[",
"=": "]",
"a": "a",
"o": "s",
"e": "d",
"u": "f",
"i": "g",
"d": "h",
"h": "j",
"t": "k",
"n": "l",
"s": ":",
"-": "'",
";": "z",
"q": "x",
"j": "c",
"k": "v",
"x": "b",
"b": "n",
"m": "m",
"w": ",",
"v": ".",
"z": "/",
]
}
================================================
FILE: Sources/SendKeysLib/KeyPresser.swift
================================================
import Carbon
import Cocoa
import Foundation
public class KeyPresser {
private var application: NSRunningApplication?
init(app: NSRunningApplication?) {
self.application = app
}
func keyPress(key: String, modifiers: [String]) throws -> CGEvent? {
if let keyDownEvent = try! keyDown(key: key, modifiers: modifiers) {
return keyUp(key: key, modifiers: modifiers, event: keyDownEvent)
}
return nil
}
func keyDown(key: String, modifiers: [String]) throws -> CGEvent? {
let keyDownEvent = try! createKeyEvent(key: key, modifiers: modifiers, keyDown: true)
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)
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(key: String, modifiers: [String], event: CGEvent) -> CGEvent? {
let keyUpEvent = try! createKeyEvent(
key: key, modifiers: modifiers, keyDown: false, parentEventSource: CGEventSource(event: event))
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
}
private func createKeyEvent(
key: String, modifiers: [String], keyDown: Bool, parentEventSource: CGEventSource? = nil
) throws -> CGEvent? {
let info = KeyCodes.getKeyInfo(key)
let flags = try! KeyPresser.getModifierFlags(modifiers)
let mergedFlags = flags.union(CGEventFlags(info?.flags ?? []))
let eventSource = parentEventSource ?? CGEventSource(stateID: .hidSystemState)
let keyEvent = CGEvent(keyboardEventSource: eventSource, virtualKey: info?.keyCode ?? 0, keyDown: keyDown)
if info == nil {
if key.count == 1 {
let utf16Chars = Array(key.utf16)
keyEvent!.keyboardSetUnicodeString(stringLength: utf16Chars.count, unicodeString: utf16Chars)
} else {
throw RuntimeError("Unrecognized key: \(key)")
}
}
if !mergedFlags.isEmpty {
keyEvent?.flags = mergedFlags
} else {
keyEvent?.flags = []
}
return keyEvent
}
static func setKeyboardLayout(_ layout: KeyMappings.Layouts) {
KeyCodes.updateMapping(KeyMappings.Mappings[layout]!)
}
static func getModifierFlags(_ modifiers: [String]) throws -> CGEventFlags {
var flags: CGEventFlags = []
for modifier in modifiers.filter({ !$0.isEmpty }) {
let flag = try getModifierFlag(modifier)
flags.insert(flag)
}
return flags
}
static func getModifierFlag(_ modifier: String) throws -> CGEventFlags {
switch modifier {
case "⌘",
"cmd",
"command":
return CGEventFlags.maskCommand
case "^",
"ctrl",
"control":
return CGEventFlags.maskControl
case "⌥",
"alt",
"option":
return CGEventFlags.maskAlternate
case "⇧",
"shift":
return CGEventFlags.maskShift
case "fn",
"function":
return CGEventFlags.maskSecondaryFn
default:
throw RuntimeError("Unrecognized modifier: \(modifier)")
}
}
}
================================================
FILE: Sources/SendKeysLib/MouseController.swift
================================================
import Cocoa
import Foundation
class MouseController {
enum ScrollAxis {
case horizontal
case vertical
}
enum mouseEventType {
case up
case down
case move
case drag
}
let animationRefreshInterval: TimeInterval
let keyPresser: KeyPresser
var downButtons = Set<CGMouseButton>()
init(animationRefreshInterval: TimeInterval, keyPresser: KeyPresser) {
self.animationRefreshInterval = animationRefreshInterval
self.keyPresser = keyPresser
}
func move(start: CGPoint?, end: CGPoint, duration: TimeInterval, flags: CGEventFlags) {
let resolvedStart = start ?? getLocation()!
let eventSource = CGEventSource(event: nil)
let button = downButtons.first
let moveType = getEventType(.move, button)
let animator = Animator(
duration, animationRefreshInterval,
{ progress in
let location = CGFloat(progress) * (end - resolvedStart) + resolvedStart
self.setLocation(location, eventSource: eventSource, moveType: moveType, button: button, flags: flags)
})
animator.animate()
}
func move(
start: CGPoint?, path: String, offset: CGPoint, scale: CGPoint, duration: TimeInterval, flags: CGEventFlags
) {
let resolvedStart = start ?? getLocation()!
let eventSource = CGEventSource(event: nil)
let button = downButtons.first
let moveType = getEventType(.move, button)
let pathData = PathData(path, resolvedStart)
let animator = Animator(
duration, animationRefreshInterval,
{ progress in
let location = offset + (pathData.getPointAtInterval(progress) * scale)
self.setLocation(location, eventSource: eventSource, moveType: moveType, button: button, flags: flags)
})
animator.animate()
}
func click(_ location: CGPoint?, button: CGMouseButton, flags: CGEventFlags, clickCount: Int) {
let resolvedLocation = location ?? getLocation()!
let downMouseType = getEventType(.down, button)
let upMouseType = getEventType(.up, button)
let downEvent = CGEvent(
mouseEventSource: nil, mouseType: downMouseType, mouseCursorPosition: resolvedLocation, mouseButton: button)
downEvent?.setIntegerValueField(.mouseEventClickState, value: Int64(clickCount))
downEvent?.flags = flags
downEvent?.post(tap: CGEventTapLocation.cghidEventTap)
let eventSource = CGEventSource(event: downEvent)
let upEvent = CGEvent(
mouseEventSource: eventSource, mouseType: upMouseType, mouseCursorPosition: resolvedLocation,
mouseButton: button)
upEvent?.post(tap: CGEventTapLocation.cghidEventTap)
}
func down(_ location: CGPoint?, button: CGMouseButton, flags: CGEventFlags) {
let resolvedLocation = location ?? getLocation()!
let downMouseType = getEventType(.down, button)
let downEvent = CGEvent(
mouseEventSource: nil, mouseType: downMouseType, mouseCursorPosition: resolvedLocation, mouseButton: button)
downEvent?.flags = flags
downEvent?.post(tap: CGEventTapLocation.cghidEventTap)
downButtons.insert(button)
}
func up(_ location: CGPoint?, button: CGMouseButton, flags: CGEventFlags) {
let resolvedLocation = location ?? getLocation()!
let upMouseType = getEventType(.up, button)
let upEvent = CGEvent(
mouseEventSource: nil, mouseType: upMouseType, mouseCursorPosition: resolvedLocation,
mouseButton: button)
upEvent?.post(tap: CGEventTapLocation.cghidEventTap)
downButtons.remove(button)
}
func drag(start: CGPoint?, end: CGPoint, duration: TimeInterval, button: CGMouseButton, flags: CGEventFlags) {
let resolvedStart = start ?? getLocation()!
let downMouseType = getEventType(.down, button)
let upMouseType = getEventType(.up, button)
let moveType = getEventType(.drag, button)
var eventSource: CGEventSource?
let animator = Animator(
duration, animationRefreshInterval,
{ progress in
let location = CGFloat(progress) * (end - resolvedStart) + resolvedStart
self.setLocation(location, eventSource: eventSource, moveType: moveType, button: button, flags: flags)
})
if !downButtons.contains(button) {
let downEvent = CGEvent(
mouseEventSource: nil, mouseType: downMouseType, mouseCursorPosition: resolvedStart, mouseButton: button
)
downEvent?.flags = flags
downEvent?.post(tap: CGEventTapLocation.cghidEventTap)
eventSource = CGEventSource(event: downEvent)
}
animator.animate()
if !downButtons.contains(button) {
let upEvent = CGEvent(
mouseEventSource: eventSource, mouseType: upMouseType, mouseCursorPosition: end, mouseButton: button)
upEvent?.post(tap: CGEventTapLocation.cghidEventTap)
}
}
func scroll(_ delta: CGPoint, _ duration: TimeInterval, flags: CGEventFlags) {
var scrolledX: Int = 0
var scrolledY: Int = 0
let eventSource = CGEventSource(event: nil)
let animator = Animator(
duration, animationRefreshInterval,
{ progress in
if delta.x != 0 {
let amount = Int((Double(delta.x) * progress) - Double(scrolledX))
scrolledX += amount
self.scrollBy(amount, .horizontal, eventSource: eventSource, flags: flags)
}
if delta.y != 0 {
let amount = Int((Double(delta.y) * progress) - Double(scrolledY))
scrolledY += amount
self.scrollBy(amount, .vertical, eventSource: eventSource, flags: flags)
}
})
animator.animate()
}
func circle(_ center: CGPoint, _ radius: CGPoint, _ fromAngle: Double, _ toAngle: Double, _ duration: TimeInterval)
{
let eventSource = CGEventSource(event: nil)
let ANGLE_OFFSET: Double = -90
let button = downButtons.first
let moveType = getEventType(.move, button)
let animator = Animator(
duration, animationRefreshInterval,
{ progress in
let angle = (toAngle - fromAngle) * progress + fromAngle + ANGLE_OFFSET
let location = CGPoint(
x: cos(angle * Double.pi / 180) * Double(radius.x) + Double(center.x),
y: sin(angle * Double.pi / 180) * Double(radius.y) + Double(center.y)
)
self.setLocation(location, eventSource: eventSource, moveType: moveType, button: .left, flags: [])
})
animator.animate()
}
func scrollBy(_ amount: Int, _ axis: ScrollAxis, eventSource: CGEventSource?, flags: CGEventFlags) {
if #available(OSX 10.13, *) {
let event = CGEvent(
scrollWheelEvent2Source: eventSource, units: .pixel, wheelCount: 1, wheel1: 0, wheel2: 0, wheel3: 0)
let field =
axis == .vertical
? CGEventField.scrollWheelEventPointDeltaAxis1 : CGEventField.scrollWheelEventPointDeltaAxis2
event?.setIntegerValueField(field, value: Int64(amount * -1))
event?.flags = flags
event?.post(tap: CGEventTapLocation.cghidEventTap)
} else {
fatalError("Scrolling is only available on 10.13 or later\n")
}
}
func getLocation() -> CGPoint? {
let event = CGEvent(source: nil)
return event?.location
}
private func setLocation(
_ location: CGPoint, eventSource: CGEventSource?, moveType: CGEventType = CGEventType.mouseMoved,
button: CGMouseButton? = nil, flags: CGEventFlags = []
) {
let moveEvent = CGEvent(
mouseEventSource: eventSource, mouseType: moveType, mouseCursorPosition: location,
mouseButton: button ?? CGMouseButton.left)
moveEvent?.flags = flags
moveEvent?.post(tap: CGEventTapLocation.cghidEventTap)
}
private func getEventType(_ mouseType: mouseEventType, _ button: CGMouseButton? = nil) -> CGEventType {
switch mouseType {
case .up:
if button == CGMouseButton.left {
return CGEventType.leftMouseUp
} else if button == CGMouseButton.right {
return CGEventType.rightMouseUp
} else {
return CGEventType.otherMouseUp
}
case .down:
if button == CGMouseButton.left {
return CGEventType.leftMouseDown
} else if button == CGMouseButton.right {
return CGEventType.rightMouseDown
} else {
return CGEventType.otherMouseDown
}
case .move:
if button == nil {
return CGEventType.mouseMoved
} else if button == CGMouseButton.left {
return CGEventType.leftMouseDragged
} else if button == CGMouseButton.right {
return CGEventType.rightMouseDragged
} else {
return CGEventType.otherMouseDragged
}
case .drag:
if button == CGMouseButton.left {
return CGEventType.leftMouseDragged
} else if button == CGMouseButton.right {
return CGEventType.rightMouseDragged
} else {
return CGEventType.otherMouseDragged
}
}
}
private func resolveLocation(_ location: CGPoint) -> CGPoint {
let currentLocation = getLocation()
return CGPoint(
x: location.x < 0 ? (currentLocation?.x ?? 0) : location.x,
y: location.y < 0 ? (currentLocation?.y ?? 0) : location.y
)
}
}
================================================
FILE: Sources/SendKeysLib/MouseEventProcessor.swift
================================================
import Cocoa
import Foundation
enum MouseEventType {
case click
case drag
}
enum MouseButton: String, CustomStringConvertible {
case left
case right
case center
case other
var description: String {
return self.rawValue
}
}
struct RawMouseEvent {
let eventType: CGEventType
let button: MouseButton
let point: CGPoint
init(eventType: CGEventType, button: MouseButton, point: CGPoint) {
self.eventType = eventType
self.button = button
self.point = point
}
}
class MouseEvent: CustomStringConvertible {
let eventType: MouseEventType
let button: MouseButton
let startPoint: CGPoint
let endPoint: CGPoint
var duration: TimeInterval
static let numberFormatter = createNumberFormatter()
init(eventType: MouseEventType, button: MouseButton, startPoint: CGPoint, endPoint: CGPoint, duration: TimeInterval)
{
self.eventType = eventType
self.button = button
self.startPoint = startPoint
self.endPoint = endPoint
self.duration = duration
}
var description: String {
switch eventType {
case .click:
var moveParts: [String] = []
var clickParts: [String] = []
moveParts.append(
"\(Self.numberFormatter.string(for: endPoint.x)!),\(Self.numberFormatter.string(for: endPoint.y)!)")
if duration > 0 {
moveParts.append(Self.numberFormatter.string(for: duration)!)
}
clickParts.append(button.description)
return "<m:\(moveParts.joined(separator: ":"))><m:\(clickParts.joined(separator: ":"))><\\>"
case .drag:
var parts: [String] = []
parts.append(
"\(Self.numberFormatter.string(for: startPoint.x)!),\(Self.numberFormatter.string(for: startPoint.y)!),\(Self.numberFormatter.string(for: endPoint.x)!),\(Self.numberFormatter.string(for: endPoint.y)!)"
)
if duration > 0 {
parts.append(Self.numberFormatter.string(for: duration)!)
}
parts.append(button.description)
return "<d:\(parts.joined(separator: ":"))><\\>"
}
}
static func createNumberFormatter() -> NumberFormatter {
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = 2
return numberFormatter
}
}
class MouseEventProcessor {
var events: [RawMouseEvent] = []
var lastDate: Date = Date()
func start() {
lastDate = Date()
}
func consumeEvent(type: CGEventType, event: CGEvent) -> MouseEvent? {
let button = getMouseButton(type: type, event: event)
let rawEvent = RawMouseEvent(eventType: type, button: button, point: event.location)
var mouseEvent: MouseEvent? = nil
switch type {
case .leftMouseUp, .rightMouseUp, .otherMouseUp:
switch events.last?.eventType {
case .leftMouseDown, .rightMouseDown, .otherMouseDown:
mouseEvent = MouseEvent(
eventType: .click, button: button, startPoint: events.first!.point, endPoint: event.location,
duration: -lastDate.timeIntervalSinceNow)
case .leftMouseDragged, .rightMouseDragged, .otherMouseDragged:
mouseEvent = MouseEvent(
eventType: .drag, button: button, startPoint: events.first!.point, endPoint: event.location,
duration: -lastDate.timeIntervalSinceNow)
default:
events.append(rawEvent)
}
lastDate = Date()
events = []
default:
events.append(rawEvent)
}
return mouseEvent
}
private func getMouseButton(type: CGEventType, event: CGEvent) -> MouseButton {
var button: MouseButton = .other
switch type {
case .leftMouseDown, .leftMouseUp, .leftMouseDragged:
button = .left
case .rightMouseDown, .rightMouseUp, .rightMouseDragged:
button = .right
case .otherMouseDown, .otherMouseUp, .otherMouseDragged:
let buttonNumber = event.getIntegerValueField(.mouseEventButtonNumber)
switch UInt32(buttonNumber) {
case CGMouseButton.left.rawValue:
button = .left
case CGMouseButton.right.rawValue:
button = .right
case CGMouseButton.center.rawValue:
button = .center
default:
button = .other
}
default:
button = .other
}
return button
}
}
================================================
FILE: Sources/SendKeysLib/MousePosition.swift
================================================
import ArgumentParser
import Cocoa
import Foundation
enum OutputMode: String, Codable, ExpressibleByArgument {
case coordinates
case commands
}
class MousePosition: ParsableCommand {
public static let configuration = CommandConfiguration(
abstract: "Prints the current mouse position."
)
@Flag(
name: .shortAndLong, inversion: FlagInversion.prefixedNo,
help: "Watch and display the mouse positions as the mouse is clicked.")
var watch: Bool?
@Option(
name: NameSpecification([.customShort("o"), .customLong("output", withSingleDash: false)]),
help: "Displays results as either a series of coordinates or commands.")
var mode: OutputMode?
@Option(
name: .shortAndLong,
help:
"Duration (in seconds) to output for mouse events. A negative value uses elapsed time between mouse events."
)
var duration: Double?
var config: MousePositionConfig
static let eventProcessor = MouseEventProcessor()
private static func createNumberFormatter() -> NumberFormatter {
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = 2
return numberFormatter
}
required init() {
self.config = MousePositionConfig(watch: false, output: .commands, duration: -1)
}
func run() {
self.config = self.config
.merge(with: ConfigLoader.loadConfig().mousePosition)
.merge(with: MousePositionConfig(watch: watch, output: mode, duration: duration))
if self.config.watch! {
watchMouseInput()
} else {
printMousePosition(nil)
}
}
func printMousePosition(_ position: CGPoint?) {
let numberFormatter = Self.createNumberFormatter()
let location =
position ?? MouseController(animationRefreshInterval: 0.01, keyPresser: KeyPresser(app: nil)).getLocation()!
printAndFlush("\(numberFormatter.string(for: location.x)!),\(numberFormatter.string(for: location.y)!)")
}
func listenForInput() {
fputs(
"Waiting for user input... Escape or ctrl + d to stop, or any other key to capture mouse position.\n",
stderr)
waitForCharInput { _ in
printMousePosition(nil)
}
}
func waitForCharInput(callback: (_ char: UInt8) -> Void) {
let stdIn = FileHandle.standardInput
let originalTerm = enableRawMode(fileHandle: stdIn)
var char: UInt8 = 0
while read(stdIn.fileDescriptor, &char, 1) == 1 {
if char == 4 /* EOF (Ctrl+D) */ || char == 27 /* escape */ {
break
}
callback(char)
}
restoreRawMode(fileHandle: stdIn, originalTerm: originalTerm)
}
func watchMouseInput() {
fputs("Waiting for mouse input... ctrl + c to stop.\n", stderr)
MousePosition.eventProcessor.start()
var eventMask =
(1 << CGEventType.leftMouseDown.rawValue)
| (1 << CGEventType.leftMouseUp.rawValue)
| (1 << CGEventType.leftMouseDragged.rawValue)
eventMask =
eventMask | (1 << CGEventType.rightMouseDown.rawValue)
| (1 << CGEventType.rightMouseUp.rawValue)
| (1 << CGEventType.rightMouseDragged.rawValue)
eventMask =
eventMask | (1 << CGEventType.otherMouseDown.rawValue)
| (1 << CGEventType.otherMouseUp.rawValue)
| (1 << CGEventType.otherMouseDragged.rawValue)
let info = UnsafeMutableRawPointer(mutating: bridge(obj: self))
guard
let eventTap = CGEvent.tapCreate(
tap: .cghidEventTap, place: .tailAppendEventTap, options: .defaultTap,
eventsOfInterest: CGEventMask(eventMask),
callback: {
(proxy: CGEventTapProxy, eventType: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?)
-> Unmanaged<CGEvent>? in
let command: MousePosition = bridge(ptr: UnsafeRawPointer(refcon)!)
if let mouseEvent = MousePosition.eventProcessor.consumeEvent(type: eventType, event: event) {
// if duration is set, override all mouse event durations
if command.config.duration! >= 0 {
mouseEvent.duration = command.config.duration!
}
switch command.config.output! {
case .coordinates:
if mouseEvent.eventType == .click {
command.printMousePosition(mouseEvent.endPoint)
}
case .commands:
command.printAndFlush(mouseEvent.description)
}
}
return Unmanaged.passRetained(event)
}, userInfo: info)
else {
MousePosition.exit(withError: RuntimeError("Failed to create event tap."))
}
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap, enable: true)
CFRunLoopRun()
}
func printAndFlush(_ message: String) {
print(message)
fflush(stdout)
}
func eventCallback(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?)
-> Unmanaged<CGEvent>?
{
switch self.config.output! {
case .coordinates:
printMousePosition(nil)
case .commands:
printMousePosition(nil)
}
printAndFlush("Event \(type) \(type.rawValue)")
return Unmanaged.passRetained(event)
}
// see https://stackoverflow.com/a/24335355/669586
func initStruct<S>() -> S {
let struct_pointer = UnsafeMutablePointer<S>.allocate(capacity: 1)
let struct_memory = struct_pointer.pointee
struct_pointer.deallocate()
return struct_memory
}
func enableRawMode(fileHandle: FileHandle) -> termios {
var raw: termios = initStruct()
tcgetattr(fileHandle.fileDescriptor, &raw)
let original = raw
raw.c_lflag &= ~(UInt(ECHO | ICANON))
tcsetattr(fileHandle.fileDescriptor, TCSAFLUSH, &raw)
return original
}
func restoreRawMode(fileHandle: FileHandle, originalTerm: termios) {
var term = originalTerm
tcsetattr(fileHandle.fileDescriptor, TCSAFLUSH, &term)
}
}
================================================
FILE: Sources/SendKeysLib/Path/Extensions.swift
================================================
import AppKit
import Foundation
extension CGPoint {
// Vector math
public static func + (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x + right.x, y: left.y + right.y)
}
public static func - (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x - right.x, y: left.y - right.y)
}
public static func * (left: CGFloat, right: CGPoint) -> CGPoint {
return CGPoint(x: left * right.x, y: left * right.y)
}
public static func * (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x * right.x, y: left.y * right.y)
}
public func distance(from: CGPoint) -> Double {
let delta = self - from
let sqr = delta * delta
return sqrt(Double(sqr.x + sqr.y))
}
}
================================================
FILE: Sources/SendKeysLib/Path/PathCommands.swift
================================================
import Foundation
public class PointValue: CustomStringConvertible {
var point: CGPoint
init(_ point: CGPoint) {
self.point = point
}
public var description: String {
return "\(point.x) \(point.y)"
}
}
public class ControlPointValue: PointValue {
var controlPoint1: CGPoint?
init(_ controlPoint: CGPoint?, _ point: CGPoint) {
super.init(point)
self.controlPoint1 = controlPoint
}
override public var description: String {
return "\(controlPoint1 != nil ? "\(controlPoint1!.x) \(controlPoint1!.y)" : "") \(point.x) \(point.y)"
.trimmingCharacters(in: [" "])
}
}
public class ControlPointsValue: ControlPointValue {
var controlPoint2: CGPoint
init(_ controlPoint1: CGPoint?, _ controlPoint2: CGPoint, _ point: CGPoint) {
self.controlPoint2 = controlPoint2
super.init(controlPoint1, point)
}
override public var description: String {
return
"\(controlPoint1 != nil ? "\(controlPoint1!.x) \(controlPoint1!.y)" : "") \(controlPoint2.x) \(controlPoint2.y) \(point.x) \(point.y)"
.trimmingCharacters(in: [" "])
}
}
public class ArchCommandValue: PointValue {
var radius: CGPoint
var angle: Double
var largeArc: Bool
var sweep: Bool
init(_ radius: CGPoint, _ angle: Double, _ largeArc: Bool, _ sweep: Bool, _ point: CGPoint) {
self.radius = radius
self.angle = angle
self.largeArc = largeArc
self.sweep = sweep
super.init(point)
}
override public var description: String {
return "\(radius.x) \(radius.y) \(angle) \(largeArc ? "1" : "0") \(sweep ? "1" : "0") \(point.x) \(point.y)"
}
}
public class PathCommandBase: Equatable, CustomStringConvertible {
var type: Character
init(_ type: Character) {
self.type = type
}
func decompose(from: CGPoint) -> [PathCommandBase] {
return [self]
}
public func equals(_ comparison: PathCommandBase) -> Bool {
return self.description == comparison.description
}
public static func == (lhs: PathCommandBase, rhs: PathCommandBase) -> Bool {
return lhs.equals(rhs) && rhs.equals(lhs)
}
public var description: String {
return "\(type)"
}
public var currentPoint: CGPoint? {
return nil
}
public var isRelative: Bool {
return type.isLowercase
}
public func makeAbsolute(_ point: CGPoint) {
if isRelative {
type = Character(type.uppercased())
}
}
public func distanceFrom(point: CGPoint) -> Double {
return 0
}
public func pointAlongPath(interval: Double, from: CGPoint) -> CGPoint {
if currentPoint == nil {
return from
}
return from + CGFloat(interval) * (currentPoint! - from)
}
}
public class PathCommand<T>: PathCommandBase {
var value: T
init(_ type: Character, _ value: T) {
self.value = value
super.init(type)
}
override public var description: String {
return "\(type) \(value)"
}
}
public class NumericPathCommand: PathCommand<Double> {
override func decompose(from: CGPoint) -> [PathCommandBase] {
if type == "H" {
return [PointPathCommand("L", PointValue(CGPoint(x: CGFloat(value), y: from.y)))]
}
if type == "V" {
return [PointPathCommand("L", PointValue(CGPoint(x: from.x, y: CGFloat(value))))]
}
return []
}
override public func makeAbsolute(_ point: CGPoint) {
if isRelative {
super.makeAbsolute(point)
if type == "H" {
value = Double(point.x) + value
}
if type == "V" {
value = Double(point.y) + value
}
}
}
override public func distanceFrom(point: CGPoint) -> Double {
fatalError("Not implemented")
}
}
public class PointPathCommand: PathCommand<PointValue> {
override public func makeAbsolute(_ point: CGPoint) {
if isRelative {
super.makeAbsolute(point)
value.point = point + value.point
}
}
override public func distanceFrom(point: CGPoint) -> Double {
if type == "L" {
return value.point.distance(from: point)
} else if type == "M" {
return 0
}
fatalError("Not implemented")
}
override public var currentPoint: CGPoint? {
return value.point
}
}
public class ArcPathCommand: PathCommand<ArchCommandValue> {
override func decompose(from: CGPoint) -> [PathCommandBase] {
if from == value.point {
return []
}
if value.radius.x == 0 || value.radius.y == 0 {
return [PointPathCommand("L", PointValue(value.point))]
}
let midpointDistance = CGFloat(0.5) * (from - value.point)
var matrix = AffineTransform()
matrix.rotate(byDegrees: CGFloat(value.angle))
let tranformedMidpoint = matrix.transform(midpointDistance)
var rx = value.radius.x
var ry = value.radius.y
let squareRx = rx * rx
let squareRy = ry * ry
let squareX = tranformedMidpoint.x * tranformedMidpoint.x
let squareY = tranformedMidpoint.y * tranformedMidpoint.y
let radiiScale = squareX / squareRx + squareY / squareRy
if radiiScale > 1 {
rx *= sqrt(radiiScale)
ry *= sqrt(radiiScale)
}
matrix = AffineTransform()
matrix.scale(x: 1 / rx, y: 1 / ry)
matrix.rotate(byDegrees: CGFloat(-value.angle))
var point1 = matrix.transform(from)
var point2 = matrix.transform(value.point)
var delta = point2 - point1
let d = delta.x * delta.x + delta.y * delta.y
var scaleFactor = sqrt(max(1 / d - 0.25, 0))
if value.sweep == value.largeArc {
scaleFactor = -scaleFactor
}
delta = scaleFactor * delta
let center = 0.5 * (point1 + point2) + CGPoint(x: -delta.y, y: delta.x)
let theta1 = Double(atan2(point1.y - center.y, point1.x - center.x))
let theta2 = Double(atan2(point2.y - center.y, point2.x - center.x))
var thetaArc = Double(theta2 - theta1)
if thetaArc < 0 && value.sweep {
thetaArc += Double.pi * 2.0
} else if thetaArc > 0 && !value.sweep {
thetaArc -= Double.pi * 2.0
}
matrix = AffineTransform()
matrix.rotate(byDegrees: CGFloat(value.angle))
matrix.scale(x: rx, y: ry)
let segments = Int(ceil(fabs(thetaArc / Double.pi / 2)))
var commands: [PathCommandBase] = []
for i in 0...segments - 1 {
let startTheta = theta1 + (Double(i) * thetaArc) / Double(segments)
let endTheta = theta1 + ((Double(i) + 1.0) * thetaArc) / Double(segments)
let t = CGFloat((8.0 / 6.0) * tan(0.25 * (endTheta - startTheta)))
// if (!std::isfinite(t))
// return false;
let sinStartTheta = CGFloat(sin(startTheta))
let cosStartTheta = CGFloat(cos(startTheta))
let sinEndTheta = CGFloat(sin(endTheta))
let cosEndTheta = CGFloat(cos(endTheta))
point1 = CGPoint(
x: cosStartTheta - t * sinStartTheta + center.x,
y: sinStartTheta + t * cosStartTheta + center.y
)
var targetPoint = CGPoint(
x: cosEndTheta + center.x,
y: sinEndTheta + center.y
)
point2 = CGPoint(x: targetPoint.x + t * sinEndTheta, y: targetPoint.y - t * cosEndTheta)
point1 = matrix.transform(point1)
point2 = matrix.transform(point2)
targetPoint = matrix.transform(targetPoint)
commands.append(CubicBezierPathCommand("C", ControlPointsValue(point1, point2, targetPoint)))
}
return commands
}
override public var currentPoint: CGPoint? {
return value.point
}
override public func makeAbsolute(_ point: CGPoint) {
if isRelative {
super.makeAbsolute(point)
value.point = point + value.point
}
}
override public func distanceFrom(point: CGPoint) -> Double {
fatalError("Not implemented")
}
}
public class QuadraticBezierPathCommand: PathCommand<ControlPointValue> {
override func decompose(from: CGPoint) -> [PathCommandBase] {
let controlPoint = value.controlPoint1 ?? from
return [
CubicBezierPathCommand(
"C",
ControlPointsValue(
from + (2.0 / 3.0) * (controlPoint - from),
value.point + (2.0 / 3.0) * (controlPoint - value.point),
value.point
))
]
}
override public var currentPoint: CGPoint? {
return value.point
}
override public func makeAbsolute(_ point: CGPoint) {
if isRelative {
super.makeAbsolute(point)
if value.controlPoint1 != nil {
value.controlPoint1 = point + value.controlPoint1!
}
value.point = point + value.point
}
}
override public func distanceFrom(point: CGPoint) -> Double {
fatalError("Not implemented")
}
}
public class CubicBezierPathCommand: PathCommand<ControlPointsValue> {
override public var currentPoint: CGPoint? {
return value.point
}
override public func makeAbsolute(_ point: CGPoint) {
if isRelative {
super.makeAbsolute(point)
if value.controlPoint1 != nil {
value.controlPoint1 = point + value.controlPoint1!
}
value.controlPoint2 = point + value.controlPoint2
value.point = point + value.point
}
}
private func lengthValueAt(t: CGFloat, p0: CGFloat, c1: CGFloat, c2: CGFloat, p1: CGFloat) -> CGFloat {
var value: CGFloat = 0.0
// (1-t)^3 * p0 + 3 * (1-t)^2 * t * c1 + 3 * (1-t) * t^2 * c2 + t^3 * p1
value += pow(1 - t, 3) * p0
value += 3 * pow(1 - t, 2) * t * c1
value += 3 * (1 - t) * pow(t, 2) * c2
value += pow(t, 3) * p1
return value
}
private func poinAt(t: CGFloat, start: CGPoint) -> CGPoint {
let x = lengthValueAt(
t: t, p0: start.x, c1: value.controlPoint1?.x ?? start.x, c2: value.controlPoint2.x, p1: value.point.x)
let y = lengthValueAt(
t: t, p0: start.y, c1: value.controlPoint1?.y ?? start.y, c2: value.controlPoint2.y, p1: value.point.y)
return CGPoint(x: x, y: y)
}
override public func distanceFrom(point: CGPoint) -> Double {
var total = 0.0
var previousPoint = point
let segments = 1_000
for i in 1...segments {
let intervalPoint = poinAt(t: CGFloat(i) / CGFloat(segments), start: point)
total += intervalPoint.distance(from: previousPoint) // Command.distanceBetweenPoints(previousPoint, intervalPoint);
previousPoint = intervalPoint
}
return Double(total)
}
override public func pointAlongPath(interval: Double, from: CGPoint) -> CGPoint {
return poinAt(t: CGFloat(interval), start: from)
}
}
================================================
FILE: Sources/SendKeysLib/Path/PathData.swift
================================================
import Foundation
struct SegmentInfo {
var startLength: Double
var length: Double
var startPoint: CGPoint
var command: PathCommandBase
init(_ startLength: Double, _ length: Double, _ startPoint: CGPoint, _ command: PathCommandBase) {
self.startLength = startLength
self.length = length
self.startPoint = startPoint
self.command = command
}
}
public class PathData {
public let commands: [PathCommandBase]
private let segments: [SegmentInfo]
convenience init(_ data: String) {
self.init(data, CGPoint.zero)
}
init(_ data: String, _ startPoint: CGPoint) {
commands = PathData.normalize(PathParser(data).parse(), startPoint)
segments = PathData.getSegments(commands, startPoint)
}
private static func normalize(_ commands: [PathCommandBase], _ startPoint: CGPoint) -> [PathCommandBase] {
var currentPoint = startPoint
var pathStart = currentPoint
var previousCubic: CubicBezierPathCommand?
var previousQuadratic: QuadraticBezierPathCommand?
return commands.flatMap { command -> [PathCommandBase] in
command.makeAbsolute(currentPoint)
if command.type == "Z" {
return [PointPathCommand("L", PointValue(pathStart))]
}
if command.type == "M", let pointCommand = command as? PointPathCommand {
pathStart = pointCommand.value.point
}
if let cubicCommand = command as? CubicBezierPathCommand {
if command.type == "S" {
if previousCubic != nil {
// reflect
cubicCommand.value.controlPoint1 = 2 * currentPoint - previousCubic!.value.controlPoint2
} else {
cubicCommand.value.controlPoint1 = currentPoint
}
}
previousCubic = cubicCommand
}
if let quadtraticCommand = command as? QuadraticBezierPathCommand {
if command.type == "T" {
if previousQuadratic != nil {
// reflect
quadtraticCommand.value.controlPoint1 =
2 * currentPoint - (previousQuadratic!.value.controlPoint1 ?? CGPoint.zero)
} else {
quadtraticCommand.value.controlPoint1 = currentPoint
}
}
previousQuadratic = quadtraticCommand
}
let newCommands: [PathCommandBase] = command.decompose(from: currentPoint)
if command.currentPoint != nil {
currentPoint = command.currentPoint!
} else if newCommands.last?.currentPoint != nil {
currentPoint = (newCommands.last?.currentPoint)!
}
return newCommands
}
}
static func getSegments(_ commands: [PathCommandBase], _ startPoint: CGPoint) -> [SegmentInfo] {
var total = 0.0
var previousPoint = startPoint
var segments: [SegmentInfo] = []
for command in commands {
let length = command.distanceFrom(point: previousPoint)
if length >= 0 {
segments.append(SegmentInfo(total, length, previousPoint, command))
}
if command.currentPoint != nil {
previousPoint = command.currentPoint!
}
total += length
}
return segments
}
func getTotalDistance() -> Double {
let last = segments.last
if last == nil {
return 0.0
}
return last!.startLength + last!.length
}
func getPointAtInterval(_ interval: Double) -> CGPoint {
let targetLength = getTotalDistance() * max(min(interval, 1), 0)
let index = segments.firstIndex { segment in
return segment.startLength > targetLength
}
var segment: SegmentInfo?
if index ?? 0 <= 0 {
segment = segments.last
} else {
segment = segments[index! - 1]
}
if segment == nil {
return CGPoint.zero
}
return segment!.command.pointAlongPath(
interval: (targetLength - segment!.startLength) / segment!.length, from: segment!.startPoint)
}
}
================================================
FILE: Sources/SendKeysLib/Path/PathParser.swift
================================================
import Foundation
public class PathParser {
private var index: Int
private let data: [Character]
private var previousType: Character?
init(_ data: String) {
self.index = 0
self.data = Array(data)
}
public func parse() -> [PathCommandBase] {
readIgnored()
var commands: [PathCommandBase] = []
while case let current = readCommand(false), current != nil {
previousType = current!.type
commands.append(current!)
}
return commands
}
private func readCommand(_ usePrevious: Bool) -> PathCommandBase? {
let type = usePrevious ? previousType : index < data.count ? data[index] : nil
var command: PathCommandBase
if type == nil {
return nil
}
if !usePrevious {
index += 1
}
switch type {
case "a", "A":
let radius = readPoint()
let rotation = readDouble()
let largeArc = readInt() != 0
let sweep = readInt() != 0
let point = readPoint()
command = ArcPathCommand(type!, ArchCommandValue(radius, rotation, largeArc, sweep, point))
case "c", "C":
let point1 = readPoint()
let point2 = readPoint()
let point = readPoint()
command = CubicBezierPathCommand(type!, ControlPointsValue(point1, point2, point))
case "h", "H", "v", "V":
command = NumericPathCommand(type!, readDouble())
case "l", "L", "m", "M":
command = PointPathCommand(type!, PointValue(readPoint()))
case "q", "Q":
let point1 = readPoint()
let point = readPoint()
command = QuadraticBezierPathCommand(type!, ControlPointValue(point1, point))
case "s", "S":
let point2 = readPoint()
let point = readPoint()
command = CubicBezierPathCommand(type!, ControlPointsValue(nil, point2, point))
case "t", "T":
let point = readPoint()
command = QuadraticBezierPathCommand(type!, ControlPointValue(nil, point))
case "z", "Z":
command = PathCommandBase(type!)
default:
index -= 1
if previousType != nil {
return readCommand(true)
} else {
return nil
}
}
readIgnored()
return command
}
private func readIgnored() {
var current: Character
while index < data.count {
current = data[index]
if " \r\n\t,".contains(current) {
index += 1
} else {
break
}
}
}
private func readDouble() -> Double {
var current: Character
readIgnored()
let start = index
while index < data.count {
current = data[index]
if "+-0123456789eE.".contains(current) {
index += 1
} else {
break
}
}
let end = index
readIgnored()
if start == index {
fatalError("Unable to read Double")
}
return Double(String(data[start..<end]))!
}
private func readInt() -> Int {
var current: Character
readIgnored()
let start = index
while index < data.count {
current = data[index]
if "+-0123456789eE".contains(current) {
index += 1
} else {
break
}
}
let end = index
readIgnored()
if start == index {
fatalError("Unable to read Int")
}
return Int(String(data[start..<end]))!
}
private func readPoint() -> CGPoint {
return CGPoint(x: readDouble(), y: readDouble())
}
}
================================================
FILE: Sources/SendKeysLib/RuntimeError.swift
================================================
struct RuntimeError: Error {
let message: String
init(_ message: String) {
self.message = message
}
public var localizedDescription: String {
return message
}
}
================================================
FILE: Sources/SendKeysLib/SendKeysCli.swift
================================================
import ArgumentParser
import Foundation
@available(OSX 10.11, *)
public struct SendKeysCli: ParsableCommand {
public static let configuration = CommandConfiguration(
commandName: "sendkeys",
abstract:
"Command line tool for automating keystrokes and mouse events. More information: https://github.com/socsieng/sendkeys/blob/main/README.md",
version: "0.0.0", /* auto-updated */
subcommands: [Sender.self, AppLister.self, MousePosition.self, Transformer.self],
defaultSubcommand: Sender.self
)
public init() {}
}
================================================
FILE: Sources/SendKeysLib/Sender.swift
================================================
import ArgumentParser
import Cocoa
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: NameSpecification([.short, .customLong("pid")]),
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?
@Flag(
name: .long, inversion: FlagInversion.prefixedNo, help: "Only send keystrokes to the targeted app or process.")
var targeted: Bool?
@Option(name: .shortAndLong, help: "Default delay between keystrokes in seconds.")
var delay: Double?
@Option(name: .shortAndLong, help: "Initial delay before sending commands in seconds.")
var initialDelay: Double?
@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?
@Option(help: "Number of seconds between animation updates.")
var animationInterval: Double?
@Option(name: .shortAndLong, help: "Character sequence to use to terminate execution (e.g. f12:command).")
var terminateCommand: String?
@Option(
name: NameSpecification([.customLong("config")]),
help: "Configuration file to load settings from (yaml format).")
var configurationFile: String?
@Option(name: .long, help: "Keyboard layout to use for sending keystrokes.")
var keyboardLayout: KeyMappings.Layouts?
var config: SendConfig
public init() {
self.config = SendConfig(
activate: true, animationInterval: 0.01, delay: 0.1, initialDelay: 1,
targeted: false, terminateCommand: nil)
}
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 activator = AppActivator(appName: applicationName, processId: processId)
let app: NSRunningApplication? = try activator.find()
let keyPresser: KeyPresser
// load from home directory by default
self.config = self.config.merge(with: ConfigLoader.loadConfig().send)
if !(configurationFile ?? "").isEmpty {
self.config = self.config.merge(with: ConfigLoader.loadConfig(configurationFile!).send)
}
self.config = self.config.merge(
with: SendConfig(
activate: activate, animationInterval: animationInterval, delay: delay, initialDelay: initialDelay,
keyboardLayout: keyboardLayout, targeted: targeted, terminateCommand: terminateCommand))
let activate = activate ?? self.config.activate!
let targeted = targeted ?? self.config.targeted!
let delay = delay ?? self.config.delay!
let initialDelay = initialDelay ?? self.config.initialDelay!
let animationInterval = animationInterval ?? self.config.animationInterval!
let terminateCommand = terminateCommand ?? self.config.terminateCommand
let keyboardLayout = keyboardLayout ?? self.config.keyboardLayout
if keyboardLayout != nil {
KeyPresser.setKeyboardLayout(keyboardLayout!)
}
if self.config.remap != nil {
KeyCodes.updateMapping(self.config.remap!)
}
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?
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
}
var listener: TerminationListener?
if terminateCommand != nil && !terminateCommand!.isEmpty {
listener = TerminationListener(sequence: terminateCommand!) {
Sender.exit()
}
listener!.listen()
}
if activate {
try activator.activate()
}
if initialDelay > 0 {
Sleeper.sleep(seconds: initialDelay)
}
if !(commandString ?? "").isEmpty {
commandProcessor.process(commandString!)
Sleeper.sleep(seconds: 0.01)
} 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))
}
if listener != nil {
listener!.stop()
}
}
}
================================================
FILE: Sources/SendKeysLib/Sleeper.swift
================================================
import Foundation
struct Sleeper {
static func sleep(seconds: Double) {
usleep(useconds_t(seconds * 1_000_000))
}
}
================================================
FILE: Sources/SendKeysLib/TerminationListener.swift
================================================
import Cocoa
import Foundation
class TerminationListener {
private var keycode: UInt16
private var modifiers: [CGEventFlags]
private var callback: () -> Void
private let flags: [CGEventFlags] = [.maskCommand, .maskControl, .maskShift, .maskAlternate]
private var runLoopSource: CFRunLoopSource?
private var runLoop: CFRunLoop?
private let expression = try! NSRegularExpression(pattern: "^(.|[\\w]+)(:([,\\w⌘^⌥⇧]+))?$")
init(sequence: String, callback: @escaping () -> Void) {
guard let groups = getRegexGroups(expression, sequence) else {
fatalError("Invalid sequence: \(sequence)")
}
let modifiers = groups[3]?.split(separator: ",") ?? []
self.keycode = KeyCodes.getKeyInfo(groups[1]!)!.keyCode
do {
self.modifiers = try modifiers.map {
(modifier) -> CGEventFlags in
try KeyPresser.getModifierFlag(String(modifier))
}
} catch {
fatalError("Failed to get modifier flags: \(error)")
}
self.callback = callback
}
func listen() {
DispatchQueue.global(qos: .background).async {
self.listenSync()
}
}
private func listenSync() {
let eventMask = 1 << CGEventType.keyDown.rawValue
self.runLoop = CFRunLoopGetCurrent()
let info = UnsafeMutableRawPointer(mutating: bridge(obj: self))
guard
let eventTap = CGEvent.tapCreate(
tap: .cghidEventTap, place: .tailAppendEventTap, options: .defaultTap,
eventsOfInterest: CGEventMask(eventMask),
callback: {
(proxy: CGEventTapProxy, eventType: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?)
-> Unmanaged<CGEvent>? in
let listener: TerminationListener = bridge(ptr: UnsafeRawPointer(refcon)!)
let keycode = event.getIntegerValueField(.keyboardEventKeycode)
if keycode == listener.keycode {
var flagsMatch = true
for flag in listener.flags {
if event.flags.contains(flag) != listener.modifiers.contains(flag) {
flagsMatch = false
break
}
}
if flagsMatch {
listener.callback()
}
}
return Unmanaged.passRetained(event)
}, userInfo: info)
else {
fatalError("Failed to create event tap.")
}
self.runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(self.runLoop, runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap, enable: true)
CFRunLoopRun()
}
func stop() {
guard let runLoopSource = self.runLoopSource else {
return
}
guard let runLoop = self.runLoop else {
return
}
CFRunLoopRemoveSource(runLoop, runLoopSource, .commonModes)
CFRunLoopStop(runLoop)
}
}
================================================
FILE: Sources/SendKeysLib/Transformer.swift
================================================
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."
)
@Flag(
name: .shortAndLong, inversion: FlagInversion.prefixedNo,
help: "Determines if the application automatically inserts indentation.")
var indent: Bool?
@Option(
name: .shortAndLong,
help:
"Specifies which brackets are automatically closed by the application and don't need to be explicitly closed."
)
var autoClose: String?
@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?
var config: TransformerConfig
public init(indent: Bool, autoClose: String = "}])") {
self.config = TransformerConfig(indent: indent, autoClose: autoClose)
}
required init() {
self.config = TransformerConfig(indent: true, autoClose: "}])")
}
func run() {
var commandString: String?
self.config = self.config
.merge(with: ConfigLoader.loadConfig().transformer)
.merge(with: TransformerConfig(indent: indent, autoClose: autoClose))
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 self.config.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 !self.config.autoClose!.isEmpty {
let removeBracketExpression = try! NSRegularExpression(
pattern:
"\\n[\\t ]*[\(NSRegularExpression.escapedPattern(for: self.config.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
}
}
================================================
FILE: Sources/SendKeysLib/Utilities.swift
================================================
import Foundation
func isTty() -> Bool {
return isatty(FileHandle.standardInput.fileDescriptor) == 1
}
func getRegexGroups(_ expression: NSRegularExpression, _ input: String) -> [String?]? {
var groups: [String?] = []
let matchResult = expression.firstMatch(
in: input, options: .anchored, range: NSRange(location: 0, length: input.utf8.count))
if matchResult == nil {
return nil
}
let numberOfRanges = matchResult!.numberOfRanges
for i in 0..<numberOfRanges {
let range = Range(matchResult!.range(at: i), in: input)
let arg = range == nil ? nil : String(input[range!])
groups.append(arg)
}
return groups
}
================================================
FILE: Sources/sendkeys/main.swift
================================================
import Foundation
import SendKeysLib
if #available(OSX 10.11, *) {
SendKeysCli.main()
} else {
print("OS version 10.11 or higher is required.")
}
================================================
FILE: Tests/LinuxMain.swift
================================================
import XCTest
import sendkeysTests
var tests = [XCTestCaseEntry]()
tests += sendkeysTests.allTests()
XCTMain(tests)
================================================
FILE: Tests/SendKeysTests/CommandIteratorTests.swift
================================================
import XCTest
@testable import SendKeysLib
final class CommandIteratorTests: XCTestCase {
var commandFactory: CommandFactory!
override func setUp() {
let keyPresser = KeyPresser(app: nil)
commandFactory = CommandFactory(
keyPresser: keyPresser,
mouseController: MouseController(animationRefreshInterval: 0.01, keyPresser: keyPresser))
}
func testParsesCharacters() throws {
let commands = getCommands(CommandsIterator("abc", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
DefaultCommand(key: "a"),
DefaultCommand(key: "b"),
DefaultCommand(key: "c"),
])
}
func testParsesKeyPress() throws {
let commands = getCommands(CommandsIterator("<c:a>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
KeyPressCommand(key: "a", modifiers: [])
])
}
func testParsesKeyPressDelete() throws {
let commands = getCommands(CommandsIterator("<c:delete>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
KeyPressCommand(key: "delete", modifiers: [])
])
}
func testParsesKeyPressesWithModifierKey() throws {
let commands = getCommands(CommandsIterator("<c:a:command>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
KeyPressCommand(key: "a", modifiers: ["command"])
])
}
func testParsesKeyPressesWithModifierKeys() throws {
let commands = getCommands(CommandsIterator("<c:a:command,shift>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
KeyPressCommand(key: "a", modifiers: ["command", "shift"])
])
}
func testParsesKeyPressAlias() throws {
let commands = getCommands(CommandsIterator("<k:a>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
KeyPressCommand(key: "a", modifiers: [])
])
}
func testParsesKeyDown() throws {
let commands = getCommands(CommandsIterator("<kd:a>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
KeyDownCommand(key: "a", modifiers: [])
])
}
func testParsesKeyDownWithModifierKey() throws {
let commands = getCommands(CommandsIterator("<kd:a:shift>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
KeyDownCommand(key: "a", modifiers: ["shift"])
])
}
func testParsesKeyDownAsModifierKey() throws {
let commands = getCommands(CommandsIterator("<kd:shift>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
KeyDownCommand(key: "shift", modifiers: [])
])
}
func testParsesKeyUp() throws {
let commands = getCommands(CommandsIterator("<ku:a>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
KeyUpCommand(key: "a", modifiers: [])
])
}
func testParsesKeyUpWithModifierKey() throws {
let commands = getCommands(CommandsIterator("<ku:a:shift>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
KeyUpCommand(key: "a", modifiers: ["shift"])
])
}
func testParsesKeyUpAsModifierKey() throws {
let commands = getCommands(CommandsIterator("<ku:shift>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
KeyUpCommand(key: "shift", modifiers: [])
])
}
func testParsesNewLines() throws {
let commands = getCommands(CommandsIterator("\n\n\n", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
NewlineCommand(),
NewlineCommand(),
NewlineCommand(),
])
}
func testParsesNewLinesWithCarriageReturns() throws {
let commands = getCommands(CommandsIterator("\r\n\r\n\n", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
NewlineCommand(),
NewlineCommand(),
NewlineCommand(),
])
}
func testParsesMultipleKeyPresses() throws {
let commands = getCommands(CommandsIterator("<c:a:command><c:c:command>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
KeyPressCommand(key: "a", modifiers: ["command"]),
KeyPressCommand(key: "c", modifiers: ["command"]),
])
}
func testParsesContinuation() throws {
let commands = getCommands(CommandsIterator("<\\>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
ContinuationCommand()
])
}
func testParsesPause() throws {
let commands = getCommands(CommandsIterator("<p:0.2>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
PauseCommand(duration: 0.2)
])
}
func testParsesStickyPause() throws {
let commands = getCommands(CommandsIterator("<P:0.2>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
StickyPauseCommand(duration: 0.2)
])
}
func testParsesMouseMove() throws {
let commands = getCommands(CommandsIterator("<m:1.5,2.5,3.5,4.5>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseMoveCommand(x1: 1.5, y1: 2.5, x2: 3.5, y2: 4.5, duration: 0, modifiers: [])
])
}
func testParsesMouseMoveWithModifier() throws {
let commands = getCommands(CommandsIterator("<m:1,2,3,4:command>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseMoveCommand(x1: 1, y1: 2, x2: 3, y2: 4, duration: 0, modifiers: ["command"])
])
}
func testParsesMouseMoveWithDuration() throws {
let commands = getCommands(CommandsIterator("<m:1,2,3,4:0.1>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseMoveCommand(x1: 1, y1: 2, x2: 3, y2: 4, duration: 0.1, modifiers: [])
])
}
func testParsesMouseMoveWithNegativeCoordinates() throws {
let commands = getCommands(CommandsIterator("<m:-1,-2,-3,-4:0.1>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseMoveCommand(x1: -1, y1: -2, x2: -3, y2: -4, duration: 0.1, modifiers: [])
])
}
func testParsesMouseMoveWithDurationAndModifiers() throws {
let commands = getCommands(CommandsIterator("<m:1,2,3,4:0.1:shift,command>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseMoveCommand(x1: 1, y1: 2, x2: 3, y2: 4, duration: 0.1, modifiers: ["shift", "command"])
])
}
func testParsesPartialMouseMove() throws {
let commands = getCommands(CommandsIterator("<m:3,4>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseMoveCommand(x1: nil, y1: nil, x2: 3, y2: 4, duration: 0, modifiers: [])
])
}
func testParsesPartialMouseMoveWithDuration() throws {
let commands = getCommands(CommandsIterator("<m:3,4:2>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseMoveCommand(x1: nil, y1: nil, x2: 3, y2: 4, duration: 2, modifiers: [])
])
}
func testParsesMouseClick() throws {
let commands = getCommands(CommandsIterator("<m:left>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseClickCommand(button: "left", modifiers: [], clicks: 1)
])
}
func testParsesMouseClickWithModifiers() throws {
let commands = getCommands(CommandsIterator("<m:left:shift,command>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseClickCommand(button: "left", modifiers: ["shift", "command"], clicks: 1)
])
}
func testParsesMouseClickWithClickCount() throws {
let commands = getCommands(CommandsIterator("<m:right:2>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseClickCommand(button: "right", modifiers: [], clicks: 2)
])
}
func testParsesMouseClickWithModifiersAndClickCount() throws {
let commands = getCommands(CommandsIterator("<m:right:command:2>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseClickCommand(button: "right", modifiers: ["command"], clicks: 2)
])
}
func testParsesMousePath() throws {
let commands = getCommands(CommandsIterator("<mpath:L 200 400:2>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MousePathCommand(
path: "L 200 400", offsetX: 0, offsetY: 0, scaleX: 1, scaleY: 1, duration: 2, modifiers: [])
])
}
func testParsesMousePathWithOffset() throws {
let commands = getCommands(CommandsIterator("<mpath:L 200 400:100,200:2>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MousePathCommand(
path: "L 200 400", offsetX: 100, offsetY: 200, scaleX: 1, scaleY: 1, duration: 2, modifiers: [])
])
}
func testParsesMousePathWithOffsetAndScale() throws {
let commands = getCommands(
CommandsIterator("<mpath:L 200 400:100,200,0.5,2.5:2>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MousePathCommand(
path: "L 200 400", offsetX: 100, offsetY: 200, scaleX: 0.5, scaleY: 2.5, duration: 2, modifiers: [])
])
}
func testParsesMousePathWithOffsetAndPartialScale() throws {
let commands = getCommands(CommandsIterator("<mpath:L 200 400:100,200,0.4:2>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MousePathCommand(
path: "L 200 400", offsetX: 100, offsetY: 200, scaleX: 0.4, scaleY: 0.4, duration: 2, modifiers: [])
])
}
func testParsesMouseDrag() throws {
let commands = getCommands(CommandsIterator("<d:1.5,2.5,3.5,4.5>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseDragCommand(x1: 1.5, y1: 2.5, x2: 3.5, y2: 4.5, duration: 0, button: "left", modifiers: [])
])
}
func testParsesMouseDragWithButton() throws {
let commands = getCommands(CommandsIterator("<d:1,2,3,4:right>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseDragCommand(x1: 1, y1: 2, x2: 3, y2: 4, duration: 0, button: "right", modifiers: [])
])
}
func testParsesMouseDragWithButtonAndModifiers() throws {
let commands = getCommands(CommandsIterator("<d:1,2,3,4:right:command,shift>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseDragCommand(
x1: 1, y1: 2, x2: 3, y2: 4, duration: 0, button: "right", modifiers: ["command", "shift"])
])
}
func testParsesMouseDragWithDuration() throws {
let commands = getCommands(CommandsIterator("<d:1,2,3,4:0.1>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseDragCommand(x1: 1, y1: 2, x2: 3, y2: 4, duration: 0.1, button: "left", modifiers: [])
])
}
func testParsesMouseDragWithDurationWithNegativeCoordinates() throws {
let commands = getCommands(CommandsIterator("<d:-1.5,-2,-3,-4:0.1>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseDragCommand(x1: -1.5, y1: -2, x2: -3, y2: -4, duration: 0.1, button: "left", modifiers: [])
])
}
func testParsesMouseDragWithDurationAndButton() throws {
let commands = getCommands(CommandsIterator("<d:1,2,3,4:0.1:right>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseDragCommand(x1: 1, y1: 2, x2: 3, y2: 4, duration: 0.1, button: "right", modifiers: [])
])
}
func testParsesMouseDragWithDurationAndButtonAndModifier() throws {
let commands = getCommands(CommandsIterator("<d:1,2,3,4:0.1:right:command>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseDragCommand(x1: 1, y1: 2, x2: 3, y2: 4, duration: 0.1, button: "right", modifiers: ["command"])
])
}
func testParsesPartialMouseDrag() throws {
let commands = getCommands(CommandsIterator("<d:3,4>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseDragCommand(x1: nil, y1: nil, x2: 3, y2: 4, duration: 0, button: "left", modifiers: [])
])
}
func testParsesPartialMouseDragWithDuration() throws {
let commands = getCommands(CommandsIterator("<d:3,4:2>", commandFactory: commandFactory))
XCTAssertEqual(
commands,
[
MouseDragCommand(x1: nil, y1: nil, x2: 3, y2: 4, duration: 2, button: "left", modifiers: [])
])
}
func testParsesPa
gitextract_cwwrie3z/ ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yaml │ │ └── feature_request.yaml │ └── workflows/ │ └── build.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.yml ├── .swift-format ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── CHANGELOG.md ├── Formula/ │ └── sendkeys_template.rb ├── LICENSE ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Sources/ │ ├── SendKeysLib/ │ │ ├── Animator.swift │ │ ├── AppActivator.swift │ │ ├── AppLister.swift │ │ ├── Bridge.swift │ │ ├── Commands/ │ │ │ ├── Command.swift │ │ │ ├── CommandExecutor.swift │ │ │ ├── CommandFactory.swift │ │ │ ├── CommandsIterator.swift │ │ │ ├── CommandsProcessor.swift │ │ │ ├── ContinuationCommand.swift │ │ │ ├── DefaultCommand.swift │ │ │ ├── KeyDownCommand.swift │ │ │ ├── KeyPressCommand.swift │ │ │ ├── KeyUpCommand.swift │ │ │ ├── MouseClickCommand.swift │ │ │ ├── MouseDownCommand.swift │ │ │ ├── MouseDragCommand.swift │ │ │ ├── MouseFocusCommand.swift │ │ │ ├── MouseMoveCommand.swift │ │ │ ├── MousePathCommand.swift │ │ │ ├── MouseScrollCommand.swift │ │ │ ├── MouseUpCommand.swift │ │ │ ├── NewlineCommand.swift │ │ │ ├── PauseCommand.swift │ │ │ └── StickyPauseCommand.swift │ │ ├── Configuration/ │ │ │ ├── AllConfiguration.swift │ │ │ ├── ConfigLoader.swift │ │ │ ├── MousePositionConfig.swift │ │ │ ├── SendConfig.swift │ │ │ └── TransformerConfig.swift │ │ ├── KeyCodes.swift │ │ ├── KeyMappings.swift │ │ ├── KeyPresser.swift │ │ ├── MouseController.swift │ │ ├── MouseEventProcessor.swift │ │ ├── MousePosition.swift │ │ ├── Path/ │ │ │ ├── Extensions.swift │ │ │ ├── PathCommands.swift │ │ │ ├── PathData.swift │ │ │ └── PathParser.swift │ │ ├── RuntimeError.swift │ │ ├── SendKeysCli.swift │ │ ├── Sender.swift │ │ ├── Sleeper.swift │ │ ├── TerminationListener.swift │ │ ├── Transformer.swift │ │ └── Utilities.swift │ └── sendkeys/ │ └── main.swift ├── Tests/ │ ├── LinuxMain.swift │ ├── SendKeysTests/ │ │ ├── CommandIteratorTests.swift │ │ ├── CommandsProcessorTests.swift │ │ ├── KeyPresserTests.swift │ │ ├── PathDataTests.swift │ │ ├── PathParserTests.swift │ │ ├── TransformerTests.swift │ │ ├── XCTestManifests.swift │ │ └── sendkeysTests.swift │ └── keys.txt ├── examples/ │ ├── .sendkeysrc.yml │ └── node.js ├── scripts/ │ ├── bottle.sh │ ├── code-coverage.sh │ ├── format.sh │ ├── install-pre-commit.sh │ ├── pre-commit.sh │ ├── update-version.sh │ └── verify-output.sh └── version.txt
SYMBOL INDEX (2 symbols across 1 files)
FILE: Formula/sendkeys_template.rb
class Sendkeys (line 3) | class Sendkeys < Formula
method install (line 12) | def install
Condensed preview — 87 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (234K chars).
[
{
"path": ".editorconfig",
"chars": 142,
"preview": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.swift]\nindent_style = "
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
"chars": 990,
"preview": "name: Bug Report\ndescription: Report a bug you've found\ntitle: \"[bug]: \"\nlabels: [bug]\nbody:\n - type: markdown\n attr"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
"chars": 842,
"preview": "name: Feature Request\ndescription: Suggest an idea for this project\ntitle: \"[feature]: \"\nlabels: [enhancement]\nbody:\n -"
},
{
"path": ".github/workflows/build.yml",
"chars": 4783,
"preview": "name: build\n\non:\n push:\n branches:\n - main\n pull_request:\n branches:\n - main\n\njobs:\n build:\n runs-"
},
{
"path": ".gitignore",
"chars": 111,
"preview": ".DS_Store\n/.build\n/Packages\n/*.xcodeproj\nxcuserdata/\n\n.swiftpm/\n\n*.bak\n\nFormula/sendkeys.rb\n*.tar.gz\n\n/.output\n"
},
{
"path": ".prettierignore",
"chars": 36,
"preview": "node_modules/\n.build/\n\nCHANGELOG.md\n"
},
{
"path": ".prettierrc.yml",
"chars": 207,
"preview": "arrowParens: avoid\nprintWidth: 120\nquoteProps: consistent\nsemi: true\nsingleQuote: true\ntabWidth: 2\ntrailingComma: all\nov"
},
{
"path": ".swift-format",
"chars": 1867,
"preview": "{\n \"fileScopedDeclarationPrivacy\": {\n \"accessLevel\": \"private\"\n },\n \"indentation\": {\n \"spaces\": 4\n },\n \"inden"
},
{
"path": ".vscode/extensions.json",
"chars": 111,
"preview": "{\n \"recommendations\": [\"vknabel.vscode-apple-swift-format\", \"sswg.swift-lang\", \"editorconfig.editorconfig\"]\n}\n"
},
{
"path": ".vscode/launch.json",
"chars": 665,
"preview": "{\n \"configurations\": [\n {\n \"type\": \"lldb\",\n \"request\": \"launch\",\n \"sourceLanguages\": [\"swift\"],\n "
},
{
"path": ".vscode/settings.json",
"chars": 34,
"preview": "{\n \"editor.formatOnSave\": true\n}\n"
},
{
"path": ".vscode/tasks.json",
"chars": 244,
"preview": "{\n // See https://go.microsoft.com/fwlink/?LinkId=733558\n // for the documentation about the tasks.json format\n \"vers"
},
{
"path": "CHANGELOG.md",
"chars": 18878,
"preview": "# Changelog\n\n## [4.3.1](https://github.com/socsieng/sendkeys/compare/v4.3.0...v4.3.1) (2024-12-14)\n\n\n### Bug Fixes\n\n* ad"
},
{
"path": "Formula/sendkeys_template.rb",
"chars": 462,
"preview": "# Documentation: https://docs.brew.sh/Formula-Cookbook\n# https://rubydoc.brew.sh/Formula\nclass Sendkeys <"
},
{
"path": "LICENSE",
"chars": 11358,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "Makefile",
"chars": 456,
"preview": "prefix ?= /usr/local\nbindir ?= $(prefix)/bin\n\n.PHONY: build\nbuild:\n\t@scripts/update-version.sh\n\t@swift build -c release "
},
{
"path": "Package.resolved",
"chars": 1695,
"preview": "{\n \"object\": {\n \"pins\": [\n {\n \"package\": \"swift-argument-parser\",\n \"repositoryURL\": \"https://gith"
},
{
"path": "Package.swift",
"chars": 1363,
"preview": "// swift-tools-version:5.5\n// The swift-tools-version declares the minimum version of Swift required to build this packa"
},
{
"path": "README.md",
"chars": 16659,
"preview": "# SendKeys\n\n\n -> Void\n\n let duration: Ti"
},
{
"path": "Sources/SendKeysLib/AppActivator.swift",
"chars": 3678,
"preview": "import Cocoa\n\nclass AppActivator: NSObject {\n private var application: NSRunningApplication!\n private let appName:"
},
{
"path": "Sources/SendKeysLib/AppLister.swift",
"chars": 1555,
"preview": "import ArgumentParser\nimport Cocoa\nimport Foundation\n\nclass AppLister: ParsableCommand {\n public static let configura"
},
{
"path": "Sources/SendKeysLib/Bridge.swift",
"chars": 511,
"preview": "func bridge<T: AnyObject>(obj: T) -> UnsafeRawPointer {\n return UnsafeRawPointer(Unmanaged.passUnretained(obj).toOpaq"
},
{
"path": "Sources/SendKeysLib/Commands/Command.swift",
"chars": 1685,
"preview": "import Foundation\n\npublic enum CommandType {\n case undefined\n case keyPress\n case keyDown\n case keyUp\n ca"
},
{
"path": "Sources/SendKeysLib/Commands/CommandExecutor.swift",
"chars": 242,
"preview": "import Foundation\n\npublic protocol CommandExecutorProtocol {\n func execute(_ command: Command)\n}\n\npublic class Comman"
},
{
"path": "Sources/SendKeysLib/Commands/CommandFactory.swift",
"chars": 1464,
"preview": "public class CommandFactory {\n public static let commands: [Command.Type] = [\n KeyPressCommand.self,\n K"
},
{
"path": "Sources/SendKeysLib/Commands/CommandsIterator.swift",
"chars": 1851,
"preview": "import Foundation\n\npublic class CommandsIterator: IteratorProtocol {\n public typealias Element = Command\n\n let com"
},
{
"path": "Sources/SendKeysLib/Commands/CommandsProcessor.swift",
"chars": 2453,
"preview": "import Foundation\n\npublic class CommandsProcessor {\n var defaultPause: TimeInterval\n\n let numberFormatter = Number"
},
{
"path": "Sources/SendKeysLib/Commands/ContinuationCommand.swift",
"chars": 470,
"preview": "import Foundation\n\npublic class ContinuationCommand: Command {\n public override class var commandType: CommandType { "
},
{
"path": "Sources/SendKeysLib/Commands/DefaultCommand.swift",
"chars": 422,
"preview": "import Foundation\n\npublic class DefaultCommand: KeyPressCommand {\n private static let _expression = try! NSRegularExp"
},
{
"path": "Sources/SendKeysLib/Commands/KeyDownCommand.swift",
"chars": 681,
"preview": "import Foundation\n\npublic class KeyDownCommand: KeyPressCommand {\n public override class var commandType: CommandType"
},
{
"path": "Sources/SendKeysLib/Commands/KeyPressCommand.swift",
"chars": 1455,
"preview": "import Foundation\n\npublic class KeyPressCommand: Command, RequiresKeyPresser {\n public override class var commandType"
},
{
"path": "Sources/SendKeysLib/Commands/KeyUpCommand.swift",
"chars": 675,
"preview": "import Foundation\n\npublic class KeyUpCommand: KeyPressCommand {\n public override class var commandType: CommandType {"
},
{
"path": "Sources/SendKeysLib/Commands/MouseClickCommand.swift",
"chars": 2213,
"preview": "import Cocoa\nimport Foundation\n\npublic class MouseClickCommand: Command, RequiresMouseController {\n public override c"
},
{
"path": "Sources/SendKeysLib/Commands/MouseDownCommand.swift",
"chars": 981,
"preview": "import Foundation\n\npublic class MouseDownCommand: MouseClickCommand {\n public override class var commandType: Command"
},
{
"path": "Sources/SendKeysLib/Commands/MouseDragCommand.swift",
"chars": 1835,
"preview": "import Foundation\n\npublic class MouseDragCommand: MouseMoveCommand {\n public override class var commandType: CommandT"
},
{
"path": "Sources/SendKeysLib/Commands/MouseFocusCommand.swift",
"chars": 2253,
"preview": "import Foundation\n\npublic class MouseFocusCommand: MouseClickCommand {\n public override class var commandType: Comman"
},
{
"path": "Sources/SendKeysLib/Commands/MouseMoveCommand.swift",
"chars": 2270,
"preview": "import Foundation\n\npublic class MouseMoveCommand: MouseClickCommand {\n public override class var commandType: Command"
},
{
"path": "Sources/SendKeysLib/Commands/MousePathCommand.swift",
"chars": 2620,
"preview": "import Foundation\n\npublic class MousePathCommand: MouseClickCommand {\n public override class var commandType: Command"
},
{
"path": "Sources/SendKeysLib/Commands/MouseScrollCommand.swift",
"chars": 1740,
"preview": "import Foundation\n\npublic class MouseScrollCommand: MouseClickCommand {\n public override class var commandType: Comma"
},
{
"path": "Sources/SendKeysLib/Commands/MouseUpCommand.swift",
"chars": 975,
"preview": "import Foundation\n\npublic class MouseUpCommand: MouseClickCommand {\n public override class var commandType: CommandTy"
},
{
"path": "Sources/SendKeysLib/Commands/NewlineCommand.swift",
"chars": 408,
"preview": "import Foundation\n\npublic class NewlineCommand: KeyPressCommand {\n private static let _expression = try! NSRegularExp"
},
{
"path": "Sources/SendKeysLib/Commands/PauseCommand.swift",
"chars": 1077,
"preview": "import Foundation\n\npublic class PauseCommand: Command {\n public override class var commandType: CommandType { return "
},
{
"path": "Sources/SendKeysLib/Commands/StickyPauseCommand.swift",
"chars": 326,
"preview": "import Foundation\n\npublic class StickyPauseCommand: PauseCommand {\n public override class var commandType: CommandTyp"
},
{
"path": "Sources/SendKeysLib/Configuration/AllConfiguration.swift",
"chars": 764,
"preview": "struct AllConfiguration: Codable {\n var send: SendConfig?\n var mousePosition: MousePositionConfig?\n var transfo"
},
{
"path": "Sources/SendKeysLib/Configuration/ConfigLoader.swift",
"chars": 1060,
"preview": "import Foundation\nimport Yams\n\nlet defaultConfigFiles = [\n NSString(\"~/.sendkeysrc.yml\").expandingTildeInPath,\n NS"
},
{
"path": "Sources/SendKeysLib/Configuration/MousePositionConfig.swift",
"chars": 573,
"preview": "struct MousePositionConfig: Codable {\n var watch: Bool?\n var output: OutputMode?\n var duration: Double?\n\n in"
},
{
"path": "Sources/SendKeysLib/Configuration/SendConfig.swift",
"chars": 1483,
"preview": "struct SendConfig: Codable {\n var activate: Bool?\n var animationInterval: Double?\n var delay: Double?\n var i"
},
{
"path": "Sources/SendKeysLib/Configuration/TransformerConfig.swift",
"chars": 445,
"preview": "struct TransformerConfig: Codable {\n var indent: Bool?\n var autoClose: String?\n\n init(indent: Bool? = nil, auto"
},
{
"path": "Sources/SendKeysLib/KeyCodes.swift",
"chars": 13004,
"preview": "import Cocoa\n\n// From: https://gist.github.com/swillits/df648e87016772c7f7e5dbed2b345066\nstruct KeyCodes {\n // Layout"
},
{
"path": "Sources/SendKeysLib/KeyMappings.swift",
"chars": 1561,
"preview": "import ArgumentParser\n\nstruct KeyMappings {\n enum Layouts: String, Codable, ExpressibleByArgument {\n case qwer"
},
{
"path": "Sources/SendKeysLib/KeyPresser.swift",
"chars": 4373,
"preview": "import Carbon\nimport Cocoa\nimport Foundation\n\npublic class KeyPresser {\n private var application: NSRunningApplicatio"
},
{
"path": "Sources/SendKeysLib/MouseController.swift",
"chars": 10102,
"preview": "import Cocoa\nimport Foundation\n\nclass MouseController {\n enum ScrollAxis {\n case horizontal\n case verti"
},
{
"path": "Sources/SendKeysLib/MouseEventProcessor.swift",
"chars": 4704,
"preview": "import Cocoa\nimport Foundation\n\nenum MouseEventType {\n case click\n case drag\n}\n\nenum MouseButton: String, CustomSt"
},
{
"path": "Sources/SendKeysLib/MousePosition.swift",
"chars": 6679,
"preview": "import ArgumentParser\nimport Cocoa\nimport Foundation\n\nenum OutputMode: String, Codable, ExpressibleByArgument {\n case"
},
{
"path": "Sources/SendKeysLib/Path/Extensions.swift",
"chars": 803,
"preview": "import AppKit\nimport Foundation\n\nextension CGPoint {\n // Vector math\n public static func + (left: CGPoint, right: "
},
{
"path": "Sources/SendKeysLib/Path/PathCommands.swift",
"chars": 11409,
"preview": "import Foundation\n\npublic class PointValue: CustomStringConvertible {\n var point: CGPoint\n\n init(_ point: CGPoint)"
},
{
"path": "Sources/SendKeysLib/Path/PathData.swift",
"chars": 4409,
"preview": "import Foundation\n\nstruct SegmentInfo {\n var startLength: Double\n var length: Double\n var startPoint: CGPoint\n "
},
{
"path": "Sources/SendKeysLib/Path/PathParser.swift",
"chars": 3904,
"preview": "import Foundation\n\npublic class PathParser {\n private var index: Int\n private let data: [Character]\n private va"
},
{
"path": "Sources/SendKeysLib/RuntimeError.swift",
"chars": 199,
"preview": "struct RuntimeError: Error {\n let message: String\n\n init(_ message: String) {\n self.message = message\n }"
},
{
"path": "Sources/SendKeysLib/SendKeysCli.swift",
"chars": 578,
"preview": "import ArgumentParser\nimport Foundation\n\n@available(OSX 10.11, *)\npublic struct SendKeysCli: ParsableCommand {\n publi"
},
{
"path": "Sources/SendKeysLib/Sender.swift",
"chars": 6093,
"preview": "import ArgumentParser\nimport Cocoa\nimport Foundation\n\n@available(OSX 10.11, *)\npublic struct Sender: ParsableCommand {\n "
},
{
"path": "Sources/SendKeysLib/Sleeper.swift",
"chars": 133,
"preview": "import Foundation\n\nstruct Sleeper {\n static func sleep(seconds: Double) {\n usleep(useconds_t(seconds * 1_000_0"
},
{
"path": "Sources/SendKeysLib/TerminationListener.swift",
"chars": 3254,
"preview": "import Cocoa\nimport Foundation\n\nclass TerminationListener {\n private var keycode: UInt16\n private var modifiers: ["
},
{
"path": "Sources/SendKeysLib/Transformer.swift",
"chars": 3614,
"preview": "import ArgumentParser\nimport Foundation\n\n@available(OSX 10.11, *)\nclass Transformer: ParsableCommand {\n public static"
},
{
"path": "Sources/SendKeysLib/Utilities.swift",
"chars": 689,
"preview": "import Foundation\n\nfunc isTty() -> Bool {\n return isatty(FileHandle.standardInput.fileDescriptor) == 1\n}\n\nfunc getReg"
},
{
"path": "Sources/sendkeys/main.swift",
"chars": 155,
"preview": "import Foundation\nimport SendKeysLib\n\nif #available(OSX 10.11, *) {\n SendKeysCli.main()\n} else {\n print(\"OS versio"
},
{
"path": "Tests/LinuxMain.swift",
"chars": 117,
"preview": "import XCTest\nimport sendkeysTests\n\nvar tests = [XCTestCaseEntry]()\ntests += sendkeysTests.allTests()\nXCTMain(tests)\n"
},
{
"path": "Tests/SendKeysTests/CommandIteratorTests.swift",
"chars": 19291,
"preview": "import XCTest\n\n@testable import SendKeysLib\n\nfinal class CommandIteratorTests: XCTestCase {\n var commandFactory: Comm"
},
{
"path": "Tests/SendKeysTests/CommandsProcessorTests.swift",
"chars": 6235,
"preview": "import XCTest\n\n@testable import SendKeysLib\n\nclass CommandExecutorSpy: CommandExecutorProtocol {\n var commands: [Comm"
},
{
"path": "Tests/SendKeysTests/KeyPresserTests.swift",
"chars": 1440,
"preview": "import XCTest\n\n@testable import SendKeysLib\n\nfinal class KeyPresserTests: XCTestCase {\n func testEscapeKey() throws {"
},
{
"path": "Tests/SendKeysTests/PathDataTests.swift",
"chars": 3428,
"preview": "import XCTest\n\n@testable import SendKeysLib\n\nfinal class PathDataTests: XCTestCase {\n func testNormalizesPathData() t"
},
{
"path": "Tests/SendKeysTests/PathParserTests.swift",
"chars": 4019,
"preview": "import XCTest\n\n@testable import SendKeysLib\n\nfinal class PathParserTests: XCTestCase {\n func testParsesRelativeComman"
},
{
"path": "Tests/SendKeysTests/TransformerTests.swift",
"chars": 1278,
"preview": "import XCTest\n\n@testable import SendKeysLib\n\nfinal class TransformerTests: XCTestCase {\n func testShouldNoteTransform"
},
{
"path": "Tests/SendKeysTests/XCTestManifests.swift",
"chars": 177,
"preview": "import XCTest\n\n#if !canImport(ObjectiveC)\n public func allTests() -> [XCTestCaseEntry] {\n return [\n "
},
{
"path": "Tests/SendKeysTests/sendkeysTests.swift",
"chars": 1434,
"preview": "import XCTest\n\nimport class Foundation.Bundle\n\nfinal class sendkeysTests: XCTestCase {\n func testHelp() throws {\n "
},
{
"path": "Tests/keys.txt",
"chars": 106,
"preview": "0123456789<\\>\nabcdefghijklmnopqrstuvwxyz<\\>\nABCDEFGHIJKLMNOPQRSTUVWXYZ<\\>\n-,;.'[]/\\`=<\\>\n<c:c:control><\\>\n"
},
{
"path": "examples/.sendkeysrc.yml",
"chars": 828,
"preview": "# All properties are optional\nsend:\n activate: true\n animationInterval: 0.01\n delay: 0.1\n initialDelay: 1\n keyboard"
},
{
"path": "examples/node.js",
"chars": 338,
"preview": "const http = require('http');\n\nconst hostname = '127.0.0.1';\nconst port = 3000;\n\nconst server = http.createServer((req, "
},
{
"path": "scripts/bottle.sh",
"chars": 1497,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncwd=`pwd`\nscript_folder=`cd $(dirname $0) && pwd`\nversion=$1\nformula_template=$script_folde"
},
{
"path": "scripts/code-coverage.sh",
"chars": 970,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncwd=`pwd`\nscript_folder=`cd $(dirname $0) && pwd`\nbuild_folder=$script_folder/../.build\nout"
},
{
"path": "scripts/format.sh",
"chars": 349,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncwd=`pwd`\nscript_folder=`cd $(dirname $0) && pwd`\nfile=$1\n\nif [ -z \"$file\" ]\nthen\n # all f"
},
{
"path": "scripts/install-pre-commit.sh",
"chars": 183,
"preview": "#!/usr/bin/env bash\nset -e\n\nscript_folder=`cd $(dirname $0) && pwd`\nrepo_folder=`git rev-parse --show-toplevel`\n\ncp -f $"
},
{
"path": "scripts/pre-commit.sh",
"chars": 406,
"preview": "#!/usr/bin/env bash\nset -e\n\n# get the repository root\nrepo_folder=`git rev-parse --show-toplevel`\n\n# use repository root"
},
{
"path": "scripts/update-version.sh",
"chars": 392,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncwd=`pwd`\nscript_folder=`cd $(dirname $0) && pwd`\nversion=${1:-`cat $script_folder/../versi"
},
{
"path": "scripts/verify-output.sh",
"chars": 685,
"preview": "#!/usr/bin/env bash\n\nset -e\n\ncwd=`pwd`\nscript_folder=`cd $(dirname $0) && pwd`\noutput_folder=\"$script_folder/../.output\""
},
{
"path": "version.txt",
"chars": 6,
"preview": "4.3.1\n"
}
]
About this extraction
This page contains the full source code of the socsieng/sendkeys GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 87 files (213.7 KB), approximately 57.1k tokens, and a symbol index with 2 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.