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 ![Build status](https://github.com/socsieng/sendkeys/workflows/build/badge.svg) ![Homebrew installs](https://img.shields.io/github/downloads/socsieng/sendkeys/total.svg?label=%F0%9F%8D%BA+installs&labelColor=32393F&color=brightgreen) 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 world" ``` ![hello world example](https://github.com/socsieng/sendkeys/raw/main/docs/images/example1.gif) _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 `: 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 `: 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 `: The path to a file containing the commands to send to the application. - `--characters `: The characters to send to the application. Note that this argument is ignored if `--input-file` is supplied. - `--delay `: The delay between keystrokes and instructions. Defaults to `0.1` seconds. - `--initial-delay `: The initial delay before sending the first keystroke or instruction. Defaults to `1` second. - `--animation-interval `: The time between mouse movements when animating mouse commands. Lower values results in smoother animations. Defaults to `0.01` seconds. - `--terminate-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 `: 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 `: 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: `` - `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`: `` - `command` + `a`: `` - `option` + `shift` + `left arrow`: `` #### 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 `` and key up ``. 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: ``. ### Mouse commands #### Move mouse cursor The mouse cursor can be moved using the following markup: `` - `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: - ``: Move mouse cursor from current position to 400, 400 over 0.5 seconds. - ``: Move mouse cursor from 400, 400 position to 0, 0 over 2 seconds. - ``: Move mouse cursor to 400, 400 instantly. ![mouse move example](https://github.com/socsieng/sendkeys/raw/main/docs/images/mouse.gif)
_Sample command: `sendkeys -c ""`_ #### Mouse click A mouse click can be activated using the following markup: `` - `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: - ``: Right mouse click at the current mouse location. - ``: Double click the left button at the current mouse location. #### Mouse drag A mouse drag be initiated with: `` 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: - ``: Drag the mouse using the left mouse button from current position to 400, 400 over 0.5 seconds. - ``: Drag the mouse using the right mouse button from 400, 400 position to 0, 0 over 2 seconds. - ``: Drag the mouse using the left mouse button to 400, 400 over 2 seconds with the `shift` key down. ![mouse drag example](https://github.com/socsieng/sendkeys/raw/main/docs/images/mouse-drag.gif) #### Mouse scrolling A mouse scroll can be initiated with: `` - `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: - ``: Scrolls down 400 pixels over 0.5 seconds. - ``: Scrolls up 400 pixels over 0.2 seconds. - ``: 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: `` - `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: - ``: 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 focus example](https://github.com/socsieng/sendkeys/raw/main/docs/images/mouse-focus.gif) #### Mouse path The mouse path command can be used move the mouse cursor along a path. The mouse path command uses the following markup: `` - `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: - ``: 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`. ![mouse path example](https://github.com/socsieng/sendkeys/raw/main/docs/images/mouse-path.gif)
_Sample command: `sendkeys -c ""`_ #### 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 `` and mouse up ``. 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: ``. ### 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: `` - `duration` is required and controls the amount of time to pause before the next keystroke/instruction is executed. `` (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. ![accessibility settings](https://github.com/socsieng/sendkeys/raw/main/docs/images/accessibility.gif) ## 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(obj: T) -> UnsafeRawPointer { return UnsafeRawPointer(Unmanaged.passUnretained(obj).toOpaque()) } func bridge(ptr: UnsafeRawPointer) -> T { return Unmanaged.fromOpaque(ptr).takeUnretainedValue() } func bridgeRetained(obj: T) -> UnsafeRawPointer { return UnsafeRawPointer(Unmanaged.passRetained(obj).toOpaque()) } func bridgeTransfer(ptr: UnsafeRawPointer) -> T { return Unmanaged.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.. 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: "\\") 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: "\\") 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: "\\") 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: "\\") 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: "\\") 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 } // private static let _expression = try! NSRegularExpression( pattern: "\\") 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: "\\") 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: "\\]+)(:(-?[\\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: "\\") 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: "\\") 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: "\\") 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: "\\") 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() 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 "<\\>" 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 "<\\>" } } 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? 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? { 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 { let struct_pointer = UnsafeMutablePointer.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: 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 { 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 { 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 { 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 { 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 { 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.. 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.. 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? 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") } 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..", commandFactory: commandFactory)) XCTAssertEqual( commands, [ KeyPressCommand(key: "a", modifiers: []) ]) } func testParsesKeyPressDelete() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ KeyPressCommand(key: "delete", modifiers: []) ]) } func testParsesKeyPressesWithModifierKey() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ KeyPressCommand(key: "a", modifiers: ["command"]) ]) } func testParsesKeyPressesWithModifierKeys() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ KeyPressCommand(key: "a", modifiers: ["command", "shift"]) ]) } func testParsesKeyPressAlias() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ KeyPressCommand(key: "a", modifiers: []) ]) } func testParsesKeyDown() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ KeyDownCommand(key: "a", modifiers: []) ]) } func testParsesKeyDownWithModifierKey() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ KeyDownCommand(key: "a", modifiers: ["shift"]) ]) } func testParsesKeyDownAsModifierKey() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ KeyDownCommand(key: "shift", modifiers: []) ]) } func testParsesKeyUp() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ KeyUpCommand(key: "a", modifiers: []) ]) } func testParsesKeyUpWithModifierKey() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ KeyUpCommand(key: "a", modifiers: ["shift"]) ]) } func testParsesKeyUpAsModifierKey() throws { let commands = getCommands(CommandsIterator("", 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("", 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("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ PauseCommand(duration: 0.2) ]) } func testParsesStickyPause() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ StickyPauseCommand(duration: 0.2) ]) } func testParsesMouseMove() throws { let commands = getCommands(CommandsIterator("", 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("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseMoveCommand(x1: 1, y1: 2, x2: 3, y2: 4, duration: 0, modifiers: ["command"]) ]) } func testParsesMouseMoveWithDuration() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseMoveCommand(x1: 1, y1: 2, x2: 3, y2: 4, duration: 0.1, modifiers: []) ]) } func testParsesMouseMoveWithNegativeCoordinates() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseMoveCommand(x1: -1, y1: -2, x2: -3, y2: -4, duration: 0.1, modifiers: []) ]) } func testParsesMouseMoveWithDurationAndModifiers() throws { let commands = getCommands(CommandsIterator("", 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("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseMoveCommand(x1: nil, y1: nil, x2: 3, y2: 4, duration: 0, modifiers: []) ]) } func testParsesPartialMouseMoveWithDuration() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseMoveCommand(x1: nil, y1: nil, x2: 3, y2: 4, duration: 2, modifiers: []) ]) } func testParsesMouseClick() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseClickCommand(button: "left", modifiers: [], clicks: 1) ]) } func testParsesMouseClickWithModifiers() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseClickCommand(button: "left", modifiers: ["shift", "command"], clicks: 1) ]) } func testParsesMouseClickWithClickCount() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseClickCommand(button: "right", modifiers: [], clicks: 2) ]) } func testParsesMouseClickWithModifiersAndClickCount() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseClickCommand(button: "right", modifiers: ["command"], clicks: 2) ]) } func testParsesMousePath() throws { let commands = getCommands(CommandsIterator("", 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("", 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("", 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("", 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("", 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("", 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("", 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("", 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("", 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("", 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("", 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("", 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("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseDragCommand(x1: nil, y1: nil, x2: 3, y2: 4, duration: 2, button: "left", modifiers: []) ]) } func testParsesPartialMouseDragWithDurationAndButton() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseDragCommand(x1: nil, y1: nil, x2: 3, y2: 4, duration: 2, button: "center", modifiers: []) ]) } func testParsesMouseScroll() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseScrollCommand(x: 0, y: 10.5, duration: 0, modifiers: []) ]) } func testParsesMouseScrollWithNegativeAmount() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseScrollCommand(x: -100, y: 10, duration: 0, modifiers: []) ]) } func testParsesMouseScrollWithDuration() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseScrollCommand(x: 0, y: 10, duration: 0.5, modifiers: []) ]) } func testParsesMouseScrollWithDurationAndModifiers() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseScrollCommand(x: 0, y: 10, duration: 0.5, modifiers: ["shift"]) ]) } func testParsesMouseScrollWithNegativeAmountAndDuration() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseScrollCommand(x: 0, y: -10, duration: 0.5, modifiers: []) ]) } func testParsesMouseDown() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseDownCommand(button: "right", modifiers: []) ]) } func testParsesMouseDownWithModifiers() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseDownCommand(button: "left", modifiers: ["shift", "command"]) ]) } func testParsesMouseUp() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseUpCommand(button: "center", modifiers: []) ]) } func testParsesMouseUpWithModifiers() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseUpCommand(button: "right", modifiers: ["option", "command"]) ]) } func testParsesMouseFocus() throws { let commands = getCommands( CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseFocusCommand(x: 0.5, y: 0.5, rx: 100.5, ry: 50.5, angleFrom: 0.5, angleTo: 360.5, duration: 1) ]) } func testParsesMouseFocusWithSingleRadius() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseFocusCommand(x: 0, y: 0, rx: 100, ry: 100, angleFrom: 0, angleTo: 360, duration: 0.1) ]) } func testParsesMouseFocusWithNegativeCoordinates() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseFocusCommand(x: -10, y: -20, rx: 100, ry: 50, angleFrom: 0, angleTo: 360, duration: 1.5) ]) } func testParsesMouseFocusWithNegativeAngles() throws { let commands = getCommands(CommandsIterator("", commandFactory: commandFactory)) XCTAssertEqual( commands, [ MouseFocusCommand(x: -10, y: -20, rx: 100, ry: 50, angleFrom: 100, angleTo: -360, duration: 1.5) ]) } private func getCommands(_ iterator: CommandsIterator) -> [Command] { var commands: [Command] = [] while true { let command = iterator.next() if command == nil { break } commands.append(command!) } return commands } } ================================================ FILE: Tests/SendKeysTests/CommandsProcessorTests.swift ================================================ import XCTest @testable import SendKeysLib class CommandExecutorSpy: CommandExecutorProtocol { var commands: [Command] = [] func execute(_ command: Command) { commands.append(command) } } final class CommandProcessorTests: XCTestCase { var commandExecutor: CommandExecutorSpy? var commandsProcessor: CommandsProcessor? override func setUp() { commandExecutor = CommandExecutorSpy() commandsProcessor = CommandsProcessor( defaultPause: 0.1, keyPresser: KeyPresser(app: nil), commandExecutor: commandExecutor) } func testExecutesSingleKeyPress() { commandsProcessor!.process("a") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ DefaultCommand(key: "a") ]) } func testExecutesColonKeyPress() { commandsProcessor!.process("") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ KeyPressCommand(key: ":", modifiers: []) ]) } func testExecutesCommandOpenKeyPress() { commandsProcessor!.process("") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ KeyPressCommand(key: "<", modifiers: []) ]) } func testExecutesCommandCloseKeyPress() { commandsProcessor!.process(">") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ KeyPressCommand(key: ">", modifiers: []) ]) } func testExecutesColonKeyPressWithModifier() { commandsProcessor!.process("") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ KeyPressCommand(key: ":", modifiers: ["control"]) ]) } func testExecutesCommandOpenKeyPressWithModifier() { commandsProcessor!.process("") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ KeyPressCommand(key: "<", modifiers: ["shift"]) ]) } func testExecutesCommandCloseKeyPressWithModifier() { commandsProcessor!.process(":shift>") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ KeyPressCommand(key: ">", modifiers: ["shift"]) ]) } func testExecutesSpecialKeyPress() { commandsProcessor!.process("") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ KeyPressCommand(key: "tab", modifiers: []) ]) } func testExecutesSpecialKeyPressWithModifier() { commandsProcessor!.process("") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ KeyPressCommand(key: "tab", modifiers: ["shift"]) ]) } func testExecutesMultipleKeyPressWithDelay() { commandsProcessor!.process("hello") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ DefaultCommand(key: "h"), PauseCommand(duration: 0.1), DefaultCommand(key: "e"), PauseCommand(duration: 0.1), DefaultCommand(key: "l"), PauseCommand(duration: 0.1), DefaultCommand(key: "l"), PauseCommand(duration: 0.1), DefaultCommand(key: "o"), ]) } func testExecutesMultipleKeyPressWithExplicitPause() { commandsProcessor!.process("a") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ DefaultCommand(key: "a"), PauseCommand(duration: 10), KeyPressCommand(key: "a", modifiers: ["command"]), ]) } func testExecutesMultipleKeyPressWithStickyPause() { commandsProcessor!.process("abcd") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ DefaultCommand(key: "a"), PauseCommand(duration: 0.1), DefaultCommand(key: "b"), StickyPauseCommand(duration: 10), DefaultCommand(key: "c"), PauseCommand(duration: 10), DefaultCommand(key: "d"), ]) } func testIgnoreContinuation() { commandsProcessor!.process("<\\>") let commands = commandExecutor!.commands XCTAssertEqual(commands, []) } func testIgnoreConsecutiveContinuations() { commandsProcessor!.process("<\\><\\>") let commands = commandExecutor!.commands XCTAssertEqual(commands, []) } func testNegateConsecutiveContinuations() { commandsProcessor!.process("<\\><\\>a") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ DefaultCommand(key: "a") ]) } func testExecutesMultipleKeyPressWithContinuation() { commandsProcessor!.process("a<\\>b") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ DefaultCommand(key: "a") ]) } func testExecutesMultipleMouseCommands() { commandsProcessor!.process("") let commands = commandExecutor!.commands XCTAssertEqual( commands, [ MouseMoveCommand(x1: nil, y1: nil, x2: 20, y2: 20, duration: 1, modifiers: []), PauseCommand(duration: 0.1), MouseMoveCommand(x1: nil, y1: nil, x2: 200, y2: 200, duration: 1, modifiers: []), PauseCommand(duration: 0.1), MouseClickCommand(button: "left", modifiers: [], clicks: 1), ]) } } ================================================ FILE: Tests/SendKeysTests/KeyPresserTests.swift ================================================ import XCTest @testable import SendKeysLib final class KeyPresserTests: XCTestCase { func testEscapeKey() throws { let presser = KeyPresser(app: nil) let event = try! presser.keyPress(key: "escape", modifiers: []) XCTAssertEqual(event!.getIntegerValueField(.keyboardEventKeycode), 53) } func testEscapeKeyUsingAlias() throws { let presser = KeyPresser(app: nil) let event = try! presser.keyPress(key: "esc", modifiers: []) XCTAssertEqual(event!.getIntegerValueField(.keyboardEventKeycode), 53) } func testEnterKey() throws { let presser = KeyPresser(app: nil) let event = try! presser.keyPress(key: "return", modifiers: []) XCTAssertEqual(event!.getIntegerValueField(.keyboardEventKeycode), 36) } func testShiftModifier() throws { let presser = KeyPresser(app: nil) let event = try! presser.keyPress(key: "a", modifiers: ["shift"]) XCTAssertEqual(event!.getIntegerValueField(.keyboardEventKeycode), 0) XCTAssertTrue(event!.flags.contains([.maskShift])) } func testCommandControlModifier() throws { let presser = KeyPresser(app: nil) let event = try! presser.keyPress(key: "c", modifiers: ["command", "control"]) XCTAssertEqual(event!.getIntegerValueField(.keyboardEventKeycode), 8) XCTAssertTrue(event!.flags.contains([.maskCommand, .maskControl])) } } ================================================ FILE: Tests/SendKeysTests/PathDataTests.swift ================================================ import XCTest @testable import SendKeysLib final class PathDataTests: XCTestCase { func testNormalizesPathData() throws { let result = PathData("M 0 1 L 100 101 H-1V+2 Z").commands XCTAssertEqual( result, [ PointPathCommand("M", PointValue(CGPoint(x: 0, y: 1))), PointPathCommand("L", PointValue(CGPoint(x: 100, y: 101))), PointPathCommand("L", PointValue(CGPoint(x: -1, y: 101))), PointPathCommand("L", PointValue(CGPoint(x: -1, y: 2))), PointPathCommand("L", PointValue(CGPoint(x: 0, y: 1))), ] ) } func testNormalizesPathDataWithRelativeCommands() throws { let result = PathData("m 10 20 h 6 v 10 l 15 -8 c 10 10 20 20 30 30").commands XCTAssertEqual( result, [ PointPathCommand("M", PointValue(CGPoint(x: 10, y: 20))), PointPathCommand("L", PointValue(CGPoint(x: 16, y: 20))), PointPathCommand("L", PointValue(CGPoint(x: 16, y: 30))), PointPathCommand("L", PointValue(CGPoint(x: 31, y: 22))), CubicBezierPathCommand( "C", ControlPointsValue(CGPoint(x: 41, y: 32), CGPoint(x: 51, y: 42), CGPoint(x: 61, y: 52))), ] ) } func testGetsTotalDistance() throws { let result = PathData("M 100 200 l 300 400").getTotalDistance() XCTAssertEqual( result, 500 ) } func testGetsTotalDistanceOfMultipleStraightLines() throws { let result = PathData("M 100 200 h 150 v 220").getTotalDistance() XCTAssertEqual( result, 370 ) } func testGetsPointAtInterval_beginning() throws { let result = PathData("M 100 200 l 300 400").getPointAtInterval(0) XCTAssertEqual( result, CGPoint(x: 100, y: 200) ) } func testGetsPointAtInterval_middle() throws { let result = PathData("M 100 200 l 300 400").getPointAtInterval(0.5) XCTAssertEqual( result, CGPoint(x: 250, y: 400) ) } func testGetsPointAtInterval_end() throws { let result = PathData("M 100 200 l 300 400").getPointAtInterval(1) XCTAssertEqual( result, CGPoint(x: 400, y: 600) ) } func testGetsPointAtIntervalOfMultipleStraightLines_middle() throws { let result = PathData("M 100 200 h 150 v 220").getPointAtInterval(0.5) XCTAssertEqual( result, CGPoint(x: 250, y: 235) ) } func testGetsPointAtIntervalOfMultipleStraightLines_beyond_end() throws { let result = PathData("M 100 200 h 150 v 220").getPointAtInterval(1.2) XCTAssertEqual( result, CGPoint(x: 250, y: 420) ) } func testGetsPointAtIntervalOfMultipleStraightLinePaths_end() throws { let result = PathData("M 100 0 h 20 20 20 l 200 0").getPointAtInterval(1) XCTAssertEqual( result, CGPoint(x: 360, y: 0) ) } func testGetsPointAtIntervalOfMultiplePaths_end() throws { let result = PathData("M 100 0 a 100,100 0 0,1 200,0 l 100 0").getPointAtInterval(1) XCTAssertEqual( result, CGPoint(x: 400, y: 0) ) } } ================================================ FILE: Tests/SendKeysTests/PathParserTests.swift ================================================ import XCTest @testable import SendKeysLib final class PathParserTests: XCTestCase { func testParsesRelativeCommand_m() throws { let result = PathParser("m 0 1").parse() XCTAssertEqual( result, [PointPathCommand("m", PointValue(CGPoint(x: 0, y: 1)))] ) } func testParsesRelativeCommand_l() throws { let result = PathParser("l 0 1").parse() XCTAssertEqual( result, [PointPathCommand("l", PointValue(CGPoint(x: 0, y: 1)))] ) } func testParsesRelativeCommand_c() throws { let result = PathParser("c 0 1 2 3 4 5").parse() XCTAssertEqual( result, [ CubicBezierPathCommand( "c", ControlPointsValue(CGPoint(x: 0, y: 1), CGPoint(x: 2, y: 3), CGPoint(x: 4, y: 5))) ] ) } func testParsesRelativeCommand_s() throws { let result = PathParser("s 0 1 2 3").parse() XCTAssertEqual( result, [CubicBezierPathCommand("s", ControlPointsValue(nil, CGPoint(x: 0, y: 1), CGPoint(x: 2, y: 3)))] ) } func testParsesRelativeCommand_a() throws { let result = PathParser("a 10,11 2 0,1 5,6").parse() XCTAssertEqual( result, [ArcPathCommand("a", ArchCommandValue(CGPoint(x: 10, y: 11), 2, false, true, CGPoint(x: 5, y: 6)))] ) } func testParsesRelativeCommand_h() throws { let result = PathParser("h 1").parse() XCTAssertEqual( result, [NumericPathCommand("h", 1)] ) } func testParsesRelativeCommand_v() throws { let result = PathParser("v 1").parse() XCTAssertEqual( result, [NumericPathCommand("v", 1)] ) } func testParsesRelativeCommand_t() throws { let result = PathParser("t 0 1").parse() XCTAssertEqual( result, [QuadraticBezierPathCommand("t", ControlPointValue(nil, CGPoint(x: 0, y: 1)))] ) } func testParsesRelativeCommand_q() throws { let result = PathParser("q 0 1 2 3").parse() XCTAssertEqual( result, [QuadraticBezierPathCommand("q", ControlPointValue(CGPoint(x: 0, y: 1), CGPoint(x: 2, y: 3)))] ) } func testParsesAbsoluteCommands() throws { let result = PathParser("M 0 1 L 100 101 H-1V+2 A 10.2,11 2 0,1 5,6 Z Q 0 1 2 3 T 2 3 C 0 1 2 3 4 5 S 0 1 2 3") .parse() XCTAssertEqual( result, [ PointPathCommand("M", PointValue(CGPoint(x: 0, y: 1))), PointPathCommand("L", PointValue(CGPoint(x: 100, y: 101))), NumericPathCommand("H", -1), NumericPathCommand("V", 2), ArcPathCommand("A", ArchCommandValue(CGPoint(x: 10.2, y: 11), 2, false, true, CGPoint(x: 5, y: 6))), PathCommandBase("Z"), QuadraticBezierPathCommand("Q", ControlPointValue(CGPoint(x: 0, y: 1), CGPoint(x: 2, y: 3))), QuadraticBezierPathCommand("T", ControlPointValue(nil, CGPoint(x: 2, y: 3))), CubicBezierPathCommand( "C", ControlPointsValue(CGPoint(x: 0, y: 1), CGPoint(x: 2, y: 3), CGPoint(x: 4, y: 5))), CubicBezierPathCommand("S", ControlPointsValue(nil, CGPoint(x: 0, y: 1), CGPoint(x: 2, y: 3))), ] ) } func testParsesAbsoluteCommandsWithAdjacent() throws { let result = PathParser("M 0 1 L 200 201 -100 -101 V2,3").parse() XCTAssertEqual( result, [ PointPathCommand("M", PointValue(CGPoint(x: 0, y: 1))), PointPathCommand("L", PointValue(CGPoint(x: 200, y: 201))), PointPathCommand("L", PointValue(CGPoint(x: -100, y: -101))), NumericPathCommand("V", 2), NumericPathCommand("V", 3), ] ) } } ================================================ FILE: Tests/SendKeysTests/TransformerTests.swift ================================================ import XCTest @testable import SendKeysLib final class TransformerTests: XCTestCase { func testShouldNoteTransformSingleLine() { let transformer = Transformer(indent: true) let result = transformer.transform("hello world") XCTAssertEqual(result, "hello world") } func testShouldNotTransformCurlyBraceOnSameLine() { let transformer = Transformer(indent: true) let result = transformer.transform("{}") XCTAssertEqual(result, "{}") } func testTransformCurlyBraceOnDifferentLine() { let transformer = Transformer(indent: true) let result = transformer.transform("{\n}") XCTAssertEqual(result, "{<\\>\n") } func testTransformCurlyBraceWithBasicContent() { let transformer = Transformer(indent: true) let result = transformer.transform("hello {\n world\n}") XCTAssertEqual(result, "hello {\nworld<\\>\n") } func testTransformBracketAndCurlyBraceWithBasicContent() { let transformer = Transformer(indent: true) let result = transformer.transform("hello ({\n world\n})") XCTAssertEqual(result, "hello ({\nworld<\\>\n") } } ================================================ FILE: Tests/SendKeysTests/XCTestManifests.swift ================================================ import XCTest #if !canImport(ObjectiveC) public func allTests() -> [XCTestCaseEntry] { return [ testCase(sendkeysTests.allTests) ] } #endif ================================================ FILE: Tests/SendKeysTests/sendkeysTests.swift ================================================ import XCTest import class Foundation.Bundle final class sendkeysTests: XCTestCase { func testHelp() throws { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. // Some of the APIs that we use below are available in macOS 10.13 and above. guard #available(macOS 10.13, *) else { return } let fooBinary = productsDirectory.appendingPathComponent("sendkeys") let process = Process() process.executableURL = fooBinary process.arguments = ["--help"] let pipe = Pipe() process.standardOutput = pipe try process.run() process.waitUntilExit() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) XCTAssertTrue(output!.hasPrefix("OVERVIEW: Command line tool for automating keystrokes and mouse events")) } /// Returns path to the built products directory. var productsDirectory: URL { #if os(macOS) for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { return bundle.bundleURL.deletingLastPathComponent() } fatalError("Couldn't find the products directory\n") #else return Bundle.main.bundleURL #endif } } ================================================ FILE: Tests/keys.txt ================================================ 0123456789<\> abcdefghijklmnopqrstuvwxyz<\> ABCDEFGHIJKLMNOPQRSTUVWXYZ<\> -,;.'[]/\`=<\> <\> ================================================ FILE: examples/.sendkeysrc.yml ================================================ # All properties are optional send: activate: true animationInterval: 0.01 delay: 0.1 initialDelay: 1 keyboardLayout: qwerty targeted: false terminateCommand: "c:control" remap: # Example of custom key mapping to support dvorak keyboard layout "[": "-" "]": "=" "'": "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": "/" mousePosition: watch: true output: commands duration: 1 transform: indent: true autoClose: "}])" ================================================ FILE: examples/node.js ================================================ const http = require('http'); const hostname = '127.0.0.1'; const port = 3000; const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World'); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); }); ================================================ FILE: scripts/bottle.sh ================================================ #!/usr/bin/env bash set -e cwd=`pwd` script_folder=`cd $(dirname $0) && pwd` version=$1 formula_template=$script_folder/../Formula/sendkeys_template.rb formula=$script_folder/../Formula/sendkeys.rb url="file://$cwd/sendkeys.tar.gz?date=`date +%s`" sed_url=`echo $url | sed 's/\\//\\\\\//g'` version=`echo $version | sed -E 's/^v//g'` rm sendkeys*.tar.gz || true tar zcvf sendkeys.tar.gz --exclude=".git" --exclude=".build" ./ cp $formula_template $formula # update url sed -E -i "" "s/url \"\"/url \"$sed_url\"/g" $formula # update version number sed -E -i "" "s/version \"[0-9]+\.[0-9]+\.[0-9]+\"/version \"$version\"/g" $formula brew install --force --formula --build-bottle $formula echo "Bottle built" brew bottle sendkeys --force-core-tap --no-rebuild --root-url "https://github.com/socsieng/sendkeys/releases/download/v${version}" bottle=`ls sendkeys--$version.*.tar.gz` bottle_big_sur="sendkeys-$version.big_sur.bottle.tar.gz" bottle_arm64_big_sur="sendkeys-$version.arm64_big_sur.bottle.tar.gz" cp $bottle $bottle_big_sur cp $bottle $bottle_arm64_big_sur echo "big_sur=$bottle_big_sur" >> $GITHUB_OUTPUT echo "arm64_big_sur=$bottle_arm64_big_sur" >> $GITHUB_OUTPUT echo "root_url=https://github.com/socsieng/sendkeys/releases/download/v${version}" >> $GITHUB_OUTPUT echo "url=https://github.com/socsieng/sendkeys/releases/download/v${version}/$bottle_rename" >> $GITHUB_OUTPUT echo "sha=$(shasum -a 256 $bottle | awk '{printf $1}')" >> $GITHUB_OUTPUT brew uninstall sendkeys ================================================ FILE: scripts/code-coverage.sh ================================================ #!/usr/bin/env bash set -e cwd=`pwd` script_folder=`cd $(dirname $0) && pwd` build_folder=$script_folder/../.build output_folder=$script_folder/../.output mkdir -p $output_folder swift test --enable-code-coverage # if in the CI environment, then use llvm-cov report instead if [ -n "$CI" ]; then xcrun llvm-cov report \ --ignore-filename-regex='(.build|Tests)[/\\].*' \ -instr-profile "$(swift test --show-codecov-path | xargs dirname)/default.profdata" \ .build/debug/sendkeysPackageTests.xctest/Contents/*/sendkeysPackageTests exit 0 fi xcrun llvm-cov export \ --format=lcov \ --ignore-filename-regex='(.build|Tests)[/\\].*' \ -instr-profile "$(swift test --show-codecov-path | xargs dirname)/default.profdata" \ .build/debug/sendkeysPackageTests.xctest/Contents/*/sendkeysPackageTests > "$output_folder/coverage.lcov" genhtml -o "$output_folder/coverage" "$output_folder/coverage.lcov" open "file://$output_folder/coverage/index.html" ================================================ FILE: scripts/format.sh ================================================ #!/usr/bin/env bash set -e cwd=`pwd` script_folder=`cd $(dirname $0) && pwd` file=$1 if [ -z "$file" ] then # all files swift-format --configuration $script_folder/../.swift-format -ir $script_folder/../*.swift $script_folder/../Sources $script_folder/../Tests else swift-format --configuration $script_folder/../.swift-format -ir $file fi ================================================ FILE: scripts/install-pre-commit.sh ================================================ #!/usr/bin/env bash set -e script_folder=`cd $(dirname $0) && pwd` repo_folder=`git rev-parse --show-toplevel` cp -f $script_folder/pre-commit.sh $repo_folder/.git/hooks/pre-commit ================================================ FILE: scripts/pre-commit.sh ================================================ #!/usr/bin/env bash set -e # get the repository root repo_folder=`git rev-parse --show-toplevel` # use repository root if there is a value, otherwise use the current folder root_folder=${repo_folder:-`pwd`} files=`git diff --cached --name-only --diff-filter=ACM` for f in $files do if [[ $f == *.swift ]] then echo "Formatting $f" $root_folder/scripts/format.sh $f git add $f fi done ================================================ FILE: scripts/update-version.sh ================================================ #!/usr/bin/env bash set -e cwd=`pwd` script_folder=`cd $(dirname $0) && pwd` version=${1:-`cat $script_folder/../version.txt`} version=`echo $version | sed -E 's/^v//g'` echo "updating version to $version" sed -E -i "" "s/version: \"[0-9]+\.[0-9]+\.[0-9]+\", \/\* auto-updated \*\//version: \"$version\", \/\* auto-updated \*\//g" $script_folder/../Sources/SendKeysLib/SendKeysCli.swift ================================================ FILE: scripts/verify-output.sh ================================================ #!/usr/bin/env bash set -e cwd=`pwd` script_folder=`cd $(dirname $0) && pwd` output_folder="$script_folder/../.output" build_folder="$script_folder/../.build/debug" examples_folder="$script_folder/../examples" mkdir -p $output_folder rm -f $output_folder/example.js touch $output_folder/example.js code $output_folder/example.js sleep 1 $build_folder/sendkeys transform -f $examples_folder/node.js | $build_folder/sendkeys -d 0.05 $build_folder/sendkeys -c '' expected_output=`cat $examples_folder/node.js` result=`cat $output_folder/example.js` if [[ "$expected_output" != "$result" ]]; then echo "transform test failed." exit 1 fi ================================================ FILE: version.txt ================================================ 4.3.1