kanata-1.9.0/.cargo_vcs_info.json0000644000000001360000000000100123020ustar { "git": { "sha1": "b111e370d2ec9fcff17a8a5193a1dbee775b88d9" }, "path_in_vcs": "" }kanata-1.9.0/.devcontainer/devcontainer.json000064400000000000000000000011521046102023000172040ustar 00000000000000{ "name": "Rust", "image": "mcr.microsoft.com/devcontainers/rust:1-buster" // Features to add to the dev container. More info: https://containers.dev/implementors/features. // "features": {}, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "rustc --version", // Configure tool-specific properties. // "customizations": {}, // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" } kanata-1.9.0/.github/ISSUE_TEMPLATE/bug_report.yml000064400000000000000000000054441046102023000175170ustar 00000000000000name: "Bug report" description: Create a report to help us improve. labels: ["bug"] assignees: ["jtroo"] title: "Bug: title_goes_here" body: - type: checkboxes attributes: label: Requirements description: Before you create a bug report, please check the following options: - label: I've searched [platform-specific issues](https://github.com/jtroo/kanata/blob/main/docs/platform-known-issues.adoc), [issues](https://github.com/jtroo/kanata/issues) and [discussions](https://github.com/jtroo/kanata/discussions) to see if this has been reported before. required: true - label: My issue does not involve multiple simultaneous key presses, OR it does but I've confirmed it is not [key rollover or ghosting](https://github.com/jtroo/kanata/discussions/822). required: true - type: textarea id: summary attributes: label: Describe the bug description: A clear and concise description of what the bug is. validations: required: true - type: textarea id: config attributes: label: Relevant kanata config description: E.g. defcfg, defsrc, deflayer, defalias items. If in doubt, feel free to include your entire config. Please ensure to use code formatting, e.g. surround with triple backticks to avoid pinging users with the @ character. validations: required: false - type: textarea id: reproduce attributes: label: To Reproduce description: | Walk us through the steps needed to reproduce the bug. value: | 1. 2. 3. validations: required: true - type: textarea id: expected attributes: label: Expected behavior description: A clear and concise description of what you expected to happen. validations: required: true - type: input id: version attributes: label: Kanata version description: The kanata version prints in the log on startup, or you can also print it by passing the `--version` flag when running on the command line. placeholder: e.g. kanata 1.3.0 validations: required: true - type: textarea id: logs attributes: label: Debug logs description: If you think it might help with a non-obvious issue, run kanata from the command line and pass the `--debug` flag. This will print more info. Include the relevant log outputs this section if you did so. validations: required: false - type: input id: os attributes: label: Operating system description: Linux or Windows? placeholder: e.g. Windows 11 validations: required: true - type: textarea id: additional attributes: label: Additional context description: Add any other context about the problem here. validations: required: false kanata-1.9.0/.github/ISSUE_TEMPLATE/config.yml000064400000000000000000000002541046102023000166060ustar 00000000000000blank_issues_enabled: true contact_links: - name: Discussions url: https://github.com/jtroo/kanata/discussions about: Ask for help or interact with the community.kanata-1.9.0/.github/ISSUE_TEMPLATE/feature_request.yml000064400000000000000000000021351046102023000205440ustar 00000000000000name: "Feature request" description: Suggest an idea for this project title: 'Feature request: feature_summary_goes_here' labels: ["enhancement"] assignees: [] body: - type: textarea attributes: label: Is your feature request related to a problem? Please describe. description: | A clear and concise description of what the problem is. placeholder: Ex. I'm always frustrated when [...] validations: required: true - type: textarea attributes: label: Describe the solution you'd like. description: | A clear and concise description of what you want to happen. validations: required: true - type: textarea attributes: label: Describe alternatives you've considered. description: | A clear and concise description of any alternative solutions or features you've considered. validations: required: true - type: textarea attributes: label: Additional context description: | Add any other context or screenshots about the feature request here. validations: required: false kanata-1.9.0/.github/pull_request_template.md000064400000000000000000000004501046102023000173720ustar 00000000000000## Describe your changes. Use imperative present tense. ## Checklist - Add documentation to docs/config.adoc - [ ] Yes or N/A - Add example and basic docs to cfg_samples/kanata.kbd - [ ] Yes or N/A - Update error messages - [ ] Yes or N/A - Added tests, or did manual testing - [ ] Yes kanata-1.9.0/.github/workflows/macos-build.yml000064400000000000000000000033471046102023000174200ustar 00000000000000name: macos-build on: workflow_dispatch: branches: [ "main" ] env: CARGO_TERM_COLOR: always RUSTFLAGS: "-Dwarnings" jobs: build-macos-aarch: runs-on: macos-latest steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true target: aarch64-apple-darwin - uses: Swatinem/rust-cache@v2 - name: Do the stuff on arm64 shell: bash run: | mkdir -p artifacts-arm64 cargo build --release --target aarch64-apple-darwin mv target/aarch64-apple-darwin/release/kanata artifacts-arm64/kanata_macos_arm64 cargo build --release --features cmd --target aarch64-apple-darwin mv target/aarch64-apple-darwin/release/kanata artifacts-arm64/kanata_macos_cmd_allowed_arm64 - uses: actions/upload-artifact@v4 with: name: macos-binaries-arm64 path: | artifacts-arm64/kanata_macos_arm64 artifacts-arm64/kanata_macos_cmd_allowed_arm64 build-macos: runs-on: macos-13 steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 with: shared-key: "persist-cross-job" - name: Do the stuff on x86-64 shell: bash run: | mkdir -p artifacts cargo build --release mv target/release/kanata artifacts/kanata_macos_x86_64 cargo build --release --features cmd mv target/release/kanata artifacts/kanata_macos_cmd_allowed_x86_64 - uses: actions/upload-artifact@v4 with: name: macos-binaries-x86-64 path: | artifacts/kanata_macos_x86_64 artifacts/kanata_macos_cmd_allowed_x86_64 kanata-1.9.0/.github/workflows/rust.yml000064400000000000000000000103231046102023000162060ustar 00000000000000name: cargo-checks on: push: branches: [ "main" ] paths: - Cargo.* - src/**/* - keyberon/**/* - cfg_samples/**/* - parser/**/* - .github/workflows/rust.yml pull_request: branches: [ "main" ] paths: - Cargo.* - src/**/* - keyberon/**/* - parser/**/* - cfg_samples/**/* - .github/workflows/rust.yml env: CARGO_TERM_COLOR: always RUSTFLAGS: "-Dwarnings" jobs: fmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Check fmt run: cargo fmt --all --check build-test-clippy-linux: runs-on: ${{ matrix.os }} strategy: matrix: include: - build: linux os: ubuntu-latest target: x86_64-unknown-linux-musl steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 with: shared-key: "persist-cross-job" workspaces: ./ - run: rustup component add clippy - name: Run tests no features run: cargo test --all --no-default-features - name: Run clippy no features run: cargo clippy --all --no-default-features -- -D warnings - name: Run tests default features run: cargo test --all - name: Run clippy default features run: cargo clippy --all -- -D warnings - name: Run tests cmd run: cargo test --all --features=cmd - name: Run clippy cmd run: cargo clippy --all --features=cmd -- -D warnings - name: Run tests simulated output run: cargo test --features=simulated_output -- sim_tests - name: Run clippy simulated output run: cargo clippy --all --features=simulated_output,cmd -- -D warnings - name: Run clippy for parser with lsp feature run: cargo clippy -p kanata-parser --features=lsp -- -D warnings build-test-clippy-windows: runs-on: ${{ matrix.os }} strategy: matrix: include: - build: windows os: windows-latest target: x86_64-pc-windows-msvc steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 with: shared-key: "persist-cross-job" workspaces: ./ - run: rustup component add clippy - name: Run tests no features run: cargo test --all --no-default-features - name: Run clippy no features run: cargo clippy --all --no-default-features -- -D warnings - name: Run tests default features run: cargo test --all - name: Run clippy default features run: cargo clippy --all -- -D warnings - name: Run tests winIOv2 run: cargo test --all --features=cmd,win_llhook_read_scancodes,win_sendinput_send_scancodes - name: Run clippy all winIOv2 run: cargo clippy --all --features=cmd,win_llhook_read_scancodes,win_sendinput_send_scancodes -- -D warnings - name: Run tests all features run: cargo test --all --features=cmd,interception_driver,win_sendinput_send_scancodes - name: Run clippy all features run: cargo clippy --all --features=cmd,interception_driver,win_sendinput_send_scancodes -- -D warnings - name: Run tests simulated output run: cargo test --features=simulated_output -- sim_tests - name: Run clippy simulated output run: cargo clippy --all --features=simulated_output,cmd -- -D warnings - name: Run tests gui run: cargo test --all --features=gui - name: Run clippy gui run: cargo clippy --all --features=gui -- -D warnings - name: Check gui+cmd+interception run: cargo check --features gui,cmd,interception_driver build-test-clippy-macos: runs-on: ${{ matrix.os }} strategy: matrix: include: - build: macos os: macos-latest target: x86_64-apple-darwin steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 with: shared-key: "persist-cross-job" workspaces: ./ - run: rustup component add clippy - name: Run tests default features run: cargo test --all - name: Run clippy default features run: cargo clippy --all -- -D warnings - name: Run tests cmd run: cargo test --all --features=cmd - name: Run clippy all features run: cargo clippy --all --features=cmd -- -D warnings kanata-1.9.0/.github/workflows/windows-build.yml000064400000000000000000000016341046102023000200050ustar 00000000000000name: windows-build on: workflow_dispatch: branches: [ "main" ] env: CARGO_TERM_COLOR: always RUSTFLAGS: "-Dwarnings" jobs: build-windows: runs-on: windows-latest steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 with: shared-key: "persist-cross-job" - name: Build x64 shell: bash run: | mkdir -p artifacts cargo build --release --target x86_64-pc-windows-msvc move target\release\kanata.exe artifacts\kanata_windows_x64.exe cargo build --release --features cmd --target x86_64-pc-windows-msvc move target\release\kanata.exe artifacts\kanata_windows_cmd_allowed_x64.exe - uses: actions/upload-artifact@v4 with: name: windows-binaries-x64 path: | artifacts/kanata_windows_x64.exe artifacts/kanata_windows_cmd_allowed_x64.exe kanata-1.9.0/.gitignore000064400000000000000000000000231046102023000130550ustar 00000000000000**/target .vscode/ kanata-1.9.0/Cargo.lock0000644000001516600000000000100102660ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anstyle" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" [[package]] name = "anyhow" version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "arboard" version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" dependencies = [ "clipboard-win", "core-graphics 0.23.2", "image", "log", "objc2", "objc2-app-kit", "objc2-foundation", "parking_lot", "windows-sys 0.48.0", "x11rb", ] [[package]] name = "arraydeque" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "atomic-polyfill" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" dependencies = [ "critical-section", ] [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", ] [[package]] name = "backtrace-ext" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" dependencies = [ "backtrace", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bitvec" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" dependencies = [ "funty", "radium", "tap", "wyz", ] [[package]] name = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ "objc2", ] [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder-lite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "cc" version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "jobserver", "libc", "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_derive" version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "clipboard-win" version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", "core-graphics-types 0.1.3", "foreign-types", "libc", ] [[package]] name = "core-graphics" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.6.0", "core-foundation 0.10.0", "core-graphics-types 0.2.0", "foreign-types", "libc", ] [[package]] name = "core-graphics-types" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", "libc", ] [[package]] name = "core-graphics-types" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.6.0", "core-foundation 0.10.0", "libc", ] [[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "critical-section" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] [[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys 0.48.0", ] [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "embed-resource" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4e24052d7be71f0efb50c201557f6fe7d237cfd5a64fd5bcd7fd8fe32dbbffa" dependencies = [ "cc", "memchr", "rustc_version", "toml", "vswhom", "winreg", ] [[package]] name = "encode_unicode" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "endian-type" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "error-code" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "evdev" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab6055a93a963297befb0f4f6e18f314aec9767a4bbe88b151126df2433610a7" dependencies = [ "bitvec", "cfg-if", "libc", "nix 0.23.2", "thiserror", ] [[package]] name = "fdeflate" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] [[package]] name = "flate2" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "foreign-types" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", "foreign-types-shared", ] [[package]] name = "foreign-types-macros" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "foreign-types-shared" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "funty" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "gethostname" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", "windows-targets 0.48.5", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "hash32" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" dependencies = [ "byteorder", ] [[package]] name = "hashbrown" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "heapless" version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ "atomic-polyfill", "hash32", "rustc_version", "spin", "stable_deref_trait", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "image" version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", "num-traits", "png", "tiff", ] [[package]] name = "indexmap" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "indoc" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "inotify" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" dependencies = [ "bitflags 1.3.2", "inotify-sys", "libc", ] [[package]] name = "inotify-sys" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" dependencies = [ "libc", ] [[package]] name = "instant" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", "js-sys", "wasm-bindgen", "web-sys", ] [[package]] name = "interception-sys" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abe875cf6439eb5d98f0fdbcc6128fdef4870c47e0f898735105ff77685dfcd1" [[package]] name = "is-docker" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" dependencies = [ "once_cell", ] [[package]] name = "is-terminal" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ "hermit-abi", "libc", "windows-sys 0.52.0", ] [[package]] name = "is-wsl" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" dependencies = [ "is-docker", "once_cell", ] [[package]] name = "is_ci" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "itertools" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "jpeg-decoder" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] [[package]] name = "kanata" version = "1.9.0" dependencies = [ "anyhow", "arboard", "clap", "core-graphics 0.24.0", "dirs", "embed-resource", "encode_unicode", "evdev", "indoc", "inotify", "instant", "kanata-interception", "kanata-keyberon", "kanata-parser", "kanata-tcp-protocol", "karabiner-driverkit", "libc", "log", "miette", "mio", "muldiv 1.0.1", "native-windows-gui", "nix 0.26.4", "objc", "once_cell", "open", "os_pipe", "parking_lot", "radix_trie", "regex", "rustc-hash", "sd-notify", "serde_json", "signal-hook", "simplelog", "strip-ansi-escapes", "time", "winapi", "windows-sys 0.52.0", ] [[package]] name = "kanata-interception" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e57b9b964d19ac86906ea56e8a88001eb03c2b9cb96e4127225438750bb606" dependencies = [ "bitflags 1.3.2", "interception-sys", "num_enum", "serde", ] [[package]] name = "kanata-keyberon" version = "0.190.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5ef0641559249e72d2d4675bdbb5060b729f024fe4c01d704c3ef390e5de1d" dependencies = [ "arraydeque", "heapless", "kanata-keyberon-macros", "rustc-hash", ] [[package]] name = "kanata-keyberon-macros" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f6f83f03390a5c13bbf68abea76a2b9527e197f5c00026805fd7af62a34752" dependencies = [ "proc-macro2", "quote", ] [[package]] name = "kanata-parser" version = "0.190.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824024d15f285efd01869821354885514a7bf58c8621d4a3db7776d5a6feb53c" dependencies = [ "anyhow", "bitflags 2.6.0", "bytemuck", "itertools", "kanata-keyberon", "log", "miette", "once_cell", "parking_lot", "patricia_tree", "rustc-hash", "thiserror", ] [[package]] name = "kanata-tcp-protocol" version = "0.190.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa2cdbb7f8e06ed6fe1548a49bb18efe3c0a140c194a76e0b40468dea7349c11" dependencies = [ "serde", "serde_derive", "serde_json", ] [[package]] name = "karabiner-driverkit" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000dc9189dc126ed5ec1ba477b3f524d88f1527260b423dbc79d23356c82165f" dependencies = [ "cc", "os_info", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", ] [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "malloc_buf" version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" dependencies = [ "libc", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ "autocfg", ] [[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "miette" version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" dependencies = [ "backtrace", "backtrace-ext", "is-terminal", "miette-derive", "once_cell", "owo-colors", "supports-color", "supports-hyperlinks", "supports-unicode", "terminal_size", "textwrap", "thiserror", "unicode-width", ] [[package]] name = "miette-derive" version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "miniz_oxide" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", "simd-adler32", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", "wasi", "windows-sys 0.48.0", ] [[package]] name = "muldiv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" [[package]] name = "muldiv" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" [[package]] name = "native-windows-gui" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f7003a669f68deb6b7c57d74fff4f8e533c44a3f0b297492440ef4ff5a28454" dependencies = [ "bitflags 1.3.2", "lazy_static", "muldiv 0.2.1", "winapi", "winapi-build", ] [[package]] name = "nibble_vec" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" dependencies = [ "smallvec", ] [[package]] name = "nix" version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" dependencies = [ "bitflags 1.3.2", "cc", "cfg-if", "libc", "memoffset 0.6.5", ] [[package]] name = "nix" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", "memoffset 0.7.1", "pin-utils", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_enum" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", ] [[package]] name = "num_threads" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] [[package]] name = "objc" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", ] [[package]] name = "objc-sys" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" [[package]] name = "objc2" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ "objc-sys", "objc2-encode", ] [[package]] name = "objc2-app-kit" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.6.0", "block2", "libc", "objc2", "objc2-core-data", "objc2-core-image", "objc2-foundation", "objc2-quartz-core", ] [[package]] name = "objc2-core-data" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", ] [[package]] name = "objc2-core-image" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2", "objc2", "objc2-foundation", "objc2-metal", ] [[package]] name = "objc2-encode" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.6.0", "block2", "libc", "objc2", ] [[package]] name = "objc2-metal" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", ] [[package]] name = "objc2-quartz-core" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", "objc2-metal", ] [[package]] name = "object" version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "open" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" dependencies = [ "is-wsl", "libc", "pathdiff", ] [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "os_info" version = "3.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" dependencies = [ "log", "serde", "windows-sys 0.52.0", ] [[package]] name = "os_pipe" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "owo-colors" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "pathdiff" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" [[package]] name = "patricia_tree" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31f2f4539bffe53fc4b4da301df49d114b845b077bd5727b7fe2bd9d8df2ae68" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "png" version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro-crate" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", "toml_edit 0.19.15", ] [[package]] name = "proc-macro2" version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "radium" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "radix_trie" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" dependencies = [ "endian-type", "nibble_vec", ] [[package]] name = "redox_syscall" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", "thiserror", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.59.0", ] [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sd-notify" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1be20c5f7f393ee700f8b2f28ea35812e4e212f40774b550cd2a93ea91684451" [[package]] name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simplelog" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" dependencies = [ "log", "termcolor", "time", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smawk" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" dependencies = [ "lock_api", ] [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strip-ansi-escapes" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" dependencies = [ "vte", ] [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "supports-color" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" dependencies = [ "is-terminal", "is_ci", ] [[package]] name = "supports-hyperlinks" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" dependencies = [ "is-terminal", ] [[package]] name = "supports-unicode" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f850c19edd184a205e883199a261ed44471c81e39bd95b1357f5febbef00e77a" dependencies = [ "is-terminal", ] [[package]] name = "syn" version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tap" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "terminal_size" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" dependencies = [ "libc", "winapi", ] [[package]] name = "textwrap" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" dependencies = [ "smawk", "unicode-linebreak", "unicode-width", ] [[package]] name = "thiserror" version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tiff" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" dependencies = [ "flate2", "jpeg-decoder", "weezl", ] [[package]] name = "time" version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", "libc", "num-conv", "num_threads", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", ] [[package]] name = "toml" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit 0.22.22", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow 0.6.20", ] [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-linebreak" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vswhom" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" dependencies = [ "libc", "vswhom-sys", ] [[package]] name = "vswhom-sys" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" dependencies = [ "cc", "libc", ] [[package]] name = "vte" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" dependencies = [ "utf8parse", "vte_generate_state_changes", ] [[package]] name = "vte_generate_state_changes" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" dependencies = [ "proc-macro2", "quote", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "web-sys" version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "weezl" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-build" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] [[package]] name = "winnow" version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] [[package]] name = "winreg" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ "cfg-if", "windows-sys 0.48.0", ] [[package]] name = "wyz" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ "tap", ] [[package]] name = "x11rb" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", "rustix", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" kanata-1.9.0/Cargo.toml0000644000000142630000000000100103060ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "kanata" version = "1.9.0" authors = ["jtroo "] build = "build.rs" autolib = false autobins = false autoexamples = false autotests = false autobenches = false default-run = "kanata" description = "Multi-layer keyboard customization" homepage = "https://github.com/jtroo/kanata" readme = "README.md" keywords = [ "keyboard", "layout", "remapping", ] categories = ["command-line-utilities"] license = "LGPL-3.0-only" repository = "https://github.com/jtroo/kanata" [features] cmd = ["kanata-parser/cmd"] default = [ "tcp_server", "win_sendinput_send_scancodes", "zippychord", ] gui = [ "win_manifest", "kanata-parser/gui", "win_sendinput_send_scancodes", "win_llhook_read_scancodes", "muldiv", "strip-ansi-escapes", "open", "dep:windows-sys", "winapi/processthreadsapi", "native-windows-gui/tray-notification", "native-windows-gui/message-window", "native-windows-gui/menu", "native-windows-gui/cursor", "native-windows-gui/high-dpi", "native-windows-gui/embed-resource", "native-windows-gui/image-decoder", "native-windows-gui/notice", "native-windows-gui/animation-timer", ] interception_driver = [ "kanata-interception", "kanata-parser/interception_driver", ] passthru_ahk = [ "simulated_input", "simulated_output", ] perf_logging = [] simulated_input = ["indoc"] simulated_output = ["indoc"] tcp_server = ["serde_json"] wasm = ["instant/wasm-bindgen"] win_llhook_read_scancodes = ["kanata-parser/win_llhook_read_scancodes"] win_manifest = [ "embed-resource", "indoc", "regex", ] win_sendinput_send_scancodes = ["kanata-parser/win_sendinput_send_scancodes"] zippychord = ["kanata-parser/zippychord"] [lib] name = "kanata_state_machine" path = "src/lib.rs" [[bin]] name = "kanata" path = "src/main.rs" [dependencies.anyhow] version = "1" [dependencies.clap] version = "4" features = [ "std", "derive", "help", "suggestions", ] default-features = false [dependencies.dirs] version = "5.0.1" [dependencies.indoc] version = "2.0.4" optional = true [dependencies.instant] version = "0.1.12" default-features = false [dependencies.kanata-keyberon] version = "0.190.0" [dependencies.kanata-parser] version = "0.190.0" [dependencies.kanata-tcp-protocol] version = "0.190.0" [dependencies.log] version = "0.4.8" default-features = false [dependencies.miette] version = "5.7.0" features = ["fancy"] [dependencies.once_cell] version = "1" [dependencies.parking_lot] version = "0.12" [dependencies.radix_trie] version = "0.2" [dependencies.rustc-hash] version = "1.1.0" [dependencies.serde_json] version = "1" features = ["std"] optional = true default-features = false [dependencies.simplelog] version = "0.12.0" [dependencies.time] version = "0.3.36" [target.'cfg(not(target_arch = "wasm32"))'.dependencies.arboard] version = "3.4" [target.'cfg(target_os = "linux")'.dependencies.evdev] version = "0.12.2" [target.'cfg(target_os = "linux")'.dependencies.inotify] version = "0.10.0" default-features = false [target.'cfg(target_os = "linux")'.dependencies.mio] version = "0.8.11" features = [ "os-poll", "os-ext", ] [target.'cfg(target_os = "linux")'.dependencies.nix] version = "0.26.1" features = ["ioctl"] [target.'cfg(target_os = "linux")'.dependencies.open] version = "5" optional = true [target.'cfg(target_os = "linux")'.dependencies.sd-notify] version = "0.4.1" [target.'cfg(target_os = "linux")'.dependencies.signal-hook] version = "0.3.14" [target.'cfg(target_os = "macos")'.dependencies.core-graphics] version = "0.24.0" [target.'cfg(target_os = "macos")'.dependencies.karabiner-driverkit] version = "0.1.5" [target.'cfg(target_os = "macos")'.dependencies.libc] version = "0.2" [target.'cfg(target_os = "macos")'.dependencies.objc] version = "0.2.7" [target.'cfg(target_os = "macos")'.dependencies.open] version = "5" optional = true [target.'cfg(target_os = "macos")'.dependencies.os_pipe] version = "1.2.1" [target.'cfg(target_os = "windows")'.dependencies.encode_unicode] version = "0.3.6" [target.'cfg(target_os = "windows")'.dependencies.kanata-interception] version = "0.3.0" optional = true [target.'cfg(target_os = "windows")'.dependencies.muldiv] version = "1.0.1" optional = true [target.'cfg(target_os = "windows")'.dependencies.native-windows-gui] version = "1.0.13" default-features = false [target.'cfg(target_os = "windows")'.dependencies.open] version = "5" features = ["shellexecute-on-windows"] optional = true [target.'cfg(target_os = "windows")'.dependencies.regex] version = "1.10.4" optional = true [target.'cfg(target_os = "windows")'.dependencies.strip-ansi-escapes] version = "0.2.0" optional = true [target.'cfg(target_os = "windows")'.dependencies.winapi] version = "0.3.9" features = [ "wincon", "timeapi", "mmsystem", ] [target.'cfg(target_os = "windows")'.dependencies.windows-sys] version = "0.52.0" features = [ "Win32_Devices_DeviceAndDriverInstallation", "Win32_Devices_Usb", "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_Security", "Win32_System_Diagnostics_Debug", "Win32_System_Registry", "Win32_System_Threading", "Win32_UI_Controls", "Win32_UI_Shell", "Win32_UI_HiDpi", "Win32_UI_WindowsAndMessaging", "Win32_System_SystemInformation", "Wdk", "Wdk_System", "Wdk_System_SystemServices", ] optional = true [target.'cfg(target_os = "windows")'.build-dependencies.embed-resource] version = "2.4.2" optional = true [target.'cfg(target_os = "windows")'.build-dependencies.indoc] version = "2.0.4" optional = true [target.'cfg(target_os = "windows")'.build-dependencies.regex] version = "1.10.4" optional = true [profile.release] opt-level = "z" lto = "fat" codegen-units = 1 panic = "abort" kanata-1.9.0/Cargo.toml.orig0000644000000111160000000000100112370ustar [workspace] members = [ "./", "parser", "keyberon", "example_tcp_client", "tcp_protocol", "windows_key_tester", "simulated_input", "simulated_passthru", ] exclude = [ "interception", "key-sort-add", "wasm", ] resolver = "2" [package] name = "kanata" version = "1.9.0" authors = ["jtroo "] description = "Multi-layer keyboard customization" keywords = ["keyboard", "layout", "remapping"] categories = ["command-line-utilities"] homepage = "https://github.com/jtroo/kanata" repository = "https://github.com/jtroo/kanata" readme = "README.md" license = "LGPL-3.0-only" edition = "2021" default-run = "kanata" [lib] name = "kanata_state_machine" path = "src/lib.rs" [[bin]] name = "kanata" path = "src/main.rs" [dependencies] anyhow = "1" clap = { version = "4", features = [ "std", "derive", "help", "suggestions" ], default-features = false } dirs = "5.0.1" indoc = { version = "2.0.4", optional = true } instant = { version = "0.1.12", default-features = false } log = { version = "0.4.8", default-features = false } miette = { version = "5.7.0", features = ["fancy"] } once_cell = "1" parking_lot = "0.12" radix_trie = "0.2" rustc-hash = "1.1.0" simplelog = "0.12.0" serde_json = { version = "1", features = ["std"], default-features = false, optional = true } time = "0.3.36" kanata-keyberon = { path = "keyberon", version = "0.190.0" } kanata-parser = { path = "parser", version = "0.190.0" } kanata-tcp-protocol = { path = "tcp_protocol", version = "0.190.0" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] arboard = "3.4" [target.'cfg(target_os = "macos")'.dependencies] karabiner-driverkit = "0.1.5" objc = "0.2.7" core-graphics = "0.24.0" open = { version = "5", optional = true } libc = "0.2" os_pipe = "1.2.1" [target.'cfg(target_os = "linux")'.dependencies] evdev = "0.12.2" inotify = { version = "0.10.0", default-features = false } mio = { version = "0.8.11", features = ["os-poll", "os-ext"] } nix = { version = "0.26.1", features = ["ioctl"] } open = { version = "5", optional = true } signal-hook = "0.3.14" sd-notify = "0.4.1" [target.'cfg(target_os = "windows")'.dependencies] encode_unicode = "0.3.6" winapi = { version = "0.3.9", features = [ "wincon", "timeapi", "mmsystem", ] } windows-sys = { version = "0.52.0", features = [ "Win32_Devices_DeviceAndDriverInstallation", "Win32_Devices_Usb", "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_Security", "Win32_System_Diagnostics_Debug", "Win32_System_Registry", "Win32_System_Threading", "Win32_UI_Controls", "Win32_UI_Shell", "Win32_UI_HiDpi", "Win32_UI_WindowsAndMessaging", "Win32_System_SystemInformation", "Wdk", "Wdk_System", "Wdk_System_SystemServices", ], optional=true } native-windows-gui = { version = "1.0.13", default-features = false} regex = { version = "1.10.4", optional = true } kanata-interception = { version = "0.3.0", optional = true } muldiv = { version = "1.0.1", optional = true } strip-ansi-escapes = { version = "0.2.0", optional = true } open = { version = "5", features = ["shellexecute-on-windows"], optional = true} # shellexecute fix allows opening files already opened for writing, needs _detached mode [target.'cfg(target_os = "windows")'.build-dependencies] embed-resource = { version = "2.4.2", optional = true } indoc = { version = "2.0.4", optional = true } regex = { version = "1.10.4", optional = true } [features] default = ["tcp_server","win_sendinput_send_scancodes", "zippychord"] perf_logging = [] tcp_server = ["serde_json"] win_sendinput_send_scancodes = ["kanata-parser/win_sendinput_send_scancodes"] win_llhook_read_scancodes = ["kanata-parser/win_llhook_read_scancodes"] win_manifest = ["embed-resource", "indoc", "regex"] cmd = ["kanata-parser/cmd"] interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] simulated_output = ["indoc"] simulated_input = ["indoc"] passthru_ahk = ["simulated_input","simulated_output"] wasm = [ "instant/wasm-bindgen" ] gui = ["win_manifest","kanata-parser/gui", "win_sendinput_send_scancodes","win_llhook_read_scancodes", "muldiv","strip-ansi-escapes","open", "dep:windows-sys", "winapi/processthreadsapi", "native-windows-gui/tray-notification","native-windows-gui/message-window","native-windows-gui/menu","native-windows-gui/cursor","native-windows-gui/high-dpi","native-windows-gui/embed-resource","native-windows-gui/image-decoder","native-windows-gui/notice","native-windows-gui/animation-timer", ] zippychord = ["kanata-parser/zippychord"] [profile.release] opt-level = "z" lto = "fat" panic = "abort" codegen-units = 1 kanata-1.9.0/Cargo.toml.orig000064400000000000000000000111161046102023000137610ustar 00000000000000[workspace] members = [ "./", "parser", "keyberon", "example_tcp_client", "tcp_protocol", "windows_key_tester", "simulated_input", "simulated_passthru", ] exclude = [ "interception", "key-sort-add", "wasm", ] resolver = "2" [package] name = "kanata" version = "1.9.0" authors = ["jtroo "] description = "Multi-layer keyboard customization" keywords = ["keyboard", "layout", "remapping"] categories = ["command-line-utilities"] homepage = "https://github.com/jtroo/kanata" repository = "https://github.com/jtroo/kanata" readme = "README.md" license = "LGPL-3.0-only" edition = "2021" default-run = "kanata" [lib] name = "kanata_state_machine" path = "src/lib.rs" [[bin]] name = "kanata" path = "src/main.rs" [dependencies] anyhow = "1" clap = { version = "4", features = [ "std", "derive", "help", "suggestions" ], default-features = false } dirs = "5.0.1" indoc = { version = "2.0.4", optional = true } instant = { version = "0.1.12", default-features = false } log = { version = "0.4.8", default-features = false } miette = { version = "5.7.0", features = ["fancy"] } once_cell = "1" parking_lot = "0.12" radix_trie = "0.2" rustc-hash = "1.1.0" simplelog = "0.12.0" serde_json = { version = "1", features = ["std"], default-features = false, optional = true } time = "0.3.36" kanata-keyberon = { path = "keyberon", version = "0.190.0" } kanata-parser = { path = "parser", version = "0.190.0" } kanata-tcp-protocol = { path = "tcp_protocol", version = "0.190.0" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] arboard = "3.4" [target.'cfg(target_os = "macos")'.dependencies] karabiner-driverkit = "0.1.5" objc = "0.2.7" core-graphics = "0.24.0" open = { version = "5", optional = true } libc = "0.2" os_pipe = "1.2.1" [target.'cfg(target_os = "linux")'.dependencies] evdev = "0.12.2" inotify = { version = "0.10.0", default-features = false } mio = { version = "0.8.11", features = ["os-poll", "os-ext"] } nix = { version = "0.26.1", features = ["ioctl"] } open = { version = "5", optional = true } signal-hook = "0.3.14" sd-notify = "0.4.1" [target.'cfg(target_os = "windows")'.dependencies] encode_unicode = "0.3.6" winapi = { version = "0.3.9", features = [ "wincon", "timeapi", "mmsystem", ] } windows-sys = { version = "0.52.0", features = [ "Win32_Devices_DeviceAndDriverInstallation", "Win32_Devices_Usb", "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_Security", "Win32_System_Diagnostics_Debug", "Win32_System_Registry", "Win32_System_Threading", "Win32_UI_Controls", "Win32_UI_Shell", "Win32_UI_HiDpi", "Win32_UI_WindowsAndMessaging", "Win32_System_SystemInformation", "Wdk", "Wdk_System", "Wdk_System_SystemServices", ], optional=true } native-windows-gui = { version = "1.0.13", default-features = false} regex = { version = "1.10.4", optional = true } kanata-interception = { version = "0.3.0", optional = true } muldiv = { version = "1.0.1", optional = true } strip-ansi-escapes = { version = "0.2.0", optional = true } open = { version = "5", features = ["shellexecute-on-windows"], optional = true} # shellexecute fix allows opening files already opened for writing, needs _detached mode [target.'cfg(target_os = "windows")'.build-dependencies] embed-resource = { version = "2.4.2", optional = true } indoc = { version = "2.0.4", optional = true } regex = { version = "1.10.4", optional = true } [features] default = ["tcp_server","win_sendinput_send_scancodes", "zippychord"] perf_logging = [] tcp_server = ["serde_json"] win_sendinput_send_scancodes = ["kanata-parser/win_sendinput_send_scancodes"] win_llhook_read_scancodes = ["kanata-parser/win_llhook_read_scancodes"] win_manifest = ["embed-resource", "indoc", "regex"] cmd = ["kanata-parser/cmd"] interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] simulated_output = ["indoc"] simulated_input = ["indoc"] passthru_ahk = ["simulated_input","simulated_output"] wasm = [ "instant/wasm-bindgen" ] gui = ["win_manifest","kanata-parser/gui", "win_sendinput_send_scancodes","win_llhook_read_scancodes", "muldiv","strip-ansi-escapes","open", "dep:windows-sys", "winapi/processthreadsapi", "native-windows-gui/tray-notification","native-windows-gui/message-window","native-windows-gui/menu","native-windows-gui/cursor","native-windows-gui/high-dpi","native-windows-gui/embed-resource","native-windows-gui/image-decoder","native-windows-gui/notice","native-windows-gui/animation-timer", ] zippychord = ["kanata-parser/zippychord"] [profile.release] opt-level = "z" lto = "fat" panic = "abort" codegen-units = 1 kanata-1.9.0/EnableUIAccess/EnableUIAccess_launch.ahk000064400000000000000000000124071046102023000204310ustar 00000000000000#requires AutoHotkey v2.0 #SingleInstance Off ; Needed for elevation with *runas. /* v2 based on EnableUIAccess.ahk v1.01 by Lexikos USE AT YOUR OWN RISK Enables the uiAccess flag in an application's embedded manifest and signs the file with a self-signed digital certificate. If the file is in a trusted location (A_ProgramFiles or A_WinDir), this allows the application to bypass UIPI (User Interface Privilege Isolation, a part of User Account Control in Vista/7). It also enables the journal playback hook (SendPlay). Command line params (mutually exclusive): SkipWarning - don't display the initial warning "" "" - attempt to run silently using the given file(s) This script and the provided Lib files may be used, modified, copied, etc. without restriction. */ #include in_file := (A_Args.Has(1))?A_Args[1]:'' ; Command line args out_file := (A_Args.Has(2))?A_Args[2]:'' if (in_file = ""){ msgResult := MsgBox("Enable the selected EXE to bypass UAC-UIPI security restrictions imposed by modifying 'UIAccess' attribute in the file's embedded manifest and signing the file using a self-signed digital certificate, which is then installed in the local machine's Trusted Root Certification Authorities store.`n`nThe resulting EXE is unusable on a system without this certificate installed!`n`nContinue at your own risk", "", 49) if (msgResult = "Cancel"){ ExitApp() } } if !A_IsAdmin { if (in_file = "") { in_file := "SkipWarning" } cmd := "`"" . A_ScriptFullPath . "`"" if !A_IsCompiled { ; Use A_AhkPath in case the "runas" verb isn't registered for ahk files. cmd := "`"" . A_AhkPath . "`" " . cmd } Try Run("*RunAs " cmd " `"" in_file "`" `"" out_file "`"", , "", ) ExitApp() } global user_specified_files := false if (in_file = "" || in_file = "SkipWarning") { ; Find AutoHotkey installation. InstallDir := RegRead("HKEY_LOCAL_MACHINE\SOFTWARE\AutoHotkey", "InstallDir") if A_LastError && A_PtrSize=8 { InstallDir := RegRead("HKLM\SOFTWARE\Wow6432Node\AutoHotkey", "InstallDir") } ; Let user confirm or select file(s). in_file := FileSelect(1, InstallDir "\AutoHotkey.exe", "Select Source File", "Executable Files (*.exe)") if A_LastError { ExitApp() } out_file := FileSelect("S16", in_file, "Select Destination File", "Executable Files (*.exe)") if A_LastError { ExitApp() } user_specified_files := true } Loop in_file { ; Convert short paths to long paths in_file := A_LoopFileFullPath } if (out_file = "") { ; i.e. only one file was given via command line out_file := in_file } else { Loop out_file { out_file := A_LoopFileFullPath } } if Crypt.IsSigned(in_file) { msgResult := MsgBox("Input file is already signed. The script will now exit" in_file,"", 48) ExitApp() } if user_specified_files && !IsTrustedLocation(out_file) { msgResult := MsgBox("Target path is not a trusted location (Program Files or Windows\System32), so 'uiAccess' will have no effect until the file is moved there","", 49) if (msgResult = "Cancel") { ExitApp() } } if (in_file = out_file) { ; The following should typically work even if the file is in use bak_file := in_file "~" A_Now ".bak" FileMove(in_file, bak_file, 1) if A_LastError { Fail("Failed to rename selected file.") } in_file := bak_file } Try { FileCopy(in_file, out_file, 1) } Catch as Err { throw OSError(Err) } if A_LastError { Fail("Failed to copy file to destination.") } if !EnableUIAccess(out_file) { ; Set the uiAccess attribute in the file's manifest Fail("Failed to set uiAccess attribute in manifest") } if (user_specified_files && in_file != out_file) { ; in interactive mode, if not overwriting the original file, offer to create an additional context menu item for AHK files uiAccessVerb := RegRead("HKCR\AutoHotkeyScript\Shell\uiAccess\Command") if A_LastError { msgResult := MsgBox("Register `"Run Script with UI Access`" context menu item?", "", 3) if (msgResult = "Yes") { RegWrite("Run with UI Access", "REG_SZ", "HKCR\AutoHotkeyScript\Shell\uiAccess") RegWrite("`"" out_file "`" `"`%1`" `%*", "REG_SZ", "HKCR\AutoHotkeyScript\Shell\uiAccess\Command") } if (msgResult = "Cancel") ExitApp() } } IsTrustedLocation(path) { ; IsTrustedLocation →true if path is a valid location for uiAccess="true" ; http://msdn.microsoft.com/en-us/library/bb756929 "\Program Files\ and \windows\system32\ are currently 2 allowable protected locations." However, \Program Files (x86)\ also appears to be allowed if InStr(path, A_ProgramFiles "\") = 1 { return true } if InStr(path, A_WinDir "\System32\") = 1 { return true } other := EnvGet(A_PtrSize=8 ? "ProgramFiles(x86)" : "ProgramW6432") ; On 64-bit systems, if this script is 32-bit, A_ProgramFiles is %ProgramFiles(x86)%, otherwise it is %ProgramW6432%. So check the opposite "Program Files" folder: if (other != "" && InStr(path, other "\") = 1) { return true } return false } Fail(msg) { ; if (%True% != "Silent") { ;??? MsgBox(msg "`nA_LastError: " A_LastError, "", 16) ; } ExitApp() } Warn(msg) { msg .= " (Err " A_LastError ")`n" OutputDebug(msg) FileAppend(msg, "*") } kanata-1.9.0/EnableUIAccess/Lib/EnableUIAccess.ahk000064400000000000000000000247511046102023000176120ustar 00000000000000#requires AutoHotkey v2.0 EnableUIAccess(ExePath) { static CertName := "AutoHotkey" hStore := DllCall("Crypt32\CertOpenStore", "ptr",10 ; STORE_PROV_SYSTEM_W , "uint",0, "ptr",0, "uint",0x20000 ; SYSTEM_STORE_LOCAL_MACHINE , "wstr","Root", "ptr") if !hStore { throw OSError() } store := CertStore(hStore) cert := CertContext() ; Find or create certificate for signing. while (cert.ptr := DllCall("Crypt32\CertFindCertificateInStore", "ptr",hStore , "uint",0x10001 ; X509_ASN_ENCODING|PKCS_7_ASN_ENCODING , "uint",0, "uint",0x80007 ; FIND_SUBJECT_STR , "wstr", CertName, "ptr",cert.ptr, "ptr")) && !(DllCall("Crypt32\CryptAcquireCertificatePrivateKey" , "ptr",cert, "uint",5 ; CRYPT_ACQUIRE_CACHE_FLAG|CRYPT_ACQUIRE_COMPARE_KEY_FLAG , "ptr",0, "ptr*", 0, "uint*", &keySpec:=0, "ptr",0) && (keySpec & 2)) { ; AT_SIGNATURE ; Keep looking for a certificate with a private key. } if !cert.ptr { cert := EnableUIAccess_CreateCert(CertName, hStore) } EnableUIAccess_SetManifest(ExePath) ; Set uiAccess attribute in manifest EnableUIAccess_SignFile(ExePath, cert, CertName) ; Sign the file (otherwise uiAccess attribute is ignored) return true } EnableUIAccess_SetManifest(ExePath) { xml := ComObject("Msxml2.DOMDocument") xml.async := false xml.setProperty("SelectionLanguage", "XPath") xml.setProperty("SelectionNamespaces" , "xmlns:v1='urn:schemas-microsoft-com:asm.v1' " . "xmlns:v3='urn:schemas-microsoft-com:asm.v3'") try { if !xml.loadXML(EnableUIAccess_ReadManifest(ExePath)) { throw Error("Invalid manifest") } } catch as e { throw Error("Error loading manifest from " ExePath,, e.Message "`n @ " e.File ":" e.Line) } node := xml.selectSingleNode("/v1:assembly/v3:trustInfo/v3:security" . "/v3:requestedPrivileges/v3:requestedExecutionLevel") if !node ; Not AutoHotkey? throw Error("Manifest is missing required elements") node.setAttribute("uiAccess", "true") xml := RTrim(xml.xml, "`r`n") data := Buffer(StrPut(xml, "utf-8") - 1) StrPut(xml, data, "utf-8") if !(hupd := DllCall("BeginUpdateResource", "str",ExePath, "int",false)) throw OSError() r := DllCall("UpdateResource", "ptr",hupd, "ptr",24, "ptr",1 , "ushort", 1033, "ptr",data, "uint",data.size) ; Retry loop to work around file locks (especially by antivirus) for delay in [0, 100, 500, 1000, 3500] { Sleep delay if DllCall("EndUpdateResource", "ptr",hupd, "int",!r) || !r return if !(A_LastError = 5 || A_LastError = 110) ; ERROR_ACCESS_DENIED || ERROR_OPEN_FAILED break } throw OSError(A_LastError, "EndUpdateResource") } EnableUIAccess_ReadManifest(ExePath) { if !(hmod := DllCall("LoadLibraryEx", "str",ExePath, "ptr",0, "uint",2, "ptr")) throw OSError() try { if !(hres := DllCall("FindResource", "ptr",hmod, "ptr",1, "ptr",24, "ptr")) { throw OSError() } size := DllCall("SizeofResource", "ptr",hmod, "ptr",hres, "uint") if !(hglb := DllCall("LoadResource", "ptr",hmod, "ptr",hres, "ptr")) { throw OSError() } if !(pres := DllCall("LockResource", "ptr",hglb, "ptr")) { throw OSError() } return StrGet(pres, size, "utf-8") } finally { DllCall("FreeLibrary", "ptr",hmod) } } EnableUIAccess_CreateCert(Name, hStore) { prov := CryptContext() ; Here Name is used as the key container name. if !DllCall("Advapi32\CryptAcquireContext", "ptr*", prov , "str",Name, "ptr",0, "uint",1, "uint",0) { ; PROV_RSA_FULL=1, open existing=0 if !DllCall("Advapi32\CryptAcquireContext", "ptr*", prov , "str",Name, "ptr",0, "uint",1, "uint",8) { ; PROV_RSA_FULL=1, CRYPT_NEWKEYSET=8 throw OSError() } if !DllCall("Advapi32\CryptGenKey", "ptr",prov , "uint",2, "uint",0x4000001, "ptr*", CryptKey()) { ; AT_SIGNATURE=2, EXPORTABLE=..01 throw OSError() } } ; Here Name is used as the certificate subject and name. Loop 2 { if A_Index = 1 { pbName := cbName := 0 } else { bName := Buffer(cbName), pbName := bName.ptr } if !DllCall("Crypt32\CertStrToName", "uint",1, "str","CN=" Name , "uint",3, "ptr",0, "ptr",pbName, "uint*", &cbName, "ptr",0) ; X509_ASN_ENCODING=1, CERT_X500_NAME_STR=3 throw OSError() } cnb := Buffer(2*A_PtrSize), NumPut("ptr",cbName, "ptr",pbName, cnb) ; Set expiry to 9999-01-01 12pm +0. NumPut("short", 9999, "sort", 1, "short", 5, "short", 1, "short", 12, endTime := Buffer(16, 0)) StrPut("2.5.29.4", szOID_KEY_USAGE_RESTRICTION := Buffer(9),, "cp0") StrPut("2.5.29.37", szOID_ENHANCED_KEY_USAGE := Buffer(10),, "cp0") StrPut("1.3.6.1.5.5.7.3.3", szOID_PKIX_KP_CODE_SIGNING := Buffer(18),, "cp0") ; CERT_KEY_USAGE_RESTRICTION_INFO key_usage; key_usage := Buffer(6*A_PtrSize, 0) NumPut('ptr', 0, 'ptr', 0, 'ptr', 1, 'ptr', key_usage.ptr + 5*A_PtrSize, 'ptr', 0 , 'uchar', (CERT_DATA_ENCIPHERMENT_KEY_USAGE := 0x10) | (CERT_DIGITAL_SIGNATURE_KEY_USAGE := 0x80), key_usage) ; CERT_ENHKEY_USAGE enh_usage; enh_usage := Buffer(3*A_PtrSize) NumPut("ptr",1, "ptr",enh_usage.ptr + 2*A_PtrSize, "ptr",szOID_PKIX_KP_CODE_SIGNING.ptr, enh_usage) key_usage_data := EncodeObject(szOID_KEY_USAGE_RESTRICTION, key_usage) enh_usage_data := EncodeObject(szOID_ENHANCED_KEY_USAGE, enh_usage) EncodeObject(structType, structInfo) { encoder := DllCall.Bind("Crypt32\CryptEncodeObject", "uint",X509_ASN_ENCODING := 1 , "ptr",structType, "ptr",structInfo) if !encoder("ptr",0, "uint*", &enc_size := 0) throw OSError() enc_data := Buffer(enc_size) if !encoder("ptr",enc_data, "uint*", &enc_size) throw OSError() enc_data.Size := enc_size return enc_data } ; CERT_EXTENSION extension[2]; CERT_EXTENSIONS extensions; NumPut("ptr",szOID_KEY_USAGE_RESTRICTION.ptr, "ptr",true, "ptr",key_usage_data.size, "ptr",key_usage_data.ptr , "ptr",szOID_ENHANCED_KEY_USAGE.ptr , "ptr",true, "ptr",enh_usage_data.size, "ptr",enh_usage_data.ptr , extension := Buffer(8*A_PtrSize)) NumPut("ptr",2, "ptr",extension.ptr, extensions := Buffer(2*A_PtrSize)) if !hCert := DllCall("Crypt32\CertCreateSelfSignCertificate" , "ptr",prov, "ptr",cnb, "uint",0, "ptr",0 , "ptr",0, "ptr",0, "ptr",endTime, "ptr",extensions, "ptr") { throw OSError() } cert := CertContext(hCert) if !DllCall("Crypt32\CertAddCertificateContextToStore", "ptr",hStore , "ptr",hCert, "uint",1, "ptr",0) { ; STORE_ADD_NEW=1 throw OSError() } return cert } EnableUIAccess_DeleteCertAndKey(Name) { ; This first call "acquires" the key container but also deletes it. DllCall("Advapi32\CryptAcquireContext", "ptr*", 0, "str",Name , "ptr",0, "uint",1, "uint",16) ; PROV_RSA_FULL=1, CRYPT_DELETEKEYSET=16 if !hStore := DllCall("Crypt32\CertOpenStore", "ptr",10 ; STORE_PROV_SYSTEM_W , "uint",0, "ptr",0, "uint",0x20000 ; SYSTEM_STORE_LOCAL_MACHINE , "wstr", "Root", "ptr") throw OSError() store := CertStore(hStore) deleted := 0 ; Multiple certificates might be created over time as keys become inaccessible while p := DllCall("Crypt32\CertFindCertificateInStore", "ptr",hStore , "uint",0x10001 ; X509_ASN_ENCODING|PKCS_7_ASN_ENCODING , "uint",0, "uint",0x80007 ; FIND_SUBJECT_STR , "wstr", Name, "ptr",0, "ptr") { if !DllCall("Crypt32\CertDeleteCertificateFromStore", "ptr",p) { throw OSError() } deleted++ } return deleted } class Crypt { static IsSigned(FilePath) { return DllCall("Crypt32\CryptQueryObject" ,"uint" , CERT_QUERY_OBJECT_FILE := 1 ,"wstr" , FilePath ,"uint" , CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED := 1<<10 ,"uint" , CERT_QUERY_FORMAT_FLAG_BINARY := 2 ,"uint" , 0 ,"uint*" , &dwEncoding:=0 ,"uint*" , &dwContentType:=0 ,"uint*" , &dwFormatType:=0 ,"ptr" , 0 ,"ptr" , 0 ,"ptr" , 0) } } class CryptPtrBase { __new(p:=0) => this.ptr := p __delete() => this.ptr && this.Dispose() } class CryptContext extends CryptPtrBase { Dispose() => DllCall("Advapi32\CryptReleaseContext", "ptr",this, "uint",0) } class CertContext extends CryptPtrBase { Dispose() => DllCall("Crypt32\CertFreeCertificateContext", "ptr",this) } class CertStore extends CryptPtrBase { Dispose() => DllCall("Crypt32\CertCloseStore", "ptr",this, "uint",0) } class CryptKey extends CryptPtrBase { Dispose() => DllCall("Advapi32\CryptDestroyKey", "ptr",this) } EnableUIAccess_SignFile(ExePath, CertCtx, Name) { file_info := struct( ; SIGNER_FILE_INFO "ptr",A_PtrSize*3, "ptr",StrPtr(ExePath)) dwIndex := Buffer(4, 0) ; DWORD subject_info := struct( ; SIGNER_SUBJECT_INFO "ptr",A_PtrSize*4, "ptr",dwIndex.ptr, "ptr",SIGNER_SUBJECT_FILE:=1, "ptr",file_info.ptr) cert_store_info := struct( ; SIGNER_CERT_STORE_INFO "ptr",A_PtrSize*4, "ptr",CertCtx.ptr, "ptr",SIGNER_CERT_POLICY_CHAIN:=2) cert_info := struct( ; SIGNER_CERT "uint",8+A_PtrSize*2, "uint",SIGNER_CERT_STORE:=2, "ptr",cert_store_info.ptr) authcode_attr := struct( ; SIGNER_ATTR_AUTHCODE "uint",8+A_PtrSize*3, "int",false, "ptr",true, "ptr",StrPtr(Name)) sig_info := struct( ; SIGNER_SIGNATURE_INFO "uint",8+A_PtrSize*4, "uint",CALG_SHA1:=0x8004, "ptr",SIGNER_AUTHCODE_ATTR:=1, "ptr",authcode_attr.ptr) hr := DllCall("MSSign32\SignerSign" , "ptr",subject_info, "ptr",cert_info, "ptr",sig_info , "ptr",0, "ptr",0, "ptr",0, "ptr",0, "hresult") ; pProviderInfo pwszHttpTimeStamp psRequest pSipData struct(args*) => ( args.Push(b := Buffer(args[2], 0)), NumPut(args*), b ) } EnableUIAccess_Verify(ExePath) { ; Verifies a signed executable file. Returns 0 on success, or a standard OS error number. wfi := Buffer(4*A_PtrSize) ; WINTRUST_FILE_INFO NumPut('ptr', wfi.size, 'ptr', StrPtr(ExePath), 'ptr', 0, 'ptr', 0, wfi) NumPut('int64', 0x11d0cd4400aac56b, 'int64', 0xee95c24fc000c28c, actionID := Buffer(16)) ; WINTRUST_ACTION_GENERIC_VERIFY_V2 wtd := Buffer(9*A_PtrSize+16) ; WINTRUST_DATA NumPut( 'ptr', wtd.Size, 'ptr', 0, 'ptr', 0, 'int', WTD_UI_NONE:=2, 'int', WTD_REVOKE_NONE:=0, 'ptr', WTD_CHOICE_FILE:=1, 'ptr', wfi.ptr, 'ptr', WTD_STATEACTION_VERIFY:=1, 'ptr', 0, 'ptr', 0, 'int', 0, 'int', 0, 'ptr', 0, wtd ) return DllCall('wintrust\WinVerifyTrust', 'ptr', 0, 'ptr', actionID, 'ptr', wtd, 'int') } kanata-1.9.0/EnableUIAccess/README.md000064400000000000000000000002241046102023000151150ustar 00000000000000# EnableUIAccess See [the guide documentation for context](https://github.com/jtroo/kanata/blob/main/docs/config.adoc#windows-only-work-elevated). kanata-1.9.0/LICENSE000075500000000000000000000167431046102023000121150ustar 00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library.kanata-1.9.0/README.md000064400000000000000000000313641046102023000123600ustar 00000000000000

Kanata

Image of a keycap with the letter K on it in pink tones

Improve your keyboard comfort
## What does this do? This is a cross-platform software keyboard remapper for Linux, macOS and Windows. A short summary of the features: - multiple layers of key functionality - advanced key behaviour customization (e.g. tap-hold, macros, unicode) To see all of the features, see the [configuration guide](./docs/config.adoc). You can find pre-built binaries in the [releases page](https://github.com/jtroo/kanata/releases) or read on for build instructions. You can see a [list of known issues here](./docs/platform-known-issues.adoc). ### Demo #### Demo video [Showcase of multi-layer functionality (30s, 1.7 MB)](https://user-images.githubusercontent.com/6634136/183001314-f64a7e26-4129-4f20-bf26-7165a6e02c38.mp4). #### Online simulator You can check out the [online simulator](https://jtroo.github.io) to test configuration validity and test input simulation. ## Why is this useful? Imagine if, instead of pressing Shift to type uppercase letters, we had giant keyboards with separate keys for lowercase and uppercase letters. I hope we can all agree: that would be a terrible user experience! A way to think of how Shift keys work is that they switch your input to another layer of functionality where you now type uppercase letters and symbols instead of lowercase letters and numbers. What kanata allows you to do is take this alternate layer concept that Shift keys have and apply it to any key. You can then customize what those layers do to suit your exact needs and workflows. ## Usage Running kanata currently does not start it in a background process. You will need to keep the window that starts kanata running to keep kanata active. Some tips for running kanata in the background: - Windows: https://github.com/jtroo/kanata/discussions/193 - Linux: https://github.com/jtroo/kanata/discussions/130#discussioncomment-10227272 - Run from tray icon: [kanata-tray](https://github.com/rszyma/kanata-tray) ### Pre-built executables See the [releases page](https://github.com/jtroo/kanata/releases) for executables and instructions. ### Build it yourself This project uses the latest Rust stable toolchain. If you installed the Rust toolchain using `rustup`, e.g. by using the instructions from the [official website](https://www.rust-lang.org/learn/get-started), you can get the latest stable toolchain with `rustup update stable`.
Instructions Using `cargo install`: cargo install kanata # On Linux and macOS, this may not work without `sudo`, see below kanata --cfg Build and run yourself in Linux: git clone https://github.com/jtroo/kanata && cd kanata cargo build # --release optional, not really perf sensitive # sudo is used because kanata opens /dev/ files # # See below if you want to avoid needing sudo: # https://github.com/jtroo/kanata/wiki/Avoid-using-sudo-on-Linux sudo target/debug/kanata --cfg Build and run yourself in Windows. git clone https://github.com/jtroo/kanata; cd kanata cargo build # --release optional, not really perf sensitive target\debug\kanata --cfg Build and run yourself in macOS: First install the Karabiner driver by following the macOS documentation in the [releases page](https://github.com/jtroo/kanata/releases/). Then you can compile and run with the instructions below: git clone https://github.com/jtroo/kanata && cd kanata cargo build # --release optional, not really perf sensitive # sudo is needed to gain permission to intercept the keyboard sudo target/debug/kanata --cfg The full configuration guide is [found here](./docs/config.adoc). Sample configuration files are found in [cfg_samples](./cfg_samples). The [simple.kbd](./cfg_samples/simple.kbd) file contains a basic configuration file that is hopefully easy to understand but does not contain all features. The `kanata.kbd` contains an example of all features with documentation. The release assets also have a `kanata.kbd` file that is tested to work with that release. All key names can be found in the [keys module](./src/keys/mod.rs), and you can also define your own key names.
### Feature flags When either building yourself or using `cargo install`, you can add feature flags that enable functionality that is turned off by default.
Instructions If you want to enable the `cmd` actions, add the flag `--features cmd`. For example: ``` cargo build --release --features cmd cargo install --features cmd ``` On Windows, if you want to compile a binary that uses the Interception driver, you should add the flag `--features interception_driver`. For example: ``` cargo build --release --features interception_driver cargo install --features interception_driver ``` To combine multiple flags, use a single `--features` flag and use a comma to separate the features. For example: ``` cargo build --release --features cmd,interception_driver cargo install --features cmd,interception_driver ```
## Other installation methods [![Packaging status](https://repology.org/badge/vertical-allrepos/kanata.svg)](https://repology.org/project/kanata/versions) ## Notable features - Human-readable configuration file. - [Minimal example](./cfg_samples/minimal.kbd) - [Full guide](./docs/config.adoc) - [Simple example with explanations](./cfg_samples/simple.kbd) - [All features showcase](./cfg_samples/kanata.kbd) - Live reloading of the configuration for easy testing of your changes. - Multiple layers of key functionality - Advanced actions such as tap-hold, unicode output, dynamic and static macros - Vim-like leader sequences to execute other actions - Optionally run a TCP server to interact with other programs - Other programs can respond to [layer changes or trigger layer changes](https://github.com/jtroo/kanata/issues/47) - [Interception driver](https://web.archive.org/web/20240209172129/http://www.oblita.com/interception) support (use `kanata_wintercept.exe`) - Note that this issue exists, which is outside the control of this project: https://github.com/oblitum/Interception/issues/25 ## Contributing Contributions are welcome! Unless explicitly stated otherwise, your contributions to kanata will be made under the LGPL-3.0-only[*] license. Some directories are exceptions: - [keyberon](./keyberon): MIT License - [interception](./interception): MIT or Apache-2.0 Licenses [Here's a basic low-effort design doc of kanata](./docs/design.md) [*]: https://www.gnu.org/licenses/identify-licenses-clearly.html ## How you can help - Try it out and let me know what you think. Feel free to file an issue or start a discussion. - Usability issues and unhelpful error messages are considered bugs that should be fixed. If you encounter any, I would be thankful if you file an issue. - Browse the open issues and help out if you are able and/or would like to. If you want to try contributing, feel free to ping jtroo for some pointers. - If you know anything about writing a keyboard driver for Windows, starting an open-source alternative to the Interception driver would be lovely. ## Community projects related to kanata - [vscode-kanata](https://github.com/rszyma/vscode-kanata): Language support for kanata configuration files in VS Code - [komokana](https://github.com/LGUG2Z/komokana): Automatic application-aware layer switching for [`komorebi`](https://github.com/LGUG2Z/komorebi) (Windows) - [kanata-tray](https://github.com/rszyma/kanata-tray): Control kanata from a tray icon - [OverKeys](https://github.com/conventoangelo/overkeys): Visual layer display for kanata - see your active layers and keymaps in real-time (Windows) - Application-aware layer switching: - [qanata (Linux)](https://github.com/veyxov/qanata) - [kanawin (Windows)](https://github.com/Aqaao/kanawin) - [window_tools (Windows)](https://github.com/reidprichard/window_tools) - [nata (Linux)](https://github.com/mdSlash/nata) - [kanata-vk-agent (macOS)](https://github.com/devsunb/kanata-vk-agent) - [hyprkan (Linux)](https://github.com/mdSlash/hyprkan) ## What does the name mean? I wanted a "k" word since this relates to keyboards. According to Wikipedia, kanata is an indigenous Iroquoian word meaning "village" or "settlement" and is the origin of Canada's name. There's also PPT✧. ## Motivation TLDR: QMK features but for any keyboard, not just fancy mechanical ones.
Long version I have a few keyboards that run [QMK](https://docs.qmk.fm/#/). QMK allows the user to customize the functionality of their keyboard to their heart's content. One great use case of QMK is its ability map keys so that they overlap with the home row keys but are accessible on another layer. I won't comment on productivity, but I find this greatly helps with my keyboard comfort. For example, these keys are on the right side of the keyboard: 7 8 9 u i o j k l m , . On one layer I have arrow keys in the same position, and on another layer I have a numpad. arrows: numpad: - - - 7 8 9 - ↑ - 4 5 6 ← ↓ → 1 2 3 - - - 0 * . One could add as many customizations as one likes to improve comfort, speed, etc. Personally my main motivator is comfort due to a repetitive strain injury in the past. However, QMK doesn't run everywhere. In fact, it doesn't run on **most** hardware you can get. You can't get it to run on a laptop keyboard or any mainstream office keyboard. I believe that the comfort and empowerment QMK provides should be available to anyone with a computer on their existing hardware, instead of having to purchase an enthusiast mechanical keyboard (which are admittedly very nice — I own a few — but can be costly). The best alternative solution that I found for keyboards that don't run QMK was [kmonad](https://github.com/kmonad/kmonad). This is an excellent project and I recommend it if you want to try something similar. The reason for this project's existence is that kmonad is written in Haskell and I have no idea how to begin contributing to a Haskell project. From an outsider's perspective I think Haskell is a great language but I really can't wrap my head around it. And there are a few [outstanding issues](./docs/kmonad_comparison.md) at the time of writing that make kmonad suboptimal for my personal workflows. This project is written in Rust because Rust is my favourite programming language and the prior work of the awesome [keyberon crate](https://github.com/TeXitoi/keyberon) exists.
## Similar Projects The most similar project is [kmonad](https://github.com/kmonad/kmonad), which served as the inspiration for kanata. [Here's a comparison document](./docs/kmonad_comparison.md). Other similar projects: - [QMK](https://docs.qmk.fm/#/): Open source keyboard firmware - [keyberon](https://github.com/TeXitoi/keyberon): Rust `#[no_std]` library intended for keyboard firmware - [ktrl](https://github.com/ItayGarin/ktrl): Linux-only keyboard customizer with layers, a TCP server, and audio support - [kbremap](https://github.com/timokroeger/kbremap): Windows-only keyboard customizer with layers and unicode - [xcape](https://github.com/alols/xcape): Linux-only tap-hold modifiers - [karabiner-elements](https://karabiner-elements.pqrs.org/): Mac-only keyboard customizer - [capsicain](https://github.com/cajhin/capsicain): Windows-only key remapper with driver-level key interception - [keyd](https://github.com/rvaiya/keyd): Linux-only key remapper very similar to QMK, kmonad, and kanata - [xremap](https://github.com/k0kubun/xremap): Linux-only application-aware key remapper inspired more by Emacs key sequences vs. QMK layers/Vim modes - [keymapper](https://github.com/houmain/keymapper): Context-aware cross-platform key remapper with a different transformation model (Linux, Windows, Mac) - [mouseless](https://github.com/jbensmann/mouseless): Linux-only mouse-focused key remapper that also has layers, key combo and tap-hold capabilities ### Why the list? While kanata is the best tool for some, it may not be the best tool for you. I'm happy to introduce you to tools that may better suit your needs. This list is also useful as reference/inspiration for functionality that could be added to kanata. ## Donations/Support? The author (jtroo) will not accept monetary donations for work on kanata. Please instead donate your time and/or money to charity. Some links are below. These links are provided for learning and as interesting reads. They are **not** an endorsement. - https://www.effectivealtruism.org/ - https://www.givewell.org/ kanata-1.9.0/assets/kanata-icon.svg000064400000000000000000000475361046102023000153210ustar 00000000000000 image/svg+xml kanata-1.9.0/assets/kanata.ico000064400000000000000000000353561046102023000143430ustar 0000000000000000 %6  % h6(0` $P^^_abcehjlnprsuvwxyyyyyyxxxwvtsrqpnmljhfdb``acS}v~odUSa{whuf `paxb>jZ{^< dT~Yj ^MV gYGQ*:SAN[N;E H5(HC0ma>+59&*.E5!TF}0P\,Nm&Mz VK oI ,H 7FvcKf '#_U= wcRE90)$ !%+3???>><;:86bd?+?9<@XSFVH>FI=S c|&  )kanata-1.9.0/assets/reload_32px.png000064400000000000000000000011441046102023000152240ustar 00000000000000PNG  IHDR }JbsRGB7MS pHYs   IDATXJ1v**RAA`";A(}NDt\ ŅnAK7>߫NR"c&$'sMM ufm>4j&ծ~͛WH$, t*͆YAOi ͹cbϭLAYeb*lIU޻W(gD 1$U'a!=;TKa,s1f!7qMK!8(c΄\$nS,")*}^D(IENDB`kanata-1.9.0/build.rs000064400000000000000000000064341046102023000125460ustar 00000000000000fn main() -> std::io::Result<()> { #[cfg(all(target_os = "windows", any(feature = "win_manifest", feature = "gui")))] { windows::build()?; } Ok(()) } #[cfg(all(target_os = "windows", any(feature = "win_manifest", feature = "gui")))] mod windows { use indoc::formatdoc; use regex::Regex; use std::fs::File; use std::io::Write; extern crate embed_resource; // println! during build macro_rules! pb { ($($tokens:tt)*) => {println!("cargo:warning={}", format!($($tokens)*))}} pub(super) fn build() -> std::io::Result<()> { let manifest_path: &str = "./target/kanata.exe.manifest"; // Note about expected version format: // MS says "Use the four-part version format: mmmmm.nnnnn.ooooo.ppppp" // https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests let re_ver_build = Regex::new(r"^(?(\d+\.){2}\d+)[-a-zA-Z]+(?\d+)$").unwrap(); let re_ver_build2 = Regex::new(r"^(?(\d+\.){2}\d+)[-a-zA-Z]+$").unwrap(); let re_version3 = Regex::new(r"^(\d+\.){2}\d+$").unwrap(); let mut version: String = env!("CARGO_PKG_VERSION").to_string(); if re_version3.find(&version).is_some() { version = format!("{}.0", version); } else if re_ver_build.find(&version).is_some() { version = re_ver_build .replace_all(&version, r"$vpre.$vpos") .to_string(); } else if re_ver_build2.find(&version).is_some() { version = re_ver_build2.replace_all(&version, r"$vpre.0").to_string(); } else { pb!("unknown version format '{}', using '0.0.0.0'", version); version = "0.0.0.0".to_string(); } let manifest_str = formatdoc!( r#" true PerMonitorV2 "#, version ); let mut manifest_f = File::create(manifest_path)?; write!(manifest_f, "{}", manifest_str)?; embed_resource::compile("./src/kanata.exe.manifest.rc", embed_resource::NONE); Ok(()) } } kanata-1.9.0/cfg_samples/artsey.kbd000064400000000000000000000031761046102023000153550ustar 00000000000000;; ARTSEY MINI 0.2 https://github.com/artseyio/artsey/issues/7 ;; Exactly one defcfg entry is required. This is used for configuration key-pairs. (defcfg ;; Your keyboard device will likely differ from this. linux-dev /dev/input/event1 ;; Windows doesn't need any input/output configuration entries; however, there ;; must still be a defcfg entry. You can keep the linux-dev entry or delete ;; it and leave it empty. ) (defsrc q w e a s d ) (deflayer base (chord base A) (chord base R) (chord base T) (chord base S) (chord base E) (chord base Y) ) (deflayer meta (chord meta A) (chord meta R) (chord meta T) (chord meta S) (chord meta E) (chord meta Y) ) (defchords base 5000 (A R T S E Y) (layer-switch meta) (A R T ) (one-shot 2000 lsft) ( S E Y) spc (A ) a ( R T S ) b ( R S ) c (A E Y) d ( E ) e (A R ) f (A E ) g ( S Y) h ( R E ) i ( T S E ) j ( T E ) k ( S E ) l ( R T ) m ( E Y) n (A S ) o (A R Y) p ( T Y) q ( R ) r ( S ) s ( T ) t (A T ) u (A T E ) v ( T S ) w (A Y) x ( Y) y ( R S E ) z ) (defchords meta 5000 (A R T S E Y) (layer-switch base) ( S E Y) spc (A R T ) caps ;; should technically be shift lock, probably need to use fake keys for that (A R ) bspc ( R T ) del ( S E ) C-c ( E Y) C-v (A ) home ( R ) up ( T ) end ( S ) left ( E ) down ( Y) rght ) kanata-1.9.0/cfg_samples/automousekeys-full-map.kbd000064400000000000000000000041131046102023000204660ustar 00000000000000(defcfg ;; F* keys and arrow keys are left unmapped process-unmapped-keys yes ;; you may wish to only capture a trackpoint and keyboard ;; but not e.g. a trackpad or external mouse ;;linux-dev-names-include ( ;; "AT Translated Set 2 keyboard" ;; "TPPS/2 Elan TrackPoint" ;;) ;; optional, but useful with the trackpoint ;;linux-use-trackpoint-property yes mouse-movement-key mvmt ) ;; ANSI layout for eg thinkpad internal or external keyboard (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt menu rctl mvmt ) (defvirtualkeys mouse (layer-while-held mouse-layer) ) (defalias mhld (hold-for-duration 750 mouse) moff (on-press release-vkey mouse) _ (multi @moff _ ) ;; mouse click extended time out for double tap mdbt (hold-for-duration 500 mouse) mbl (multi mlft @mdbt ) mbm (multi mmid @mdbt ) mbr (multi mrgt @mdbt ) ) ;; no mappings (deflayer qwerty grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt menu rctl @mhld ) ;; places mouse keys on the row above the home row. ;; pressing any other keys exits the mouse layer until mouse movement stops and restarts again. (deflayer mouse-layer @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ mrgt mmid @mbl @_ @_ @mbl mmid mrgt @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @mhld ) kanata-1.9.0/cfg_samples/automousekeys-only.kbd000064400000000000000000000023201046102023000177300ustar 00000000000000(defcfg ;; we are only mapping the keys we want to use for mouse keys process-unmapped-keys yes ;; you may wish to only capture a trackpoint and keyboard ;; but not e.g. a trackpad or external mouse ;;linux-dev-names-include ( ;; "Lenovo TrackPoint Keyboard II" ;;) ;; optional, but useful with the trackpoint ;;linux-use-trackpoint-property yes mouse-movement-key mvmt ) (defsrc) (defvirtualkeys mouse (layer-while-held mouse-layer) ) (defalias mhld (hold-for-duration 750 mouse) moff (on-press release-vkey mouse) _ (multi @moff _ ) ;; mouse click extended time out for double tap mdbt (hold-for-duration 500 mouse) mbl (multi mlft @mdbt ) mbm (multi mmid @mdbt ) mbr (multi mrgt @mdbt ) ) ;; no key mappings (deflayermap (base) mvmt @mhld ) ;; places mouse keys on the row above the home row. ;; pressing any other keys exits the mouse layer until mouse movement stops and restarts again. (deflayermap (mouse-layer) w mrgt e mmid r @mbl u @mbl i mmid o mrgt mvmt @mhld ___ @_ ) kanata-1.9.0/cfg_samples/chords.tsv000064400000000000000000000030411046102023000153730ustar 00000000000000rus rust col cool nice nice you you th the a a an an man man name name an and as as or or bu but if if so so dn then bc because to to of of in in f for w with on on at at fm from by by abt about up up io into ov over af after wo without i I me me my my ou you ur your he he hm him his his sh she hr her it it ts its we we us us our our dz they dr their dm them wc which wn when wt what wr where ho who hw how wz why is is ar are wa was er were be be hv have hs has hd had nt not cn can do do wl will cd could wd would sd should li like bn been ge get maz may mad made mk make ai said wk work uz use sz say g go kn know tk take se see lk look cm come thk think wnt want gi give ct cannot de does di did sem seem cl call tha thank im I'm id I'd dt that dis this des these tes test al all o one mo more the there out out ao also tm time sm some js just ne new odr other pl people n no dan than oz only m most ay any may many el well fs first vy very much much now now ev even go good grt great way way t two yr year bk back day day qn question sc second dg thing y yes cn' can't dif different dgh though tru through sr sorry mv move dir dir stop stop tye type nx next sam same tp top cod code git git to TODO cls class clus cluster sure sure lets let's sup super such such thig thing yet yet don done sem seem ran ran job job bot bot fx effect nce once rad read ltr later lot lot brw brew unst uninstall rmv remove ad add poe problem buld build tol tool got got les less 0 zero 1 one 2 two 3 three 4 four 5 five 6 six 7 seven 8 eight 9 nine kanata-1.9.0/cfg_samples/colemak.kbd000064400000000000000000000023361046102023000154560ustar 00000000000000;; ;; Learn Colemak, a few keys at a time. ;; ;; The "j" key moves around the keyboard each step, ;; until you reach the full Colemak layout. ;; ;; To select the layout for your current step, press the ;; letter "m" and the number of your current step, as a chord. ;; ;; Check out: https://dreymar.colemak.org/tarmak-intro.html ;; and: https://colemak.com ;; (defsrc q w e r t y u i o p a s d f g h j k l ; z x c v b n m ) (deflayer colemak_j1 _ _ j _ _ _ _ _ _ _ _ _ _ _ _ _ n e _ _ _ _ _ _ _ k _ ) (deflayer colemak_j2 _ _ f _ g _ _ _ _ _ _ _ _ t j _ n e _ _ _ _ _ _ _ k _ ) (deflayer colemak_j3 _ _ f j g _ _ _ _ _ _ r s t d _ n e _ _ _ _ _ _ _ k _ ) (deflayer colemak_j4 _ _ f p g j _ _ y ; _ r s t d _ n e _ o _ _ _ _ _ k _ ) (deflayer colemak _ _ f p g j l u y ; _ r s t d _ n e i o _ _ _ _ _ k _ ) (defcfg process-unmapped-keys yes concurrent-tap-hold yes allow-hardware-repeat no ) (defchordsv2 (m 1) (layer-switch colemak_j1) 300 all-released () (m 2) (layer-switch colemak_j2) 300 all-released () (m 3) (layer-switch colemak_j3) 300 all-released () (m 4) (layer-switch colemak_j4) 300 all-released () (m 5) (layer-switch colemak) 300 all-released () ) kanata-1.9.0/cfg_samples/deflayermap.kbd000064400000000000000000000013421046102023000163300ustar 00000000000000;; A configuration showcasing deflayermap. ;; ;; The process-unmapped-keys defcfg item is not used ;; and the lctl and ralt keys are unmapped ;; because mapping them can cause problems on Windows ;; with non-US layouts. (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lmet lalt spc rmet rctl ) (deflayermap (base) caps (tap-hold 200 200 (caps-word 2000) lctl) spc (tap-hold 200 200 spc (layer-while-held nav)) ) (deflayermap (nav) i up j left k down l right ) kanata-1.9.0/cfg_samples/f13_f24.kbd000064400000000000000000000003541046102023000151050ustar 00000000000000(defcfg linux-dev /dev/input/by-path/platform-i8042-serio-0-event-kbd ) (defsrc f13 f14 f15 f16 f17 f18 f19 f20 f21 f22 f23 f24 ) (deflayer test f13 f14 f15 f16 f17 f18 f19 f20 f21 f22 f23 f24 ) kanata-1.9.0/cfg_samples/fancy_symbols.kbd000064400000000000000000000061571046102023000167200ustar 00000000000000;; Turns ⎇› RightAlt into a symbol key to insert valid kanata unicode symbols for the pressed key ;; Turns ⇧›⎇› RightShift+RightAlt into a symbol key to insert extra symbols for the same keys ;; e.g., ⎇›Delete will print ␡ (defcfg) (defalias 🔣 (layer-while-held fancy-symbol) ⇧🔣 (layer-while-held ⇧fancy-symbol)) (defsrc ‹🖰 🖰› 🖰3 🖰4 🖰5 ▶⏸ ◀◀ ▶▶ 🔇 🔉 🔊 🔅 🔆 🎛 ⌨💡+ ⌨💡− ⎋ ˋ 1 2 3 4 5 6 7 8 9 0 - = ␈ ⎀ ⇤ ⇞ ⇭ 🔢⁄ 🔢∗ 🔢₋ ⭾ q w e r t y u i o p [ ] \ ␡ ⇥ ⇟ 🔢₇ 🔢₈ 🔢₉ 🔢₊ ⇪ a s d f g h j k l ; ' ⏎ 🔢₄ 🔢₅ 🔢₆ ‹⇧ z x c v b n m , . / ⇧› ▲ 🔢₁ 🔢₂ 🔢₃ 🔢⏎ ‹⎈ ‹◆ ‹⎇ ␠ ⎇› ☰ ⎈› ◀ ▼ ▶ 🔢₀ 🔢⸴ ) (deflayer qwerty ;; =base with ⎇› as a fancy symbol key ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ @🔣 ‗ ‗ ‗ ‗ ‗ ‗ ‗ ) (deflayer fancy-symbol ;; •block all other keys 🔣‹🖰 🔣🖰› 🔣🖰3 🔣🖰4 🔣🖰5 🔣▶⏸ 🔣◀◀ 🔣▶▶ 🔣🔇 🔣🔉 🔣🔊 🔣🔅 🔣🔆 🔣🎛 🔣⌨💡+ 🔣⌨💡− 🔣⎋ 🔣ˋ • • • • • • • • • • 🔣‐ 🔣₌ 🔣␈ 🔣⎀ 🔣⇤ 🔣⇞ 🔣⇭ 🔣🔢⁄ 🔣🔢∗ 🔣🔢₋ 🔣⭾ • • • • • • • • • • 🔣【 🔣】 🔣⧵ 🔣␡ 🔣⇥ 🔣⇟ 🔣🔢₇ 🔣🔢₈ 🔣🔢₉ 🔣🔢₊ 🔣⇪ • • • • • • • • • 🔣︔ ' 🔣⏎ 🔣🔢₄ 🔣🔢₅ 🔣🔢₆ 🔣⇧ • • • • • • • 🔣⸴ 🔣. 🔣⁄ @⇧🔣 🔣▲ 🔣🔢₁ 🔣🔢₂ 🔣🔢₃ 🔣🔢⏎ 🔣⎈ 🔣◆ 🔣⎇ 🔣␠ • 🔣☰ • 🔣◀ 🔣▼ 🔣▶ 🔣🔢₀ 🔣🔢⸴ ) (deflayer ⇧fancy-symbol ;; •block all other keys 🔣🖰1 🔣🖰2 • • • • • • 🔣🔈⓪⓿₀ • 🔣🔈−➖₋⊖ 🔣🔈+➕₊⊕ • • 🔣⌨💡➕₊⊕ 🔣⌨💡➖₋⊖ • 🔣˜ • • • • • • • • • • - = 🔣⌫ • 🔣⤒↖ 🔣🔢 • • • • 🔣↹ • • • • • • • • • • 🔣「〔⎡ 🔣」〕⎣ 🔣\ 🔣⌦ 🔣⤓↘ • • • • • • • • • • • • • • • • • 🔣↩⌤␤ • • • • • • • • • • • • • / • • • • • 🔣🔢↩⌤␤ 🔣⌃ 🔣❖⌘ 🔣⌥ 🔣␣ 🔣▤𝌆 • • • • • • • ) kanata-1.9.0/cfg_samples/home-row-mod-advanced.kbd000064400000000000000000000034041046102023000201150ustar 00000000000000;; Home row mods QWERTY example with more complexity. ;; Some of the changes from the basic example: ;; - when a home row mod activates tap, the home row mods are disabled ;; while continuing to type rapidly ;; - tap-hold-release helps make the hold action more responsive ;; - pressing another key on the same half of the keyboard ;; as the home row mod will activate an early tap action (defcfg process-unmapped-keys yes ) (defsrc a s d f j k l ; ) (defvar ;; Note: consider using different time values for your different fingers. ;; For example, your pinkies might be slower to release keys and index ;; fingers faster. tap-time 200 hold-time 150 left-hand-keys ( q w e r t a s d f g z x c v b ) right-hand-keys ( y u i o p h j k l ; n m , . / ) ) (deflayer base @a @s @d @f @j @k @l @; ) (deflayer nomods a s d f j k l ; ) (deffakekeys to-base (layer-switch base) ) (defalias tap (multi (layer-switch nomods) (on-idle-fakekey to-base tap 20) ) a (tap-hold-release-keys $tap-time $hold-time (multi a @tap) lmet $left-hand-keys) s (tap-hold-release-keys $tap-time $hold-time (multi s @tap) lalt $left-hand-keys) d (tap-hold-release-keys $tap-time $hold-time (multi d @tap) lctl $left-hand-keys) f (tap-hold-release-keys $tap-time $hold-time (multi f @tap) lsft $left-hand-keys) j (tap-hold-release-keys $tap-time $hold-time (multi j @tap) rsft $right-hand-keys) k (tap-hold-release-keys $tap-time $hold-time (multi k @tap) rctl $right-hand-keys) l (tap-hold-release-keys $tap-time $hold-time (multi l @tap) ralt $right-hand-keys) ; (tap-hold-release-keys $tap-time $hold-time (multi ; @tap) rmet $right-hand-keys) )kanata-1.9.0/cfg_samples/home-row-mod-basic.kbd000064400000000000000000000015331046102023000174320ustar 00000000000000;; Basic home row mods example using QWERTY ;; For a more complex but perhaps usable configuration, ;; see home-row-mod-advanced.kbd (defcfg process-unmapped-keys yes ) (defsrc a s d f j k l ; ) (defvar ;; Note: consider using different time values for your different fingers. ;; For example, your pinkies might be slower to release keys and index ;; fingers faster. tap-time 200 hold-time 150 ) (defalias a (tap-hold $tap-time $hold-time a lmet) s (tap-hold $tap-time $hold-time s lalt) d (tap-hold $tap-time $hold-time d lctl) f (tap-hold $tap-time $hold-time f lsft) j (tap-hold $tap-time $hold-time j rsft) k (tap-hold $tap-time $hold-time k rctl) l (tap-hold $tap-time $hold-time l ralt) ; (tap-hold $tap-time $hold-time ; rmet) ) (deflayer base @a @s @d @f @j @k @l @; )kanata-1.9.0/cfg_samples/included-file.kbd000064400000000000000000000001031046102023000165350ustar 00000000000000(defalias included-alias (macro i spc a m spc i n c l u d e d) ) kanata-1.9.0/cfg_samples/japanese_mac_eisu_kana.kbd000064400000000000000000000006411046102023000204650ustar 00000000000000#| Using meta keys as japanese eisu and kana on Mac with US keyboard. | Source | Tap | Hold | | ------- | ------------ | ---- | | lmet | lang2 (eisu) | lmet | | rmet | lang1 (kana) | rmet | |# (defcfg process-unmapped-keys yes ) (defsrc lmet rmet ) (deflayer default @lmet @rmet ) (defalias lmet (tap-hold-press 200 200 eisu lmet) rmet (tap-hold-press 200 200 kana rmet) ) kanata-1.9.0/cfg_samples/jtroo.kbd000064400000000000000000000124441046102023000152010ustar 00000000000000(defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) (defalias ;; toggle layer aliases num (layer-toggle numbers) chr (layer-toggle chords) arr (layer-toggle arrows) msc (layer-toggle misc) lay (layer-toggle layers) mse (layer-toggle mouse) ;; change the base layer between qwerty and dvorak dvk (layer-switch dvorak) qwr (layer-switch qwerty) ;; tap-hold keys with letters for tap and layer change for hold anm (tap-hold-release 200 200 a @num) oar (tap-hold-release 200 200 o @arr) ech (tap-hold-release 200 200 e @chr) umc (tap-hold-release 200 200 u @msc) grl (tap-hold-release 200 200 grv @lay) .ms (tap-hold-release 200 200 . @mse) ;; tap for capslk, hold for lctl cap (tap-hold-release 200 200 caps lctl) ;; chords csv C-S-v csc C-S-c ) (deflocalkeys-linux pho 445 ) (defalias ;; shifted keys { S-[ } S-] : S-; ral (tap-hold-release 200 200 sldr ralt) ) (defalias alp (multi a b c d e f g h i j k l m n o p q r s t u v w x y z) ls (macro l s spc min a l ret) ) (deflayer dvorak @grl 1 2 3 4 5 6 7 8 9 0 [ ] bspc tab ' , @.ms p y f g c r l / = \ @cap @anm @oar @ech @umc i d h t n s - ret lsft ; q j k x b m w v z rsft lctl lmet lalt spc @ral rmet rctl ) (deflayer qwerty @grl 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) (deflayer layers _ @qwr @dvk lrld _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) (deflayer numbers _ _ _ _ _ _ nlk kp7 kp8 kp9 _ _ _ _ _ _ C-S-tab C-tab _ XX _ kp4 kp5 kp6 min _ _ _ _ _ C-z C-y M-tab XX _ kp1 kp2 kp3 + _ _ _ C-z C-x C-c C-v XX _ kp0 kp0 . / _ _ _ _ _ _ _ _ ) (deflayer misc _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ins @{ @} [ ] _ _ _ _ _ _ _ C-u _ C-bspc bspc esc ret _ _ _ _ C-z C-x C-c C-v _ _ del _ _ _ _ _ _ _ _ _ _ _ ) (deflayer chords _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @csc _ @ls _ _ _ _ @alp _ _ C-u _ C-d _ S-; _ C-s _ _ _ _ _ _ _ _ C-b _ _ @csv _ _ _ _ _ _ _ _ _ ) (deflayer arrows _ f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 _ _ _ _ _ _ _ C-S-tab pgup up pgdn C-tab _ _ _ _ _ _ _ _ _ home left down rght end _ _ _ _ _ _ _ _ _ _ C-w _ _ _ _ _ _ _ _ _ _ ) (defalias mwu (mwheel-up 50 120) mwd (mwheel-down 50 120) mwl (mwheel-left 50 120) mwr (mwheel-right 50 120) ) (deflayer mouse _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @mwu bck _ fwd _ _ _ _ _ _ _ _ _ _ @mwd mlft _ mrgt mmid _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) (defseq "git status" (g s t) "git commit --amend --no-edit" (g c a) "git rebase -i HEAD~" (g r e b) "git log --pretty=oneline --abbrev-commit" (g l s) "git diff --ignore-space-change" (g d f) "git diff --cached --ignore-space-change" (g d c) "git push -f" (g p f) "git commit -m 'fix_this_commit_message'" (g c m) ) (deffakekeys "git status" (macro g i t spc s t a t u s) "git commit --amend --no-edit" (macro g i t spc c o m m i t spc min min a m e n d spc min min n o min e d i t) "git rebase -i HEAD~" (macro g i t spc r e b a s e spc min i spc S-h S-e S-a S-d S-grv) "git log --pretty=oneline --abbrev-commit" (macro g i t spc l o g spc min min p r e t t y = o n e l i n e spc min min a b b r e v min c o m m i t ) "git diff --ignore-space-change" (macro g i t spc d i f f spc min min i g n o r e min s p a c e min c h a n g e ) "git diff --cached --ignore-space-change" (macro g i t spc d i f f spc min min c a c h e d spc min min i g n o r e min s p a c e min c h a n g e ) "git push -f" (macro g i t spc p u s h spc min f) "git commit -m 'fix_this_commit_message'" (macro g i t spc c o m m i t spc min m spc ' f i x S-min t h i s S-min c o m m i t S-min m e s s a g e ' ) ) kanata-1.9.0/cfg_samples/kanata.kbd000064400000000000000000001655611046102023000153140ustar 00000000000000#| This is a sample configuration file that showcases every feature in kanata. A more detailed and less terse configuration guide is found here: https://github.com/jtroo/kanata/blob/main/docs/config.adoc Other configuration samples are found here: https://github.com/jtroo/kanata/tree/main/cfg_samples If anything is confusing or hard to discover, please file an issue or contribute a pull request to help improve the document. Since it may be important for you to know, pressing and holding all of the three following keys together at the same time will cause kanata to exit: Left Control, Space, Escape This is for the physical key input rather than after any remappings that are done by kanata. |# ;; Single-line comments are prefixed by double-semicolon. A single semicolon ;; is parsed as the keyboard key. Comments are ignored for the configuration ;; file. #| A multi-line comment block begin with an octothorphe followed by a pipe: `#|`. To end the multi-line comment block, have a pipe followed by an octothorpe, like the following sequence with the colon removed: `|:#`. The actual two character sequence is not used because it would end this multi-line comment block and cause a parsing error. This configuration language is Lisp-like and uses S-Expression syntax. If you're unfamiliar with Lisp, don't be alarmed. The maintainer jtroo is also unfamiliar with Lisp. You don't need to know Lisp in-depth to be able to configure kanata. If you follow along with the examples, you should be fine. Kanata should also hopefully have helpful error messages in case something goes wrong. If you need help, please feel welcome to ask in the GitHub discussions. |# ;; One defcfg entry may be added if desired. This is used for configuration ;; key-value pairs that change kanata's behaviour at a global level. ;; All defcfg options are optional. (defcfg ;; Your keyboard device will likely differ from this. I believe /dev/input/by-id/ ;; is preferable; I recall reading that it's less likely to change names on you, ;; but I didn't find any keyboard device in there in my VM. If you are on Linux ;; and omit this entry, kanata will try to attach to every device found on your ;; system that seems like a keyboard. ;; linux-dev /dev/input/by-path/platform-i8042-serio-0-event-kbd ;; If you want to read from multiple devices, separate them by `:`. ;; linux-dev /dev/input/:/dev/input/ ;; ;; If you have a colon in your device path, add a backslash before it so that ;; kanata does not parse it as multiple devices. ;; linux-dev /dev/input/path-to\:device ;; Alternatively, you can use list syntax, where both backslashes and colons ;; are parsed literally. List items are separated by spaces or newlines. ;; Using quotation marks for each item is optional, and only required if an ;; item contains spaces. ;; linux-dev ( ;; /dev/input/by-path/pci-0000:00:14.0-usb-0:1:1.0-event ;; /dev/input/by-id/usb-Dell_Dell_USB_Keyboard-event-kbd ;; ) ;; The linux-dev-names-include entry is parsed identically to linux-dev. It ;; defines a list of device names that should be included. This is only ;; used if linux-dev is omitted. ;; linux-dev-names-include device-1-name:device\:2\:name ;; The linux-dev-names-exclude entry is parsed identically to linux-dev. It ;; defines a list of device names that should be excluded. This is only ;; used if linux-dev is omitted. This and linux-dev-names-include are not ;; mutually exclusive but in practice it probably makes sense to only use ;; one of them. ;; linux-dev-names-exclude device-1-name:device\:2\:name ;; By default, kanata will crash if no input devices are found. You can change ;; this behaviour by setting `linux-continue-if-no-devs-found`. ;; ;; linux-continue-if-no-devs-found yes ;; Kanata on Linux automatically detects and grabs input devices ;; when none of the explicit device configurations are in use. ;; In case kanata is undesirably grabbing mouse-like devices, ;; you can use a configuration item to change detection behaviour. ;; ;; linux-device-detect-mode keyboard-only ;; On Linux, you can ask kanata to run `xset r rate ` on startup ;; and on live reload via the config below. The first number is the delay in ms ;; and the second number is the repeat rate in repeats/second. ;; ;; linux-x11-repeat-delay-rate 400,50 ;; On linux, you can ask kanata to label itself as a trackpoint. This has several ;; effects on libinput including enabling middle mouse button scrolling and using ;; a different acceleration curve. Otherwise, a trackpoint intercepted by kanata ;; may not behave as expected. ;; ;; If using this feature, it is recommended to filter out any non-trackpoint ;; pointing devices using linux-only-linux-dev-names-include, ;; linux-only-linux-dev-names-exclude or linux-only-linux-dev to avoid changing ;; their behavior as well. ;; ;; linux-use-trackpoint-property yes ;; Unicode on Linux works by pressing Ctrl+Shift+U, typing the unicode hex value, ;; then pressing Enter. However, if you do remapping in userspace, e.g. via ;; xmodmap/xkb, the keycode "U" that kanata outputs may not become a keysym "u" ;; after the userspace remapping. This will be likely if you use non-US, ;; non-European keyboards on top of kanata. For unicode to work, kanata needs to ;; use the keycode that outputs the keysym "u", which might not be the keycode ;; "U". ;; ;; You can use `evtest` or `kanata --debug`, set your userspace key remapping, ;; then press the key that outputs the keysym "u" to see which underlying keycode ;; is sent. Then you can use this configuration to change kanata's behaviour. ;; ;; Example: ;; ;; linux-unicode-u-code v ;; Unicode on Linux terminates with the Enter key by default. This may not work in ;; some applications. The termination is configurable with the following options: ;; ;; - `enter` ;; - `space` ;; - `enter-space` ;; - `space-enter` ;; ;; Example: ;; ;; linux-unicode-termination space ;; Kanata on Linux creates an evdev output device named "kanata". ;; This name can be changed with this linux-output-device-name. ;; ;; Examples: ;; ;; linux-output-device-name kanata_laptop ;; linux-output-device-name "kanata output device" ;; Kanata on Linux needs to declare a "bus type" for its evdev output device. ;; The options are USB and I8042. The default is I8042. ;; Using USB can break disable-touchpad-while-typing on Wayland. ;; But using I8042 appears to break some other scenarios. Thus it is configurable. ;; ;; Examples: ;; ;; linux-output-device-bus-type USB ;; linux-output-device-bus-type I8042 ;; There is an optional configuration entry for Windows to help mitigate strange ;; behaviour of AltGr if your layout uses that. Uncomment one of the items below ;; to change what kanata does with the key. ;; ;; For more context, see: https://github.com/jtroo/kanata/issues/55. ;; ;; windows-altgr cancel-lctl-press ;; remove the lctl press that comes as a combo with ralt ;; windows-altgr add-lctl-release ;; add an lctl release when ralt is released ;; ;; NOTE: even with these workarounds, putting lctl+ralt in your defsrc may ;; not work too well with other applications that use WH_KEYBOARD_LL. ;; Known applications with issues: GWSL/VcXsrv ;; Enable kanata to execute commands. ;; ;; I consider this feature a hazard so it is conditionally compiled out of ;; the default binary. ;; ;; This is dangerous because it allows kanata to execute arbitrary commands. ;; Using a binary compiled with the cmd feature enabled, uncomment below to ;; enable command execution: ;; ;; danger-enable-cmd yes ;; Enable processing of keys that are not in defsrc. ;; This is useful if you are only mapping a few keys in defsrc instead of ;; most of the keys on your keyboard. Without this, the tap-hold-release and ;; tap-hold-press actions will not activate for keys that are not in defsrc. ;; ;; The reason this is not enabled by default is because some keys may not ;; work correctly if they are intercepted. E.g. rctl/altgr on Windows; see the ;; windows-altgr configuration item above for context. ;; ;; process-unmapped-keys yes ;; We need to set it to yes in this kanata.kbd example config to allow the use of __ and ___ ;; in the deflayer-custom-map example below in the file ;; This also accepts a list parameter (all-except key1 ... keyN) ;; which behaves like "yes" but excludes the keys within the list. process-unmapped-keys (all-except f19) ;; Disable all keys not mapped in defsrc. ;; Only works if process-unmapped-keys is also yes. ;; block-unmapped-keys yes ;; Intercept mouse buttons for a specific mouse device. ;; The intended use case for this is for laptops such as a Thinkpad, which have ;; mouse buttons that may be useful to activate kanata actions with. This only ;; works with the Interception driver. ;; ;; To know what numbers to put into the string, you can run the ;; kanata-wintercept variant with this defcfg item defined with random numbers. ;; Then when a button is first pressed on the mouse device, kanata will print ;; its hwid in the log; you can then copy-paste that into this configuration ;; entry. If this defcfg item is not defined, the log will not print. ;; ;; windows-interception-mouse-hwid "70, 0, 90, 0, 20" ;; There is also a list version of windows-interception-mouse-hwid: ;; ;; windows-interception-mouse-hwids ( ;; "70, 0, 60, 0" ;; "71, 0, 62, 0" ;; ) ;; List configuration for kanata-wintercept variants ;; that allows intercepting only some connected keyboards. ;; Use similarly to mouse-hwid above. ;; ;; windows-interception-keyboard-hwids ( ;; "90, 80, 11, 34" ;; "99, 88, 77, 66" ;; ) ;; There are also exclude variants of the wintercept device configurations. ;; These cannot be defined at the same time as the non-exclude variants. ;; ;; windows-interception-keyboard-hwids-exclude ( ;; "90, 80, 11, 34" ;; "99, 88, 77, 66" ;; ) ;; ;; windows-interception-mouse-hwids-exclude ( ;; "70, 0, 60, 0" ;; "71, 0, 62, 0" ;; ) ;; Transparent keys on layers will delegate to the corresponding defsrc key ;; when found on a layer activated by `layer-switch`. This config entry ;; changes the behaviour to delegate to the action of the first layer, ;; which is the layer active upon startup, that is in the same position. ;; ;; delegate-to-first-layer yes ;; This config entry alters the behavior of movemouse-accel actions. ;; By default, this setting is disabled - vertical and horizontal ;; acceleration are independent. Enabling this setting will emulate QMK mouse ;; move acceleration behavior, i.e. the acceleration state of new mouse ;; movement actions are inherited if others are already being pressed. ;; ;; movemouse-inherit-accel-state yes ;; This config entry alters the behavior of movemouseaccel actions. ;; This makes diagonal movements simultaneous to mitigate choppiness in ;; drawing apps, if you're using kanata mouse movements to draw for ;; whatever reason. ;; ;; movemouse-smooth-diagonals yes ;; This configuration allows you to customize the length limit on dynamic macros. ;; The default limit is 128 keys. ;; ;; dynamic-macro-max-presses 1000 ;; This configuration makes multiple tap-hold actions that are activated near ;; in time expire their timeout quicker. Without this, the timeout for the 2nd ;; tap-hold onwards will start from 0ms after the previous tap-hold expires. ;; concurrent-tap-hold yes ;; This configuration makes the release of one-shot-press and of the tap in a tap-hold ;; by the defined number of milliseconds (approximate). ;; The default value is 5. ;; While the release is delayed, further processing of inputs is also paused. ;; This means that there will be a minor input latency impact in the mentioned scenarios. ;; The reason for this configuration existing is that some environments ;; do not process the scenarios correctly due to the rapidity of the release. ;; Kanata does send the events in the correct order, ;; so the fault is more in the environment, but kanata provides a workaround anyway. rapid-event-delay 5 ;; This setting defaults to yes but can be configured to no to save on ;; logging. However, if --log-layer-changes is passed as a command line ;; argument, a "no" in the configuration file will be overridden and layer ;; changes will be logged. ;; ;; log-layer-changes no ;; This configuration will press and then immediately release the non-modifier key ;; as soon as the override activates, meaning you are unlikely as a human to ever ;; release modifiers first, which can result in unintended behaviour. ;; ;; The downside of this configuration is that the non-modifier key ;; does not remain held which is important to consider for your use cases. override-release-on-activation yes ;; Accepts a single key name. ;; When configured, whenever a mouse cursor movement is received, ;; the configured key name will be "tapped" by Kanata, activating ;; the key's action. ;; ;; This enables reporting of every relative mouse movement, which ;; corresponds to standard mice, trackballs, trackpads and ;; trackpoints. Absolute movements, which can be generated by ;; touchscreens, drawing tablets and some mouse replacement or ;; accessibility software, are ignored. Scrolling events and mouse ;; buttons are also ignored. ;; ;; The intended use of these events is to provide a way to ;; automatically enable a mouse keys layer while mousing, which can ;; be disabled by a timeout or typing on other keys, rather than ;; explicit toggling. see cfg_examples/automousekeys-*.kbd for more. ;; ;; The `mvmt` key name is specially intended for this purpose. It ;; has no output key mapping and cannot be supplied as an action; ;; however, any key may be used. ;; ;; Supports live reload on Linux, but with Windows-interception, ;; this option must be present on startup to enable mouse movement ;; event collection, so restart is required to enable it. Changing ;; the key name is always supported, however. ;; ;; mouse-movement-key mvmt ) ;; deflocalkeys-* enables you to define and use key names that match your locale ;; by defining OS code number mappings for that character. ;; ;; There are five variants of deflocalkeys-*: ;; - deflocalkeys-win ;; - deflocalkeys-winiov2 ;; - deflocalkeys-wintercept ;; - deflocalkeys-linux ;; - deflocalkeys-macos ;; ;; Only one of each deflocalkeys-* variant is allowed. The variants that are ;; not applicable will be ignored, e.g. deflocalkeys-linux and deflocalkeys-wintercept ;; are both ignored when using the default Windows kanata binary. ;; ;; The configuration item is parsed similarly to defcfg; it is composed of ;; pairs of keys and values. ;; ;; You can check docs/locales.adoc for the mapping for your locale. If your ;; locale is not there, please ask for help if needed, and add the mapping for ;; your locale to the document. ;; ;; Web link for locales: https://github.com/jtroo/kanata/blob/main/docs/locales.adoc ;; ;; This example is for an Italian keyboard remapping in Linux. The numbers will ;; unfortunately differ between Linux, Windows-hooks, and Windows-interception. ;; ;; To see how you can discover key mappings for yourself, see the "Non-US keyboards" ;; section of docs/config.adoc. ;; ;; Web link or config: https://github.com/jtroo/kanata/blob/main/docs/config.adoc (deflocalkeys-win ì 187 ) (deflocalkeys-wintercept ì 187 ) (deflocalkeys-winiov2 ì 187 ) (deflocalkeys-linux ì 13 ) (deflocalkeys-macos ì 13 ) ;; Only one defsrc is allowed. ;; ;; defsrc defines the keys that will be intercepted by kanata. The order of the ;; keys matches the deflayer declarations and all deflayer declarations must ;; have the same number of keys as defsrc. ;; ;; The visual/spatial positioning is *not* mandatory; it is done by convention ;; for visual ease. These items are parsed as a long list with newlines being ;; ignored. ;; ;; If you are looking for other keys, the file src/keys/mod.rs should hopefully ;; provide some insight. (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) ;; The first layer defined is the layer that will be active by default when ;; kanata starts up. This layer is the standard QWERTY layout except for the ;; backtick/grave key (@grl) which is an alias for a tap-hold key. (deflayer qwerty @grl 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) ;; The dvorak layer remaps the keys to the dvorak layout. There are several ;; tap-hold aliases configured on the left side. (deflayer dvorak @grl 1 2 3 4 5 6 7 8 9 0 [ ] bspc tab ' , @.ms p y f g c r l / = \ @cap @anm @oar @ech @umc @ifk d h t n s - ret lsft ; q j k x b m w v z rsft lctl lmet lalt spc @ralt rmet @rcl ) ;; This is an alternative to deflayer and does not rely on defsrc. ;; It has the advantage of simpler config if only remapping a few keys. ;; You might still prefer the standard deflayer for its visual printing in ;; the log as you are learning a new configuration. (deflayermap (custom-map-example) caps esc esc caps ;; You can use _ , __ or ___ instead of specifying a key name to map all ;; keys that are not explicitly mapped in the layer. ;; E.g. esc and caps above will not be overwritten. ;; ;; _ maps only keys that are in defsrc. ;; __ excludes mapping keys that are in defsrc. ;; ___ maps both, keys that are in `defsrc`, and keys that are not. ;; ;; The two- and three-underscore variants require ;; "process-unmapped-keys yes" in defcfg to work. ;; ___ XX ;; maps all keys that are not mapped explicitly in the layer ;; ;; (i.e. esc and caps above) to "no-op" to disable the key. _ XX ;; maps all keys that are in defsrc and are not mapped in the layer __ XX ;; maps all keys that are NOT in defsrc and are not mapped in the layer ) ;; defvar can be used to declare commonly-used values (defvar tap-repress-timeout 100 hold-timeout 200 tt $tap-repress-timeout ht $hold-timeout ;; A list value in defvar that begins with concat behaves in a special manner ;; where strings will be joined together. ;; ;; Below results in 100200 a "hello" b "world" ct (concat $a " " $b) ) (defalias th1 (tap-hold $tt $ht caps lctl) th2 (tap-hold $tt $ht spc lsft) ) ;; defalias is used to declare a shortcut for a more complicated action to keep ;; the deflayer declarations clean and aligned. The alignment in deflayers is not ;; necessary, but is strongly recommended for ease of understanding visually. ;; ;; Aliases are referred to by `@`. Aliases can refer to each other, ;; e.g. in the `anm` alias. However, an alias can only refer to another alias ;; that has been defined before it in the file. (defalias ;; aliases to change the base layer to qwerty or dvorak dvk (layer-switch dvorak) qwr (layer-switch qwerty) ;; Aliases for layer "toggling". It's not quite toggling because the layer ;; will be active while the key is held and deactivated when the key is ;; released. An alternate action name you can use is layer-while-held. ;; However, the rest of this document will use The term "toggle" for brevity. num (layer-toggle numbers) chr (layer-toggle chords) arr (layer-toggle arrows) msc (layer-toggle misc) lay (layer-toggle layers) mse (layer-toggle mouse) fks (layer-while-held fakekeys) ;; tap-hold aliases with tap for dvorak key, and hold for toggle layers ;; WARNING(Linux only): key repeat with tap-hold can behave unexpectedly. ;; For full context, see https://github.com/jtroo/kanata/discussions/422 ;; ;; tap-hold parameter order: ;; 1. tap repress timeout ;; 2. hold timeout ;; 3. tap action ;; 4. hold action ;; ;; The hold timeout is the number of milliseconds after which the hold action ;; will activate. ;; ;; The tap repress timeout is best explained in a roundabout way. When you press and ;; hold a standard key on your keyboard (e.g. 'a'), your operating system will ;; read that and keep sending 'a' to the active application. To be able to ;; replicate this behaviour with a tap-hold key, you must press-release-press ;; the key within the tap repress timeout window (number is milliseconds). Simply ;; holding the key results in the hold action activating, which is why you ;; need to double-press for the tap action to stay pressed. ;; ;; There are two additional versions of tap-hold available: ;; 1. tap-hold-press: if there is a key press, the hold action is activated ;; 2. tap-hold-release: if there is a press and release of another key, the ;; hold action is activated ;; ;; These versions are useful if you don't want to wait for the whole hold ;; timeout to activate, but want to activate the hold action immediately ;; based on the next key press or press and release of another key. These ;; versions might be great to implement home row mods. ;; ;; If you come from kmonad, tap-hold-press and tap-hold-release are similar ;; to tap-hold-next and tap-hold-next-release, respectively. If you know ;; the underlying keyberon crate, tap-hold-press is the HoldOnOtherKeyPress ;; and tap-hold-release is the PermissiveHold configuration. anm (tap-hold 200 200 a @num) ;; tap: a hold: numbers layer oar (tap-hold 200 200 o @arr) ;; tap: o hold: arrows layer ech (tap-hold 200 200 e @chr) ;; tap: e hold: chords layer umc (tap-hold 200 200 u @msc) ;; tap: u hold: misc layer grl (tap-hold 200 200 grv @lay) ;; tap: grave hold: layers layer .ms (tap-hold 200 200 . @mse) ;; tap: . hold: mouse layer ifk (tap-hold 200 200 i @fks) ;; tap: i hold: fake keys layer ;; There are additional variants of tap-hold-press and tap-hold-release that ;; activate the timeout action (the 5th parameter) when the action times out ;; as opposed to the hold action being activated by other keys. ;; tap: o hold: arrows layer timeout: backspace oat (tap-hold-press-timeout 200 200 o @arr bspc) ;; tap: e hold: chords layer timeout: esc ect (tap-hold-release-timeout 200 200 e @chr esc) ;; There is another variant of `tap-hold-release` that takes a 5th parameter ;; that is a list of keys that will trigger an early tap. ;; tap: u hold: misc layer early tap if any of: (a o e) are pressed umk (tap-hold-release-keys 200 200 u @msc (a o e)) ;; tap: u hold: misc layer always tap if any of: (a o e) are pressed uek (tap-hold-except-keys 200 200 u @msc (a o e)) ;; tap for capslk, hold for lctl cap (tap-hold 200 200 caps lctl) ;; Below is an alias for the `multi` action which executes multiple actions ;; in order but at the same time. ;; ;; It may result in some incorrect/unexpected behaviour if combining complex ;; actions, so be reasonable with it. One reasonable use case is this alias: ;; press right-alt while also toggling to the `ralted` layer. The utility of ;; this is better revealed if you go see `ralted` and its aliases. ralt (multi ralt (layer-toggle ralted)) ) ;; Wrapping a top-level configuration item in a list beginning with ;; (environment (env-var-name env-var-value) ...configuration...) ;; will make the configuration only active if the environment variable matches. (environment (LAPTOP lp1) (defalias met @lp1met) ) (environment (LAPTOP lp2) (defalias met @lp2met) ) ;; NOTE: the configuration below is an older and less general variant ;; of the environment configuration above. ;; ;; The defaliasenvcond variant of defalias is parsed similarly, but there must ;; be a list parameter first. The list must contain two strings. In order, ;; these strings are: an environment variable name, and the environment ;; variable value. When the environment variable defined by name has the ;; corresponding value when running kanata, the aliases within will be active. ;; Otherwise, the aliases will be skipped. (defaliasenvcond (LAPTOP lp1) met @lp1met ) (defaliasenvcond (LAPTOP lp2) met @lp2met ) (defalias ;; shifted keys { S-[ } S-] : S-; ;; alias numbers as themselves for use in macro 8 8 0 0 ) (defalias ;; For the multi action, all keys are pressed for the whole sequence ;; but still in the listed order which may be undesirable, particularly ;; for modifiers like shift. You probably want to use macro instead. ;; ;; Chording can be more succinctly described by the modifier prefixes ;; `C-`, `A-`, `S-`, and `M-` for lctrl, lalt, lshift, lmeta, but are possible ;; by using `multi` as well. The lmeta key is also known by some other ;; names: "Windows", "GUI", "Command", "Super". ;; ;; For ralt/altgr, you can use either of: `RA-` or `AG-`. They both work the ;; same and only one is allowed in a single chord. This chord can be useful for ;; international layouts. ;; ;; A special behaviour of output chords is that if another key is pressed, ;; all of the chord keys will be released. For the explanation about why ;; this is the case, see the configuration guide. ;; ;; This use case for multi is typing an all-caps string. alp (multi lsft a b c d e f g h i j k l m n o p q r s t u v w x y z) ;; Within multi you can also include reverse-release-order to release keys ;; from last-to-first order instead of first-to-last which is the default. S-a-reversed (multi lsft a reverse-release-order) ;; Chords using the shortcut syntax. These ones are used for copying/pasting ;; from some Linux terminals. csv C-S-v csc C-S-c ;; Windows shortcut for displaying all windows win M-tab ;; Accented e characters for France layout using altgr sequence. Showcases ;; both of the shortcuts. You can just use one version of shortcut at your ;; preference. é AG-2 è RA-7 testmacro (macro AG-2 RA-7) 🙃 (unicode 🙃) ;; macro accepts keys, chords, and numbers (a delay in ms). Note that numbers ;; will be parsed as delays, so they will need to be aliased to be used. lch (macro h t t p @: / / 100 l o c a l h o s t @: @8 @0 @8 @0) tbm (macro A-(tab 200 tab 200 tab) 200 S-A-(tab 200 tab 200 tab)) hpy (macro S-i spc a m spc S-(h a p p y) spc m y S-f r S-i e S-n d @🙃) rls (macro-release-cancel Digit1 500 bspc S-1 500 bspc S-2) cop (macro-cancel-on-press Digit1 500 bspc S-1 500 bspc S-2) rlpr (macro-release-cancel-and-cancel-on-press Digit1 500 bspc S-1 500 bspc S-2) ;; repeat variants will repeat while held, once ALL macros have ended, ;; including the held macro. mr1 (macro-repeat mltp) mr2 (macro-repeat-release-cancel mltp) mr3 (macro-repeat-cancel-on-press mltp) mr4 (macro-repeat-release-cancel-and-cancel-on-press mltp) ;; Kanata also supports dynamic macros. Dynamic macros can be nested, but ;; cannot recurse. dms dynamic-macro-record-stop dst (dynamic-macro-record-stop-truncate 3) dr0 (dynamic-macro-record 0) dr1 (dynamic-macro-record 1) dr2 (dynamic-macro-record 2) dp0 (dynamic-macro-play 0) dp1 (dynamic-macro-play 1) dp2 (dynamic-macro-play 2) ;; unmod will release all modifiers temporarily and send the . ;; So for example holding shift and tapping a @um1 key will still output 1. um1 (unmod 1) ;; dead keys é (as opposed to using AltGr) that outputs É when shifted dké (macro (unmod ') e) ;; unshift is like unmod but only releases shifts ;; In ISO German QWERTZ, force unshifted symbols even if shift is held de{ (unshift ralt 7) de[ (unshift ralt 8) ;; unmod can optionally take a list as the first parameter, ;; and then will only temporarily remove ;; the listed modifiers instead of all modifiers. unalt-a (unmod (lalt ralt) a) ;; unicode accepts a single unicode character. The unicode character will ;; not be automatically repeated by holding the key down. The alias name ;; is the unicode character itself and is referenced by @🙁 in deflayer. 🙁 (unicode 🙁) ;; You may output parentheses or double quotes using unicode ;; by quotes as well as special quoting syntax. lp1 (unicode r#"("#) rp1 (unicode r#")"#) dq (unicode r#"""#) lp2 (unicode "(") rp2 (unicode ")") ;; fork accepts two actions and a key list. The first (left) action will ;; activate by default. The second (right) action will activate if any of ;; the keys in the third parameter (right-trigger-keys) are currently active. frk (fork @🙃 @🙁 (lsft rsft)) ;; switch accepts triples of keys check, action, and fallthrough|break. ;; The default usage of keys check behaves similarly to fork. ;; However, it also accepts boolean operators and|or to allow more ;; complex use cases. ;; ;; The order of cases matters. If two different cases match the ;; currently pressed keys, the case listed earlier in the configuration ;; will activate first. If the early case uses break, the second case will ;; not activate at all. Otherwise if fallthrough is used, the second case ;; will also activate sequentially after the first case. swt (switch ;; translating this keys check to some other common languages ;; this might look like: ;; ;; (a && b && (c || d) && (e || f)) ((and a b (or c d) (or e f))) a break ;; this case behaves like fork, i.e. ;; ;; (or a b c) ;; ;; or for some other common languages: ;; ;; a || b || c (a b c) b fallthrough ;; key-history evaluates to true if the n'th most recent typed key, ;; {n | n ∈ [1, 8]}, matches the given key. ((key-history a 1) (key-history b 8)) c break ;; key-timing evaluates to true if the n'th most recent typed key, ;; {n | n ∈ [1, 8]}, was typed at a time less-than/greater-than the ;; given number of milliseconds. ((key-timing 1 lt 3000) (key-timing 2 gt 30000) ) c break ((key-timing 7 less-than 200) (key-timing 8 greater-than 500)) c break ;; not means "not any of the list constituents". ;; The example below behaves like: ;; ;; !(a || b || c) ;; ;; and is equivalent to: ;; ;; ((not (or a b c))) ((not a b c)) c break ;; input logic ((input real lctl)) d break ((input virtual sft)) e break ((input-history real lsft 2)) f break ((input-history virtual ctl 2)) g break ;; layer evaluates to `true` if the active layer matches the given name ((layer dvorak)) x break ((layer qwerty)) y break ;; base-layer evaluates to `true` if the base layer matches the given name ;; The base layer is the most recent target of layer-switch. ;; The base layer is not always the active layer. ((base-layer dvorak)) x break ((base-layer qwerty)) y break ;; default case, empty list always evaluates to true. ;; break vs. fallthrough doesn't matter here () c break ) ;; Having a cmd action in your configuration without explicitly enabling ;; `danger-enable-cmd` **and** using the cmd-enabled executable will make ;; kanata refuse to load your configuration. The aliases below are commented ;; out since commands aren't allowed by this configuration file. ;; ;; Note that the parameters to `cmd` are executed directly as opposed to ;; passed to a shell. So for example, `~` and `$HOME` would not refer ;; to your home directory on Linux. ;; ;; You can use: ;; `cmd bash -c "your_stuff_here"` to run your command inside of bash. ;; ;; cm1 (cmd bash -c "echo hello world") ;; cm2 (cmd rm -fr /tmp/testing) ;; One variant of `cmd` is `cmd-log`, which lets you control how ;; running command, stdout, stderr, and execution failure are logged. ;; ;; The command takes two extra arguments at the beginning ``, ;; and ``. `` controls where the name ;; of the command is logged, as well as the success message and command ;; stdout and stderr. ;; ;; `` is only used if there is a failure executing the initial ;; command. This can be if there is trouble spawning the command, or ;; the command is not found. This means if you use `bash -c "thisisntacommand"`, as ;; long as bash starts up correctly, nothing would be logged to this channel, but ;; something like `thisisntacommand` would be. ;; ;; The log level can be `debug`, `info`, `warn`, `error`, or `none`. ;; ;; cmd-log info error bash -c "echo these are the default levels" ;; cmd-log none none bash -c "echo nothing back in kanata logs" ;; cmd-log none error bash -c "only if command fails" ;; cmd-log debug debug bash -c "echo log, but require changing verbosity levels" ;; cmd-log warn warn bash -c "echo this probably isn't helpful" ;; Another variant of `cmd` is `cmd-output-keys`. This reads the output ;; of the command and treats it as an S-Expression, similarly to `macro`. ;; However, only delays, keys, chords, and chorded lists are supported. ;; Other actions are not. ;; ;; bash: type date-time as YYYY-MM-DD HH:MM ;; cmd-output-keys bash -c "date +'%F %R' | sed 's/./& /g' | sed 's/:/S-;/g' | sed 's/\(.\{20\}\)\(.*\)/\(\1 spc \2\)/'" ) ;; The underscore _ means transparent. The key on the base layer will be used ;; instead. XX means no-op. The key will do nothing. ;; ;; A similar concept to transparent, use-defsrc means the key will always ;; behave as the key as defined by defsrc. (defalias src use-defsrc) (deflayer numbers @src _ _ _ _ _ nlk kp7 kp8 kp9 _ _ _ _ _ _ _ _ _ XX _ kp4 kp5 kp6 - _ _ _ _ _ C-z _ _ XX _ kp1 kp2 kp3 + _ _ _ C-z C-x C-c C-v XX _ kp0 kp0 . / _ _ _ _ _ _ _ _ ) ;; The `lrld` action stands for "live reload". ;; ;; NOTE: live reload does not read changes to device-related configurations, ;; such as `linux-dev`, `macos-dev-names-include`, ;; or `windows-only-windows-interception-keyboard-hwids`. ;; ;; The variants `lrpv` and `lrnx` will cycle between multiple configuration files ;; if they are specified in the startup arguments. ;; The list action variant `lrld-num` takes a number parameter and ;; reloads the configuration file specified by the number, according to the ;; order passed into the arguments on kanata startup. ;; ;; Upon a successful reload, the kanata state will begin on the default base layer ;; in the configuration. E.g. in this example configuration, you would start on ;; the qwerty layer. (deflayer layers _ @qwr @dvk lrld lrpv lrnx (lrld-num 1) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) (defalias ;; Alias for one-shot which will activate an action until either the timeout ;; expires or a different key is pressed. The timeout is the first parameter ;; and the action is the second parameter. ;; ;; The intended use cases are pressing a modifier for exactly one key or ;; switching to another layer for exactly one key. ;; ;; If a one-shot key is held then it will act as a regular key. E.g. for os1 ;; below, holding an @os1 key will keep lsft held and holding an @os3 key ;; will keep the layer set to misc. os1 (one-shot 500 lsft) os2 (one-shot 500 C-S-lalt) os3 (one-shot 500 (layer-toggle misc)) ;; Another name for one-shot is one-shot-press, since it ends on the first ;; press of another key. ;; ;; There is another variant one-shot-release which ends on the first release ;; of another key. ;; ;; There are further variants of both of these: ;; - one-shot-press-pcancel ;; - one-shot-release-pcancel ;; ;; These will cancel the one-shot action and all other active one-shot actions ;; if a one-shot key is repressed while already active. osp (one-shot-press 500 lsft) osr (one-shot-release 500 lsft) opp (one-shot-press-pcancel 500 lsft) orp (one-shot-release-pcancel 500 lsft) ;; one-shot-pause-processing can be useful in some cases ;; to preserve an activated one-shot state when it otherwise ;; would get deactivated by some action that isn't intended ;; to consume the one-shot. ;; The unit is number of milliseconds. ops (one-shot-pause-processing 5) ;; Alias for tap-dance which will activate one of the actions in the action ;; list depending on how many taps were done. Tapping once will output the ;; first action and tapping N times will output the N'th action. ;; ;; The first parameter is a timeout. Tapping the same tap-dance key again ;; within the timeout will reset the timeout and advance the tap-dance to the ;; next key. ;; ;; The action activates either when any of the following happens: ;; - the timeout expires ;; - the tap sequence reaches the end ;; - a different key is pressed td (tap-dance 200 (a b c d spc)) ;; There is a variant of tap-dance — tap-dance-eager — that will activate ;; every action tapped in the sequence rather than a single one. The example ;; below is rather simple and behaves similarly to the original tap-dance. td2 (tap-dance-eager 500 ( (macro a) ;; use macro to prevent auto-repeat of the key (macro bspc b b) (macro bspc bspc c c c) )) ;; arbitrary-code allows sending an arbitrary number as an OS code. This is ;; not cross platform! This can be useful for testing keys that are not yet ;; named or mapped in kanata. Please contribute findings with names and/order ;; mappings, either in a GitHub issue or as a pull request! This is currently ;; not supported with Windows using the interception driver. ab1 (arbitrary-code 700) ) (defalias ;; caps-word will add an lsft to the active key list for all alphanumeric keys ;; a-z, and the US layout minus key; meaning it will be converted to an ;; underscore. ;; ;; The caps-word state will also be cleared if any key that doesn't get auto- ;; capitalized and also doesn't belong in this list is pressed: ;; - 0-9 ;; - kp0-kp9 ;; - bspc, del ;; - up, down, left, rght ;; ;; The single parameter is a timeout in milliseconds after which the caps-word ;; state will be cleared and lsft will not be added anymore. The timer is reset ;; any time a capitalizable or extra non-terminating key is active. cw (caps-word 2000) ;; Like caps-word, but you get to choose the key lists where lsft gets added. ;; This example is similar to the default caps-word behaviour but it moves the ;; 0-9 keys to capitalized key list from the extra non-terminating key list. cwc (caps-word-custom 2000 (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) ) ) ;; -toggle variants of caps-word will terminate caps-word on repress if it is ;; currently active, otherwise caps-word will be activated. (defalias cwt (caps-word-toggle 2000) cct (caps-word-custom-toggle 2000 (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) ) ) ;; Can see a new action `rpt` in this layer. This repeats the most recently ;; pressed key. Holding down the `rpt` key will not repeatedly send the key. ;; The intended use case is to be able to use a different finger to repeat a ;; double letter, as opposed to double-tapping a letter. ;; ;; The `rpt` action only repeats the last key output. For example, it won't ;; output a chord like `ctrl+c` if the previous key pressed was `C-c` - it ;; will only output `c`. There is a variant `rpt-any` which will repeat the ;; previous action and would work for that use case. (deflayer misc _ _ _ _ _ _ _ _ _ @é @è _ ì #|random custom key for testing|# _ _ _ @ab1 _ _ _ ins @{ @} [ ] _ _ + @cw _ _ _ C-u _ del bspc esc ret _ _ _ @cwc C-z C-x C-c C-v _ _ _ @td @os1 @os2 @os3 rpt rpt-any _ _ _ _ _ ) (deflayer chords ;; you can put list actions directly in deflayer but it's ugly, so prefer aliases. _ _ _ _ _ _ _ _ _ _ @🙁 (unicode 😀) _ _ _ _ _ _ _ _ _ _ @csc @hpy @lch @tbm _ _ _ @alp _ _ _ _ _ @ch1 @ch2 @ch4 @ch8 _ _ _ _ _ _ _ _ _ _ _ @csv _ _ _ _ _ _ _ _ _ ) (deflayer arrows _ f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 _ _ _ _ _ _ _ _ pgup up pgdn _ _ _ _ _ _ _ _ _ _ home left down rght end _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) ;; In Windows, using mouse buttons on the kanata window seems to cause it to hang. ;; Using the mouse on other windows seems to be fine though. ;; ;; The mouse buttons can be clicked using mlft, mrgt, mmid, mfwd and mbck, representing the ;; left, right, middle, forward and backward mouse buttons respectively. If the key is held, the ;; button press will also be held. ;; ;; If there are multiple mouse click actions within a single multi action, e.g. ;; (multi mrgt mlft), then all the buttons except the last will be clicked then ;; unclicked. The last button will remain held until key release. In the example ;; given, the button sequence will be: ;; press key->click right->unclick right->click left->release key->release left ;; ;; There are variants of the standard mouse buttons which "tap" the button. ;; These are mltp, mrtp, and mmtp. Rather than holding until key release, this ;; action will click and unclick the button once the key is pressed. Nothing ;; happens on key release. The action (multi lctl mltp) will result in the ;; sequence below: ;; press key->press lctl->click left->unclick left->release key->release lctl ;; ;; One can also see mouse movement actions at the lower right side, with the ;; arrow unicode characters. (deflayer mouse _ @mwu @mwd @mwl @mwr _ _ _ _ _ @ma↑ _ _ _ _ pgup bck _ fwd _ _ _ _ @ma← @ma↓ @ma→ _ _ _ pgdn mlft _ mrgt mmid _ mbck mfwd _ @ms↑ _ _ @fms _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ _ _ _ _ _ _ _ ) (defalias ;; Mouse wheel actions. The first number is the interval in milliseconds ;; between scroll actions. The second number is the distance in some arbitrary ;; unit. Play with the parameters to see what feels correct. Both numbers ;; must be in the range 1-65535 ;; ;; In both Windows and Linux, 120 distance units is equivalent to a single ;; notch movement on a physical wheel. In Linux, not all desktop environments ;; support the REL_WHEEL_HI_RES event so if you experience issues with `mwheel` ;; actions in Linux, using a distance value that is multiple of 120 may help. mwu (mwheel-up 50 120) mwd (mwheel-down 50 120) ;; Horizontal mouse wheel actions. Similar story to vertical mouse wheel. mwl (mwheel-left 50 120) mwr (mwheel-right 50 120) ;; Mouse movement actions.The first number is the interval in milliseconds ;; between mouse actions. The second number is the distance traveled per interval ;; in pixels. ms↑ (movemouse-up 1 1) ms← (movemouse-left 1 1) ms↓ (movemouse-down 1 1) ms→ (movemouse-right 1 1) ;; Mouse movement actions with linear acceleration. The first number is the ;; interval in milliseconds between mouse actions. The second number is the time ;; in milliseconds for the distance to linearly ramp up from the minimum distance ;; to the maximum distance. The third number is the minimum distance traveled ;; per interval in pixels. The fourth number is the maximum distance traveled ;; per interval in pixels. ma↑ (movemouse-accel-up 1 1000 1 5) ma← (movemouse-accel-left 1 1000 1 5) ma↓ (movemouse-accel-down 1 1000 1 5) ma→ (movemouse-accel-right 1 1000 1 5) ;; setmouse places the cursor at a specific pixel x-y position. This ;; example puts it in the middle of the screen. The coordinates go from 0,0 ;; which is the upper-left corner of the screen to 65535,65535 which is the ;; lower-right corner of the screen. If you have multiple monitors, they are ;; treated as one giant screen, which may make it a bit confusing for how to ;; set up the pixels. You will need to experiment. sm (setmouse 32228 32228) ;; movemouse-speed takes a percentage by which it then scales all of the ;; mouse movements while held. You can have as many of these active at a ;; given time as you would like, but be warned that some values, such as 33 ;; may not have correct pixel distance representations. fms (movemouse-speed 200) ) (defalias lft (multi (release-key ralt) left) ;; release ralt if held and also press left rgt (multi (release-key ralt) rght) ;; release ralt if held and also press rght rlr (release-layer ralted) ;; release layer-toggle of ralted ) ;; It's not clear what the practical use case is for the @rlr alias, but the ;; combination of @ralt on the dvorak layer and this layer with @lft and @rgt ;; results in the physical ralt key behaving mostly as ralt, **except** for ;; holding it **then** pressing specific keys. These specific keys release the ;; ralt because it would cause them to have undesired behaviour without the ;; release. ;; ;; E.g. ralt+@lft will result in only left being pressed instead of ralt+left, ;; while ralt(hold)+tab+tab+tab still works as intended. (deflayer ralted _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @lft @rlr @rgt _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) ;; Virtual key actions (defvirtualkeys ;; Define some virtual keys that perform modifier actions vkctl lctl vksft lsft vkmet lmet vkalt lalt ;; A virtual key that toggles all modifier virtual keys above vktal (multi (on-press toggle-virtualkey vkctl) (on-press toggle-virtualkey vksft) (on-press toggle-virtualkey vkmet) (on-press toggle-virtualkey vkalt) ) ;; Virtual key that activates a macro vkmacro (macro h e l l o spc w o r l d) ) (defalias psfvk (on-press press-virtualkey vksft) rsfvk (on-press release-virtualkey vksft) palvk (on-press tap-vkey vktal) macvk (on-press tap-vkey vkmacro) isfvk (on-idle 1000 tap-vkey vksft) ) ;; Press and release fake keys. ;; ;; Fake keys can't be pressed by any physical keyboard buttons and can only be ;; acted upon by the actions: ;; - on-press-fakekey ;; - on-release-fakekey ;; - on-idle-fakekey ;; ;; One use case of fake keys is for holding modifier keys ;; for any number of keypresses and then releasing the modifiers when desired. ;; ;; The actions associated with fake keys in deffakekeys are parsed before ;; aliases, so you can't use aliases within deffakekeys. Other than the lack ;; of alias support, fake keys can do any action that a normal key can, ;; including doing operations on previously defined fake keys. ;; ;; Operations on fake keys can occur either on press (on-press-fakekey), ;; on release (on-release-fakekey), or on idle for a specified time ;; (on-idle-fakekey). ;; ;; Fake keys are flexible in usage but can be obscure to discover how they ;; can be useful to you. (deflayer fakekeys _ @fcp @fsp @fmp @pal _ _ _ _ _ _ _ _ _ _ @fcr @fsr @fap @ral _ _ _ _ _ _ _ _ _ _ @fct @fst @rma _ _ _ _ _ _ _ _ _ _ @t1 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) (deffakekeys ctl lctl sft lsft lsft lsft met lmet alt lalt mmid mmid pal (multi (on-press-fakekey ctl press) (on-press-fakekey sft press) (on-press-fakekey met press) (on-press-fakekey alt press) ) ral (multi (on-press-fakekey ctl release) (on-press-fakekey sft release) (on-press-fakekey met release) (on-press-fakekey alt release) ) ) (defalias fcp (on-press-fakekey ctl press) fcr (on-press-fakekey ctl release) fct (on-press-fakekey ctl tap) fsp (on-release-fakekey sft press) fsr (on-release-fakekey sft release) fst (on-release-fakekey sft tap) fsg (on-release-fakekey sft toggle) fmp (on-press-fakekey met press) fap (on-press-fakekey alt press) rma (multi (on-press-fakekey met release) (on-press-fakekey alt release) ) pal (on-press-fakekey pal tap) ral (on-press-fakekey ral tap) rdl (on-idle-fakekey ral tap 1000) hfd (hold-for-duration 1000 met) ;; Test of on-press-fakekey and on-release-fakekey in a macro t1 (macro-release-cancel @fsp 5 a b c @fsr 5 c b a) ;; If you find that an application isn't registering keypresses correctly ;; with multi, you can try out: ;; - on-press-fakekey-delay ;; - on-release-fakekey-delay ;; ;; Do note that processing a fakekey-delay and even a sequence of delays will ;; delay any other inputs from being processed until the fakekey-delays are ;; all complete, so use with care. stm (multi ;; Shift -> middle mouse with a delay (on-press-fakekey lsft press) (on-press-fakekey-delay 200) (on-press-fakekey mmid press) (on-release-fakekey mmid release) (on-release-fakekey-delay 200) (on-release-fakekey lsft release) ) ) ;; Vim-style leader-key sequences. Activate a fakekey-tap by pressing a "leader" ;; key and then a sequence of characters. ;; See: https://github.com/jtroo/kanata/issues/97 ;; ;; You can add an entry to defcfg to change the sequence timeout (default is 1000): ;; sequence-timeout ;; ;; If you want multiple timeouts with different leaders, you can also activate the ;; sequence action: ;; (sequence ) ;; This acts like `sldr` but uses a different timeout. ;; ;; There is also an option to customize the key sequence input mode. Its default ;; value when not configured is `hidden-suppressed`. ;; ;; The options are: ;; ;; - `visible-backspaced`: types sequence characters as they are inputted. The ;; typed characters will be erased with backspaces for a valid sequence termination. ;; - `hidden-suppressed`: hides sequence characters as they are typed. Does not ;; output the hidden characters for an invalid sequence termination. ;; - `hidden-delay-type`: hides sequence characters as they are typed. Outputs the ;; hidden characters for an invalid sequence termination either after either a ;; timeout or after a non-sequence key is typed. ;; ;; For `visible-backspaced` and `hidden-delay-type`, a sequence leader input will ;; be ignored if a sequence is already active. For historical reasons, and in case ;; it is desired behaviour, a sequence leader input using `hidden-suppressed` will ;; reset the key sequence. ;; ;; Example: ;; sequence-input-mode visible-backspaced (defseq git-status (g s t)) (deffakekeys git-status (macro g i t spc s t a t u s)) (defalias rcl (tap-hold-release 200 200 sldr rctl)) (defseq dotcom (. S-3) dotorg (. S-4) ) (deffakekeys dotcom (macro . c o m) dotorg (macro . o r g) ) ;; Enter sequence mode and input . (defalias dot-sequence (macro (sequence 250) 10 .)) (defalias dot-sequence-inputmode (macro (sequence 250 hidden-delay-type) 10 .)) ;; There are special keys that you can assign in your actions which will ;; never output events to your operating system, but which you can use ;; in sequences. They are named: nop0-nop9. (defseq dotcom (nop0 nop1) dotorg (nop8 nop9) ) ;; A key list within O-(...) signifies simultaneous presses. (defseq dotcom (O-(. c m)) dotorg (O-(. r g)) ) ;; Input chording. ;; ;; Not to be confused with output chords (like C-S-a or the chords layer ;; defined above), these allow you to perform actions when a combination of ;; input keys (a "chord") are pressed all at once (order does not matter). ;; Each combination/chord can perform a different action, allowing you to bind ;; up to `2^n - 1` different actions to just `n` keys. ;; ;; Each `defchords` defines a named group of such chord-action pairs. ;; The 500 is a timeout after which a chord triggers if it isn't triggered by a ;; key release or press of a non-chord key before the timeout expires. ;; If a chord is not defined, no action will occur when it is triggered but the ;; keys used to input it will be consumed regardless. ;; ;; Each pair consists of the keys that make up a given chord in the parenthesis ;; followed by the action that should be executed when the given chord is ;; pressed. ;; Note that these keys do not directly correspond to real keys and are merely ;; arbitrary labels that make sense within the context of the chord. ;; They are mapped to real keys in layers by configuring the key in the layer to ;; map to a `(chord name key)` action (like those in the `defalias` below) where ;; `name` is the name of the chords group (here `binary`) and `key` is one of the ;; arbitrary labels of the keys in a chord (here `1`, `2`, `4` and `8`). ;; ;; Note that it is perfectly valid to nest these `chord` actions that enter ;; "chording mode" within other actions like `tap-dance` and that will work as ;; one would expect. ;; However, this only applies to the first key used to enter "chording mode". ;; Once "chording mode" is active, all other keys will be directly handled by ;; "chording mode" with no regard for wrapper actions; e.g. if a key is pressed ;; and it maps to a tap-hold with a chord as the hold action within, that chord ;; key will immediately activate instead of the key needing to be held for the ;; timeout period. ;; ;; The action executed by a chord (the right side of the chord-action pairs) may ;; be any regular or advanced action, including aliases. They currently cannot ;; however contain a `chord` action. (defchords binary 500 (1 ) 1 ( 2 ) 2 (1 2 ) 3 ( 4 ) 4 (1 4 ) 5 ( 2 4 ) 6 (1 2 4 ) 7 ( 8) 8 (1 8) 9 ( 2 8) (multi 1 0) (1 2 8) (multi 1 1) ( 4 8) (multi 1 2) (1 4 8) (multi 1 3) ( 2 4 8) (multi 1 4) (1 2 4 8) (multi 1 5) ) (defalias ch1 (chord binary 1) ch2 (chord binary 2) ch4 (chord binary 4) ch8 (chord binary 8) ) ;; The top-level action `include` will read a configuration from a new file. ;; At the time of writing, includes can only be placed at the top level. The ;; included files also cannot contain includes themselves. ;; ;; (include included-file.kbd) ;; The top-level item `deftemplate` declares a template ;; which can be expanded multiple times to reduce repetition. ;; ;; Expansion of a template is done via `expand-template`. ;; This template defines a chord group and aliases that use the chord group. ;; The purpose is to easily define the same chord position behaviour ;; for multiple layers that have different underlying keys. (deftemplate left-hand-chords (chordgroupname k1 k2 k3 k4 alias1 alias2 alias3 alias4) (defalias $alias1 (chord $chordgroupname $k1) $alias2 (chord $chordgroupname $k2) $alias3 (chord $chordgroupname $k3) $alias4 (chord $chordgroupname $k4) ) (defchords $chordgroupname $chord-timeout ($k1) $k1 ($k2) $k2 ($k3) $k3 ($k4) $k4 ($k1 $k2) lctl ($k3 $k4) lsft ) ) (defvar chord-timeout 200) (template-expand left-hand-chords qwerty a s d f qwa qws qwd qwf) ;; You can use t! as a short form of template-expand (t! left-hand-chords dvorak a o e u dva dvo dve dvu) (deflayer template-example _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @qwa @qws @qwd @qwf _ _ _ _ _ _ _ _ _ _ @dva @dvo @dve @dvu _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) ;; Within a deftemplate you can use if-equal to conditionally insert content ;; into the template. (deftemplate home-row (version) a s d f g h (if-equal $version v1 j) (if-equal $version v2 (tap-hold 200 200 j lctl)) k l ; ' ) (deftemplate common-overrides () (lctl 7) (lctl lsft tab) (lctl 9) (lctl tab) (lalt 7) (lalt lsft tab) (lalt 9) (lalt tab) ) ;; Wrapping a top-level configuration item in a list beginning with ;; (platform (applicable-platforms...) ...configuration...) ;; will make the configuration only active on a specific platform. (platform (macos) ;; Only on macos, use command arrows to jump/delete words ;; because command is used for so many other things ;; and it's weird that these cases use alt. (defoverrides (lmet bspc) (lalt bspc) (lmet left) (lalt left) (lmet right) (lalt right) (template-expand common-overrides) ) ) (platform (win winiov2 wintercept linux) (defoverrides (template-expand common-overrides) ) ) #| Global input chords. Syntax (5-tuples): (defchordsv2 (participating-keys1) action1 timeout1 release-behaviour1 (disabled-layers1) ... (participating-keysN) actionN timeoutN release-behaviourN (disabled-layersN) ) |# (defchordsv2 (a b c) (macro a l p h a b e t) 200 all-released (qwerty arrows) (h l o) (macro h e l l o) 250 first-release (qwerty arrows) (g b y e) (macro g o o d b y e) 400 first-release (qwerty arrows) ) #| Yet another chording implementation - zippychord: ;; This is a sample for US international layout. (defzippy zippy.txt on-first-press-chord-deadline 500 idle-reactivate-time 500 smart-space-punctuation (? ! . , ; :) output-character-mappings ( ! S-1 ? S-/ % S-5 "(" S-9 ")" S-0 : S-; < S-, > S-. r#"""# S-' | S-\ _ S-- ® AG-r ;; In case you use dead keys or compose keys ;; where multiple keys are pressed ;; to produce a single backspaceable symbol, ;; use no-erase or single-output ’ (no-erase `) é (single-output ' e) ) ) Example file content of zippy.txt: --- dy day dy 1 Monday abc Alphabet r df recipient w a Washington rq request rqa request assistance --- You can read about zippychord in more detail in the configuration guide. |# #| Clipboard actions allow you to manipulate the clipboard. To paste, you should manually output C-v, or whatever key output is necessary to paste. E.g. S-ins might also work. |# (deflayermap (clip) a (clipboard-set clip) b (clipboard-save 0) c (clipboard-restore 0) d (clipboard-save-swap 0 65535) #| actions with cmd only works with the compilation flags and defcfg enablement. e (clipboard-cmd-set powershell.exe -c "echo 'hello world'") f (clipboard-save-cmd-set 0 bash -c "echo 'goodbye'") |# ) kanata-1.9.0/cfg_samples/key-toggle_press-only_release-only.kbd000064400000000000000000000013621046102023000227620ustar 00000000000000#| This configuration showcases all of: - key toggle - press-only - release-only |# (deftemplate toggle-key (vkey-name output-key alias) (defvirtualkeys $vkey-name $output-key) (defalias $alias (on-press toggle-vkey $vkey-name)) ) (deftemplate press-only-release-only-pair (vkey-name output-key press-alias release-alias) (defvirtualkeys $vkey-name $output-key) (defalias $press-alias (on-press press-vkey $vkey-name)) (defalias $release-alias (on-press release-vkey $vkey-name)) ) (template-expand toggle-key v-lctl lctl lcl) (template-expand toggle-key v-rctl rctl rcl) ;; t! is a short form of template-expand (t! press-only-release-only-pair v-lalt lalt p-a r-a) (defsrc lctl rctl lalt ralt ) (deflayer base @lcl @rcl @p-a @r-a ) kanata-1.9.0/cfg_samples/minimal.kbd000064400000000000000000000025571046102023000154760ustar 00000000000000#| This minimal config changes Caps Lock to act as Caps Lock on quick tap, but if held, it will act as Left Ctrl. It also changes the backtick/grave key to act as backtick/grave on quick tap, but change ijkl keys to arrow keys on hold. This text between the two pipe+octothorpe sequences is a multi-line comment. |# ;; Text after double-semicolons are single-line comments. #| One defcfg entry may be added, which is used for configuration key-pairs. These configurations change kanata's behaviour at a more global level than the other configuration entries. |# (defcfg #| This configuration will process all keys pressed inside of kanata, even if they are not mapped in defsrc. This is so that certain actions can activate at the right time for certain input sequences. By default, unmapped keys are not processed through kanata due to a Windows issue related to AltGr. If you use AltGr in your keyboard, you will likely want to follow the simple.kbd file while unmapping lctl and ralt from defsrc. |# process-unmapped-keys yes ) (defsrc caps grv i j k l lsft rsft ) (deflayer default @cap @grv _ _ _ _ _ _ ) (deflayer arrows _ _ up left down rght _ _ ) (defalias cap (tap-hold-press 200 200 caps lctl) grv (tap-hold-press 200 200 grv (layer-toggle arrows)) ) kanata-1.9.0/cfg_samples/simple.kbd000064400000000000000000000072171046102023000153370ustar 00000000000000;; Comments are prefixed by double-semicolon. A single semicolon is parsed as the ;; keyboard key. Comments are ignored for the configuration file. ;; ;; This configuration language is Lisp-like. If you're unfamiliar with Lisp, ;; don't be alarmed. The maintainer jtroo is also unfamiliar with Lisp. You ;; don't need to know Lisp in-depth to be able to configure kanata. ;; ;; If you follow along with the examples, you should be fine. Kanata should ;; also hopefully have helpful error messages in case something goes wrong. ;; If you need help, you are welcome to ask. ;; Only one defsrc is allowed. ;; ;; defsrc defines the keys that will be intercepted by kanata. The order of the ;; keys matches with deflayer declarations and all deflayer declarations must ;; have the same number of keys as defsrc. Any keys not listed in defsrc will ;; be passed straight to the operating system. (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) ;; The first layer defined is the layer that will be active by default when ;; kanata starts up. This layer is the standard QWERTY layout except for the ;; backtick/grave key (@grl) which is an alias for a tap-hold key. (deflayer qwerty @grl 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) ;; The dvorak layer remaps the keys to the dvorak layout. In addition there is ;; another tap-hold key: @cap. This key retains caps lock functionality when ;; quickly tapped but is read as left-control when held. (deflayer dvorak @grl 1 2 3 4 5 6 7 8 9 0 [ ] bspc tab ' , . p y f g c r l / = \ @cap a o e u i d h t n s - ret lsft ; q j k x b m w v z rsft lctl lmet lalt spc ralt rmet rctl ) ;; defalias is used to declare a shortcut for a more complicated action to keep ;; the deflayer declarations clean and aligned. The alignment in deflayers is not ;; necessary, but is strongly recommended for ease of understanding visually. ;; ;; Aliases are referred to by `@`. (defalias ;; tap: backtick (grave), hold: toggle layer-switching layer while held grl (tap-hold 200 200 grv (layer-toggle layers)) ;; layer-switch changes the base layer. dvk (layer-switch dvorak) qwr (layer-switch qwerty) ;; tap for capslk, hold for lctl cap (tap-hold 200 200 caps lctl) ) ;; The `lrld` action stands for "live reload". This will re-parse everything ;; except for linux-dev, meaning you cannot live reload and switch keyboard ;; devices. ;; ;; The keys 1 and 2 switch the base layer to qwerty and dvorak respectively. ;; ;; Apart from the layer switching and live reload, all other keys are the ;; underscore _ which means "transparent". Transparent means the base layer ;; behaviour is used when pressing that key. (deflayer layers _ @qwr @dvk lrld _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) kanata-1.9.0/cfg_samples/tray-icon/3trans.parent.png000064400000000000000000000017001046102023000204700ustar 00000000000000PNG  IHDR\rf pHYs+rIDATxM1E?(}I)"a "!,T$[W63Io Vxr}Wπ/]׹ng| VxzƯ9ϫ'ҙ|ӿbc>whm[='| L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L lc` /{lmKIENDB`kanata-1.9.0/cfg_samples/tray-icon/6name-match.png000064400000000000000000000016431046102023000200740ustar 00000000000000PNG  IHDR\rf pHYs+UIDATx 1A"dG Zq]Z돏^@mz0G L L L L L L L L L L L L L L L >=`̾GH1?00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000~O/\^SxL/9O;Z{z0aaaaaaaaaaaaaaaaa;H m@eIENDB`kanata-1.9.0/cfg_samples/tray-icon/_custom-icons/s.png000064400000000000000000000140771046102023000210250ustar 00000000000000PNG  IHDR\rf pHYs+IDATxytU@F$!I2(*ւX{PՊme"8rEI&I@H H dkeR}ށg QJY)B:@)%G@)(e1,tRPb:JYL@)(e1,tXt DH 9 qiѝSDCD$DA˿""75|X MUC? uUBM9ԖB. JtXH>F@Hʁ.= !b:wGOGZU{ 8.^$|MngCе?@DtU57v8 *P_.]fz 2΂[=.PJ?H!Tt|$*(O.ػJWA]}`G0/p 5P1O_tN =H(Dt#LoI1" VB%Mf(~It !%m۠H,ą7U ҄*Tvt?^KWP+( ZBR$؉X=IWP+Y*]_@l?z͔./kܗ"68X U+8I}-Nʺ2J?cY7f8Sk+ͪȟC+W?fD'YHW(/ѷ50/"]TtY1 Ca4 奚 xGa ?٣[H yzo,._dCO59?n?@/:"c:$V1zӠjs8XwB 5vsO A\gB\:$LHL M ՟u*Bنpa x[6 g | e `\w>ĹV]8ɯ@@Hyq.D^+ U :ꊠPzϧ]ρ c(t _WiVBpko'rtM$ ߂q:%v{W z2!m'?u1S C =n!{ $e{[9 gtE*?rMJ|-R΃~@Ķ=UfGAҌ_a`g\|+?@5=b:{'y0@]"]g *{|^z owOyϨH6芿?/]l`t-]C%PWQ0j:r_.ҡTG5ݤ q`tQ*]}F @\wWJ([5NQp0ʯC8 7Z? `ȟ&%O@YtGt|(t%]l`>7K(5M 5" Qd?cUIL_JW(ә5 56JQP_]i0% e2L}}/AIW(S5U{ Bc+JC/ KiK B' L.Q&1jAag&#@~bPؿE r!J _H2.AW=K',tM/]⽲ua7WD @ C9p"H(]qPtzt3K!"Z?B~@# (^!]٣ u(xQÄᚽpƟStfSX;?B5JG^.7̆on?7#4vtJ. հC(xN l`|rtEx:΁O@]t  & KWS=-u^X7&`06 3TַP^Fu \Y\j[# %`Ll~> Т XtYs$[ $#]ʊ#>vBlly?GHS?ӂ^g¦P_.]NĊ-HW!=Jń~^[A(ZkKǨV@t\ι^jXCDYEC̿jKu&\L^ ݧJb>,~}ZP0esvF/a&;\l\ ]c+_8֘Y0PE}>f X}i] #;5ҢPPBH σs|)|0ϗ.QG{!L[ }~(]b&@[%hI011k̢p/KWc厇ik '߆`t:Z\*\ :''@a5zLb{KDŽ7S(y^\ SK0b9t\$|Ft:VB7UȽ_$<RbX~ob''K„A|u/ʛ+‡0?| 47Jר F*]>t)|z;̹Hר ͑:T1˟")Our:.0s$l_D8չ+:1| .5 [T¿tB  U{kԘC=3p\z0"AOo;jS2 Ptx$*?!KMx ^ɀOA.s]aGk@H8H>M8&Axﻰo!9[!ct~pPDMwB s5hPtL3iP>[DeyCιzTලM˟vHOq&+>|vVB@ywAɐ#]* aVt0RȻzOdas :FDIG:˃ o tɒ ng+`aw10ott,0}9*#`3<ԹMd%9W}9=HK #]=4ײ`PM?r/.av3ERr!yttw !r6˽F[:_ffš砱NFFegt46PtD Z %?jDDAP'axc]g 2HxG@ҡ0' ޒ.FP , Vϐ. PmNX ЊMv`f[UQP?aN'] n˯4@ovk %&] !K`OR :{{`lt7tT5TIWSt7t+6E]P8.pOdt7tkNP$]J@R (Tm.pOY:5ukk"(4IQ:5ѽ S_%] ؞?,] x.ut7tkR f%wHPI/]*hrM sX:IcKt{ZFۥ \JGxC@uX$7E=KȾM\$ |];V @4\Y -C{$.}W] cc!r_z¸28wΡ13̻~^"X1)1FV7^#D_A w5AL 1gc` pr Mz%;IJoHWl-#Ⱥ=" cuP{A*(y*? JXRj`dK@!a(_@Ooș&<[^.aS+BZ?銯Tl ޿4'BY=z#voa@twigsf2hU{vRuO7zAboH {Al>Qg zT^8D@B7+چZhv~TM Ώ# *qtsBZ}|tc stAhDMfB%80;2I(; "]!JTQBcW7A垪2^ yF@8weg:e .P~k1^HH+]>[?[:忱='b#]pL_t}z,NH']>JKǨHyORG[ϖ'@cm[/ C>RߌH[ e#Z @-^^ `;Ĝ8ԿFoxwݟo s@[l>ʈf@JRBX0^ʈ# 2FBI4 {1J( ͍T_G /P^ڽ[K_@zl[47?KX|/TXx P |4lwR@l*` sia鯜*3Tlo?Ԍ=xo:~3$&]P }>*]bchٷÀ {T!fJ`m>"q8^ 5Xaq}K1z&w :b:Kثv-3Uew@oBhѳ,mswpPFn2o^1H%]d {gJרz8BX8b=P vQpi c"d!t4ލ{ T[AȘ΄!/tJV}5 etQt:Hz6>]̸CёP * o oI@p !1:45WAuVRb1!]BM@@T$t>r!'Ħ@L2ĦBLDCT<;:v~Q#M四j@Cr8ԖAMZ =+_PbNy,tRPb:JYL@)(e1,tRPb:JYw/ݣghIENDB`kanata-1.9.0/cfg_samples/tray-icon/icons/1symbols.png000064400000000000000000000067041046102023000206630ustar 00000000000000PNG  IHDR\rf pHYs+ vIDATxrTVTm SE 2L 2(diQ \H qaѺteYѵ[g}GZ l}wE' H! 1H# 1H# 1H# 1H# 1H# 1HQ1O|-Ď)I_/7v̡osc4??1%i1o<s! E1h* G  "jc^2VC]6vV"m0)SQl1 "LX#UoHJ_:}X`aFiZbX=. Nď&; E1b =tJ05ME&bWMEͰ&]H86q?u*&C`QW Q>4_I~dC+E! މof>óqW"`a0b%O p톢pqx͆=1` ͎g%.7?1ņ6iCv E;jG'Oa\` jOq"L qkJE6̊u$@0W[lYY, sxV\ PO*8h:`I@񾝐ΌǏ{dm9p@k0f%#9vv xV[: z,.@K/&:4+%#J m EzMjw ` t|sC, d_lZ9 +!lKTݜbA2zh 4 I86`\b< ^U`Z7Л P&!VM#8u?W @sFd sìxCZkvc4`0+uwL{P=E;Qc ( W_qcjA7.@dž(X@VY(-6+FC+syì8p! %@kR/aV3,p`fƤۓ:nڕt.S_}1Un=kc_?CE'-A×i.%fWW 1H# 1H# 1H# 1H# 1H# 1H# 1H# U};p_uJ;褴1%J7 c̀2Dŏ)I; :lzՎX&;ďyT݆3`l35St*+Bd(L&ó% A,͆d(5Z!韼X=ғq?2E[ݺTWU`b(qw&`1\' C,G_t|$ C,fEdz^`ae/;"=[4+ 5=8ِ& ChoUl(>vihCQ|3! 8ZhCQv,ТL!P@[c]-,2!Z\1E"=?g|(ТM9CQ -8ѐީǎ ЂCZC ?÷/z!Z(K:;_8kȳhAo] vhcVteI x@ ;+Cp@ 7k1Z#_j DeC4PƤۓ0|h-cM|`I.@Z1&9^6̊ϵIOv~J ⧆oI8-@\diXMx0+~Y:vSƊq/j}‡$F@b$F@b$F@b$F@b$F@b$F@b$F@bX4q8oJ;%z$vLIq@|_ ƎXGcJZ7Lϊt`PGkR{%~+0]Ui]{5cJE?:)}6?NóJqY SA40?.GQ7cJ7ytUpG1+^u z0G (uHk ;`{Rxz4~æ%C`!32iCQ|a(cxtvvVDa"搩PFX0G Z@a(iNpmy8XZg E$y~Ge EvH˂nCQ|Yqw~]R fh S%YlOlyٿ4Y8⮍1 :ip-dtGl.`æ EIApt:g Gav qD34y3X5͈ ՘`i38$`38bwyIB( IC pR!I5ÿ%CQf mBLӗ~g=O 0t܇xLw8A 0:saЃqi{̍;InˇƱ5ֵ1pzd(y(`OL-@5t|Sbs0SmNSWCLM3YG'3;`~Z,K8/xV~PNG[v-@G[U|wU4{8 I}1fŗMVDsAdCz0+5> PwdoHx(%@ZgM 8xS,x-.@њTMr8smҏ~M-@GMI>wJһpO>vf{cW.7PZ 8ewR@K+}Xb& v[H E bN6XI^@b$F@b$F@b$F@b$Frm-hIENDB`kanata-1.9.0/cfg_samples/tray-icon/img/2Nav Num.png000064400000000000000000000071261046102023000201000ustar 00000000000000PNG  IHDR\rf pHYs+IDATxi$U񧪻aFPAQԌ *h45D1D`eaܗqI\EQcFⒸ&bD\P:ȝ[rsefkyz*Ke)QJCQ@( b1 D"FQ@( b1 D"6d-BìS(Y7G,vYEth|uP@cz(KI)@ǒ4oXAȞ* βNP6):@" /NQ&Z'@(CqR^`lb)9:bEKkS V@gHӬS F@M!# 'FJÝ) G&['@l(IkS &@ό/) g# tGifSGKËS t@MNQ=>HY@(TJNPQ=lwY@(ϒ'Z@( ۬C D'FX@h(L^g<Z@H(ό/{Y@(($Gq+q)}u Hb!<=A:|GxlruDi+BIۭCG@ Ӥ) =  H#fP+$N_PI[/(ϕSS@riNtil; ` w@FS(/N>+N"'%'Y@QH&xu P$MST=Um}CTtiu5$ RquzY@PU-;*-:H=KO(ʯӏ+=Ng}ATtɼK_m5~h}@Tf v1Hw[@P~%fQɟ.ϰNk@UmyT|.J#)ATdym* wZ% mq5iG6Q0:,Q< K_F*[ﺏҖR:,P%D9ڔ)Z //^-wtM3tPz`ӂoQ%[+Ҷѣ˭SK@U M]&E8*@E?}Y'QH(eS+@U\mv4v7Q\TJNʂupQ> 5~]-X ETŏyiΓ8-ON(|uù4~uFTj6kJ S% Z'hn|uIVPf@[/~.K'H#x|Z(>f)⯵$zHeBr:\OŋOѥJGJvMjhKʟ1\`W@{Q@~vG|-}Fˁz$7gW@s>isnݩmosP(9X0QzU*>e#)Φ;֢zhzEረ(:jBOjm.A g(#czT~?g7FS;Yc NЂډ(^.b)9Fx8@-\od"e )MAܠjHu=ϕyRrL\kd4zS K_Yhn2 ӡh){{ug@nco\p!1oIT_ܠ<24u ܠkpkO ~l%d@=p3=_΋<$U@eOƩvQ+o>ll헁L~Jp{wJ{U>i)V+`4`(pc+/=Q(,pP*e{V("S쒗A+2 . 2s)K- /f߱N BūΕ4m"pT)}D Rm>:E8(:Py8"pAD AhAh$ I ppB Ņ Ń Ńu1 b8(|6pP(lᠰQA%E`. \ 1 sc8(<*a8(,*a8(,*c8(ja8( ja8( jc8jc8a8oa8oc8_c8_Zp( Ph A*BU7HoZ(B y@f?nNyPpbS*a[Dy;A>iG NPpji3Z'F(8Yi XNW @g a8(t~9@ & & fG A(b8S ٢` ;]*KlpD"FQ@( b1 D"FQ@( b1 DҮIENDB`kanata-1.9.0/cfg_samples/tray-icon/license_icons.txt000064400000000000000000000024231046102023000206410ustar 00000000000000BSD 2-Clause License Copyright (c) 2024, Fred Vatin Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. kanata-1.9.0/cfg_samples/tray-icon/tray-icon.kbd000064400000000000000000000044761046102023000176640ustar 00000000000000(defcfg process-unmapped-keys yes ;;|no| enable processing of keys that are not in defsrc, useful if mapping a few keys in defsrc instead of most of the keys on your keyboard. Without this, the tap-hold-release and tap-hold-press actions will not activate for keys that are not in defsrc. Disabled because some keys may not work correctly if they are intercepted. E.g. rctl/altgr on Windows; see the windows-altgr configuration item above for context. log-layer-changes yes ;;|no| overhead tray-icon "./_custom-icons/s.png" ;; should activate for layers without icons like '5no-icn' ;;opt val |≝| icon-match-layer-name yes ;;|yes| match layer name to icon files even without an explicit (icon name.ico) config tooltip-layer-changes yes ;;|false| tooltip-show-blank yes ;;|no| tooltip-duration 500 ;;|500| tooltip-size 24,24 ;;|24 24| notify-cfg-reload yes ;;|yes| notify-cfg-reload-silent no ;;|no| notify-error yes ;;|yes| ) (defalias l1 (layer-while-held 1emoji)) (defalias l2 (layer-while-held 2icon-quote)) (defalias l3 (layer-while-held 3emoji_alt)) (defalias l4 (layer-while-held 4my-lmap)) (defalias l5 (layer-while-held 5no-icn)) (defalias l6 (layer-while-held 6name-match)) (defsrc 1 2 3 4 5 6) (deflayer (⌂ icon base.png) @l1 @l2 @l3 @l4 @l5 @l6) ;; find in the 'icon' subfolder (deflayer (1emoji 🖻 1symbols.png) q q q q q q) ;; find in the 'icons' subfolder (deflayer (2icon-quote 🖻 "2Nav Num.png") w w w w w w) ;; find in the 'img' subfolder (deflayer (3emoji_alt 🖼 3trans.parent) e e e e e e) ;; find '.png' (deflayermap (4my-lmap 🖻 "..\..\assets\kanata.ico") 1 r 2 r 3 r 4 r 5 r 6 r) ;; find in relative path (deflayer 5no-icn t t t t t t) ;; match file name from 'tray-icon' config, whithout which would fall back to 'tray-icon.png' as it's the only valid icon matching 'tray-icon.kbd' name (deflayer 6name-match y y y y y y) ;; uses '6name-match' with any valid extension since 'icon-match-layer-name' is set to 'yes' kanata-1.9.0/cfg_samples/tray-icon/tray-icon.png000064400000000000000000000017741046102023000177060ustar 00000000000000PNG  IHDR\rf pHYs+IDATx1j`FQɨ1hY@\fGY*l| qߺ˱Iw9aaaaaaaaaaaaaaaa\ܬ˲1 ¶\g2`κ1= uz_8 L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L L lu^=.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em} div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0} a{color:#2156a5;text-decoration:underline;line-height:inherit} a:hover,a:focus{color:#1d4b8f} a img{border:0} p{line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility} p aside{font-size:.875em;line-height:1.35;font-style:italic} h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em} h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0} h1{font-size:2.125em} h2{font-size:1.6875em} h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em} h4,h5{font-size:1.125em} h6{font-size:1em} hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em} em,i{font-style:italic;line-height:inherit} strong,b{font-weight:bold;line-height:inherit} small{font-size:60%;line-height:inherit} code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)} ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} ul,ol{margin-left:1.5em} ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0} ul.circle{list-style-type:circle} ul.disc{list-style-type:disc} ul.square{list-style-type:square} ul.circle ul:not([class]),ul.disc ul:not([class]),ul.square ul:not([class]){list-style:inherit} ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} dl dt{margin-bottom:.3125em;font-weight:bold} dl dd{margin-bottom:1.25em} blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd} blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)} @media screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2} h1{font-size:2.75em} h2{font-size:2.3125em} h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em} h4{font-size:1.4375em}} table{background:#fff;margin-bottom:1.25em;border:1px solid #dedede;word-wrap:normal} table thead,table tfoot{background:#f7f8f7} table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left} table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)} table tr.even,table tr.alt{background:#f8f8f7} table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{line-height:1.6} h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em} h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400} .center{margin-left:auto;margin-right:auto} .stretch{width:100%} .clearfix::before,.clearfix::after,.float-group::before,.float-group::after{content:" ";display:table} .clearfix::after,.float-group::after{clear:both} :not(pre).nobreak{word-wrap:normal} :not(pre).nowrap{white-space:nowrap} :not(pre).pre-wrap{white-space:pre-wrap} :not(pre):not([class^=L])>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background:#f7f7f8;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed} pre{color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;line-height:1.45;text-rendering:optimizeSpeed} pre code,pre pre{color:inherit;font-size:inherit;line-height:inherit} pre>code{display:block} pre.nowrap,pre.nowrap pre{white-space:pre;word-wrap:normal} em em{font-style:normal} strong strong{font-weight:400} .keyseq{color:rgba(51,51,51,.8)} kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background:#f7f7f7;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 0 rgba(0,0,0,.2),inset 0 0 0 .1em #fff;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap} .keyseq kbd:first-child{margin-left:0} .keyseq kbd:last-child{margin-right:0} .menuseq,.menuref{color:#000} .menuseq b:not(.caret),.menuref{font-weight:inherit} .menuseq{word-spacing:-.02em} .menuseq b.caret{font-size:1.25em;line-height:.8} .menuseq i.caret{font-weight:bold;text-align:center;width:.45em} b.button::before,b.button::after{position:relative;top:-1px;font-weight:400} b.button::before{content:"[";padding:0 3px 0 2px} b.button::after{content:"]";padding:0 2px 0 3px} p a>code:hover{color:rgba(0,0,0,.9)} #header,#content,#footnotes,#footer{width:100%;margin:0 auto;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em} #header::before,#header::after,#content::before,#content::after,#footnotes::before,#footnotes::after,#footer::before,#footer::after{content:" ";display:table} #header::after,#content::after,#footnotes::after,#footer::after{clear:both} #content{margin-top:1.25em} #content::before{content:none} #header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0} #header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #dddddf} #header>h1:only-child{border-bottom:1px solid #dddddf;padding-bottom:8px} #header .details{border-bottom:1px solid #dddddf;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:flex;flex-flow:row wrap} #header .details span:first-child{margin-left:-.125em} #header .details span.email a{color:rgba(0,0,0,.85)} #header .details br{display:none} #header .details br+span::before{content:"\00a0\2013\00a0"} #header .details br+span.author::before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)} #header .details br+span#revremark::before{content:"\00a0|\00a0"} #header #revnumber{text-transform:capitalize} #header #revnumber::after{content:"\00a0"} #content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #dddddf;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem} #toc{border-bottom:1px solid #e7e7e9;padding-bottom:.5em} #toc>ul{margin-left:.125em} #toc ul.sectlevel0>li>a{font-style:italic} #toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0} #toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none} #toc li{line-height:1.3334;margin-top:.3334em} #toc a{text-decoration:none} #toc a:active{text-decoration:underline} #toctitle{color:#7a2518;font-size:1.2em} @media screen and (min-width:768px){#toctitle{font-size:1.375em} body.toc2{padding-left:15em;padding-right:0} body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #dddddf;padding-bottom:8px} #toc.toc2{margin-top:0!important;background:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #e7e7e9;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto} #toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em} #toc.toc2>ul{font-size:.9em;margin-bottom:0} #toc.toc2 ul ul{margin-left:0;padding-left:1em} #toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em} body.toc2.toc-right{padding-left:0;padding-right:15em} body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #e7e7e9;left:auto;right:0}} @media screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0} #toc.toc2{width:20em} #toc.toc2 #toctitle{font-size:1.375em} #toc.toc2>ul{font-size:.95em} #toc.toc2 ul ul{padding-left:1.25em} body.toc2.toc-right{padding-left:0;padding-right:20em}} #content #toc{border:1px solid #e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;border-radius:4px} #content #toc>:first-child{margin-top:0} #content #toc>:last-child{margin-bottom:0} #footer{max-width:none;background:rgba(0,0,0,.8);padding:1.25em} #footer-text{color:hsla(0,0%,100%,.8);line-height:1.44} #content{margin-bottom:.625em} .sect1{padding-bottom:.625em} @media screen and (min-width:768px){#content{margin-bottom:1.25em} .sect1{padding-bottom:1.25em}} .sect1:last-child{padding-bottom:0} .sect1+.sect1{border-top:1px solid #e7e7e9} #content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400} #content h1>a.anchor::before,h2>a.anchor::before,h3>a.anchor::before,#toctitle>a.anchor::before,.sidebarblock>.content>.title>a.anchor::before,h4>a.anchor::before,h5>a.anchor::before,h6>a.anchor::before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em} #content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible} #content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none} #content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221} details,.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em} details{margin-left:1.25rem} details>summary{cursor:pointer;display:block;position:relative;line-height:1.6;margin-bottom:.625rem;outline:none;-webkit-tap-highlight-color:transparent} details>summary::-webkit-details-marker{display:none} details>summary::before{content:"";border:solid transparent;border-left:solid;border-width:.3em 0 .3em .5em;position:absolute;top:.5em;left:-1.25rem;transform:translateX(15%)} details[open]>summary::before{border:solid transparent;border-top:solid;border-width:.5em .3em 0;transform:translateY(15%)} details>summary::after{content:"";width:1.25rem;height:1em;position:absolute;top:.3em;left:-1.25rem} .admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic} table.tableblock.fit-content>caption.title{white-space:nowrap;width:0} .paragraph.lead>p,#preamble>.sectionbody>[class=paragraph]:first-of-type p{font-size:1.21875em;line-height:1.6;color:rgba(0,0,0,.85)} .admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%} .admonitionblock>table td.icon{text-align:center;width:80px} .admonitionblock>table td.icon img{max-width:none} .admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase} .admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere} .admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} .exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px} .sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px} .sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} .exampleblock>.content>:first-child,.sidebarblock>.content>:first-child{margin-top:0} .exampleblock>.content>:last-child,.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} .literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em} @media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}} @media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}} .literalblock pre,.listingblock>.content>pre:not(.highlight),.listingblock>.content>pre[class=highlight],.listingblock>.content>pre[class^="highlight "]{background:#f7f7f8} .literalblock.output pre{color:#f7f7f8;background:rgba(0,0,0,.9)} .listingblock>.content{position:relative} .listingblock code[data-lang]::before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:inherit;opacity:.5} .listingblock:hover code[data-lang]::before{display:block} .listingblock.terminal pre .command::before{content:attr(data-prompt);padding-right:.5em;color:inherit;opacity:.5} .listingblock.terminal pre .command:not([data-prompt])::before{content:"$"} .listingblock pre.highlightjs{padding:0} .listingblock pre.highlightjs>code{padding:1em;border-radius:4px} .listingblock pre.prettyprint{border-width:0} .prettyprint{background:#f7f7f8} pre.prettyprint .linenums{line-height:1.45;margin-left:2em} pre.prettyprint li{background:none;list-style-type:inherit;padding-left:0} pre.prettyprint li code[data-lang]::before{opacity:1} pre.prettyprint li:not(:first-child) code[data-lang]::before{display:none} table.linenotable{border-collapse:separate;border:0;margin-bottom:0;background:none} table.linenotable td[class]{color:inherit;vertical-align:top;padding:0;line-height:inherit;white-space:normal} table.linenotable td.code{padding-left:.75em} table.linenotable td.linenos,pre.pygments .linenos{border-right:1px solid;opacity:.35;padding-right:.5em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} pre.pygments span.linenos{display:inline-block;margin-right:.75em} .quoteblock{margin:0 1em 1.25em 1.5em;display:table} .quoteblock:not(.excerpt)>.title{margin-left:-1.5em;margin-bottom:.75em} .quoteblock blockquote,.quoteblock p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify} .quoteblock blockquote{margin:0;padding:0;border:0} .quoteblock blockquote::before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)} .quoteblock blockquote>.paragraph:last-child p{margin-bottom:0} .quoteblock .attribution{margin-top:.75em;margin-right:.5ex;text-align:right} .verseblock{margin:0 1em 1.25em} .verseblock pre{font-family:"Open Sans","DejaVu Sans",sans-serif;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility} .verseblock pre strong{font-weight:400} .verseblock .attribution{margin-top:1.25rem;margin-left:.5ex} .quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic} .quoteblock .attribution br,.verseblock .attribution br{display:none} .quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)} .quoteblock.abstract blockquote::before,.quoteblock.excerpt blockquote::before,.quoteblock .quoteblock blockquote::before{display:none} .quoteblock.abstract blockquote,.quoteblock.abstract p,.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{line-height:1.6;word-spacing:0} .quoteblock.abstract{margin:0 1em 1.25em;display:block} .quoteblock.abstract>.title{margin:0 0 .375em;font-size:1.15em;text-align:center} .quoteblock.excerpt>blockquote,.quoteblock .quoteblock{padding:0 0 .25em 1em;border-left:.25em solid #dddddf} .quoteblock.excerpt,.quoteblock .quoteblock{margin-left:0} .quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{color:inherit;font-size:1.0625rem} .quoteblock.excerpt .attribution,.quoteblock .quoteblock .attribution{color:inherit;font-size:.85rem;text-align:left;margin-right:0} p.tableblock:last-child{margin-bottom:0} td.tableblock>.content{margin-bottom:1.25em;word-wrap:anywhere} td.tableblock>.content>:last-child{margin-bottom:-1.25em} table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede} table.grid-all>*>tr>*{border-width:1px} table.grid-cols>*>tr>*{border-width:0 1px} table.grid-rows>*>tr>*{border-width:1px 0} table.frame-all{border-width:1px} table.frame-ends{border-width:1px 0} table.frame-sides{border-width:0 1px} table.frame-none>colgroup+*>:first-child>*,table.frame-sides>colgroup+*>:first-child>*{border-top-width:0} table.frame-none>:last-child>:last-child>*,table.frame-sides>:last-child>:last-child>*{border-bottom-width:0} table.frame-none>*>tr>:first-child,table.frame-ends>*>tr>:first-child{border-left-width:0} table.frame-none>*>tr>:last-child,table.frame-ends>*>tr>:last-child{border-right-width:0} table.stripes-all>*>tr,table.stripes-odd>*>tr:nth-of-type(odd),table.stripes-even>*>tr:nth-of-type(even),table.stripes-hover>*>tr:hover{background:#f8f8f7} th.halign-left,td.halign-left{text-align:left} th.halign-right,td.halign-right{text-align:right} th.halign-center,td.halign-center{text-align:center} th.valign-top,td.valign-top{vertical-align:top} th.valign-bottom,td.valign-bottom{vertical-align:bottom} th.valign-middle,td.valign-middle{vertical-align:middle} table thead th,table tfoot th{font-weight:bold} tbody tr th{background:#f7f8f7} tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold} p.tableblock>code:only-child{background:none;padding:0} p.tableblock{font-size:1em} ol{margin-left:1.75em} ul li ol{margin-left:1.5em} dl dd{margin-left:1.125em} dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0} li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em} ul.checklist,ul.none,ol.none,ul.no-bullet,ol.no-bullet,ol.unnumbered,ul.unstyled,ol.unstyled{list-style-type:none} ul.no-bullet,ol.no-bullet,ol.unnumbered{margin-left:.625em} ul.unstyled,ol.unstyled{margin-left:0} li>p:empty:only-child::before{content:"";display:inline-block} ul.checklist>li>p:first-child{margin-left:-1em} ul.checklist>li>p:first-child>.fa-square-o:first-child,ul.checklist>li>p:first-child>.fa-check-square-o:first-child{width:1.25em;font-size:.8em;position:relative;bottom:.125em} ul.checklist>li>p:first-child>input[type=checkbox]:first-child{margin-right:.25em} ul.inline{display:flex;flex-flow:row wrap;list-style:none;margin:0 0 .625em -1.25em} ul.inline>li{margin-left:1.25em} .unstyled dl dt{font-weight:400;font-style:normal} ol.arabic{list-style-type:decimal} ol.decimal{list-style-type:decimal-leading-zero} ol.loweralpha{list-style-type:lower-alpha} ol.upperalpha{list-style-type:upper-alpha} ol.lowerroman{list-style-type:lower-roman} ol.upperroman{list-style-type:upper-roman} ol.lowergreek{list-style-type:lower-greek} .hdlist>table,.colist>table{border:0;background:none} .hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none} td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em} td.hdlist1{font-weight:bold;padding-bottom:1.25em} td.hdlist2{word-wrap:anywhere} .literalblock+.colist,.listingblock+.colist{margin-top:-.5em} .colist td:not([class]):first-child{padding:.4em .75em 0;line-height:1;vertical-align:top} .colist td:not([class]):first-child img{max-width:none} .colist td:not([class]):last-child{padding:.25em 0} .thumb,.th{line-height:0;display:inline-block;border:4px solid #fff;box-shadow:0 0 0 1px #ddd} .imageblock.left{margin:.25em .625em 1.25em 0} .imageblock.right{margin:.25em 0 1.25em .625em} .imageblock>.title{margin-bottom:0} .imageblock.thumb,.imageblock.th{border-width:6px} .imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em} .image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0} .image.left{margin-right:.625em} .image.right{margin-left:.625em} a.image{text-decoration:none;display:inline-block} a.image object{pointer-events:none} sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super} sup.footnote a,sup.footnoteref a{text-decoration:none} sup.footnote a:active,sup.footnoteref a:active,#footnotes .footnote a:first-of-type:active{text-decoration:underline} #footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em} #footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0} #footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;margin-bottom:.2em} #footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none;margin-left:-1.05em} #footnotes .footnote:last-of-type{margin-bottom:0} #content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0} div.unbreakable{page-break-inside:avoid} .big{font-size:larger} .small{font-size:smaller} .underline{text-decoration:underline} .overline{text-decoration:overline} .line-through{text-decoration:line-through} .aqua{color:#00bfbf} .aqua-background{background:#00fafa} .black{color:#000} .black-background{background:#000} .blue{color:#0000bf} .blue-background{background:#0000fa} .fuchsia{color:#bf00bf} .fuchsia-background{background:#fa00fa} .gray{color:#606060} .gray-background{background:#7d7d7d} .green{color:#006000} .green-background{background:#007d00} .lime{color:#00bf00} .lime-background{background:#00fa00} .maroon{color:#600000} .maroon-background{background:#7d0000} .navy{color:#000060} .navy-background{background:#00007d} .olive{color:#606000} .olive-background{background:#7d7d00} .purple{color:#600060} .purple-background{background:#7d007d} .red{color:#bf0000} .red-background{background:#fa0000} .silver{color:#909090} .silver-background{background:#bcbcbc} .teal{color:#006060} .teal-background{background:#007d7d} .white{color:#bfbfbf} .white-background{background:#fafafa} .yellow{color:#bfbf00} .yellow-background{background:#fafa00} span.icon>.fa{cursor:default} a span.icon>.fa{cursor:inherit} .admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default} .admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c} .admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111} .admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900} .admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400} .admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000} .conum[data-value]{display:inline-block;color:#fff!important;background:rgba(0,0,0,.8);border-radius:50%;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold} .conum[data-value] *{color:#fff!important} .conum[data-value]+b{display:none} .conum[data-value]::after{content:attr(data-value)} pre .conum[data-value]{position:relative;top:-.125em} b.conum *{color:inherit!important} .conum:not([data-value]):empty{display:none} dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility} h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em} p strong,td.content strong,div.footnote strong{letter-spacing:-.005em} p,blockquote,dt,td.content,td.hdlist1,span.alt,summary{font-size:1.0625rem} p{margin-bottom:1.25rem} .sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} .exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc} .print-only{display:none!important} @page{margin:1.25cm .75cm} @media print{*{box-shadow:none!important;text-shadow:none!important} html{font-size:80%} a{color:inherit!important;text-decoration:underline!important} a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important} a[href^="http:"]:not(.bare)::after,a[href^="https:"]:not(.bare)::after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em} abbr[title]{border-bottom:1px dotted} abbr[title]::after{content:" (" attr(title) ")"} pre,blockquote,tr,img,object,svg{page-break-inside:avoid} thead{display:table-header-group} svg{max-width:100%} p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3} h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid} #header,#content,#footnotes,#footer{max-width:none} #toc,.sidebarblock,.exampleblock>.content{background:none!important} #toc{border-bottom:1px solid #dddddf!important;padding-bottom:0!important} body.book #header{text-align:center} body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em} body.book #header .details{border:0!important;display:block;padding:0!important} body.book #header .details span:first-child{margin-left:0!important} body.book #header .details br{display:block} body.book #header .details br+span::before{content:none!important} body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important} body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always} .listingblock code[data-lang]::before{display:block} #footer{padding:0 .9375em} .hide-on-print{display:none!important} .print-only{display:block!important} .hide-for-print{display:none!important} .show-for-print{display:inherit!important}} @media amzn-kf8,print{#header>h1:first-child{margin-top:1.25rem} .sect1{padding:0!important} .sect1+.sect1{border:0} #footer{background:none} #footer-text{color:rgba(0,0,0,.6);font-size:.9em}} @media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}} /* DARK MODE */ @media (prefers-color-scheme: dark) { body, body .btn, body table, body th { background-color: #222; !important color: #e0e0e0; !important } body .btn { box-shadow: 0 0 5px #616161; !important border: 1px solid #222; !important } body .btn:focus { box-shadow: 0 0 5px #9e9e9e; !important } body .theme-switcher { background: url("../img/sun.svg") no-repeat center; !important } .subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{ color:#ff8a80;!important } .quoteblock blockquote::before{ color:#ff8a80;!important } body h1, body h2, body h3, body h4, body h5, body h6, body #toctitle, body .sidebarblock .title, body .imageblock .title { color: #ff8a80 !important; } body blockquote::before { color: #d32f2f !important; } body code, body pre { background-color: #2f2f2f !important; color: #e0e0e0; !important } body .sectlevel1 li a { color: #ff8a80 !important; } body a, body a code, body .sectlevel2 li a { color: #90caf9 !important; } body a:hover, body a:hover code, body a code:hover, body .sectlevel2 li a:hover { color: #42a5f5 !important; } body #toc, body .pwa-install-div { background-color: #222 !important; } body #toc { border-left-color: #212121; !important border-right-color: #212121; !important } body .pwa-install-div { box-shadow: 0 0 5px #2f2f2f; !important } body #pwa-install-btn { box-shadow: 0 0 5px #2f2f2f; !important background-color: #e0e0e0; !important border: 1px solid #e0e0e0; !important color: #222; !important } body li, body p, body .details, body details, body details summary, body td, body blockquote, body .attribution cite { color: #e0e0e0 !important; } body .sidebarblock { background-color: #222 !important; } * { scrollbar-color: #818181 #333; !important } *::-webkit-scrollbar-track:hover { scrollbar-color: #a1a181 #333; !important } /* code style */ :not(pre):not([class^=L])>code{background:#2f2f2f;!important} kbd{background:#2f2f2f;!important} .literalblock pre,.listingblock>.content>pre:not(.highlight),.listingblock>.content>pre[class=highlight],.listingblock>.content>pre[class^="highlight "]{background:#2f2f2f;!important} .literalblock.output pre{color:#2f2f2f;!important} .prettyprint{background:#2f2f2f;!important} } kanata-1.9.0/docs/config.adoc000064400000000000000000004733571046102023000141420ustar 00000000000000= Kanata Configuration Guide :last-update-label!: ifndef::env-github[] :toc: left endif::[] :stylesheet: config-stylesheet.css This document describes how to create a kanata configuration file. The kanata configuration file will determine your keyboard behaviour upon running kanata. == How to read the guide ifdef::env-github[] See the triple bullet-lines at the upper right to open or close a Table of Contents sidebar. You may want to view the guide rendered to HTML. https://jtroo.github.io/config.html[Link to guide]. endif::[] The **Reference** sections are shorter and are intended for reviewing how precisely to configure different sections. The **Description** sections are longer and contain more details such as advice, motivation, and examples. The configuration guide you are reading may have content not applicable to the version you are using. See below for links to specific guide versions. ifdef::env-github[] * https://github.com/jtroo/kanata/blob/v1.8.0/docs/config.adoc[v1.8.0] * https://github.com/jtroo/kanata/blob/v1.7.0/docs/config.adoc[v1.7.0] * https://github.com/jtroo/kanata/blob/v1.6.1/docs/config.adoc[v1.6.1] * https://github.com/jtroo/kanata/blob/v1.6.0/docs/config.adoc[v1.6.0] endif::[] ifndef::env-github[] * link:/config-1.8.0.html[v1.8.0] * link:/config-1.7.0.html[v1.7.0] * link:/config-1.6.1.html[v1.6.1] * link:/config-1.6.0.html[v1.6.0] endif::[] == Preamble The configuration file uses S-expression syntax from Lisps. If you are not familiar with any Lisp-like programming language, do not be too worried. This document will hopefully be a sufficient guide to help you customize your keyboard behaviour to your exact liking. Useful terminology to learn early: [cols="1,5"] |=== | string | A sequence of characters. Optionally surrounded by quotes. Examples: `backspace`, `"string with spaces and 1 number"`. | list | A sequence of strings or nested lists within round brackets. List items are separated by any amount of whitespace characters, or by round brackets. Examples: `(lrld-num 1)`, `(tap-dance 200 (f1(unicode 😀)f2(unicode 🙂)))`. |=== If you have any questions, confusions, suggestions, etc., feel free to https://github.com/jtroo/kanata/discussions/new/choose[start a discussion] or https://github.com/jtroo/kanata/issues/new/choose[file an issue]. If you have ideas for how to improve this document or any other part of the project, please be welcome to make a pull request or file an issue. == Forcefully exit kanata [[force-exit]] Though this isn't configuration-related, it may be important for you to know that pressing and holding all of the three following keys together at the same time will cause kanata to exit: - Left Control - Space - Escape This mechanism works on the key input **before** any remappings done by kanata. [[comments]] == Comments You can add comments to your configuration file. Comments are prefixed with two semicolons. E.g: [source] ---- ;; This is a comment in a kanata configuration file. ;; Comments will be ignored and are intended for you to help understand your ;; own configuration when reading it later. ---- You can begin a multi-line comment block with `+#|+` and end it with `+|#+`: [source] ---- #| This is a multi-line comment block |# ---- [[required-configuration-entries]] == Required configuration entries [[defsrc]] === defsrc **Reference** Your configuration file must have exactly one `defsrc` list. This defines the order of keys that the `+deflayer+` entries will operate on. .Syntax: [source] ---- (defsrc $key1 $key2 ... $keyN) ---- [cols="1,6"] |=== | `$key` | The name of a key. This can be a default key name or one defined in <>. When physically pressing this input key, the action defined at the same order position on the active layer will activate. |=== **Description** The `defsrc` configuration entry defines which of your key inputs will be processed by kanata and how the keys map to defined layers. Keys excluded from `defsrc` will not be processed by Kanata unless you have `process-unmapped-keys yes` in <>. Keys not processed by Kanata has implications on various actions. For example: - Pressing an excluded key will type a letter while a prior `tap-hold` decision is still pending, resulting in potentially incorrect results. - Excluded keys do not trigger early activation in actions such as `tap-hold-press` or `tap-dance` - Excluded keys cannot be read by `fork` or `switch` logic. The `defsrc` entry is treated as a long sequence. The amount of whitespace (spaces, tabs, newlines) are not relevant. You may use spaces, tabs, or newlines however you like to visually format `defsrc` to your liking. The primary source of all key names are the `str_to_oscode` and `default_mappings` functions in https://github.com/jtroo/kanata/blob/main/parser/src/keys/mod.rs[the source]. Please feel welcome to file an issue if you're unable to find the key you're looking for. An example `defsrc` containing the US QWERTY keyboard keys as an approximately 60% keyboard layout: .Example: [source] ---- (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) ---- Note that some keyboards have a Menu key instead of a right Meta key. In this case you can use `menu` instead of `rmet`. For non-US keyboards, see <>. [[deflayer]] === deflayer **Reference** Your configuration file must have at least one `+deflayer+` entry. This defines how each physical key mapped in `+defsrc+` behaves when kanata runs. .Syntax: [source] ---- (deflayer $layer-name $action1 $action2 ... $actionN) ---- [cols="1,5"] |=== | `$layer-name` | A string representing the layer name. This name is used to reference this layer in other actions. | `$action` | The action that activates while this layer is active when the corresponding `defsrc` input key is pressed. |=== **Description** A `+deflayer+` configuration entry is followed by the layer name then a list of keys or actions. The usable key names are the same as in defsrc. Actions are explained further on in this document. The whitespace story is the same as with `+defsrc+`. The order of keys/actions in `+deflayer+` corresponds to the physical key in the same sequence position defined in `+defsrc+`. The first layer defined in your configuration file will be the starting layer when kanata runs. Other layers can be temporarily activated or switched to using actions. An example `defsrc` and `deflayer` that remaps QWERTY to the Dvorak layout would be: .Example: [source] ---- (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) (deflayer dvorak grv 1 2 3 4 5 6 7 8 9 0 [ ] bspc tab ' , . p y f g c r l / = \ caps a o e u i d h t n s - ret lsft ; q j k x b m w v z rsft lctl lmet lalt spc ralt rmet rctl ) ---- A <> also allows specifying layer icons in `+deflayer+` and `+deflayermap+` to show in the tray menu on layer activation, see https://github.com/jtroo/kanata/blob/main/cfg_samples/tray-icon/tray-icon.kbd[example config] ==== deflayermap **Reference** An alternative method for defining a layer exists: `deflayermap`. This method maps inputs to actions by defining input-output pairs, ignoring `defsrc` entirely. You will likely want to either enable <> or define most of your keyboard keys within <> when using `deflayermap`. Otherwise many actions do not behave as intended. See one of the links for more context. .Syntax: [source] ---- (deflayermap ($layer-name) $input1 $action1 $input2 $action2 ... $inputN $actionN) ---- [cols="1,5"] |=== | `$layer-name` | A string representing the layer name. This name is used to reference this layer in other actions. | `$input` | The input key mapped to the corresponding output. | `$action` | The action that activates while this layer is active when the corresponding input key is pressed. |=== **Description** The `deflayermap` variant has the advantage of terser configuration when only a few keys on a layer need to be mapped. When practicing a new configuration, the standard `deflayer` has an advantage of looking more like a physical keyboard layout, which may be helpful to some. Within `deflayermap`, the very first item must be the layer name. The layer name must be in parentheses unlike with `deflayer`. After the layer name, the layer is configured via pairs of items: * input key * output action An example complete configuration that maps Caps Lock to Escape is: [source] ---- ;; defsrc is still necessary (defsrc) (deflayermap (base-layer) caps esc) ---- The input key takes the same role as `defsrc` keys. The output action takes the role that items in the normal `deflayer` have. As special input names, you can use one of `_`, `__`, or `___` to map all the keys that are not explicitly mapped in the layer, e.g. in the example above, these affect keys other than `caps`. [cols="1,6"] |=== | `_` | Map all unmapped keys in this layer that are defined in `defsrc`. | `__` | Map all unmapped keys in this layer that are not defined in `defsrc`. | `___` | Map all unmapped keys in this layer. |=== [[review-of-required-configuration-entries]] === Review of required configuration entries If you're reading in order, you have now seen all of the required entries: * `+defsrc+` * `+deflayer+` [[minimal-config]] An example minimal configuration is: [source] ---- (defsrc a b c) (deflayer start 1 2 3) ---- This will make kanata remap your `a b c` keys to `1 2 3`. This is almost certainly undesirable but is a valid configuration. NOTE: Please have a read through link:https://github.com/jtroo/kanata/blob/main/docs/platform-known-issues.adoc[the known platform issues] because they may have implications on what you should include/exclude in `defsrc`. The Windows LLHOOK I/O mechanism has the most issues by far. [[key-names]] == Key names for defsrc and deflayermap The source of truth for all default key names are the functions `str_to_oscode` and `add_default_str_osc_mappings` in the link:https://github.com/jtroo/kanata/blob/main/parser/src/keys/mod.rs[keys/mod.rs file]. https://www.toptal.com/developers/keycode[This online tool] will also work for most keys to tell you the key name. It will be shown as the `event.code` field in the web page after you press the key. [[non-us-keyboards]] == Non-US keyboards For non-US keyboard users, you may have some keys on your keyboard with characters that are not allowed in `defsrc` by default, at least according to the symbol shown on the physical keys. The two sections below can help you understand how to remap all your keys. === Browser event.code Ensure kanata and other key remapping programs are **not** running. Then you can use https://www.toptal.com/developers/keycode[this online tool] and press the key. The `event.code` field tells you the key name to use in Kanata. Alternatively, you can read through https://www.w3.org/TR/uievents-code/[this reference]. Due to the lengthy key names, you may want to use `deflayermap` if remapping using these key names. IMPORTANT: On Windows, you should use either `kanata_winIOv2.exe` or Interception when using key names according to the browser `event.code`. The default `kanata.exe` does not do mappings according to the browser `event.code` key names. [[deflocalkeys]] === deflocalkeys **Reference** You can use `deflocalkeys` to define additional key names that can be used in `defsrc`, `deflayer`, and anywhere else in the configuration. .Syntax: [source] ---- (deflocalkeys-$variant $key-name1 $key-number1 $key-name2 $key-number2 ... $key-nameN $key-numberN) ---- [cols="1,5"] |=== | `$variant` | One of: `win winiov2 wintercept linux macos` | `$key-name` | A key name of your choice that can be used in the rest of the configuration. | `$key-number` | A key number that varies based on the kanata variant you are using. |=== Only one of each deflocalkeys-* variant is allowed. The variants that are not applicable will be ignored, e.g. `deflocalkeys-linux` and `deflocalkeys-wintercept` are both ignored when using the default Windows `kanata.exe` binary. **Description** The `deflocalkeys` configurations are not strictly necessary. Their purpose is to help you match your physical keyboard's appearance to your kanata configuration, in the hopes it will be more readable and less confusing. In the underlying hardware, all keyboard positions send the same scan codes according to their position, regardless of what is printed on the key cap. The scan code names are typically referred to by the corresponding US layout name. It is the job of the operating system to translate the same scan code to the correct outputs according to the configured locale and layout. You can find configurations that others have made in https://github.com/jtroo/kanata/blob/main/docs/locales.adoc[this document]. If you do not see your keyboard there and are not confident in using the available tools, please feel welcome to ask for help in a discussion or issue. Please contribute to the document if you are able! There are five variants of deflocalkeys: - `deflocalkeys-win` - `deflocalkeys-winiov2` - `deflocalkeys-wintercept` - `deflocalkeys-linux` - `deflocalkeys-macos` .Example: [source] ---- (deflocalkeys-win ì 187 ) (deflocalkeys-winiov2 ì 187 ) (deflocalkeys-wintercept ì 187 ) (deflocalkeys-linux ì 13 ) (deflocalkeys-macos ì 13 ) (defsrc grv 1 2 3 4 5 6 7 8 9 0 - ì bspc ) ---- The number used for a custom key represents the converted value for an OsCode in base 10. This differs between Windows-hooks, Windows-interception, and Linux. Running kanata with the `--debug` flag lets you read the correct number, shown in parenthesis of `code` in the `KeyEvent` log lines. It also possible to use native tools, as described below. In Linux, `evtest` will give the correct number for the physical key you press. In Windows using the default hook mechanism, the non-interception version of the keyboard tester in the kanata repository will give the correct number in the `code: ` section. (https://github.com/jtroo/kanata/releases/tag/win-keycode-tester-v0.3.0[prebuilt binary]) In Windows uning `winIOv2`, the winIOv2 executable variant will give the correct number in the `code: ` section. In Windows using Interception, the interception version of the keyboard tester will give the correct number i the `num: ` section. Between the hook and interception versions, some keys may agree but others may not; do be aware that they are **not** compatible! However, Interception and winIOv2 should generally agree with each other. Ideas for improving the user-friendliness of this system are welcome! As mentioned before, please ask for help in an issue or discussion if needed, and help with https://github.com/jtroo/kanata/blob/main/docs/locales.adoc[this document] is very welcome so that future users can have an easier time 🙂. [[introduction-defcfg]] == Introduction to defcfg Your configuration file may include a single `defcfg` entry. The `defcfg` can be empty or omitted. There are options that change kanata's behaviour, but this introduction will introduce only the most prevalent entry: `process-unmapped-keys`. All other options can be found later in the <> section. .Example of an empty defcfg: [source] ---- (defcfg) ---- [[process-unmapped-keys]] === process-unmapped-keys The `process-unmapped-keys` option in `defcfg` is probably the most generally impactful option. Enabling this configuration makes kanata process keys that are not defined in `defsrc`. This might be useful if you are only mapping a few keys in defsrc instead of most of the keys on your keyboard. By default, keys excluded from `defsrc` will not work in various scenarios. Some examples: - The early hold for prior `+tap-hold-press+` actions will not - Prior `+one-shot+` actions will not be released - `fork` and `switch` logic will not see the key This option is disabled by default. The reason this is not enabled by default is because some keys may not work correctly if they are intercepted. A known issue being AltGr/ralt/Right Alt; see <>. .Example: [source] ---- (defcfg process-unmapped-keys yes) (defcfg process-unmapped-keys (all-except lctl ralt)) ---- == Aliases and variables[[aliases-and-vars]] Before learning about actions, it will be useful to first learn about aliases and variables. [[aliases]] === Aliases **Reference** Using the `defalias` configuration entry, you can introduce a shortcut label for an action. .Syntax: [source] ---- (defalias $alias-name1 $action1 $alias-name2 $action2 ... $alias-nameN $actionN) ---- [cols="1,5"] |=== | `$alias-name` | The chosen shortcut label for the action. This shortcut label can be used in the rest of the configuration by prefixing it with the `@` character. | `$action` | The ouput action used wherever the alias name is referenced. |=== **Description** The `defalias` entry reads pairs of items in a sequence where the first item in the pair is the alias name and the second item is the action it can be substituted for. A list is a sequence of strings or nested lists separated by whitespace, surrounded by parentheses. All of the configuration entries we've looked at so far are lists; `defalias` is where we'll first see nested lists in this guide. .Example: [source] ---- (defalias ;; tap for caps lock, hold for left control cap (tap-hold 200 200 caps lctl) ) ---- This alias can be used in `deflayer` as a substitute for the long action. The alias name is prefixed with `@` to signify that it's an alias as opposed to a normal key. [source] ---- (deflayer example @cap a s d f ) ---- You may have multiple `defalias` entries and multiple aliases within a single `defalias`. Aliases may also refer to other aliases that were defined earlier in the configuration file. .Example: [source] ---- (defalias one (tap-hold 200 200 caps lctl)) (defalias two (tap-hold 200 200 esc lctl)) (defalias three C-A-del ;; Ctrl+Alt+Del four (tap-hold 200 200 @three ralt) ) ---- You can choose to put actions without aliasing them right into `deflayer`. However, for long actions it is recommended not to do so to keep a nice visual alignment. Visually aligning your `deflayer` entries will hopefully make your configuration file easier to read. .Example: [source] ---- (deflayer example ;; this is equivalent to the previous deflayer example (tap-hold 200 200 caps lctl) a s d f ) ---- [[variables]] === Variables **Reference** Using the `defvar` configuration entry, you can introduce a shortcut label for an arbitrary string or list. .Syntax: [source] ---- (defvar $var-name1 $var-value1 $var-name2 $var-value2 ... $var-nameN $var-valueN) ---- [cols="1,5"] |=== | `$var-name` | The chosen shortcut label for the string or list. This shortcut label can be used in the rest of the configuration by prefixing it with `$`. | `$var-value` | An arbitrary string or list that will be substituted wherever the variable is used. |=== **Description** Unlike an alias, a variable does not need to be a valid standalone action. In other words, a variable can be used as components of actions. The most common use case is to define common number strings for actions such as `tap-hold`, `tap-dance`, and `one-shot`. Similar to how `defalias` works, `defvar` reads pairs of items in a sequence where the first item in the pair is the variable name and the second item is a string or list. Variables are allowed to refer to previously defined variables. Variables can be used to substitute most values. Some notable exceptions are: - variables cannot be used in `defcfg`, `defsrc`, or `deflocalkeys` - variables cannot be used to substitute an action name Variables are referred to by prefixing their name with `$`. .Example: [source] ---- (defvar tap-repress-timeout 100 hold-timeout 200 tt $tap-repress-timeout ht $hold-timeout ) (defalias th1 (tap-hold $tt $ht caps lctl) th2 (tap-hold $tt $ht spc lsft) ) ---- [[concat-in-defvar]] ==== concat in defvar Within the second item of `defvar`, a list that begins with the special keyword `concat` will concatenate all subsequent items in the list together into a single string value. Without using `concat`, lists are saved as-is. .Example: [source] ---- (defvar rootpath "/home/myuser/mysubdir" ;; $otherpath will be the string: /home/myuser/mysubdir/helloworld otherpath (concat $rootpath "/helloworld") ) ---- [[actions]] == Actions The actions kanata provides are what make it truly customizable. This section explains the available actions. [[live-reload]] === Live reload **Reference** Live reload variants: [cols="1,5"] |=== | `lrld` | String action that live-reloads the currently-used configuration file. | `lrld-next` | String action that live-reloads the configuration file specified consecutively later in the command line order. Cycles to the first-specified file if currently using the last file specified. | `lrld-prev` | String action that live-reloads the configuration file specified consecutively earlier in the command line order. Cycles to the last-specified file if currently using the first file specified. | `(lrld-num $n)` | List action that live-reloads the n'th file as specified in the command line order. The first file specified is `n=1`. |=== Live reload does not read or apply changes to device-related configurations. Examples of device-related configurations: `linux-dev`, `macos-dev-names-include`, `linux-use-trackpoint-property`, `windows-only-windows-interception-keyboard-hwids`. **Description** You can put the `+lrld+` action onto a key to live reload your configuration file. If kanata can't parse the file, the previous configuration will continue to be used. When live reload is activated, the active kanata layer will be the first `deflayer` defined in the configuration. .Example: [source] ---- (deflayer has-live-reload lrld a s d f ) ---- There are variants of `lrld`: `lrld-prev` and `lrld-next`. These will cycle through different configuration files that you specify on kanata's startup. The first configuration file specified will be the one loaded on startup. The prev/next variants can be used with shortened names of `lrpv` and `lrnx` as well. Another variant is the list action `lrld-num`. This reloads the configuration file specified by the number, according to the order that the configuration file arguments are passed into kanata's startup command. .Example: [source] ---- (deflayer has-live-reloads lrld lrpv lrnx (lrld-num 3) ) ---- Example specifying multiple config files in the command line: [source] ---- kanata -c startup.cfg -c 2nd.cfg -c 3rd.cfg ---- Given the above startup command, activating `(lrld-num 2)` would reload the `2nd.cfg` file. [[layer-switch]] === layer-switch **Reference** A list action that changes the active base layer. .Syntax: [source] ---- (layer-switch $layer-name) ---- [cols="1,5"] |=== | `$layer-name` | Layer name to switch to. |=== **Description** This action allows you to switch to another "base" layer. This is permanent until a `layer-switch` to another layer is activated. The concept of a base layer makes more sense when looking at the next action: `layer-while-held`. This action accepts a single subsequent string which must be a layer name defined in a `deflayer` entry. .Example: [source] ---- (defalias dvk (layer-switch dvorak)) ---- [[layer-while-held]] === layer-while-held **Reference** A list action that changes the active layer while the key is held. .Syntax: [source] ---- (layer-while-held $layer-name) ---- [cols="1,5"] |=== | `$layer-name` | Layer name to activate while key is held. |=== **Description** This action allows you to temporarily change to another layer while the key remains held. When the key is released, you go back to the currently active "base" layer. This action accepts a single subsequent string which must be a layer name defined in a `deflayer` entry. .Example: [source] ---- (defalias nav (layer-while-held navigation)) ---- You may also use `layer-toggle` in place of `layer-while-held`; they behave exactly the same. The `layer-toggle` name is slightly shorter but is a bit inaccurate with regards to its meaning. [[transparent-key]] === Transparent key **Reference** [cols="1,5"] |=== | `+_+` | String action that activates the action of the layer "underneath" the active one. |=== **Description** If you use a single underscore for a key `+_+` then it acts as a "transparent" key in a `+deflayer+`. The behaviour depends if `+_+` is on a base layer or a while-held layer. When `+_+` is pressed on the active base layer, the key will default to the corresponding `defsrc` key. If `+_+` is pressed on the active while-held layer, the base layer's behaviour will activate. [[use-defsrc]] === use-defsrc **Reference** [cols="1,6"] |=== | `use-defsrc` | String action that outputs the corresponding `defsrc` input key. |=== **Description** A similar concept to transparent key is the `+use-defsrc+` action. When activated, the underlying `defsrc` key will be the output action. .Example: [source] ---- (defsrc a b c d) (defalias src use-defsrc) (deflayer remap-only-c-to-d _ _ d @src) ---- [[no-op]] === No-op **Reference** [cols="1,6"] |=== | `XX` | String action that will output nothing. |=== **Description** You may use the action `+XX+` as a "no operation" key, meaning pressing the key will do nothing. This might be desirable in place of a transparent key on a layer that is not fully mapped so that a key that is intentionally not mapped will do nothing as opposed to typing a letter. Alternatively you can use `+✗+` `+∅+` `+•+` to mean no-op. .Example: [source] ---- (deflayer contains-no-ops XX ✗ ∅ •) ---- [[unicode]] === Unicode **Reference** List action that outputs a single unicode codepoint. The unicode codepoint will not be repeatedly typed if you hold the key down. .Syntax: [source] ---- (unicode $unicode-codepoint) ---- [cols="1,4"] |=== | `$unicode-codepoint` | One unicode codepoint. Be warned that many emojis/glyphs/graphemes are composed of multiple codepoints. |=== **Description** NOTE: The <> may output unicode characters more consistently. The `+unicode+` (or `+🔣+`) action accepts a single unicode character (but not a composed character, so 🤲, but not 🤲🏿). If you want to output a glyph that is composed of multiple codepoints, you can use <> with multiple `unicode` actions. You may use a unicode character as an alias if desired or in its simplified form `+🔣😀+` (vs the usual `+(🔣 😀)+`). NOTE: The unicode action may not be correctly accepted by the active application. NOTE: If using Linux, make sure to look at the <> in defcfg. .Example: [source] ---- (defalias sml (unicode 😀) 😀 (🔣 😀) 🙁 (unicode 🙁) ) (deflayer has-happy-sad @sml @🙁 @😀 🔣😀 d f ) ---- If you want output parentheses `+( )+` via unicode you can quote them. .Example with parentheses [source] ---- (defalias lp (unicode "(") rp (unicode ")") ) ---- If you want to output double quotes via unicode you need a special quoting syntax. .Example use of double-quote within a string [source] ---- (defalias dq (unicode r#"""#) ) ---- [[output-chordscombos]] === Output chords/combos **Reference** Prefixing a known key name with the following strings will output the key alongside the specified modifier. Multiple prefixes can be combined to add more modifiers to the same key output. Duplicate prefixes are not allowed. [cols="1,6"] |=== | `+C-+` | Left Control | `+RC-+` | Right Control | `+A-+` | Left Alt | `+RA-+` | Right Alt, also known as AltGr | `+AG-+` | Also means Right Alt/AltGr | `+S-+` | Left Shift | `+RS-+` | Right Shift | `+M-+` | Left Meta | `+RM-+` | Right Meta |=== A special behaviour of output chords is that if another key is pressed, all of the chord keys will be released before the newly pressed key action activates. The modifier keys are often not desired for subsequent actions and without this behaviour, rapid typing can result in undesired modified key presses. If you want keys to remain pressed, use <> instead. **Description** You may want to remap a key to automatically be pressed in combination with modifiers such as Control or Shift. Output chords are a way for you to achieve this. Output chords are typically used do one-off actions such as: - type a symbol, e.g. `S-1` to output `!` for the US layout. - type an accented character, e.g. `RA-a` to output `á` for the US international layout. - do a special action like `C-c` to send `SIGTERM` in the terminal It should be noted that output chords are not usable in all configuration items. If you get an unknown key error where you expected an output chord to be usable, you must split the output chord into its component keys. For example, `+(unmod C-l)+` is an error; instead you should use `+(unmod lctl l)+`. The output chord prefix strings are: * `+C-+`: Left Control (also `+‹⎈+` `+‹⌃+` or without the `+‹+` side indicator) * `+RC-+`: Right Control (also `+⎈›+` `+⌃›+`) * `+A-+`: Left Alt (also `+‹⎇+` `+‹⌥+` or without the `+‹+` side indicator)) * `+RA-+`: Right Alt, a.k.a. AltGr (also `+AG+` `+⎇›+` `+⌥›+`) * `+S-+`: Left Shift (also `+‹⇧+` or without the `+‹+` side indicator)) * `+RS-+`: Right Shift (also `+⇧›+`) * `+M-+`: Left Meta, a.k.a. Windows, GUI, Command, Super (also `+‹⌘+` `+‹❖+` `+‹◆+` or without the `+‹+` side indicator)) * `+RM-+`: Right Meta (also `+⌘›+` `+❖›+` `+◆›+`) .Example: [source] ---- (defalias ;; Type exclamation mark (US layout) ex! S-1 ;; Ctrl+C: send SIGINT to a Linux terminal program int C-c ;; Win+Tab: open Windows' Task View tsk M-tab ;; Ctrl+Shift+(C|V): copy or paste from certain terminal programs cpy C-S-c pst C-S-v ) ---- [[repeat-key]] === Repeat key **Reference** [cols="1,5"] |=== | `rpt` | String action that outputs the single most-recently typed key. | `rpt-any` | String action that outputs the most-recently outputted action. |=== **Description** The action `+rpt+` repeats the most recently typed key. Holding down this key will not repeatedly send the key. The intended use case is to be able to use a different finger or even thumb key to repeat a typed key, as opposed to double-tapping a key. .Example: [source] ---- (deflayer has-repeat rpt a s d f ) ---- The `rpt` action only repeats the last key output. For example, it won't output a chord like `ctrl+c` if the previous key pressed was `C-c`. The `rpt` action will only output `c` in this case. There is a variant `rpt-any` which will repeat any previous action and would output `ctrl+c` in the example case. ---- (deflayer has-repeat-any rpt-any a s d f ) ---- [[release-a-key-or-layer]] === Release a key or layer **Reference** [cols="1,2"] |=== | `(release-key $key)` | List action that releases the defined key from output actions. Notably this does not act on key inputs. | `(release-layer $layer-name)` | List action that releases `layer-while-held` activations for the given layer name. |=== **Description** You can release a held key or layer via these actions: * `release-key` or `key↑`: release a key, accepts `defsrc` compatible names * `release-layer` or `layer↑`: release a while-held layer A lower-level detail of these actions is that they operate on output states as opposed to virtually releasing an input key. This does have some practical significance. For example, if the action `(macro-repeat a 50)` were on the `a` key, activating `(release-key a)` will not stop the repeating macro. An example practical use case for `release-key` is seen in the `multi` section directly below. There is currently no known practical use case for `release-layer`, but it exists nonetheless. [[multi]] === multi **Reference** Activate multiple actions in sequence. .Syntax: [source] ---- (multi $action1 $action2 ... $actionN) ---- [cols="1,3"] |=== | `$action` | An output action. |=== **Description** The `+multi+` action executes multiple keys or actions in order but also simultaneously. It accepts one or more actions. An example use case is to press the "Alt" key while also activating another layer. In the example below, holding the physical "Alt" key will result in a held layer being activated while also holding "Alt" itself. The held layer operates nearly the same as the standard keyboard, so for example the sequence (hold Alt)+(Tab+Tab+Tab) will work as expected. This is in contrast to having a layer where `tab` is mapped to `A-tab`, which results in repeated press+release of the two keys and has different behaviour than expected. Some special keys will release the "Alt" key and do some other action that requires "Alt" to be released. In other words, the "Alt" key serves a dual purpose of still fulfilling the "Alt" key role for some button presses (e.g. Tab), but also as a new layer for keys that aren't typically used with "Alt" to have added useful functionality. [source] ---- (defalias atl (multi alt (layer-while-held alted-with-exceptions)) lft (multi (release-key alt) left) ;; release alt if held and also press left rgt (multi (release-key alt) rght) ;; release alt if held and also press rght ) (defsrc alt a s d f ) (deflayer base @atl _ _ _ _ ) (deflayer alted-with-exceptions _ _ _ @lft @rgt ) ---- WARNING: This action can sometimes behave in surprising ways with regards to simultaneity and order of actions. For example, an action like `(multi sldr ')` will not behave as expected. Due to implementation details, `sldr` will activate after the `'` even though it is listed before. This example could instead be written as `(macro sldr 10 ')`, and that would work as intended. It is recommended to avoid `multi` if it can be replaced with a different action like `macro` or an output chord. ==== reverse-release-order **Reference** String item that can be used inside of `(multi ...)` to reverse the release order of any keys that were pressed as part of `multi`. .Syntax: [source] ---- (multi ... reverse-release-order) ---- **Description** Within `multi` you can use include `reverse-release-order` to do what the action states: reverse the typical release order from if you have multiple keys in multi. For example, pressing then releasing a key with the action: `(multi a b c)` would press a b c in the stated order and then release a b c in the stated order. Changing it to `(multi a b c reverse-release-order)` would press a b c in the stated order and then release c b a in the stated order. .Example: [source] ---- (defalias S-a-reversed (multi lsft a reverse-release-order) ) ---- [[mouse-actions]] === Mouse actions You can click the left, middle, and right buttons using kanata actions, do vertical/horizontal scrolling, and move the mouse. [[mouse-buttons]] ==== Mouse buttons **Reference** You can activate mouse actions with the string actions below. [cols="1,5"] |=== | `mlft` | Hold left mouse button. | `mmid` | Hold middle mouse button. | `mrgt` | Hold right mouse button. | `mfwd` | Hold forward mouse button. | `mbck` | Hold backward mouse button. | `mltp` | Tap left mouse button. | `mmtp` | Tap middle mouse button. | `mrtp` | Tap right mouse button. | `mftp` | Tap forward mouse button. | `mbtp` | Tap backward mouse button. |=== In Linux and Windows-Interception, the hold actions can be used within `defsrc` and `deflayermap` to remap mouse buttons like keyboard keys. **Description** The mouse button actions are: * `mlft`: left mouse button * `mmid`: middle mouse button * `mrgt`: right mouse button * `mfwd`: forward mouse button * `mbck`: backward mouse button The mouse button will be held while the key mapped to it is held. Using Linux and Windows-Interception, the above actions are also usable in `defsrc` to enable remapping specified mouse actions in your layers, like you would with keyboard keys. If there are multiple mouse click actions within a single multi action, e.g. `+(multi mrgt mlft)+` then all the buttons except the last will be clicked then unclicked. The last button will remain held until key release. In the example above, pressing then releasing the key mapped to this action will result in the following event sequence: . press key mapped to `+multi+` . click right mouse button . unclick right mouse button . click left mouse button . release key mapped to `+multi+` . release left mouse button There are variants of the standard mouse buttons which "tap" the button. Rather than holding the button while the key is held, a mouse click will be immediately followed by the release. Nothing happens when the key is released. The actions are as follows: * `mltp`: tap left mouse button * `mmtp`: tap middle mouse button * `mrtp`: tap right mouse button * `mftp`: tap forward mouse button * `mbtp`: tap bacward mouse button [[mouse-wheel]] ==== Mouse wheel **Reference** The `mwheel-*` actions allow you to emulate a mouse wheel. Holding the action will repeatedly scroll according to the action configuration. .Syntax: [source] ---- (mwheel-$variant $interval $distance) ---- [cols="1,4"] |=== | `$variant` | One of `up down left right` representing the scroll direction to use. | `$interval` | Number of milliseconds between scroll actions. | `$distance` | Distance to travel per activation. The number `120` represents a complete notch on standard resolution mice and in some environments, 120 or a multiple of it should be what is used. |=== You may use these key names within `defsrc` to remap scroll events as if they were keys, corresponding to up, down, left, right respectively: `mwu`, `mwd`, `mwl`, `mwr`. **Description** The mouse wheel actions are: * `mwheel-up` or `🖱☸↑`: vertical scroll up * `mwheel-down` or `🖱☸↓`: vertical scroll down * `mwheel-left` or `🖱☸←`: horizontal scroll left * `mwheel-right` or `🖱☸→`: horizontal scroll right All of these actions accept two number strings. The first is the interval (unit: ms) between scroll actions. The second number is the distance (unit: arbitrary). In both Windows and Linux, 120 distance units is equivalent to a notch movement on a physical wheel. You can play with the parameters to see what feels correct to you. Both numbers must be in the range [1,65535]. NOTE: In Linux, not all desktop environments support the `REL_WHEEL_HI_RES` event. If this is the case for yours, it will likely be a better experience to use a distance value that is a multiple of 120. On Linux and Interception, you can also choose to read from a mouse device. When doing so, using the `mwu`, `mwd`, `mwl`, `mwr` key names in `defsrc` allow you to remap the mouse scroll up/down/left/right actions like you would with keyboard keys. NOTE: If you are using a high-resolution mouse in Linux, only a full "notch" of the scroll wheel will activate the action. NOTE: If you are using a high-resolution mouse with Interception, you will probably get way more events than you intended. [[mouse-movement]] ==== Mouse movement **Reference** The `movemouse-*` actions allow you to move the mouse cursor. Holding the action will repeatedly move the cursor according to the configuration. .Syntax: [source] ---- (movemouse-$variant $interval $distance) ---- [cols="1,4"] |=== | `$variant` | One of `up down left right` representing the direction to move. | `$interval` | Number of milliseconds between move activations. | `$distance` | Distance to travel per activation in unit of pixels. |=== There is a move mouse variant that increases distance per activation at a constant rate until a maximum is reached. .Syntax: [source] ---- (movemouse-accel-$variant $interval $acceleration-time $min $max) ---- [cols="1,4"] |=== | `$variant` | One of `up down left right` representing the direction to move. | `$interval` | Number of milliseconds between move activations. | `$acceletaion-time` | Number of milliseconds until max distance per activation is reached. | `$min` | Initial distance to travel per activation in unit of pixels. | `$max` | Maximum distance to travel per activation in unit of pixels. |=== **Description** The mouse movement actions are: * `movemouse-up` or `🖱↑` * `movemouse-down` or `🖱↓` * `movemouse-left` or `🖱←` * `movemouse-right` or `🖱→` Similar to the mouse wheel actions, all of these actions accept two number strings. The first is the interval (unit: ms) between movement actions and the second number is the distance (unit: pixels) of each movement. The following are variants of the above mouse movements that apply linear mouse acceleration from the minimum distance to the maximum distance as the mapped key is held. * `movemouse-accel-up` or `🖱accel↑` * `movemouse-accel-down` or `🖱accel↓` * `movemouse-accel-left` or `🖱accel←` * `movemouse-accel-right` or `🖱accel→` All these actions accept four number strings. The first number is the interval (unit: ms) between movement actions. The second number is the time it takes (unit: ms) to linearly ramp up from the minimum distance to the maximum distance. The third and fourth numbers are the minimum and maximum distances (unit: pixels) of each movement. There is a toggable defcfg option related to `movemouse-accel` - <>. You might want to enable it, especially if you're coming from QMK. [[set-mouse]] ==== Set absolute mouse position The action `setmouse` or `set🖱` sets the absolute mouse position. WARNING: This is only supported in Windows right now. For an interesting keyboard-centric mouse solution in Linux, try looking at https://github.com/rvaiya/warpd[warpd]. This list action takes two parameters which are `x` and `y` positions of the absolute movement. The values go from 0,0 which is the upper-left corner of the screen to 65535,65535 which is the lower-right corner of the screen. If you have multiple monitors, `setmouse` treats them all as a single large screen. This can make it a little confusing for how to set the `x, y` values to get the positions that you want. Experimentation will be needed. [[mouse-speed]] ==== Modify the speed of mouse movements The action `movemouse-speed` or `🖱speed` modifies the speed at which `movemouse` and `movemouse-accel` function at runtime. It does this by expanding or shrinking `min_distance` and `max_distance` while the action key is pressed. This action accepts one number (unit: percentage) by which the mouse movements will be accelerated. WARNING: Due to the nature of pixels being whole numbers, some values such as 33 may not result in an exact third of the distance. .Example: [source] ---- (defalias fst (movemouse-speed 200) slw (movemouse-speed 50) ) ---- [[mouse-all-actions-example]] ==== Mouse all actions example [source] ---- (defalias mwu (mwheel-up 50 120) mwd (mwheel-down 50 120) mwl (mwheel-left 50 120) mwr (mwheel-right 50 120) ms↑ (movemouse-up 1 1) ms← (movemouse-left 1 1) ms↓ (movemouse-down 1 1) ms→ (movemouse-right 1 1) ma↑ (movemouse-accel-up 1 1000 1 5) ma← (movemouse-accel-left 1 1000 1 5) ma↓ (movemouse-accel-down 1 1000 1 5) ma→ (movemouse-accel-right 1 1000 1 5) sm (setmouse 32228 32228) fst (movemouse-speed 200) ) (deflayer mouse _ @mwu @mwd @mwl @mwr _ _ _ _ _ @ma↑ _ _ _ _ pgup bck _ fwd _ _ _ _ @ma← @ma↓ @ma→ _ _ _ pgdn mlft _ mrgt mmid _ mbck mfwd _ @ms↑ _ _ @fst _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ _ _ _ _ _ _ _ ) ---- [[tap-dance]] === tap-dance **Reference** The `tap-dance` action allows performing different actions based on number of consecutive taps of the same key. .Syntax: [source] ---- (tap-dance $timeout $action-list) ---- [cols="1,4"] |=== | `$timeout` | Number of milliseconds after which the tap-dance ends. | `$action-list` | A list of actions that can be selected, ordered by number of taps. |=== The `tap-dance-eager` variant will eagerly perform actions. Use of `macro` and `bspc` can help to backtrack for the 2nd tap onwards. .Syntax: [source] ---- (tap-dance-eager $timeout $action-list) ---- **Description** The `+tap-dance+` action allows repeated tapping of a key to result in different actions. It is followed by a timeout (unit: ms) and a list of keys or actions. Each time the key is pressed, its timeout will reset. The action will be chosen if one of the following events occur: * the timeout expires * a different key is pressed * the key is repeated up to the final action You may put normal keys or other actions in `+tap-dance+`. .Example: [source] ---- (defalias ;; 1 tap : "A" key ;; 2 taps: Control+C ;; 3 taps: Switch to another layer ;; 4 taps: Escape key td (tap-dance 200 (a C-c (layer-switch l2) esc)) ) ---- There is a variant of `tap-dance` with the name `tap-dance-eager`. The variant is parsed identically but the difference is that it will activate every action in the sequence as the taps progress. In the example below, repeated taps will, in order: 1. type `a` 2. erase the `a` and type `bb` 3. erase the `bb` and type `ccc` [source] ---- (defalias td2 (tap-dance-eager 500 ( (macro a) ;; use macro to prevent auto-repeat of the key (macro bspc b b) (macro bspc bspc c c c) )) ) ---- [[one-shot]] === one-shot **Reference** Activate keys or layers for a time without keeping the input key held, for one subsequent key. Activating other one-shot actions, while one or more are already active, will reset the timeout, and overlap the one-shot actions. .Syntax: [source] ---- ($one-shot-variant $timeout $action) ---- Values for `$variant`: [cols="1,3"] |=== | `one-shot-press` | End on the first press of another key. This is also the variant selected by the name `one-shot`. | `+one-shot-release+` | End on the first release of a newly pressed key. | `+one-shot-press-pcancel+` | End on the first press of another key or on re-press of another active one-shot key | `+one-shot-release-pcancel+` | End on the first release of a newly pressed key or on re-press of another active one-shot key. |=== Other items: [cols="1,3"] |=== | `$timeout` | Number of milliseconds after which if not deactivated due to user input, one-shot will deactivate on its own. | `$action` | Layer action, key, or output chord. |=== **Description** The `+one-shot+` action is similar to "sticky keys", if you know what that is. This activates an action or key until either the timeout expires or a different key is used. The `+one-shot+` action must be followed by a timeout (unit: ms) and another key or action. Some of the intended use cases are: * press a modifier for exactly one following key press * switch to another layer for exactly one following key press If a `+one-shot+` key is held then it will act as the regular key. E.g. holding a key assigned with `+@os2+` in the example below will keep Left Shift held for every key, not just one, as long as it's still physically pressed. Pressing multiple `+one-shot+` keys in a row within the timeout will combine the actions of those keys and reset the timeout to the value of the most recently pressed `+one-shot+` key. There are four variants of the `+one-shot+` action: - `+one-shot-press+` or `+one-shot↓+`: end on the first press of another key - `+one-shot-release+` or `+one-shot↑+`: end on the first release of another key - `+one-shot-press-pcancel+` or `+one-shot↓⤫+`: end on the first press of another key or on re-press of another active one-shot key - `+one-shot-release-pcancel+` or `+one-shot↑⤫+`: end on the first release of another key or on re-press of another active one-shot key It is important to note that the first activation of a one-shot key determines the behaviour with regards to the 4 variants for all subsequent one-shot key activations, even if a following one-shot key has a different configuration than the initial key pressed. The default name `+one-shot+` corresponds to `+one-shot-press+`. NOTE: When using one-shot with keys that will trigger defoverrides, you will likely want to adjust <> to yes in `defcfg`. .Example: [source] ---- (defalias os1 (one-shot 500 (layer-while-held another-layer)) os2 (one-shot-press 2000 lsft) os3 (one-shot-release 2000 lctl) os4 (one-shot-press-pcancel 2000 lalt) os5 (one-shot-release-pcancel 2000 lmet) ) ---- [[one-shot-pause-processing]] ==== one-shot-pause-processing **Reference** Pause `one-shot` processing of new input keypresses for a time, to allow actions that are not intended to consume `one-shot` to take place. .Syntax: [source] ---- (one-shot-pause-processing $time) ---- [cols="1,5"] |=== | `time` | Number of milliseconds to ignore processing. Something notable is that one virtual key press or releas (tap is a separate press and subsequent release) will take 1ms to process. If using virtual keys this number must be larger than the number of virtual key events that are taking place. |=== **Description** The `one-shot-pause-processing` list action allows you to pause the key press processing of one-shot activations. An example of when this is useful the following sequence: - Activate a layer-while-held - Activate a one-shot action on that layer - Release the layer-while-held key, which has an `(on-release ...)` action associated with it. - The on-release action is not intended to consume one-shot activations In the scenario above, by default the on-release activation would trigger deactivation of one-shot; thus the pause processing action must be used to stop this from happening. [[tap-hold]] === tap-hold WARNING: The `tap-hold` action and all variants can behave unexpectedly on Linux with respect to repeat of antecedent key presses. The full context is in https://github.com/jtroo/kanata/discussions/422[discussion #422]. In brief, the workaround is to use `tap-hold` inside of <>, combined with another key action that behaves as a no-op like `f24`. + Example: `(multi f24 (tap-hold ...))`. If multiple `tap-hold` actions may be pressed subsequently, all using the `f24` workaround, you may need to release the `f24` within the same `multi` to avoid repeats from one double-tapped `tap-hold` action followed by another, different `tap-hold` action. Example: `(defvirtualkeys relf24 (release-key f24)) ... (multi f24 (tap-hold ...) (macro 5 (on-press tap-vkey relf24)))` **Reference** The `tap-hold` action lets you activate different actions depending it a key is tapped or held. .Syntax: [source] ---- (tap-hold $tap-repress-timeout $hold-timeout $tap-action $hold-action) ---- [cols="1,4"] |=== | `$tap-repress-timeout` | Number of milliseconds for the window that a tap into re-press with hold results in the `$tap-action` being held. | `$hold-timeout` | Number of milliseconds after which the `$hold-action` activates. Releasing the key before this elapses results in `$tap-action` activating. | `$tap-action` | Action to activate when the input is determined to be a "tap". | `$hold-action` | Action to activate when the input is determined to be a "hold". |=== .Variants: ---- (tap-hold-press $tap-repress-timeout $hold-timeout $tap-action $hold-action) (tap-hold-release $tap-repress-timeout $hold-timeout $tap-action $hold-action) (tap-hold-press-timeout $tap-repress-timeout $hold-timeout $tap-action $hold-action $timeout-action) (tap-hold-release-timeout $tap-repress-timeout $hold-timeout $tap-action $hold-action $timeout-action) (tap-hold-release-keys $tap-repress-timeout $hold-timeout $tap-action $hold-action $tap-keys) (tap-hold-except-keys $tap-repress-timeout $hold-timeout $tap-action $hold-action $tap-keys) ---- [cols="1,2"] |=== | `tap-hold-press` | Activate `$hold-action` early if held and another input key is pressed. | `tap-hold-release` | Activate `$hold-action` early if held and another input key is pressed and released. | `tap-hold-press-timeout` | Activate `$hold-action` if held and another input key is pressed. If the defined timeout elapses, `$timeout-action` will activate. | `tap-hold-release-timeout` | Activate `$hold-action` early if held and another input key is pressed and released. If the defined timeout elapses, `$timeout-action` will activate. | `tap-hold-release-keys` | Activate `$hold-action` early if held and another input key is pressed and released. The `$tap-keys` parameter is a list of key names. Activates `$tap-action` early if a key within `$tap-keys` is pressed before hold activates. | `tap-hold-except-keys` | The `$tap-keys` parameter is a list of key names. Activates $tap-action if a key within $tap-keys is pressed or if the action key is released before hold timeout. No key is ever output until the action key is released or another key is pressed, which differs from the default `tap-hold` behaviour. |=== **Description** The `+tap-hold+` action allows you to have one action/key for a "tap" and a different action/key for a "hold". A tap is a rapid press then release of the key whereas a hold is a long press. The action takes 4 parameters in the listed order: . tap repress timeout (unit: ms) . hold timeout (unit: ms) . tap action . hold action The tap repress timeout is the number of milliseconds within which a rapid press+release+press of a key will result in the tap action being held instead of the hold action activating. .Tap repress timeout in more detail [%collapsible,indent=4] ==== The way a `tap-hold` action works with respect to the tap repress timeout is often unclear to newcomers. To make it concrete, the output event sequence of the `tap-hold` action `(tap-hold $tap-repress-timeout 200 a lctl)` for varying values of `$tap-repress-timeout` with a fixed input event sequence will be described. The input event sequence is: - press - 50 ms elapses - release - 50 ms elapses - press - 300 ms elapses - release With `(defvar $tap-repress-timeout 0)`, the output event sequence is: - 50 ms elapses - press `a` - release `a` - 250 ms elapses - press `lctl` - 100 ms elapses - release `lctl` The above output sequence is the same for all `$tap-repress-timeout` values between and including `0` and `99`. For a value of `100` or greater for `$tap-repress-timeout`, the output event sequence is instead: - 50 ms elapses - press `a` - release `a` - 50 ms elapses - press `a` - 300 ms elapses - release `a` ==== The hold timeout is the number of milliseconds after which the hold action will activate. There are two additional variants of `+tap-hold+`: * `+tap-hold-press+` or `+tap⬓↓+` ** If there is a press of a different key, the hold action is activated even if the hold timeout hasn't expired yet * `+tap-hold-release+` or `+tap⬓↑+` ** If there is a press+release of a different key, the hold action is activated even if the hold timeout hasn't expired yet These variants may be useful if you want more responsive tap-hold keys, but you should be wary of activating the hold action unintentionally. .Example: [source] ---- (defalias anm (tap-hold 200 200 a @num) ;; tap: a hold: numbers layer oar (tap-hold-press 200 200 o @arr) ;; tap: o hold: arrows layer ech (tap-hold-release 200 200 e @chr) ;; tap: e hold: chords layer ) ---- There are further additional variants of `tap-hold-press` and `tap-hold-release`: - `tap-hold-press-timeout` or `tap⬓↓timeout` - `tap-hold-release-timeout` or `tap⬓↑timeout` These variants take a 5th parameter, in addition to the same 4 as the other variants. The 5th parameter is another action, which will activate if the hold timeout expires as opposed to being triggered by other key actions, whereas the non `-timeout` variants will activate the hold action in both cases. - `tap-hold-release-keys` or `tap⬓↑keys` This variant takes a 5th parameter which is a list of keys that trigger an early tap when they are pressed while the `tap-hold-release-keys` action is waiting. Otherwise this behaves as `tap-hold-release`. The keys in the 5th parameter correspond to the physical input keys, or in other words the key that corresponds to `defsrc`. This is in contrast to the `fork` and `switch` actions which operates on outputted keys, or in other words the outputs that are in `deflayer`, `defalias`, etc. for the corresponding `defsrc` key. .Example: [source] ---- (defalias ;; tap: o hold: arrows layer timeout: backspace oat (tap-hold-press-timeout 200 200 o @arr bspc) ;; tap: e hold: chords layer timeout: esc ect (tap-hold-release-timeout 200 200 e @chr esc) ;; tap: u hold: misc layer early tap if any of: (a o e) are pressed umk (tap-hold-release-keys 200 200 u @msc (a o e)) ) ---- - `tap-hold-except-keys` or `tap-hold⤫keys` This variant takes a 5th parameter which is a list of keys that always trigger a tap when they are pressed while the `tap-hold-except-keys` action is waiting. No key is ever output until there is either a release of the key or any other key is pressed. This differs from `tap-hold` behaviour. The keys in the 5th parameter correspond to the physical input keys, or in other words the key that corresponds to `defsrc`. This is in contrast to the `fork` and `switch` actions which operates on outputted keys, or in other words the outputs that are in `deflayer`, `defalias`, etc. for the corresponding `defsrc` key. .Example: [source] ---- (defalias ;; tap: o hold: arrows layer timeout: backspace oat (tap-hold-press-timeout 200 200 o @arr bspc) ;; tap: e hold: chords layer timeout: esc ect (tap-hold-release-timeout 200 200 e @chr esc) ;; tap: u hold: misc layer always tap if any of: (a o e) are pressed umk (tap-hold-except-keys 200 200 u @msc (a o e)) ) ---- [[macro]] === macro **Reference** The macro action taps the configured sequence of keys or actions. Numbers can be used to delay the sequence by the defined number of milliseconds. .Syntax: [source] ---- (macro $macro-action1 $macro-action2 ... $macro-actionN) ---- [cols="1,4"] |=== | `$macro-action` | A delay, key, action within the subset allowed within macros, or an output-chord-prefixed list of more macro-actions. |=== .Variants: ---- (macro-release-cancel ...) (macro-cancel-on-press ...) (macro-release-cancel-and-cancel-on-press ...) (macro-repeat ...) (macro-repeat-$cancel-variant ...) ---- [cols="1,2"] |=== | `macro-release-cancel` | Cancel all active macros if the key is released. | `macro-cancel-on-press` | Cancel all active macros if a different key is pressed. | `macro-release-cancel-and-cancel-on-press` | Cancel all active macros if either the key is released or a different key is pressed. | `macro-repeat` | Repeat the macro while held. | `macro-repeat-$cancel-variant` | Repeat the macro while held. Cancels the final repeat according the behaviour of one of the variants: `release-cancel`, `cancel-on-press`, `release-cancel-and-cancel-on-press`. |=== **Description** The `+macro+` action will tap a sequence of keys with optional delays. This is different from `+multi+` because in the `+multi+` action, all keys are held, whereas in `+macro+`, keys are pressed then released. This means that with `+macro+` you can have some letters capitalized and others not. This is not possible with `+multi+`. The `+macro+` action accepts one or more keys, some actions, chords, and delays (unit: ms). It also accepts a list prefixed with <> modifiers where the list is subject to the aforementioned restrictions. IMPORTANT: The number keys `0-9` will be parsed as millisecond delays whereas in other contexts they would be parsed as key names. To use the numbered keys they must be aliased or otherwise use the key names `Digit0-Digit9`. Up to 4 macros can be active at the same time. The actions supported in `+macro+` are: * <> * <> * <> * <> * <> * <> * <> * <> * <> * <> NOTE: Some of these actions may need short delays between. For example, `(macro a (unmod b) 5 (unmod c) d))` needs the delay of `5` to work correctly. .Example: [source] ---- (defalias : S-; 8 8 0 0 🙃 (unicode 🙃) ;; Type "http://localhost:8080" lch (macro h t t p @: / / 100 l o c a l h o s t @: @8 @0 @8 @0) ;; Type "I am HAPPY my FrIeNd 🙃" hpy (macro S-i spc a m spc S-(h a p p y) spc m y S-f r S-i e S-n d spc @🙃) ;; alt-tab(x3) and alt-shift-tab(x3) with macro tfd (macro A-(tab 200 tab 200 tab)) tbk (macro A-S-(tab 200 tab 200 tab)) ) ---- [[macro-release-cancel]] ==== macro-release-cancel The `macro-release-cancel` variant of the `+macro+` action will cancel all active macros upon releasing the key. Shorter unicode variant: `+macro↑⤫+`. This variant is parsed identically to the non-cancelling version. An example use case for this action is holding down a key to get different outputs, similar to tap-dance but one can see which keys are being outputted. E.g. in the example below, when holding the key, first `1` is typed, then replaced by `!` after 500ms, and finally that is replaced by `@` after another 500ms. However, if the key is released, the last character typed will remain and the rest of the macro does not run. [source] ---- (defalias 1 1 ;; macro-release-cancel to output different characters with visual feedback ;; after holding for different amounts of time. 1!@ (macro-release-cancel @1 500 bspc S-1 500 bspc S-2) ) ---- [[macro-cancel-on-press]] ==== macro-cancel-on-press The `macro-cancel-on-press` variant of the `macro action` enables a cancellation trigger for all active macros including itself, which is activated when a physical press of any other key happens. The trigger is enabled while the macro is in progress. [source] ---- (defalias 1 1 1!@ (macro-cancel-on-press @1 500 bspc S-1 500 bspc S-2) ) ---- [[macro-release-cancel-and-cancel-on-press]] ==== macro-release-cancel-and-cancel-on-press The `macro-release-cancel-and-cancel-on-press` variant combines the cancel behaviours of both the release-cancel and cancel-on-press. [source] ---- (defalias 1 1 1!@ (macro-release-cancel-and-cancel-on-press @1 500 bspc S-1 500 bspc S-2) ) ---- [[macro-repeat]] ==== macro-repeat There are further `macro-repeat` variants of the three `macro` actions described previously. These variants repeat while held. The repeat will only occur once all macros have completed, including the held macro key. If multiple repeating macros are being held simulaneously, only the most recently pressed macro will be repeated. [source] ---- (defalias mr1 (macro-repeat mltp) mr2 (macro-repeat-release-cancel mltp) mr3 (macro-repeat-cancel-on-press mltp) mr4 (macro-repeat-release-cancel-and-cancel-on-press mltp) ) ---- [[dynamic-macro]] === dynamic-macro **Reference** Record and replay key inputs. .Syntax: [source] ---- (dynamic-macro-record $id) (dynamic-macro-play $id) (dynamic-macro-record-stop) (dynamic-macro-record-stop-truncate $count) ---- [cols="1,3"] |=== | `dynamic-macro-record` | Record a dynamicro macro which will be saved with the defined `$id`. | `dynamic-macro-play` | Play back a macro saved with the defined `$id`. | `dynamic-macro-record-stop` | Stop and save a macro recording. This can also be achieved by recording a new macro or re-pressing record with the same `$id`. | `dynamic-macro-record-stop-truncate` | Stop and save a macro recording while truncating `$count` events from the end of the recording. This can be useful if the record/stop button is on a different layer. |=== **Description** The dynamic-macro actions allow for recording and playing key presses. The dynamic macro records physical key presses, as opposed to kanata's outputs. This allows the dynamic macro to replicate any action, but it means that if the macro starts and ends on different layers, then the macro might not be properly repeatable. The action `dynamic-macro-record` accepts one number (0-65535), which represents the macro ID. Activating this action will begin recording physical key inputs. If `dynamic-macro-record` with the same ID is pressed again, the recording will end and be saved. If `dynamic-macro-record` with a different ID is pressed then the current recording will end and be saved, then a new recording with the new ID will begin. The action `dynamic-macro-record-stop` will stop and save any active recording. There is a variant of this: `dynamic-macro-record-stop-truncate` This is a list action that takes a single parameter: the number of key actions to remove at the end of a dynamic macro. This variant is useful if the macro stop button is on a different layer. The action `dynamic-macro-play` accepts one number (0-65535), which represents the macro ID. Activating this action will play the saved recording of physical keys from a previous `dynamic-macro-record` with the same macro ID, if it exists. One can nest dynamic macros within each other, e.g. activate `(dynamic-macro-play 1)` while recording with `(dynamic-macro-record 0)`. However, dynamic macros cannot recurse; e.g. activating `(dynamic-macro-play 0)` while recording with `(dynamic-macro-record 0)` will be ignored. .Example: [source] ---- (defalias dr0 (dynamic-macro-record 0) dr1 (dynamic-macro-record 1) dr2 (dynamic-macro-record 2) dp0 (dynamic-macro-play 0) dp1 (dynamic-macro-play 1) dp2 (dynamic-macro-play 2) dms dynamic-macro-record-stop dst (dynamic-macro-record-stop-truncate 1) ) ---- [[caps-word]] === caps-word **Reference** The `caps-word` action puts Kanata into a state where typed keys are automatically shifted by `lsft`. The state persists until terminated by timeout or by typing a key that ends the state. Typing a non-terminating key refreshes the timeout duration. The `-toggle` variants will end the caps-word state if pressed while caps-word is active, whereas the re-pressing the standard variants will keep the state active and refresh the timeout duration. .Syntax: [source] ---- (caps-word $timeout) (caps-word-toggle $timeout) (caps-word-custom $timeout $shifted-list $non-terminal-list) (caps-word-custom-toggle $timeout $shifted-list $non-terminal-list) ---- [cols="1,4"] |=== | `$timeout` | Number of milliseconds after which the caps-word state ends. The duration is refreshed upon typing a non-terminating character. | `$shifted-list` | List of keys that will be automatically shifted. | `$non-terminal-list` | List of keys that are not shifted but which do not terminate the caps-word state. |=== **Description** The `caps-word` or `word⇪` action triggers a state where the `lsft` key will be added to the active key list when a set of specific keys are active. The keys are: `a-z` and `-`, which will be outputted as `A-Z` and `_` respectively when using the US layout. Examples where this is helpful is capitalizing a single important word like in `IMPORTANT!` or defining a constant in code like `const P99_99_VALUE: ...`. This has an advantage over the regular caps lock because it automatically ends so it doesn't need to be toggled off manually, and it also shifts `-` to `_` which caps lock does not do. The `caps-word` state ends when the keyboard is idle for the duration of the defined timeout (1st parameter), or a terminating key is pressed. Every key is a terminating key except the keys which get capitalized and the extra keys in this list: - `0-9` - `kp0-kp9` - `bspc del` - `up down left rght` You can use `caps-word-custom` or `word⇪-custom` instead of `caps-word` if you want to manually define which keys are capitalized (2nd parameter) and what the extra non-terminal+non-capitalized keys should be (3rd parameter). .Example: [source] ---- (defalias cw (caps-word 2000) ;; This example is similar to the default caps-word behaviour but it moves the ;; 0-9 keys to the capitalized key list from the extra non-terminating key list. cwc (caps-word-custom 2000 (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) ) ) ---- ==== caps-word-toggle[[caps-word-toggle]] There are `-toggle` variants of the `caps-word` actions. By default re-pressing `caps-word` will keep `caps-word` active. The `-toggle` variants will end `caps-word` if it is currently active, otherwise `caps-word` will be activate as normal. .Example: [source] ---- (defalias cwt (caps-word-toggle 2000) cct (caps-word-custom-toggle 2000 (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) ) ) ---- === unmod[[unmod]] **Reference** The `unmod` action will deactivate modifier keys while outputting one or more defined keys. .Syntax: [source] ---- (unmod $key1 $key2 ... $keyN) (unmod ($mod1 $mod2 ... $modN) $key1 $key2 ... $keyN) ---- [cols="1,5"] |=== | `$key` | A key name to output while unmodded. | `$mod` | By default `unmod` will deactivate all modifier keys. An optional list as the first parameter allows specfying a subset of modifiers to deactivate during the action. |=== **Description** The `unmod` action will release all modifiers temporarily and send one or more keys. After the `unmod` key is released, the released modifiers are pressed again. The affected modifiers are: `lsft,rsft,lctl,rctl,lmet,rmet,lalt,ralt`. A variant of `unmod` is `unshift` or `un⇧`. This action only releases the `lsft,rsft` keys. This can be useful for forcing unshifted keys while AltGr is still held. NOTE: In case the modifiers to be undone are not part of `defsrc`, <> needs to be enabled in `defcfg` in order for their states to be tracked correctly. .Example: [source] ---- (defalias ;; holding shift and tapping a @um1 key will still output 1. um1 (unmod 1) ;; dead keys é (as opposed to using AltGr) that outputs É when shifted dké (macro (unmod ') e) ;; In ISO German QWERTZ, force unshifted symbols even if shift is held { (unshift ralt 7) [ (unshift ralt 8) ) ---- A list may optionally be used as the first parameter of `unmod`. The list must be non-empty and must contain only modifier keys, which are the keys in the affected modifiers list from earlier in this document section. When this list exists, the action will temporarily release only the keys listed rather than all modifiers. .Example: [source] ---- (defalias ;; only unshift the alt keys unalt-a (unmod (lalt ralt) a) ) ---- [[fork]] === fork **Reference** The `fork` action allows choosing between a default and an alternate action based on whether specific keys are active. `fork` is the equivalent of the basic key checks in `switch`, using none of the list logic items, i.e. virtual keys are not supported. .Syntax: [source] ---- (fork $left-action $right-action $right-trigger-keys) ---- [cols="1,3"] |=== | `$left-action` | Action to activate by default. | `$right-action` | Action to activate if any of `$right-trigger-keys` are active. | `$right-trigger-keys` | List of keys that, if active when fork activates, causes `$right-action` to happen in place of `$left-action`. |=== TIP: The keys `nop0-nop9` can be used as no-op outputs that can still be checked within `fork`, unlike what `XX` does. [[switch]] === switch **Reference** The `switch` action allows conditionally activating 0 or more actions, depending on conditional checks. .Syntax: [source] ---- (switch $logic-check1 $action1 $post-activate1 $logic-check2 $action2 $post-activate2 ... $logic-checkN $actionN $post-activateN) ---- [cols="1,4"] |=== | `$logic-check` | The condition, which if it evaluates to true, will trigger the corresponding action. | `$action` | Action to activate when logic evaluates to true. | `$post-activate` | Valid values are `fallthrough` and `break`. With `fallthrough`, when an action activates switch will continue evaluating further logic checks and potentially trigger more actions. With `break`, further actions will not activate. |=== The logic check is a list. The items within the list can either be key names or a special list check. A key name item evaluates to true if that key name is a currently active key output of Kanata upon activating `switch` The outer-most list evaluates to true as whole if any of the items evaluates to true. .Syntax of logic check: [source] ---- ($item1 $item2 ... $itemN) ---- .Syntax of special checks: [source] ---- (or $item1 $item2 ... $itemN) (and $item1 $item2 ... $itemN) (not $item1 $item2 ... $itemN) (key-history $key-name $key-recency) (key-timing $key-recency $comparator $time) (input $input-type $key-name) (input-history $input-type $key-name $input-recency) (layer $layer-name) (base-layer $layer-name) ---- [cols="1,4"] |=== | `or` | Evaluates to true if any `$item` is true. | `and` | Evaluates to true if all of `$item` are true. | `not` | Evaluates to true if all of `$item` are false. | `key-history` | Evaluates to true if the key in the recency slot matches `$key-name`. A `$key-recency` of 1 is the most recent key pressed according to Kanata processing. The max recency is 8. | `key-timing` | The valid values for `$comparator` are `less-than` and `greater-than`, with `lt` and `gt` as shorthand if desired. This item evaluates to true if the key with the corresponding recency was pressed — for `lt` more recently than, or for `gt` later than — the defined `$time` with unit milliseconds. | `input` | Evaluates to true if the `$key-name` is currently pressed. The `$input-type` must be either `real` or `virtual`. If using `real`, this will check against the defsrc inputs. If using `virtual`, this will check against virtual key activations. | `input-history` | Evaluates to true if the input in the `$input-recency` slot matches `$key-name`. Two input types use the same history with respect to recency slots. A recency of 1 is the most recent input i.e. the input activating `switch` itself. The max recency is 8. | `layer` | Evaluates to true if the active layer matches `$layer-name`. | `base-layer` | Evaluates to true if the most-recently-switched-to layer from a `layer-switch` action matches `$layer-name`. |=== **Description** Conceptually, the `switch` action is similar to <> but has more capabilities as well as more complexity. The `switch` action accepts multiple cases. One case is a triple of: - logic check - action: to activate if logic check evaluates to true - `fallthrough|break`: choose to continue vs. stop evaluating cases The default use of the logic check behaves similarly to fork. For example, the logic check `(a b c)` will activate the corresponding action if any of a, b, or c are currently pressed. TIP: the keys `nop0-nop9` can be used as no-op outputs that can still be checked within `switch`, unlike what `XX` does. The logic check also accepts the boolean operators `and|or|not` to allow more complex use cases. The order of cases matters. For example, if two different cases match the currently pressed keys, the case listed earlier in the configuration will activate first. If the early case uses break, the second case will not activate. Otherwise if fallthrough is used, the second case will activate sequentially after the first case. This idea generalizes to more than two cases, but the two case example is hopefully simple and effective enough. .Example: [source] ---- (defalias swt (switch ;; case 1 ((and a b (or c d) (or e f))) @ac1 break ;; case 2 (a b c) @ac2 fallthrough ;; case 3 () @ac3 break ) ) ---- Below is a description of how this example behaves. ==== Case 1 ---- ((and a b (or c d) (or e f))) a break ---- Translating case 1's logic check to some other common languages might look like: ---- (a && b && (c || d) && (e || f)) ---- If the logic check passes, the action `@ac1` will activate. No other action will activate since `break` is used. ==== Cases 2 and 3 ---- (a b c) c fallthrough () b break ---- Case 2's key check behaves like that of `fork`, i.e. (or a b c) or for some other common languages: a || b || c If this logic check passes and the case 1 does not pass, the action `@ac2` will activate first. Since the logic check of case 3 always passes, `@ac3` will activate next. If neither case 1 or case 2 pass their logic checks, case 3 will always activate with `@ac3`. [[key-history-and-key-timing]] ==== key-history and key-timing In addition to simple keys there are two list items that can be used within the case logic check that compare against your typed key history: * `key-history` * `key-timing` The `key-history` item compares the order that keys were typed. It accepts, in order: * a key * the key recency The key recency must be in the range 1-8, where 1 is the most recent key that was pressed and 8 is 8th most recent key pressed. .Example: [source] ---- (defalias swh (switch ((key-history a 1)) S-a break ((key-history b 1)) S-b break ((key-history c 1)) S-c break ((key-history d 8)) (macro d d d) break () XX break ) ) ---- The `key-timing` compares how long ago recent key typing events occurred. It accepts, in order, * the key recency * a comparison string, which is one of: `less-than|greater-than|lt|gt` * number of milliseconds to compare against The key recency must be in the range 1-8, where 1 is the most recent key that was pressed and 8 is 8th most recent key pressed. Most use cases are expected to use a value of 1 for this parameter, but perhaps you can find a creative use for the other values. The comparison string determines how the actual key event timing will be compared to the provided timing. The number of milliseconds must be 0-65535. WARNING: The maximum milliseconds value of this configuration item across your whole configuration will be a lower bound of how long it takes for kanata to become idle and stop processing its state machine every approximately 1ms. .Example: [source] ---- (defalias swh (switch ((key-timing 1 less-than 200)) S-a break ((key-timing 1 greater-than 500)) S-b break ((key-timing 2 lt 1000)) S-c break ((key-timing 8 gt 2000)) (macro d d d) break () XX break ) ) ---- ==== not The examples presented so far have not included the `not` boolean operator. This operator will now be discussed. Syntactically, the `not` operator is used similarly to `or|and`. Functionally, it means "not **any** of" the list elements. .Example: [source] ---- (defalias swn (switch ((not x y z)) S-a break ;; the above and below cases are equivalent in logic ((not (or x y z))) S-a break ) ) ---- In potentially more familiar notation, both cases have the logic: !(x || y || z) ==== input Until now, all `switch` logic has been associated to key code outputs. It is also possible to operate on inputs. Inputs can be either real keys or "virtual" (fake) keys. .Example: [source] ---- (defalias switch-input-example (switch ((input real lctl)) $ac1 break ((input virtual vk1)) $ac2 break () $ac3 break ) ) ---- Similar to `key-history` for regular active keys, `input-history` also exists. A perhaps surprising, but hopefully logical, behaviour of input-history when compared to key-history is that, at the time of switch activation, the history of `input-history` for recency `1` will be the just-pressed input. In other words recency `1` is the input activating the `switch` action itself. Whereas with `key-history` for example, the key that will be next outputted may be determined by the switch logic itself, so is not in the history. The consequence of this is that you should use a recency of `2` when referring to the previously pressed input because the current input is in the recency `1` slot. .Example: [source] ---- (defalias switch-input-history-example (switch ((input-history real lsft 2)) $var1 break ((input-history virtual vk2 2)) $var1 break () $ac3 break ) ) ---- ==== layer The `layer` list item can be used in `switch` logic to operate on the active layer. It accepts a single layer name and evaluates to true if the configured layer name is the active layer, otherwise it evaluates to false. .Example: [source] ---- (defalias switch-layer-example (switch ((layer base)) x break ((layer other)) y break () z break ) ) ---- ==== base-layer The `base-layer` list item evaluates to true if the configured layer name is the base layer. The base layer is the most recently switched-to layer from a `layer-switch` action, or the first layer defined in your configuration if `layer-switch` has never been activated. .Example: [source] ---- (defalias switch-layer-example (switch ((base-layer base)) x break ((base-layer other)) y break () z break ) ) ---- [[cmd]] === cmd WARNING: This action does not work unless you use the appropriate binary or - if compiling yourself - the appropriate feature flag. Additionally you must add the <> `defcfg` option. **Reference** The `cmd` action allows you to execute arbitrary binaries with arbitrary arguments. The `cmd-log` variant behaves similarly but allows customization of the stdout and stderr log levels within Kanata's output logging. The `cmd-output-keys` is like `cmd`, but stdout of the command will be parsed as a list of keys, output chords, and delays similar to <> and be typed as kanata outputs. .Syntax: [source] ---- (cmd $binary $arg1 $arg2 ... $argN) (cmd-log $stdout-log-level $stderr-log-level) (cmd-output-keys $binary $arg1 $arg2 ... $argN) ---- [cols="1,3"] |=== | `$binary` | Executable binary to run. | `$arg` | Argument passed into the binary. | `$stdout-log-level` | Log level for stdout of the command. Must be `+debug+`, `+info+`, `+warn+`, `+error+`, or `+none+`. | `$stderr-log-level` | Log level for stderr of the command. Must be `+debug+`, `+info+`, `+warn+`, `+error+`, or `+none+`. |=== **Description** The `+cmd+` action executes a program with arguments. It accepts one or more strings. The first string is the program that will be run and the following strings are arguments to that program. The arguments are provided to the program in the order written in the config file. Lists may also be used within `cmd` which you may desire to do for reuse via `defvar`. Lists will be flattened such that arguments are provided to the program in the order written in the config file, regardless of list nesting. To be technical, it would be a depth-first flattening (similar to DFS). Commands are executed directly and not via a shell, so you cannot make use of environment variables or symbols with special meaning. For example `+~+` or `+$HOME+` in Linux will not be substituted with your home directory. If you want to execute with a shell program use the shell as the first parameter, e.g. `bash` or `powershell.exe`. The user executing the command is the user that kanata was started with. For example, if kanata was started by root, the command will be run by the root user. If you need to execute as a different user, on Unix platforms you can use `sudo -u USER` before the rest of your command to achieve this. .Example: [source] ---- (defalias cm1 (cmd rm -fr /tmp/testing) ;; You can use bash -c and then a quoted string to execute arbitrary text in ;; bash. All text within double-quotes is treated as a single string. cm2 (cmd bash -c "echo hello world") ;; You can prefix commands with sudo -u USER ;; to execute commands as a different user. cm3 (cmd sudo -u other_user bash -c "echo goodbye") ) ---- By default, `+cmd+` logs start of command, completion of command, stdout, and stderr. Using the variant `+cmd-log+`, these log levels can be changed, and even disabled. It takes two arguments, `++` and `++`. `++` will be the level where the command to run, stdout, and stderr are logged. The error channel is logged only if there is a failure with running the command (typically if the command can't be found, or there is trouble spawning it). The valid levels are `+debug+`, `+info+`, `+warn+`, `+error+`, and `+none+`. .Example: [source] ---- (defalias ;; The first two arguments are the log levels, then just the normal command ;; This will only error if `bash` is not found or something else goes ;; wrong with the initial execution. Any logs produced by bash will not ;; be shown. noisy-cmd (cmd-log none error bash -c "echo hello this produces a log") ;; This will only log the output of the command, but it won't start ;; because the command doesn't exist. ignore-failure-cmd (cmd-log info none thiscmddoesnotexist) verbose-only-log (cmd-log verbose verbose bash -c "echo yo") ) ---- There is a variant of `cmd`: `cmd-output-keys`. This variant reads the output of the executed program and reads it as an S-expression, similarly to the <>. However — unlike macro — only delays, keys, chords, and chorded lists are supported. Other actions are not supported. [source] ---- (defalias ;; bash: type date-time as YYYY-MM-DD HH:MM pdb (cmd-output-keys bash -c "date +'%F %R' | sed 's/./& /g' | sed 's/:/S-;/g' | sed 's/\(.\{20\}\)\(.*\)/\(\1 spc \2\)/'") ;; powershell: type date-time as YYYY-MM-DD HH:MM pdp (cmd-output-keys powershell.exe "echo '(' (((Get-Date -Format 'yyyy-MM-dd HH:mm').toCharArray() -join ' ').insert(20, ' spc ') -replace ':','S-;') ')'") ) ---- [[clipboard-actions]] === clipboard actions **Reference** Clipboard actions can manipulate the operating system clipboard alongside "save ids". To paste, you would use an action such as `C-v`; Kanata has no builtin paste action. .Syntax: [source] ---- (clipboard-set $clipboard-string) (clipboard-save $save-id) (clipboard-restore $save-id) (clipboard-save-swap $save-id $save-id) (clipboard-cmd-set $binary $arg1 $arg2 ... $argN) (clipboard-save-cmd-set $save-id $binary $arg1 $arg2 ... $argN) ---- [cols="1,3"] |=== | `clipboard-set` | Sets the clipboard to the specified string. | `clipboard-save` | Saves the current clipboard content with the specified ID. | `clipboard-restore` | Sets the clipboard to content saved with the specified ID. If the save ID content is blank, this will do nothing. | `clipboard-save-swap` | Swaps the content of two save IDs. | `clipboard-cmd-set` | Sets the clipboard to the output of the command. The current content of the clipboard is passed to stdin of the command if the current content is text. If the content is an image, nothing is passed to stdin. | `clipboard-save-cmd-set` | Sets the save ID content to the output of the command. The current content of the save ID is passed to stdin of the command if the current content is text. If the content is an image, nothing is passed to stdin. | `$clipboard-string` | Fixed string to set the operating system clipboard to. | `$save-id` | A number `0-65535` representing an ID of saved clipboard content. | `$binary` | Executable binary to run. | `$arg` | Argument passed into the binary. |=== **Description** You can use clipboard actions to save clipboard content, paste arbitrary content, and then restore the original clipboard content. This functionality is similar to what some text expanders do. You can additionally use shell commands to manipulate clipboard content in arbitrary ways. This can all be done in a single command via <>. Note that you will likely want to add delays in between components, because clipboard systems take some time to propagate updates. The example below is a macro that pastes the content of the clipboard twice with a space in between, while restoring the original clipboard content at the end. Notable is that the example uses `C-v` but this may not work for you. If you have OS-level remapping, the `v` may be different to effectively paste. Something that may also work is `S-ins` .Example: [source] ---- (macro (clipboard-save 0) 20 (clipboard-cmd-set powershell.exe -c r#"$v = ($Input | Select-Object -First 1); Write-Host -NoNewLine "$v $v""#) 300 C-v (clipboard-restore 0) ) ---- As another example you can use templates with clipboard actions to have a convenient clipboard-based text output while preserving the old clipboard content. This can fit the use case of "text expansion". .Example: [source] ---- (deftemplate text-paste (text) (macro (clipboard-save 0) 20 (clipboard-set $text) 300 C-v (clipboard-restore 0) )) (defalias myalias1 (t! text-paste "Hello world") myalias2 (t! text-paste "Goodbye my old friend") ) ---- [[arbitrary-code]] === arbitrary-code The `arbitrary-code` action allows sending an arbitrary number to kanata's output mechanism. The press is sent when pressed, and the release sent when released. This action can be useful for testing keys that are not yet named or mapped in kanata. Please contribute findings with names and mappings, either in a GitHub issue or as a pull request! WARNING: This is not cross platform! WARNING: When using the Interception driver, this action is still sent over SendInput. [source] ---- (defalias ab1 (arbitrary-code 700)) ---- [[global-overrides]] == Global overrides The `defoverrides` optional configuration item allows you to create global key overrides, irrespective of what actions are used to generate those keys. It accepts pairs of lists: 1. the input key list that gets replaced 2. the output key list to replace the input keys with Both input and output lists accept 0 or more modifier keys (e.g. lctl, rsft) and exactly 1 non-modifier key (e.g. 1, bspc). Only zero or one `defoverrides` is allowed in a configuration file. NOTE: Depending on your use case you may want to adjust <> in `defcfg`. .Example: [source] ---- ;; Swap numbers and their symbols with respect to shift (defoverrides (1) (lsft 1) (2) (lsft 2) ;; repeat for all remaining numbers (lsft 1) (1) (lsft 2) (2) ;; repeat for all remaining numbers ) ---- == Include other files[[include]] The `include` optional configuration item allows you to include other files into the configuration. This configuration accepts a single string which is a file path. The file path can be an absolute path or a relative path. The path will be relative to the defined configuration file. At the time of writing, includes can only be placed at the top level. The included files also cannot contain includes themselves. Non-existing files will be ignored. .Example: ---- ;; This is in the file initially read by kanata, e.g. kanata.kbd (include other-file.kbd) ;; This is in the other file (defalias included-alias XX ;; ... ) ;; This is in the other file (deflayer included-layer ;; ... ) ---- [[platform]] == Platform-specific configuration If you put any top-level configuration item within a list beginning with `platform`, it will become a platform-specific configuration that is only active for the specified platforms. .Syntax: [source] ---- (platform (applicable-platforms) ...) ---- The valid values for applicable platforms are: - `win` - `winiov2` - `wintercept` - `linux` - `macos` .Example: [source] ---- (platform (macos) ;; Only on macos, use command arrows to jump/delete words ;; because command is used for so many other things ;; and it's weird that these cases use alt. (defoverrides (lmet bspc) (lalt bspc) (lmet left) (lalt left) (lmet right) (lalt right) ) ) (platform (win winiov2 wintercept) (defalias run-my-script (cmd #| something involving powershell |#)) ) (platform (macos linux) (defalias run-my-script (cmd #| something involving bash |#)) ) ---- [[environment]] == Environment-conditional configuration .Syntax: [source] ---- (environment (env-var-name env-var-value) ...) ---- The items `env-var-name` and `env-var-value` can be arbitrary strings. The name is the environment variable that is read for determining if the configuration is used or not. If the value of the environment variable (set only on kanata startup) matches `env-var-value`, the configuration is used; otherwise it is ignored. An empty string for `env-var-value` — `""` — will use the configuration if the environment variable is an empty string and also if the environment variable is not defined. .Example: [source] ---- (environment (LAPTOP lp1) (defalias met @lp1met) ) (environment (LAPTOP lp2) (defalias met @lp2met) ) ---- .Set environment variables in the current terminal process: [source] ---- # powershell $env:VAR_NAME = "var_value" # bash VAR_NAME=var_value ---- [[input-chords-v2]] == Input chords / combos (v2) You may define a single `+defchordsv2+` configuration item. This enables you to define global input chord behaviour. One might also find this functionality called another name of "combos" in other projects. Input chords enables you to press two or more keys in quick succession to activate a different action than would normally be associated with those keys. When activating a chord, the order of presses is not important; when all keys belonging to a chord are pressed, the action activates regardless of press order. The `+defchordsv2+` feature is configured as shown below: .Syntax example [source] ---- (defchordsv2 (participating-keys1) action1 timeout1 release-behaviour1 (disabled-layers1) ... (participating-keysN) actionN timeoutN release-behaviourN (disabled-layersN) ) ---- The configuration is made up of 5-tuples of: [cols="1,3"] |=== | `$participating-keys` | These are key names you would use in `defsrc`. A minimum of two keys must be defined per chord. The list must be unique per chord. | `$action` | These are actions as you would configure in `deflayer` or `defalias`. The action activates if all participating keys are activated within the timeout. | `$timeout` | The time (unit: milliseconds) within which, if all participating keys are pressed, the chord action will activate; otherwise the key presses are handled by the active layer. The time begins when the first participant is pressed. | `$release-behaviour` | This must be either `first-release` or `all-released`; `first-release` means the chord action will be released when the first participant is released, while `all-released` means the chord action will be released only when all of the participants have been released. | `$disabled-layers` | A list of layer names on which this chord is disabled. |=== Input chords have a related `defcfg` item: <>. When any non-chord activation happens, a timeout begins with duration configured by `chords-v2-min-idle` (unit: milliseconds). Until this timeout expires, all inputs will immediately skip chords processing and be processed by the active layer. IMPORTANT: When opting into input chords v2, you must enable `concurrent-tap-hold`. This is enforced for a more responsive `tap-hold` experience when activated by a chord. .Example: [source] ---- (defcfg concurrent-tap-hold yes) (defchordsv2 (a s) c 200 all-released (non-chord-layer) (a s d) (macro h e l l o) 250 first-release (non-chord-layer) (s d f) (macro b y e) 400 first-release (non-chord-layer) ) ---- NOTE: Also see <>, which are configured differently and can be defined per-layer. [[optional-defcfg-options]] == defcfg options [[danger-enable-cmd]] === danger-enable-cmd This option can be used to enable the `cmd` action in your configuration. The `+cmd+` action allows kanata to execute programs with arguments passed to them. This requires using a kanata program that is compiled with the `cmd` action enabled. The reason for this is so that if you choose to, there is no way for kanata to execute arbitrary programs even if you download some random configuration from the internet. This configuration is disabled by default and can be enabled by giving it the value `yes`. .Example: [source] ---- (defcfg danger-enable-cmd yes ) ---- [[sequence-timeout]] === sequence-timeout This option customizes the key sequence timeout (unit: ms). Its default value is 1000. The purpose of this item is explained in <>. .Example: [source] ---- (defcfg sequence-timeout 2000 ) ---- [[sequence-input-mode]] === sequence-input-mode This option customizes the key sequence input mode. Its default value when not configured is `hidden-suppressed`. The options are: - `visible-backspaced`: types sequence characters as they are inputted. The typed characters will be erased with backspaces for a valid sequence termination. - `hidden-suppressed`: hides sequence characters as they are typed. Does not output the hidden characters for an invalid sequence termination. - `hidden-delay-type`: hides sequence characters as they are typed. Outputs the hidden characters for an invalid sequence termination either after a timeout or after a non-sequence key is typed. For `visible-backspaced` and `hidden-delay-type`, a sequence leader input will be ignored if a sequence is already active. For historical reasons, and in case it is desired behaviour, a sequence leader input using `hidden-suppressed` will reset the key sequence. See <> for more about sequences. .Example: [source] ---- (defcfg sequence-input-mode visible-backspaced ) ---- [[sequence-backtrack-modcancel]] === sequence-backtrack-modcancel This option customizes the behaviour of key sequences when modifiers are used. The default is `yes` and can be overridden to `no` if desired. Setting it to `yes` allows both `fk1` and `fk2` to be activated in the following configuration, but with `no`, `fk1` will be impossible to activate ---- (defseq fk1 (lsft a b) fk2 (S-(c d)) ) ---- See <> for more about sequences and https://github.com/jtroo/kanata/blob/main/docs/sequence-adding-chords-ideas.md[this document] for more context about this specific configuration. .Example: [source] ---- (defcfg sequence-backtrack-modcancel no ) ---- [[log-layer-changes]] === log-layer-changes By default, kanata will log layer changes. However, logging has some processing overhead. If you do not care for the logging, you can choose to disable it. .Example: [source] ---- (defcfg log-layer-changes no ) ---- If `+--log-layer-changes+` is passed as a command line argument, a `no` in the configuration file will be overridden and layer changes will again be logged. This flag can be helpful when testing new configuration changes while keeping the default behaviour as "no logging" to save on processing, so that the `defcfg` item does not need to be adjusted back and forth when experimenting vs. stable usage. [[delegate-to-first-layer]] === delegate-to-first-layer By default, transparent keys on layers will delegate to the corresponding defsrc key when found on a layer activated by `layer-switch`. This config entry changes the behaviour to delegate to the action in the same position on the first layer defined in the configuration, which is the active layer on startup. For more context, see https://github.com/jtroo/kanata/issues/435. .Example: [source] ---- (defcfg delegate-to-first-layer yes ) ---- [[movemouse-inherit-accel-state]] === movemouse-inherit-accel-state By default `movemouse-accel` actions will track the acceleration state for vertical and horizontal axes separately. When this setting is enabled, `movemouse-accel` will behave exactly like mouse movements in https://qmk.fm[QMK], i.e. the acceleration state of new mouse movement actions will be inherited if others are already being pressed. .Example: [source] ---- (defcfg movemouse-inherit-accel-state yes ) ---- [[movemouse-smooth-diagonals]] === movemouse-smooth-diagonals By default, mouse movements move one direction at a time and vertical/horizontal movements are on independent timers. This can result in non-smooth diagonals when drawing a line in some app. This option adds a small imperceptible amount of latency to synchronize the mouse movements. .Example: [source] ---- (defcfg movemouse-smooth-diagonals yes ) ---- === dynamic-macro-max-presses [[dynamic-macro-max-presses]] This configuration allows you to customize the length limit on dynamic macros. The default length limit is 128 keys. .Example: [source] ---- (defcfg dynamic-macro-max-presses 1000 ) ---- === concurrent-tap-hold [[concurrent-tap-hold]] This configuration makes multiple tap-hold actions that are activated near in time expire their timeout quicker. By default this is disabled. When disabled, the timeout for a following tap-hold will start from 0ms **after** the previous tap-hold expires. When enabled, the timeout will start as soon as the tap-hold action is pressed even if a previous tap-hold action is still held and has not expired. .Example: [source] ---- (defcfg concurrent-tap-hold yes ) ---- [[block-unmapped-keys]] === block-unmapped-keys If you desire to use only a subset of your keyboard you can use `block-unmapped-keys` to make every key other than those that exist in `defsrc` a no-op. NOTE: this only functions correctly if you also set <> to yes. .Example: [source] ---- (defcfg block-unmapped-keys yes ) ---- [[rapid-event-delay]] === rapid-event-delay This configuration applies to the following events: * the release of one-shot-press activation * the release of the tapped key in a tap-hold activation * a non-eager tap-dance activation from interruption by another key * input chord activations, both v1 and v2 Key event processing is paused the defined number of milliseconds (approximate). The default value is 5. There will be a minor input latency impact in the mentioned scenarios. Since 5ms is 1 frame at 200 Hz refresh rate, in most scenarios this will not be perceptible. The reason for this configuration existing is that some environments do not process the scenarios correctly due to the rapidity of key events. Kanata does send the events in the correct order, so the fault is more in the environment, but kanata provides a workaround anyway. If you are negatively impacted by the latency increase of these events and your environment is not impacted by increased rapidity, you can reduce the value to a number between 0 and 4. .Example: [source] ---- (defcfg ;; If your environment is particularly buggy, might need to delay even more rapid-event-delay 20 ) ---- [[chords-v2-min-idle]] === chords-v2-min-idle This configuration affects the timer during which chords processing is disabled. NOTE: For more info, see <>. The default (and minimum) value is `5` and the unit is milliseconds. .Example: [source] ---- (defcfg chords-v2-min-idle 200 ) ---- [[override-release-on-activation]] === override-release-on-activation This configuration item changes activation behaviour from `defoverrides`. Take this example override: [source] ---- (defoverrides (lsft a) (lsft 9)) ---- The default behaviour is that if `lsft` is released **before** releasing `a`, kanata's behaviour would be to send `a`. A future improvement could be to make the `9` continue to be the key held, but that is not implemented today. The workaround in case the above behaviour negatively impacts your workflow is to enable this configuration. This configuration will press and then immediately release the `9` output as soon as the override activates, meaning you are unlikely as a human to ever release `lsft` first. The effect of this configuration is that the `9` key cannot remain held when activated by the override which is important to consider for your use cases. .Example: [source] ---- (defcfg override-release-on-activation yes ) ---- [[allow-hardware-repeat]] === allow-hardware-repeat By default, any repeat-key events generated by the physical keyboard (or operating system) will be passed through to the application. On Linux, under Wayland, this is wasted effort since the DE handles key-repeat on its own. Such events can also be distracting when debugging your configuration with evtest, etc. Setting this option to "false" will cause such events to be dropped, and not passed through. This is primarily meant for Linux, but may find some use on Mac. It is not implemented on Windows, and will be silently ignored. .Example: [source] ---- (defcfg allow-hardware-repeat false ) ---- [[alias-to-trigger-on-load]] === alias-to-trigger-on-load Select an alias to execute when first starting, and after each live-reload of the config. You can use this to run external commands, or to stack layers (with layer-while-held). The name of an alias, without a leading "@", is expected as a parameter. The example below will beep at startup (assuming your system has a beep command), and will already be blocking the swapped "i" and "o" keys. .Example: [source] ---- (defcfg alias-to-trigger-on-load S danger-enable-cmd yes ) (deffakekeys B (layer-while-held block)) (defalias P (on-press toggle-vkey B) S (macro @P (cmd beep)) ) (defsrc i o p ) (deflayer base o i @P ) (deflayer block • • _ ) ---- [[mouse-movement-key]] === Linux or Windows-interception only: mouse-movement-key Accepts a single key name. When configured, whenever a mouse cursor movement is received, the configured key name will be "tapped" by Kanata, activating the key's action. This enables reporting of every relative mouse movement, which corresponds to standard mice, trackballs, trackpads and trackpoints. Absolute movements, which can be generated by touchscreens, drawing tablets and some mouse replacement or accessibility software, are ignored. Scrolling events and mouse buttons are also ignored. The intended use of these events is to provide a way to automatically enable a mouse keys layer while mousing, which can be disabled by a timeout or typing on other keys, rather than explicit toggling. see cfg_examples/automousekeys-*.kbd for more. The `mvmt` key name is specially intended for this purpose. It has no output key mapping and cannot be supplied as an action; however, any key may be used. Supports live reload on Linux, but with Windows-interception, this option must be present on startup to enable mouse movement event collection, so restart is required to enable it. Changing the key name is always supported, however. .Example: [source] ---- (defcfg process-unmapped-keys yes mouse-movement-key mvmt ) (defsrc k l ; mvmt ) (defvirtualkeys mouse (layer-while-held mouse-layer) ) (defalias mhld (hold-for-duration 750 mouse) ) (deflayer qwerty k l ; @mhld ) (deflayer mouse-layer mlft mmid mrgt @mhld ) ---- [[linux-only-linux-dev]] === Linux only: linux-dev By default, kanata will try to detect which input devices are keyboards and try to intercept them all. However, you may specify exact keyboard devices from the `/dev/input` directories using the `linux-dev` configuration. .Example: [source] ---- (defcfg linux-dev /dev/input/by-path/platform-i8042-serio-0-event-kbd ) ---- If you want to specify multiple keyboards, you can separate the paths with a colon `+:+`. .Example: [source] ---- (defcfg linux-dev /dev/input/dev1:/dev/input/dev2 ) ---- Due to using the colon to separate devices, if you have a device with colons in its file name, you must escape those colons with backslashes: [source] ---- (defcfg linux-dev /dev/input/path-to\:device ) ---- Alternatively, you can use list syntax, where both backslashes and colons are parsed literally. List items are separated by spaces or newlines. Using quotation marks for each item is optional, and only required if an item contains spaces. [source] ---- (defcfg linux-dev ( /dev/input/path:to:device "/dev/input/path to device" ) ) ---- For devices that do not have an easily identifiable device path like Bluetooth keyboards using the `linux-dev-names-include` option below is recommended. [[linux-only-linux-dev-names-include]] === Linux only: linux-dev-names-include In the case that `linux-dev` is omitted, this option defines a list of device names that should be included. Device names that do not exist in the list will be ignored. Device paths are not supported by this option; instead use the device names as output by Kanata during startup. Launch Kanata with a <> (any use of a `linux-dev*` option may hide devices) and look for lines beginning with "registering": ---- registering /dev/input/eventX: "Device name 1" registering /dev/input/eventY: "Device name 2" ---- The entire name within quotes must be used, partial matches and regex's are not supported. .Example: [source] ---- (defcfg linux-dev-names-include ( "Device name 1" "Device name 2" ) ) ---- [[linux-only-linux-dev-names-exclude]] === Linux only: linux-dev-names-exclude In the case that `linux-dev` is omitted, this option defines a list of device names that should be excluded. This option is parsed identically to `linux-dev-names-include`. The `linux-dev-names-include` and `linux-dev-names-exclude` options are not mutually exclusive but in practice it probably only makes sense to use one and not both. .Example: [source] ---- (defcfg linux-dev-names-exclude ( "Device Name 1" "Device Name 2" ) ) ---- [[linux-only-linux-continue-if-no-devs-found]] === Linux only: linux-continue-if-no-devs-found By default, kanata will crash if no input devices are found. You can change this behaviour by setting `linux-continue-if-no-devs-found`. .Example: [source] ---- (defcfg linux-continue-if-no-devs-found yes ) ---- [[linux-only-linux-device-detect-mode]] === Linux only: linux-device-detect-mode Kanata on Linux automatically detects and grabs input devices when none of the explicit device configurations are in use. In case kanata is undesirably grabbing mouse-like devices, you can use a configuration item to change detection behaviour. The configuration is `linux-device-detect-mode` and it has the options: [cols="1,4"] |=== | `keyboard-only` | Grab devices that seem to be a keyboard only. | `keyboard-mice` | Grab devices that seem to be a keyboard only and devices that declare **both** keyboard and mouse functionality. | `any` | Grab all keyboard-like and mouse-like devices. |=== The default behaviour is: [cols="1,4"] |=== | `keyboard-mice` | When no mouse events are in `defsrc`. | `any` | When any mouse buttons or mouse scroll events are in `defsrc`. |=== [[linux-only-linux-unicode-u-code]] === Linux only: linux-unicode-u-code Unicode on Linux works by pressing Ctrl+Shift+U, typing the unicode hex value, then pressing Enter. However, if you do remapping in userspace, e.g. via xmodmap/xkb, the keycode "U" that kanata outputs may not become a keysym "u" after the userspace remapping. This will be likely if you use non-US, non-European keyboards on top of kanata. For unicode to work, kanata needs to use the keycode that outputs the keysym "u", which might not be the keycode "U". You can use `evtest` or `kanata --debug`, set your userspace key remapping, then press the key that outputs the keysym "u" to see which underlying keycode is sent. Then you can use this configuration to change kanata's behaviour. .Example: [source] ---- (defcfg linux-unicode-u-code v ) ---- [[linux-only-linux-unicode-termination]] === Linux only: linux-unicode-termination Unicode on Linux terminates with the Enter key by default. This may not work in some applications. The termination is configurable with the following options: - `enter` - `space` - `enter-space` - `space-enter` .Example: [source] ---- (defcfg linux-unicode-termination space ) ---- === Linux only: linux-x11-repeat-delay-rate[[linux-only-x11-repeat-rate]] On Linux, you can tell kanata to run `xset r rate ` on startup and on live reload via the configuration item `linux-only-x11-repeat-rate`. This takes two numbers separated by a comma. The first number is the delay in ms and the second number is the repeat rate in repeats/second. This configuration item does not affect Wayland or no-desktop environments. .Example: [source] ---- (defcfg linux-x11-repeat-delay-rate 400,50 ) ---- [[linux-only-linux-use-trackpoint-property]] === Linux only: linux-use-trackpoint-property On linux, you can ask kanata to label itself as a trackpoint. This has several effects on libinput including enabling middle mouse button scrolling and using a different acceleration curve. Otherwise, a trackpoint intercepted by kanata may not behave as expected. If using this feature, it is recommended to filter out any non-trackpoint pointing devices using <>, <> or <> to avoid changing their behavior as well. .Example: [source] ---- (defcfg linux-use-trackpoint-property yes ) ---- [[linux-only-linux-output-device-name]] === Linux only: linux-output-device-name This option defines the name of the evdev output device. The default value is kanata. .Example: [source] ---- (defcfg linux-output-device-name "kanata output" ) ---- [[linux-only-linux-output-device-bus-type]] === Linux only: linux-output-device-bus-type Kanata on Linux needs to declare a "bus type" for its evdev output device. The options are `USB` and `I8042`, with the default as `I8042`. Using USB can https://github.com/jtroo/kanata/pull/661[break disable-touchpad-while-typing on Wayland]. But using I8042 appears to break https://github.com/jtroo/kanata/issues/1131[some other scenarios]. Thus the output bus type is configurable. .Example: [source] ---- (defcfg linux-output-device-bus-type USB ) ---- [[macos-only-macos-dev-names-include]] === macOS only: macos-dev-names-include This option defines a list of device names that should be included. By default, kanata will try to detect which input devices are keyboards and try to intercept them all. However, you may specify exact keyboard devices to intercept using the `macos-dev-names-include` configuration. Device names that do not exist in the list will be ignored. This option is parsed identically to `linux-dev`. Use `kanata -l` or `kanata --list` to list the available keyboards. .Example: [source] ---- (defcfg macos-dev-names-include ( "Device name 1" "Device name 2" ) ) ---- [[macos-only-macos-dev-names-exclude]] === macOS only: macos-dev-names-exclude This option defines a list of device names that should be excluded. By default, kanata will try to detect which input devices are keyboards and try to intercept them all. However, you may specify certain keyboard devices to be ignored using the `macos-dev-names-exclude` configuration. Device names that do not exist in the list will be included. This option is parsed identically to `linux-dev`. Use `kanata -l` or `kanata --list` to list the available keyboards. .Example: [source] ---- (defcfg macos-dev-names-exclude ( "Device name 1" "Device name 2" ) ) ---- [[windows-only-windows-altgr]] === Windows only: windows-altgr There is an option for Windows to mitigate the strange behaviour of AltGr (ralt) if you're using `process-unmapped-keys yes` or have the key in your defsrc. This is applicable for many non-US layouts. You can use one of the listed values to change what kanata does with the key: * `cancel-lctl-press` ** This will remove the `lctl` press that is generated alonside `ralt` * `add-lctl-release` ** This adds an `lctl` release when `ralt` is released Without these workarounds, you should use `process-unmapped-keys (all-except lctl ralt))`, or use `process-unmapped-keys no` **and** also omit both `ralt` and `lctl` from `defsrc`. .Example: [source] ---- (defcfg windows-altgr add-lctl-release ) ---- For more context, see: https://github.com/jtroo/kanata/issues/55. NOTE: Even with these workarounds, putting `+lctl+` and `+ralt+` in your defsrc may not work properly with other applications that also use keyboard interception. Known application with issues: GWSL/VcXsrv === Windows only: windows-interception-mouse-hwid[[windows-only-windows-interception-mouse-hwid]] This defcfg item allows you to intercept mouse buttons for a specific mouse device. This only works with the Interception driver (the -wintercept variants of the release binaries). The original use case for this is for laptops such as a Thinkpad, which have mouse buttons that may be desirable to activate kanata actions with. To know what numbers to put into the string, you can run the variant with this defcfg item defined with any numbers. Then when a button is first pressed on the mouse device, kanata will print its hwid in the log; you can then copy-paste that into this configuration entry. If this defcfg item is not defined, the log will not print. Hwids in Kanata are byte array representations of a concatenation of the ASCII hardware ids, which can be seen in Device Manager on Windows. As such, they are an arbitrary length and can be very long. https://github.com/jtroo/kanata/issues/108[Relevant issue]. .Example: [source] ---- (defcfg windows-interception-mouse-hwid "70, 0, 60, 0" ) ---- === Windows only: windows-interception-mouse-hwids[[windows-only-windows-interception-mouse-hwids]] This item has a similar purpose as the singular version documented above, but is instead a list of strings that allows multiple mice to be intercepted. If both the singular and list items are used, the singular version will behave as if added to the list. .Example: [source] ---- (defcfg windows-interception-mouse-hwids ( "70, 0, 60, 0" "71, 0, 62, 0" ) ) ---- === Windows only: windows-interception-keyboard-hwids[[windows-only-windows-interception-keyboard-hwids]] This defcfg item allows you to intercept only specific keyboards. Its value must be a list of strings with each string representing one hardware ID. To know what numbers to put into the string, you can run the variant with this defcfg item empty. Then when a button is first pressed on the keyboard, kanata will print its hwid in the log. You can then copy-paste that into this configuration entry. If this defcfg item is not defined, the log will not print. Hwids in Kanata are byte array representations of a concatenation of the ASCII hardware ids, which can be seen in Device Manager on Windows. As such, they are an arbitrary length and can be very long. .Example: [source] ---- (defcfg windows-interception-keyboard-hwids ( "70, 0, 60, 0" "71, 72, 73, 74" ) ) ---- === Windows only: windows-interception-keyboard-hwids-exclude[[windows-only-windows-interception-keyboard-hwids-exclude]] This defcfg item allows you to exclude certain keyboards from being intercepted. You cannot define this alongside the inclusive keyboard configuration. It is parsed identically to the inclusive configuration. .Example: [source] ---- (defcfg windows-interception-keyboard-hwids-exclude ( "70, 0, 60, 0" "71, 72, 73, 74" ) ) ---- === Windows only: windows-interception-mouse-hwids-exclude[[windows-only-windows-interception-mouse-hwids-exclude]] This defcfg item allows you to exclude certain mice from being intercepted. You cannot define this alongside the inclusive mouse configuration. It is parsed identically to the inclusive configuration. .Example: [source] ---- (defcfg windows-interception-mouse-hwids-exclude ( "70, 0, 60, 0" "71, 0, 62, 0" ) ) ---- [[windows-only-tray-icon]] === Windows only: tray-icon Show a custom tray icon file for a <> gui-enabled build of kanata on Windows. Accepts either the full path (including the file name with an extension) to the icon file or just the file name, which is then searched in the following locations: * Default parent folders: ** config file's, executable's ** env vars: `XDG_CONFIG_HOME`, `APPDATA` (`C:\Users\\AppData\Roaming`), `USERPROFILE` `/.config` (`C:\Users\\.config`) * Default config subfolders: `kanata` `kanata-tray` * Default image subfolders (optional): `icon` `img` `icons` * Supported image file formats: `ico` `jpg` `jpeg` `png` `bmp` `dds` `tiff` If not specified, tries to load any icon file from the same locations with the name matching config name with extension replaced by one of the supported ones. See https://github.com/jtroo/kanata/blob/main/cfg_samples/tray-icon/tray-icon.kbd[example config] for more details. .Example: [source] ---- ;; in a config file C:\Users\\AppData\Roaming\kanata\kanata.kbd (defcfg tray-icon base.png ;; will load C:\Users\\AppData\Roaming\kanata\base.png ) ---- [[windows-only-icon-match-layer-name]] === Windows only: icon-match-layer-name When enabled, attempt to switch to a custom tray icon that matches the name of the active layer if the layer doesn't specify an explicit icon. If no icon file is found, the default icon will be used (see <>). File search rules are the same as in <>. Defaults to true. See https://github.com/jtroo/kanata/blob/main/cfg_samples/tray-icon/tray-icon.kbd[example config] for more details. [[windows-only-tooltip-layer-changes]] === Windows only: tooltip-layer-changes Show a custom layer icon near the mouse pointer position. Defaults to false. Requires <> gui-enabled build. [[windows-only-tooltip-show-blank]] === Windows only: tooltip-show-blank Show a blank square when instead of an icon if a layer isn't configured to have one. Defaults to false. Requires <> gui-enabled build. [[windows-only-tooltip-no-base]] === Windows only: tooltip-no-base Don't show a tooltip layer icon for the base layer (1st deflayer). Defaults to true. Requires <> gui-enabled build. [[windows-only-tooltip-duration]] === Windows only: tooltip-duration Set duration (in ms) for showing a custom layer icon near the mouse pointer position. 0 to never hide. Defaults to 500. Requires <> gui-enabled build. [[windows-only-tooltip-size]] === Windows only: tooltip-size Set the size (comma-separated Width,Height without spaces) for a custom layer icon near the mouse pointer position. Defaults to 24,24. Requires <> gui-enabled build. [[windows-only-notify-cfg-reload]] === Windows only: notify-cfg-reload Show system notification message on config reload. Defaults to true. Requires <> gui-enabled build. [[windows-only-notify-cfg-reload-silent]] === Windows only: notify-cfg-reload-silent Disable sound for the system notification message on config reload. Defaults to false. Requires <> gui-enabled build. [[windows-only-notify-error]] === Windows only: notify-error Show system notification message on kanata errors. Defaults to true. Requires <> gui-enabled build. [[using-multiple-defcfg-options]] === Using multiple defcfg options The `defcfg` entry is treated as a list with pairs of strings. For example: [source] ---- (defcfg a 1 b 2) ---- This will be treated as configuration `a` having value `1` and configuration `b` having value `2`. An example defcfg containing many of the options is shown below. It should be noted options that are Linux-only, Windows-only, or macOS-only will be ignored when used on a non-applicable operating system. [source] ---- ;; Don't actually use this exact configuration, ;; it's almost certainly not what you want. (defcfg process-unmapped-keys yes danger-enable-cmd yes sequence-timeout 2000 sequence-input-mode visible-backspaced sequence-backtrack-modcancel no log-layer-changes no delegate-to-first-layer yes movemouse-inherit-accel-state yes movemouse-smooth-diagonals yes dynamic-macro-max-presses 1000 linux-dev (/dev/input/dev1 /dev/input/dev2) linux-dev-names-include ("Name 1" "Name 2") linux-dev-names-exclude ("Name 3" "Name 4") linux-continue-if-no-devs-found yes linux-unicode-u-code v linux-unicode-termination space linux-x11-repeat-delay-rate 400,50 windows-altgr add-lctl-release windows-interception-mouse-hwid "70, 0, 60, 0" ) ---- == Advanced features[[advanced-features]] [[virtual-keys]] === Virtual keys You can define up to 767 virtual keys. These keys are not directly mapped to any physical key presses or releases. Virtual keys can be activated via special actions: * `(on-press )` or `on↓`: Activate a virtual key action when pressing the associated input key. * `(on-release )` or `on↑`: Activate a virtual key action when releasing the associated input key. * `(on-idle )`: Activate a virtual key action when kanata has been idle for at least `idle time` milliseconds. * `(hold-for-duration `): Press a virtual key for `hold time` milliseconds. If `hold-for-duration` retriggered on a virtual key before release, the time will be reset with no additional press/release events. The `` parameter can be one of: * `tap-virtualkey | tap-vkey`: Press and release the virtual key. If the key is already pressed, this only releases it. * `press-virtualkey | press-vkey`: Press the virtual key. It will not be released until another action triggers a release or tap. If the key is already pressed, this does nothing. * `release-virtualkey | release-vkey`: Release the virtual key. If it is not already pressed, this does nothing. * `toggle-virtualkey | toggle-vkey`: Press the virtual key if it is not already pressed, otherwise release it. A virtual key can be defined in a `defvirtualkeys` configuration entry. Configuring this entry is similar to `+defalias+`, but you cannot make use of aliases inside to shorten an action. You can refer to previously defined virtual keys. Expanding on the `on-idle` action some more, the wording that "kanata" has been idle is important. Even if the keyboard is idle, kanata may not yet be idle. For example, if a long-running macro is playing, or kanata is waiting for the timeout of actions such as `caps-word` or `tap-dance`, kanata is not yet idle, and the tick count for the `` parameter will not yet be counting even if you no longer have any keyboard keys pressed. .Example: [source] ---- (defvirtualkeys ;; Define some virtual keys that perform modifier actions ctl lctl sft lsft met lmet alt lalt ;; A virtual key that toggles all modifier virtual keys above tal (multi (on-press toggle-virtualkey ctl) (on-press toggle-virtualkey sft) (on-press toggle-virtualkey met) (on-press toggle-virtualkey alt) ) ;; Virtual key that activates a macro vkmacro (macro h e l l o spc w o r l d) ) (defalias psf (on-press press-virtualkey sft) rsf (on-press release-virtualkey sft) tal (on-press tap-vkey tal) mac (on-press tap-vkey vkmacro) isf (on-idle 1000 tap-vkey sft) hfd (hold-for-duration 1000 met) ) (deflayer use-virtual-keys @psf @rsf @tal @mac a s d f @isf @hfd ) ---- .Older fake keys documentation [%collapsible] ==== The older configuration style of fake keys are still supported but the new style is preferred due to (hopefully) clearer naming. Fake keys can be defined inside of `deffakekeys`. The actions are: * `+(on-press-fakekey )+` or `on↓fakekey`: Activate a fake key action when pressing the key mapped to this action. * `+(on-release-fakekey )+` or `on↑fakekey`: Activate a fake key action when releasing the key mapped to this action. * `+(on-idle-fakekey )+`: Activate a fake key action when kanata has been idle for at least `idle time` milliseconds. The aforementioned `++` can be one of four values: * `+press+`: Press the fake key. It will not be released until another action triggers a release or tap. * `+release+`: Release the fake key. If it's not already pressed, this does nothing. * `+tap+`: Press and release the fake key. If it's already pressed, this only releases it. * `+toggle+`: Press the fake key if not already pressed, otherwise release it. .Example: [source] ---- (deffakekeys ctl lctl sft lsft met lmet alt lalt ;; Press all modifiers pal (multi (on-press fakekey ctl press) (on-press-fakekey sft press) (on-press-fakekey met press) (on-press-fakekey alt press) ) ;; Release all modifiers ral (multi (on-press-fakekey ctl release) (on-press-fakekey sft release) (on-press-fakekey met release) (on-press-fakekey alt release) ) ) (defalias psf (on-press-fakekey sft press) rsf (on-press-fakekey sft release) pal (on-press-fakekey pal tap) ral (on-press-fakekey ral tap) isf (on-idle-fakekey sft tap 1000) ) (deflayer use-virtual-keys @psf @rsf @pal @ral a s d f @isf ) ---- ==== For more context, you can read the https://github.com/jtroo/kanata/issues/80[issue that sparked the creation of virtual keys]. Something notable about virtual keys is that they don't always interrupt the state of an active `+tap-dance-eager+`. If a `macro` action is assigned to a virtual key, this won't interrupt a tap dance. However, most other action types, notably a "normal" key action like `+rsft+` will still interrupt a tap dance. [[sequences]] === Sequences The `+sldr+` action makes kanata go into "sequence" mode. The action name is short for "sequence leader". This comes from Vim which has the concept of a configurable sequence leader key. When in sequence mode, keys are not typed (<>) but are saved until one of the following happens: * A key is typed that does not match any sequence * `+sequence-timeout+` milliseconds elapses since the most recent key press Sequences are configured similarly to `+defvirtualkeys+`. The first parameter of a pair must be a defined virtual key name. The second parameter is a list of keys that will activate a virtual key tap when typed in the defined order. More precisely, the action triggered is: `+(on-press tap-vkey )+` .Example: [source] ---- (defseq git-status (g s t)) (defvirtualkeys git-status (macro g i t spc s t a t u s)) (defalias rcl (tap-hold-release 200 200 sldr rctl)) (defseq dotcom (. S-3) dotorg (. S-4) ;; The shifted letters in parentheses means a single press of lsft ;; must remain held while both h and then s are pressed. ;; This is not the same as S-h S-s, which means that the lsft key ;; must be released and repressed between the h and s presses. https (S-(h s)) ) (defvirtualkeys dotcom (macro . c o m) dotorg (macro . o r g) https (macro h t t p s S-; / /) ) ---- There are 10 special keys with names `nop0-nop9` which kanata treats specially. Kanata will never send OS events for these keys but they can still participate in sequences. See an example of using the nop keys alongside templates to define sequences below. .Example: [source] ---- (defsrc f7 f8 f9 f10) (deflayer base sldr nop0 nop1 nop2) (deftemplate seq (vk-name input-keys output-action) (defvirtualkeys $vk-name $output-action) (defseq $vk-name $input-keys) ) ;; template-expand has a shortened form: t! (t! seq dotcom (nop0 nop1) (macro . c o m)) (t! seq dotorg (nop0 nop2) (macro . o r g)) ---- If 10 special nop keys do not seem sufficient, you can get creative with your sequences and treat some as a prefix modifier. For example, you can get 24 "keys" by treating `nop0-nop5` as normal while treating `nop6-nop9` as prefixes that are always followed by a second nop key. .Example: [source] ---- (defalias nop0 nop0 ;; ... nop5 nop5 nop6 (macro nop6 nop0) ;; ... nop11 (macro nop6 nop5) ;; ... nop18 (macro nop9 nop0) ;; ... nop23 (macro nop9 nop5) ) ---- ==== Overlapping keys in any order Within the key list of `defseq` configuration items, the special `O-` list prefix can be used to denote a set of keys that must all be pressed before any are released in order to match the sequence. For an example, `O-(a b c)` is equivalent to `O-(c b a)`. .Example: [source] ---- (defvirtualkey hello (macro h (unshift e l) 5 (unshift l o))) (defseq hello (O-(h l o))) ---- WARNING: The way that sequences implements this functionality behind the scenes is by generating a sequence for every permutation of the overlapping keys. This can make kanata use up a lot of memory. Due to this, the maximum keys allowed in a given `O-(...)` list is 6, but you are still permitted to add more to the sequence, including more `O-(...)` lists. Doing the above can balloon kanata's memory consumption. .Sample of more advanced usage [%collapsible] ==== The configuration below showcases context-dependent chording with auto-space and auto-deleted spaces from typing punctuation. For example, chording `(d a y)` and then `(t u e)` will output `Tuesday`, while chording `(t u e)` by itself does nothing. .Example configuration: [source] ---- (defsrc f1) (deflayer base lrld) (defcfg process-unmapped-keys yes sequence-input-mode visible-backspaced concurrent-tap-hold true) (deftemplate seq (vk-name in out) (defvirtualkeys $vk-name $out) (defseq $vk-name $in)) (defvirtualkeys rls-sft (multi (release-key lsft)(release-key rsft))) (defvar rls-sft (on-press tap-vkey rls-sft)) (deftemplate rls-sft () $rls-sft 5) (defchordsv2 (d a y) (macro sldr d (t! rls-sft) a y spc nop0) 200 first-release () (h l o) (macro h (t! rls-sft) e l l o sldr spc nop0) 200 first-release () ) (t! seq Monday (d a y spc nop0 O-(m o n)) (macro S-m $rls-sft o n d a y nop9 sldr spc nop0)) (t! seq Tuesday (d a y spc nop0 O-(t u e)) (macro S-t $rls-sft u e s d a y nop9 sldr spc nop0)) (t! seq DelSpace_. (spc nop0 .) (macro .)) (t! seq DelSpace_; (spc nop0 ;) (macro ;)) ---- .Try using the above configuration to type the text: [source] ---- day; Day; Tuesday. day hello hello day Hello day. hello Tuesday Hello Monday; ---- ==== ==== Override the global timeout and input mode An alternative to using `sldr` is the `sequence` action. The syntax is `(sequence )`. This enters sequence mode with a sequence timeout different from the globally configured one. The `sequence` action can also be called with a second parameter. The second parameter is an override for `sequence-input-mode`: ---- (sequence ) ---- .Example: [source] ---- ;; Enter sequence mode and input . with a timeout of 250 (defalias dot-sequence (macro (sequence 250) 10 .)) ;; Enter sequence mode and input . with a timeout of 250 and using hidden-delay-type (defalias dot-sequence (macro (sequence 250 hidden-delay-type) 10 .)) ---- ==== More about sequences For more context about sequences, you can read the https://github.com/jtroo/kanata/issues/97[design and motivation of sequences]. You may also be interested in https://github.com/jtroo/kanata/blob/main/docs/sequence-adding-chords-ideas.md[the document describing chords in sequences] to read about how chords in sequences behave. [[input-chords]] === Input chords Not to be confused with <>, `+chord+` actions allow you to perform various actions based on which specific combination of input keys are pressed together. Such an unordered combination of keys is called a "chord". Each chord can perform a different action, allowing you to bind up to `+2^n - 1+` different actions to just `+n+` keys. Input chords are configured similarly to `+defalias+` with two extra parameters at the beginning of each `+defchords+` group: the name of the group and a timeout value after which a chord triggers if it isn't triggered by a key release or press of a non-chord key before the timeout expires. [source] ---- (defsrc a b c) (deflayer default @cha @chb @chc ) (defalias cha (chord example a) chb (chord example b) chc (chord example c) ) (defchords example 500 (a ) a ( b ) b (a c) C-v (a b c) @three ) ---- The first item of each pair specifies the keys that make up a given chord. The second item of each pair is the action to be executed when the given chord is pressed and may be any regular or advanced action, including aliases. It currently cannot however contain another `+chord+` action. Note that unlike with `+defseq+`, these keys do not directly correspond to real keys and are merely arbitrary labels that make sense within the context of the chord. They are mapped to real keys in layers by configuring the key in the layer to map to a `+(chord name key)+` action where `+name+` is the name of the chords group (above `+example+`) and `+key+` is one of these arbitrary labels. It is perfectly valid to nest these `+chord+` actions that enter "chording mode" within other actions like `+tap-dance+` and that will work as one would expect. However, this only applies to the first key used to enter "chording mode". Once "chording mode" is active, all other keys will be directly handled by "chording mode" with no regard for wrapper actions; e.g. if a key is pressed and it maps to a tap-hold with a chord as the hold action within, that chord key will immediately activate instead of the key needing to be held for the timeout period. **Release behaviour** For single key actions and output chords — like `lctl` or `S-tab` — and for `layer-while-held`, an input chord will release the action only when all keys that are part of the input chord have been released. In other words, if even one key is held for the input chord then the output action will be continued to be held, but only for the mentioned action categories. The behaviour also applies to the actions mentioned above when used inside of `multi` but not within any other action. An exception to the behaviour described above for the action categories that would normally apply is if a chord decomposition occurs. A chord decomposition occurs when you input a chord that does not correspond to any action. When this happens, kanata splits up the key presses to activate other actions from the components of the input chord. In this scenario, the behaviour described in the next paragraph will occur. For chord decompositions and all other action categories, the release behaviour is more confusing: the output action will end when any key is released during the timeout, or if the timeout expires, the output action ends when the *first* key that was pressed in the chord gets released. This inconsistency is a limitation of the current implementation. In these scenarios it is recommended to hold down all keys if you want to keep holding and to release all keys if you want to do a release. This is because it will probably be difficult to know which key was pressed first. If you want to bypass the behaviour of keys being held for chord outputs, you could change the chord output actions to be <> instead. Using a macro will guarantee a rapid press+release for the output keys. [[defaliasenvcond]] === defaliasenvcond NOTE: this configuration item is older and instead you may want to use the newer and more generalized <> configuration. There is a variant of `defalias`: `defaliasenvcond`. This variant is parsed similarly, but there must be an extra list parameter that comes before all of the name-action pairs. The list must contain two strings. In order, these strings are: an environment variable name, and the environment variable value. When the environment variable defined by the name has the corresponding value when starting kanata, the aliases within will be active. Otherwise, the aliases will be skipped. A use case for `defaliasenvcond` is when one has multiple devices which vary in layout of keys, e.g. different special keys on the bottom row. Using environment variables, one can use the same kanata configuration across those multiple devices while changing key behaviours to keep consistent behaviour of specific key positions across the multiple devices, when the hardware keys at those physical key positions are not the same. .Example: [source] ---- (defaliasenvcond (LAPTOP lp1) met @lp1met ) (defaliasenvcond (LAPTOP lp2) met @lp2met ) ---- .Set environment variables in the current terminal process: [source] ---- # powershell $env:VAR_NAME = "var_value" # bash VAR_NAME=var_value ---- [[templates]] === Templates The top-level configuration item `deftemplate` declares a template that can be expanded multiple times via the list item `template-expand`. The short form of `template-expand` is `t!`. The parameters to `deftemplate` in order are: * Template name * List of template variables * Template content (any combination of lists / strings) Within the template content, variable names prefixed with `$` will be substituted with the expression passed into `template-expand`. The list item `template-expand` can be placed as a top-level list or within another list. Its parameters in order are: * template name * parameters to substitute into the template NOTE: Template expansion happens after file includes and before any other parsing. One consequence of this early parsing is that variables defined in `defvar` are **not** substituted when used inside of `template-expand`. This has consequences for condtional content, e.g. with `if-equal`. This is discussed further in Example 5. Example 1: In a simple example, let's say you wanted to set a large group of keys to do something different when you're holding alt. Yes, this could also be handled with remapping alt to a layer shift, but there are cases where you wouldn't want this. Rather than retyping the code with `fork` and `unmod` (to release alt) a bunch of times, you could template it like so: [source] ---- (deftemplate alt-fork (original-action new-action) (fork $original-action (multi (unmod (ralt lalt) nop0) $new-action) (lalt ralt)) ) (defsrc 1 2 3) (defalias fn1 (template-expand alt-fork 1 f1)) ;; Templates are a simple text substitution, so the above is exactly equivalent to: ;; (defalias fn1 (fork 1 (multi (unmod (ralt lalt) nop0) f1) (lalt ralt))) (defalias fn2 (template-expand alt-fork 2 f2)) ;; You can use t! as a short form of template-expand (defalias fn3 (t! alt-fork 3 f3)) (deflayer default @fn1 @fn2 @fn3) ---- .Example 2: [source] ---- (defvar chord-timeout 200) (defcfg process-unmapped-keys yes) ;; This template defines a chord group and aliases that use the chord group. ;; The purpose is to easily define the same chord position behaviour ;; for multiple layers that have different underlying keys. (deftemplate left-hand-chords (chordgroupname k1 k2 k3 k4 alias1 alias2 alias3 alias4) (defalias $alias1 (chord $chordgroupname $k1) $alias2 (chord $chordgroupname $k2) $alias3 (chord $chordgroupname $k3) $alias4 (chord $chordgroupname $k4) ) (defchords $chordgroupname $chord-timeout ($k1) $k1 ($k2) $k2 ($k3) $k3 ($k4) $k4 ($k1 $k2) lctl ($k3 $k4) lsft ) ) (template-expand left-hand-chords qwerty a s d f qwa qws qwd qwf) ;; t! is short for template-expand (t! left-hand-chords dvorak a o e u dva dvo dve dvu) (defsrc a s d f) (deflayer dvorak @dva @dvo @dve @dvu) (deflayer qwerty @qwa @qws @qwd @qwf) ---- .Example 3: [source] ---- ;; This template defines a home row that customizes a single key's behaviour (deftemplate home-row (j-behaviour) a s d f g h $j-behaviour k l ; ' ) (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ ;; usable even inside defsrc caps (t! home-row j) ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) (deflayer base grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ ;; lists can be passed in too! caps (t! home-row (tap-hold 200 200 j lctl)) ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) ---- ==== if-equal Within a template you can use the list item `if-equal` to have condiditionally-used items within a template. It accepts a minimum of 2 parameters. The first two parameters must be strings and are compared against each other. If they match, the following parameters are inserted into the template in place of the `if-equal` list. Otherwise if the strings do not match then the whole `if-equal` list is removed from the template. .Example 4: ---- (deftemplate home-row (version) a s d f g h (if-equal $version v1 j) (if-equal $version v2 (tap-hold 200 200 j lctl)) k l ; ' ) (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps (template-expand home-row v1) ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) (deflayer base grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps (template-expand home-row v2) ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) ---- Similar to `if-equal` are three more conditional operators for templates: * `if-not-equal` ** the content is used if the first two string parameters are not equal * `if-in-list` ** the content is used if the first string parameter exists in the second list-of-strings parameter * `if-not-in-list` ** the content is used if the first string parameter does not exist in the second list-of-strings parameter .Example 5: ---- ;; defvar is parsed AFTER template expansion occurs. (defvar a hello) (deftemplate template1 (var1) a (if-equal hello $var1 b) c ) ;; Below will expand to: `a c` because the string ;; $a itself is compared against the string hello ;; and they are not equal. (template-expand template1 $a) (deftemplate template2 (var1) a (if-equal $a $var1 b) c ) ;; Below will expand to: `a b c` because the string ;; $a is compared against the string $a and they are equal. ;; But note that the variable $a is still not substituted ;; with its defvar value of: hello. (template-expand template2 $a) ---- [[concat-in-deftemplate]] ==== concat in deftemplate Like <>, a list beginning with `concat` within the content of `deftemplate` will be replaced with a single string that consists of all the subsequent items in the list concatenated to each other. [[custom-tap-hold-behaviour]] === Custom tap-hold behaviour This is not currently configurable without modifying the source code, but if you're willing and/or capable, there is a tap-hold behaviour that is currently not exposed. Using this behaviour, one can be very particular about when and how tap vs. hold will activate by using extra information. The available information that can be used is exactly which keys have been pressed or released as well as the timing in milliseconds of those key presses. The action `+tap-hold-release-keys+` makes use of some of this capability, but doesn't make full use of the power of this functionality. For more context, you can read the https://github.com/jtroo/kanata/issues/128[motivation for custom tap-hold behaviour]. [[fancy-key-symbols]] === Fancy key symbols Instead of using the same `+a-z+` letters for special keys, e.g., `+lsft+` for `+LeftShift+` you can use much shorter, yet more visible, key symbols like `+‹⇧+`. For more details see https://github.com/jtroo/kanata/blob/main/docs/fancy_symbols.md[symbol list] and https://github.com/jtroo/kanata/blob/main/cfg_samples/fancy_symbols.kbd[example config], which not only uses these symbols in layer definitions, but also repurposes `+⎇›+` and `+⇧›+` `+⎇›+` keys into "symbol" keys that allow you to insert these fancy symbols by pressing the key, e.g., * hold `+⎇›+` and tap `+Delete+` would insert `+␡+` [[windows-only-work-elevated]] === Windows only: enable in elevated windows The default `kanata.exe` binary doesn't work in elevated windows (run with administrative privileges), e.g., `Control Panel`. However, you can use AutoHotkey's "EnableUIAccess" script to self-sign the binary, move it to "Program Files", then launching kanata from there will also work in these elevated windows. See https://github.com/jtroo/kanata/blob/main/EnableUIAccess[EnableUIAccess] folder with the script and its required libraries (needs https://www.autohotkey.com/download/[AutoHotkey v2] installed) If compiling yourself, you should add the feature flag `win_manifest` to enable the use of the `EnableUIAccess` script: ``` cargo build --win_manifest ``` [[windows-only-win-tray]] === Windows only: win-tray Kanata can be compiled as a Windows GUI tray app with the feature flag `gui`. This can simplify launching the app on user login by placing a `.lnk` at `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`, show custom icon indicator per config image:https://github.com/jtroo/kanata/blob/main/docs/win-tray/win-tray-screen.png[icon indicator per config,477,129] as well as dynamic icon indicator per layer (might need to click on the gif below to play) image:https://github.com/jtroo/kanata/blob/main/docs/win-tray/win-tray-layer-change.gif[icon indicator per layer,33,35,opts=autoplay] (see <>). It also supports (re)loading configs. Currently the only configuration supported is tray icon per profile, all other configuration should be done by passing cli flags in the `Target` field of `.lnk`, e.g., `"C:\Program Files\kanata\kanata.exe" -d -n` to launch kanata without a delay in a debug mode When launched from a command line, the app outputs log to the console, but otherwise the logs are currently only available via an app capable of viewing `OutputDebugString` debugs, e.g., https://github.com/smourier/TraceSpy[TraceSpy]. [[test-your-config]] === Test your config Kanata has a `+kanata_simulated_input+` tool to help test your configuration in a predictable manner. You can try it out on https://jtroo.github.io/[GitHub pages]. Code for the CLI tool can be found under link:https://github.com/jtroo/kanata/blob/main/simulated_input/[simulated_input]. Instead of physically typing to test something and wondering whether you didn't get the expected result because your config is wrong or because you mistyped something, you can write a sequence of key presses in a `+sim.txt+` file, run the tool with your config and get a "timeline" view of input/output events that can help understand how kanata translates your input into various key/mouse presses. WARNING: The format of this view may change. Emoji output may break vertical alignment. For more details download the files below and run `kanata_simulated_input -c sim.kbd -s sim.txt` + - https://github.com/jtroo/kanata/blob/main/docs/simulated_output/sim.kbd[example config] with simple home row mod bindings + - https://github.com/jtroo/kanata/blob/main/docs/simulated_output/sim.txt[example input sequence] + - https://github.com/jtroo/kanata/blob/main/docs/simulated_output/sim_out.txt[example output sequence] + Input sequence file format: whitespace insensitive list of `prefix:key` pairs where prefix is one of: + - `🕐`, `t`, or `tick` to add time between key events in `ms` + - `↓`, `d`, `down`, or `press` + - `↑`, `u`, `up`, or `release` + - `⟳`, `r`, or `repeat` + And key names are defined in the https://github.com/jtroo/kanata/blob/main/parser/src/keys/mod.rs[str_to_oscode function], for example, `1` for the numeric key 1 or `kp1`/`🔢₁` for the keypad numeric key 1 Using unicode symbols `🕐`,`↓`,`↑`,`⟳` allows skipping the `:` separator, e.g., `↓k` ≝ `↓:k` ≝ `d:k` [[zippychord]] === Zippychord **Reference** You may define a single `+defzippy+` configuration item. This configuration enables chorded text expansion. .Configuration syntax within the kanata configuration [source] ---- (defzippy $zippy-filename ;; required on-first-press-chord-deadline $deadline-millis ;; optional idle-reactivate-time $idle-time-millis ;; optional smart-space $smart-space-cfg ;; optional smart-space-punctuation ( ;; optional $punc1 $punc2 ... $puncN) output-character-mappings ( ;; optional $character1 $output-mapping1 $character2 $output-mapping2 ;; ... $characterN $output-mappingN ) ) ---- [cols="1,4"] |=== | `$zippy-filename` | Relative or absolute file path. If relative, its path is relative to the directory containing the kanata configuration file. This must be the first item following `defzippy`. | `$deadline-millis` | Number of milliseconds. After the first press while zippy is enabled, if no chord activates within this configured time, zippy is temporarily disabled. | `$idle-time-millis` | Number of milliseconds. After typing ends and this configured number of milliseconds elapses, zippy will be re-enabled from being temporarily disabled. | `$smart-space-cfg` | Determines the smart space behaviour. The options are `none`, `add-space-only`, and `full`. With `none`, outputs are typed as-is. With `add-space-only`, spaces are automatically added after outputs which end with neither a space or a backspace ⌫. With `full`, the `add-space-only` behaviour applies alongside additional behaviour: typing punctuation (default characters: `, . ;`) after a zippy activation will delete a prior automatically-added space. | `$punc` | A character defined in `output-character-mappings` or a known key name, which shall be considered as punctuation. The `smart-space-punctuation` configuration will overwrite the default punctuation list considered by smart-space; if you want to include the default characters, you must include them in this configuration. | `$character` | A single unicode codepoint for use in the output column of the zippy configuration file. | `$output-mapping` | Key or output chord to tell kanata how to type `$character` when seen in the zippy file output column. Must be a single key or output chord. The output chord may contain `AG-` to tell kanata to press with AltGr and may contain `S-` to tell kanata to press with Shift. The list items `no-erase` and `single-output` are also usable in this position. | `no-erase` | Accepts a single key or output chord as a parameter. The zippy system will not backspace this character in case of auto-erasure by a superset chord or followup chord. Use for dead keys or compose keys. | `single-output` | Accepts one or more keys or output chords as a parameter. The zippy system send only one backspace in case of auto-erasure by a superset chord or followup chord. Use for a dead key or compose key sequence with one output symbol. |=== Regarding output mappings, you can configure the output of the special-lisp-syntax characters `+) ( "+` via these lines: [source] ---- ")" $right-paren-output "(" $left-paren-output r#"""# $double-quote-output ---- As an example, for the US layout these should be the correct lines: [source] ---- ")" S-0 "(" S-9 r#"""# S-' ---- .Configuration syntax within the zippy configuration file [source] ---- // This is a comment. // inputs ↹ outputs $chord1 $follow-chord1.1...1.M $output1 $chord2 $follow-chord2.1...2.M $output2 // ... $chordN $follow-chordN.1...N.M $outputN ---- The format is two columns separated by a single Tab character. The first column is input and the second is output. [cols="1,4"] |=== |`$chord` | A set of characters. You can use space by including it as the first character in the chord; for an example see `Alphabet` in the sample below. With 0 optional follow chords, the corresponding output on the same line (`$output`) will activate when zippy is enabled and all the defined chord keys are pressed simultaneously. The order of presses is not important. | `$follow-chord` | 0 or more chords, used the same way as `$chord`. Having follow chords means the `$output` on the same line will activate upon first activating the earlier chord(s) in the same line, releasing all keys, and pressing the keys in `$follow-chordN.M`. Follow chords are separated from the previous chord by a space. If using a space in the follow chord, use two spaces; for an example see `Washington` in the sample below. | `$output` | The characters to type when the chord and optional follow chord(s) are all pressed by the user. This is separated from the input chord column by a single Tab character. The characters are typed in sequence and must all be singular-name key names as one would configure in `defsrc`. A capitalized single-character key name will be parsed successfully and these will be outputted alongside Shift to output the capitalized key. Additionally, `output-character-mappings` configuration can be used to inform kanata of additional mappings that may use Shift or AltGr. |=== **Examples** .Sample kanata configuration [source] ---- (defzippy zippy.txt on-first-press-chord-deadline 500 idle-reactivate-time 500 smart-space-punctuation (? ! . , ; :) output-character-mappings ( ;; This should work for US international. ! S-1 ? S-/ % S-5 "(" S-9 ")" S-0 : S-; < S-, > S-. r#"""# S-' | S-\ _ S-- ® AG-r ’ (no-erase `) é (single-output ' e) ) ) ---- .Sample zippy file content [source] ---- dy day dy 1 Monday dy 2 Tuesday abc alphabet w a Washington gi git gi f p git fetch -p ---- **Description** Zippychord is yet another chording mechanism in Kanata. The inspiration behind it is primarily the https://github.com/psoukie/zipchord[zipchord project]. The name is similar; it is named "zippy" instead of "zip" because Kanata's implementation is not a port and does not aim for 100% behavioural compatibility. The intended use case is shorthands, or accelerating character output. Within zippychord, inputs are keycode chords or sequences, and the outputs are also purely keycodes. In other words, all other actions are unsupported; e.g. layers, switch, one-shot. Zippychord behaves on outputted keycodes, i.e. the key outputs after kanata has finished processing your inputs, layers, switch logic and other configurations. This is similar to how sequences operate and is unlike chords(v1) and chordsv2. Furthermore, outputs are all eager like `visible-backspaced` on sequences. If a zippychord activation occurs, typed keys are backspaced. To give an example, if one configures zippychord with a line like: [source] ---- gi git ---- then either of the following typing event sequences will erase the input characters and then proceed to type the output "git" like if it was `(macro bspc bspc g i t)`. [source] ---- (press g) (press i) (press i) (press g) ---- Note that there aren't any release events listed. To contrast, the following event sequence would not result in an activation: [source] ---- (press g) (release g) (press i) ---- Zippychord supports fully overlapping chords and sequences. For example, this configuration is allowed: [source] ---- gi git␣ gi s git␣status gi c git checkout␣ gi c b git checkout -b␣ gi c a git commit --amend␣ gi c n git commit --amend --no-edit gi c a m git commit --amend -m 'FIX_THIS_COMMIT_MESSAGE' ---- When you begin with the `(g i)` chord, you can follow up with various character sequences to output different git commands. This use case is quite similar to git aliases. One advantage of zippychord is that it eagerly shows you the true underlying command as you type. kanata-1.9.0/docs/design.md000064400000000000000000000020141046102023000136120ustar 00000000000000# Design doc ## Obligatory diagram ## main - read args - read config - start event loops ## event loop - read key events - send events to processing loop on channel ## processing loop - check for events on mpsc - if event: send event to layout - tick() the keyberon layout, send any events needed - if no event: sleep for 1ms - separate monotonic time checks, because can't rely on sleep to be fine-grained or accurate - send `ServerMessage`s to the TCP server ## TCP server - listen for `ClientMessage`s and act on them - recv `ServerMessage`s from processing loop and forward to all connected clients ## layout - uses keyberon - indices of `kanata_keyberon::layout::Event::{Press, Release}(x,y)`: x = 0 or 1 (0 is for physical key presses, 1 is for fake keys) y = OS code of key used as an index ## OS-specific code Most of the OS specific code is in `oskbd/` and `keys/`. There's a bit of it in `kanata/` since the event loops to receive OS events are different. kanata-1.9.0/docs/fancy_symbols.md000064400000000000000000000057641046102023000152300ustar 00000000000000### Supported key symbols |Symbol(s)[^1] |Key `name` | |--------- |-------- | |‹x x› | Left/Right modifiers (e.g., ‹⎈ LCtrl) | |⇧ | Shift | |⎈ ⌃ | Control | |⌘ ◆ ❖ | Windows/Command | |⎇ ⌥ | Alt | |⇪ | capslock | |⎋ |`escape` | |⭾ ↹ |`tab` | |␠ ␣ | `spc` spacebar | |␈ ⌫ |`bspc` backspace (delete backward) | |␡ ⌦ |`del` delete forward | |⏎ ↩ ⌤ ␤ |`ret` return or enter | |︔ ⸴ .⁄ |semicolon `;` / comma `,` / period `.` / slash `/` | |⧵ \ | backslash `\` | |﹨ < |`non_us_backslash` | |【 「 〔 ⎡ |`open_bracket` | |】 」 〕 ⎣ |`close_bracket` | |ˋ ˜ |`grave_accent_and_tilde` | |‐ ₌ |`hyphen` `equal_sign` | |▲ ▼ ◀ ▶ |`up`/`down`/`left`/`right` (arrows) | |⇞ ⇟ |`pgup`/`pgdn` (page up, page down) | |⎀ |`insert` | |⇤ ⤒ ↖ |`home` | |⇥ ⤓ ↘ |`end` | |⇭ |`numlock` | |🔢₁ 🔢₂ 🔢₃ 🔢₄ 🔢₅ |`keypad_` `1`–`5` | |🔢₆ 🔢₇ 🔢₈ 🔢₉ 🔢₀ |`keypad_` `6`–`0` | |🔢₋ 🔢₌ 🔢₊ |`keypad_` `hyphen`/`equal_sign`/`plus` | |🔢⁄ 🔢.🔢∗ |`keypad_` `slash`/`period`/`asterisk` | |◀◀ ▶⏸ ▶▶ |`vk_consumer_` `previous`/`play`/`next` | |🔊 🔈+ or ➕₊⊕ |`volume_up` | |🔉 🔈− or ➖₋⊖ |`volume_down` | |🔇 🔈⓪ or ⓿ ₀ |`mute` | |🔆 🔅 |`vk_consumer_brightness_` `up`/`down` | |⌨💡+ or ➕₊⊕ |`vk_consumer_illumination_up` | |⌨💡− or ➖₋⊖ |`vk_consumer_illumination_down` | |🎛 |`vk_dashboard` | |▤ ☰ 𝌆 |`application` | |🖰1 🖰2 ... 🖰5 |`button` `1`–`5` | |‹🖰 🖰› |`button` `1` `2` | [^1]: space-separated list of keys; `or` means only last symbol in a pair changes kanata-1.9.0/docs/interception.md000064400000000000000000000017231046102023000150520ustar 00000000000000# Windows Interception driver implementation notes - Interception handle is `!Send` and `!Sync` - means a single thread should own both input and output - `KbdOut` will need to send keyboard output events to that thread as opposed to Linux using `uinput` and the original Windows code using `SendInput` which are independent of the input devices. - Maybe save channel in kanata struct as part of new kanata - Interception can filter for only keyboard events - should use this filter feature; don't want to intercept mouse - Need to save previous device for sending to, in case wait/receive (with timeout) don't return anything so that sending stuff can be sent to some device. - Input `ScanCode` maps to the keyberon `KeyCode`; they both use the USB standard codes. - For ease of integration will probably need to unfortunately convert it to an `OsCode` even though the processing loop will soon after just convert it back to `KeyCode`. Oh well. kanata-1.9.0/docs/kanata-basic-diagram.svg000064400000000000000000000545511046102023000164750ustar 00000000000000
OS InputEvent
OS InputEvent
Kanata KeyEvent
Kanata KeyEvent
event loop
event loop

OsCode used as key matrix
index to update state machine
OsCode used as key matrix...
press/release OsCode
press/release OsCode
ServerMessage
(e.g. LayerChange)
ServerMessage...
processing loop
processing loop

advance state machine and
read active keyberon KeyCodes
advance state machine and...

keyberon layout
state machine
keyberon layout...
OS mechanism
OS mechanism
OS mechanism
OS mechanism
ServerMessage
(e.g. LayerChange)
ServerMessage...
handle ClientMessage
(e.g. change layer)
handle ClientMessage...
TCP server
TCP server
ClientMessage
(e.g. ChangeLayer)
ClientMessage...
TCP connection
TCP connection
Text is not SVG - cannot display
kanata-1.9.0/docs/kmonad_comparison.md000064400000000000000000000027531046102023000160560ustar 00000000000000# Comparison with kmonad The kmonad project is the closest alternative for this project. ## Benefits of kmonad over kanata - ~MacOS support~ (this is implemented now) - Different features ## Why I built and use kanata - [Double-tapping a tap-hold key](https://github.com/kmonad/kmonad/issues/163) did not behave [how I want it to](https://docs.qmk.fm/#/tap_hold?id=tapping-force-hold) - Some key sequences with tap-hold keys [didn't behave how I want](https://github.com/kmonad/kmonad/issues/466): - `(press lsft) (press a) (release lsft) (release a)` (a is a tap-hold key) - The above outputs `a` in kmonad, but I want it to output `A` - kmonad was missing [mouse buttons](https://github.com/kmonad/kmonad/issues/150) The issues listed are all fixable in kmonad and I hope they are one day! For me though, I didn't and still don't know Haskell well enough to contribute to kmonad. That's why I instead built kanata based off of the excellent work that had already gone into the [keyberon](https://github.com/TeXitoi/keyberon), [ktrl](https://github.com/ItayGarin/ktrl), and [kbremap](https://github.com/timokroeger/kbremap) projects. If you want to see the features that kanata offers, the [configuration guide](./config.adoc) is a good starting point. I dogfood kanata myself and it works great for my use cases. Though kanata is a younger project than kmonad, it now has more features. If you give kanata a try, feel free to ask for help in an issue or discussion, or let me know how it went 🙂. kanata-1.9.0/docs/locales.adoc000064400000000000000000000214561046102023000143040ustar 00000000000000//// Commented out since it doesn't seem to add anything for now, but maybe in the future :sectlinks: :sectanchors: //// ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: :important-caption: :heavy_exclamation_mark: :caution-caption: :fire: :warning-caption: :warning: endif::[] = Keyboard locales //// Commented out since doc is short enough without a ToC for the time being. :toc: :toc-title: pass:[TABLE OF CONTENTS] :toclevels: 3 //// == ISO 100% Keyboard (event.code) NOTE: Tested on Linux only [%collapsible] ==== ---- (defsrc Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 PrintScreen ScrollLock Pause Backquote Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal Backspace Insert Home PageUp NumLock NumpadDivide NumpadMultiply NumpadSubtract Tab KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight Enter Delete End PageDown Numpad7 Numpad8 Numpad9 NumpadAdd CapsLock KeyA KeyS KeyD KeyF KeyG KeyH KeyJ KeyK KeyL Semicolon Quote Backslash Numpad4 Numpad5 Numpad6 ShiftLeft IntlBackslash KeyZ KeyX KeyC KeyV KeyB KeyN KeyM Comma Period Slash ShiftRight ArrowUp Numpad3 Numpad2 Numpad1 NumpadEnter ControlLeft MetaLeft AltLeft Space AltRight MetaRight ContextMenu ControlRight ArrowLeft ArrowDown ArrowRight Numpad0 NumpadDecimal ) ---- ==== == ISO German QWERTZ (Windows, non-interception)[[german]] === Using `deflocalkeys-win`:[[german-defwin]] [%collapsible] ==== ---- (defcustomkeys ü 186 + 187 # 191 ö 192 ß 219 ^ 220 ´ 221 ä 222 < 226 ) (defsrc ^ 1 2 3 4 5 6 7 8 9 0 ß ´ bspc tab q w e r t z u i o p ü + caps a s d f g h j k l ö ä # ret lsft < y x c v b n m , . - rsft lctl lmet lalt spc ralt rmet rctl ) ---- ==== === Without using `deflocalkeys`:[[german-nodeflocalkeys]] [%collapsible] ==== ---- (defsrc \ 1 2 3 4 5 6 7 8 9 0 [ ] bspc tab q w e r t z u i o p ; = caps a s d f g h j k l grv ' / ret lsft 102d y x c v b n m , . - rsft lctl lmet lalt spc ralt rmet rctl ) ---- ==== === Example aliases[[german-aliases]] [%collapsible] ==== ---- (defalias ;; shifted german keys ! S-1 ˝ S-2 ;; unicode 02DD ˝ look-a-like is used because @" is no valid alias, to be displayed correctly ;; in console requires a font that can - e.g. cascadia § S-3 $ S-4 % S-5 & S-6 / S-7 ﴾ S-8 ;; unicode FD3E ﴾ look-a-like is used because @( is no valid alias, to be displayed correctly... ﴿ S-9 ;; unicode FD3F ﴿ look-a-like is used because @) is no valid alias, to be displayed correctly ... = S-0 ? S-ß * S-+ ' S-# ; S-, : S-. _ S-- > S-< < < ;; not really needed but having @< and @> looks consistent ;; change dead keys in normal keys ´ (macro ´ spc ) ;; ´ ` (macro S-´ spc ) ;; ` ^ (macro ^ spc ) ;; ^ = \ - shifting @^ will produce an incorrect space now ° S-^ ;; AltGr german keys ~ A-C-+ \ A-C-ß ẞ A-C-S-ß | A-C-< } A-C-0 { A-C-7 ] A-C-9 [ A-C-8 € A-C-e @ A-C-q ² A-C-2 ³ A-C-3 µ A-C-m ) ---- ==== == ISO German QWERTZ (MacOS)[[german]] === Using `deflocalkeys-macos`:[[german-defmac]] [%collapsible] ==== ---- (deflocalkeys-macos ß 12 ´ 13 z 21 ü 26 + 27 ö 39 ä 40 < 41 # 43 y 44 - 53 ^ 86 ) (defsrc ⎋ f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 ^ 1 2 3 4 5 6 7 8 9 0 ß ´ ⌫ ↹ q w e r t z u i o p ü + ⇪ a s d f g h j k l ö ä # ↩ ‹⇧ < y x c v b n m , . - ▲ ⇧› fn ‹⌃ ‹⌥ ‹⌘ ␣ ⌘› ⌥› ◀ ▼ ▶ ) ---- ==== == ISO French Azerty (MacOS)[[french]] === Using `deflocalkeys-macos`:[[french-defmac]] [%collapsible] ==== ---- (deflocalkeys-macos @ 50 par 12 ;; Close parentheses - 13 ^ 73 $ 164 ù 85 ` 192 < 41 / 191 = 53 a 16 q 30 z 17 w 44 m 39 ) (defsrc ⎋ f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 @ 1 2 3 4 5 6 7 8 9 0 par - ⌫ ↹ a z e r t y u i o p ^ $ ⇪ q s d f g h j k l m ù ` ↩ ‹⇧ < w x c v b n , . / = ▲ ⇧› fn ‹⌃ ‹⌥ ‹⌘ ␣ ⌘› ⌥› ◀ ▼ ▶ ) ---- ==== == ISO French AZERTY (Windows, non-interception)[[french]] NOTE: This is for the https://kbdlayout.info/kbdfr?arrangement=ISO105[French AZERTY layout] (ISO105 arrangement). Tested on Windows only. [%collapsible] ==== ---- (deflocalkeys-win k252 223 ;; ref to the key [!] (VK_OEM_8) ) (defsrc ;; french ' 1 2 3 4 5 6 7 8 9 0 [ eql bspc tab a z e r t y u i o p ] ; caps q s d f g h j k l m ` bksl ret lsft nubs w x c v b n comm . / k252 rsft lctl lmet lalt spc ralt rctl ) ---- ==== == ISO Turkish QWERTY (Linux)[[turkish]] NOTE: This is for the https://kbdlayout.info/kbdtuq?arrangement=ISO105[Turkish QWERTY layout] (ISO105 arrangement). Tested on Linux only. [%collapsible] ==== ---- (deflocalkeys-linux * 12 - 13 ı 23 ğ 26 ü 27 ş 39 İ 40 , 43 < 86 ö 51 ç 52 . 53 ) (defsrc ;; turkish-iso105 grv 1 2 3 4 5 6 7 8 9 0 * - bspc tab q w e r t y u ı o p ğ ü caps a s d f g h j k l ş İ , ret lsft < z x c v b n m ö ç . rsft lctl lmet lalt spc ralt rmet rctl ) ;; We use İ instead of i because kanata doesn't allow using i in deflocalkeys, as it is a default key name. ---- ==== == ABNT2 Brazillian Portuguese QWERTY (Linux)[[portuguese]] NOTE: This is for the https://kbdlayout.info/kbdbr[ABNT2 QWERTY layout]. Tested on Linux only. [%collapsible] ==== ---- (deflocalkeys-linux ´ 26 [ 27 ç 39 ~ 40 ' 41 ] 43 ; 53 \ 86 / 89 ) (defsrc ;; brazillian-abnt2 esc f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 ' 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p ´ [ ret caps a s d f g h j k l ç ~ ] lsft \ z x c v b n m , . ; rsft lctl lmet lalt spc ralt / ) ---- ==== == ISO Swedish QWERTY (Linux)[[swedish]] [%collapsible] ==== ---- ;; Swedish ISO105 (deflocalkeys-linux § 41 + 12 ´ 13 ;; Acute accent. Opposite to the grave accent (grv). å 26 ¨ 27 ö 39 ä 40 ' 43 < 86 , 51 . 52 - 53 ) (defsrc ;; Swedish ISO105 § 1 2 3 4 5 6 7 8 9 0 + ´ bspc tab q w e r t y u i o p å ¨ caps a s d f g h j k l ö ä ' ret lsft < z x c v b n m , . - rsft lctl lmet lalt spc ralt rmet menu rctl ) ;; Empty layer that matches the Swedish layout (deflayer default _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) ---- ==== == Swedish QWERTY Localkeys (Windows)[[swedish]] [%collapsible] ==== ---- (deflocalkeys-win § 220 + 187 ´ 219 å 221 ¨ 186 ö 192 ä 222 ' 191 < 226 , 188 . 190 - 189 ) ---- ==== kanata-1.9.0/docs/platform-known-issues.adoc000064400000000000000000000076661046102023000171600ustar 00000000000000= Hardware known issues At the electric circuit layer of many keyboards, cost-saving measures can lead to key presses not registering when pressing multiple keys simultaneously. Usually this happens with at least 3 key presses. Kanata cannot fix this issue. You can work around it by avoiding the problem key combination, or using a different keyboard. = Platform-dependent known issues == Preface This document contains a list of known issues which are unique to a given platform. The platform supported by the core maintainer (jtroo) are Windows 11 and Linux. Windows 10 is expected to work fine, but as Windows 10 end-of-support is approaching in 2025, jtroo no longer has any devices with it installed. On Windows, there are two backing mechanisms that can be used for keyboard input/output and they have different issues. These will be differentiated by the words "LLHOOK" and "Interception", which map to the binaries `kanata.exe` and `kanata_wintercept.exe` respectively. == Windows 11 LLHOOK * Mouse inputs cannot be used for processing or remapping ** https://github.com/jtroo/kanata/issues/108 ** https://github.com/jtroo/kanata/issues/170 * Some input key combinations (e.g. Win+L) cannot be intercepted before running their default action ** https://github.com/jtroo/kanata/issues/192 ** https://github.com/jtroo/kanata/discussions/428 * OS-level key remapping behaves differently vs. Linux or Interception ** Does not affect winiov2 variant ** https://github.com/jtroo/kanata/issues/152 * Certain applications that also use the LLHOOK mechanism may not behave correctly ** https://github.com/jtroo/kanata/issues/55 ** https://github.com/jtroo/kanata/issues/250 ** https://github.com/jtroo/kanata/issues/430 ** https://github.com/espanso/espanso/issues/1488 * AltGr / ralt / Right Alt can misbehave ** https://github.com/jtroo/kanata/blob/main/docs/config.adoc#windows-only-windows-altgr * NumLock state can mess with arrow keys in unexpected ways ** Does not affect winiov2 variant ** https://github.com/jtroo/kanata/issues/78 ** https://github.com/jtroo/kanata/issues/667 ** Workaround: use the correct https://github.com/jtroo/kanata/discussions/354[numlock state] * Without `process-unmapped-keys yes`, using arrow keys without also having the shift keys in `defsrc` will break shift highlighting ** Does not affect winiov2 variant ** https://github.com/jtroo/kanata/issues/858 ** Workaround: add shift keys to `defsrc` or use `process-unmapped-keys yes` in `defcfg` == Windows 11 Interception * Sleeping your system or unplugging/replugging devices enough times causes inputs to stop working ** https://github.com/oblitum/Interception/issues/25 * Some less-frequently used keys are not supported or handled correctly ** https://github.com/jtroo/kanata/issues/127 ** https://github.com/jtroo/kanata/issues/164 ** https://github.com/jtroo/kanata/issues/425 ** https://github.com/jtroo/kanata/issues/532 == Linux * Key repeats can occur when they normally wouldn't in some cases ** https://github.com/jtroo/kanata/discussions/422 ** https://github.com/jtroo/kanata/issues/450 ** https://github.com/jtroo/kanata/issues/1441 * Unicode support has limitations, using xkb is a more consistent solution ** https://github.com/jtroo/kanata/discussions/703 * Key actions can behave incorrectly due to the rapidity of key events ** https://github.com/jtroo/kanata/discussions/733 ** https://github.com/jtroo/kanata/issues/740 ** adjusting https://github.com/jtroo/kanata/blob/main/docs/config.adoc#rapid-event-delay[rapid-event-delay] can potentially be a workaround * Macro keys on certain gaming keyboards might stop being processed ** Context: search for `POTENTIAL PROBLEM - G-keys` in link:../src/kanata/mod.rs[the code]. ** Workaround: leave `process-unmapped-keys` disabled and explicitly map keys in `defsrc` instead == MacOS * Only left, right, and middle mouse buttons are implemented for clicking * Mouse input processing is not implemented, e.g. putting `mlft` into `defsrc` does nothing kanata-1.9.0/docs/release-template.md000064400000000000000000000177221046102023000156060ustar 00000000000000## Configuration guide Link to the appropriate configuration guide version: [guide link TODO: FIX LINK](https://github.com/jtroo/kanata/blob/FIXME/docs/config.adoc). ## Changelog (since )
Change log * TODO: fill this out
## Sample configuration file The attached `kanata.kbd` file is tested to work with the current version. The one in the `main` branch of the repository may have extra features that are not supported in this release. ## Windows
Instructions **NOTE:** All Windows binaries are compiled for x86-64 architectures only. Download `kanata.exe`. Optionally, download `kanata.kbd`. With the two files in the same directory, you can double-click the `exe` to start kanata. Kanata does not start a background process, so the window needs to stay open after startup. See [this discussion](https://github.com/jtroo/kanata/discussions/193) for tips to run kanata in the background. You need to run `kanata.exe` via `cmd` or `powershell` to use a different configuration file: `kanata.exe --cfg ` --- **NOTE:** The `kanata_winIOv2.exe` variant contains an experimental breaking change that fixes [an issue](https://github.com/jtroo/kanata/issues/152) where the Windows LLHOOK+SendInput version of kanata does not handle `defsrc` consistently compared to other versions and other operating systems. This variant will be of interest to you for any of the following reasons: - you are a new user - you are a cross-platform user - you use multiple language layouts within Windows and want kanata to handle the key positions consistently This variant contains the same output change as in the `scancode` variant below, and also changes the input to also operate on scancodes. --- **NOTE:** The `kanata_legacy_output.exe` variant has the same input `defsrc` handling as the standard `kanata.exe` file. It uses the same output mechanism as the standard `kanata.exe` variant in version 1.6.1 and earlier. In other words the formerly `experimental_scancode` variant is now the default binary. The non-legacy variants contain changes for [an issue](https://github.com/jtroo/kanata/issues/567); the fix is omitted from this legacy variant. The legacy variant is included in case issues are found with the new output mechanism. ---
## Linux
Instructions **NOTE:** All Linux binaries are compiled for x86 architectures only. Download `kanata`. Run it in a terminal and point it to a valid configuration file. Kanata does not start a background process, so the window needs to stay open after startup. See [this discussion](https://github.com/jtroo/kanata/discussions/130) for how to set up kanata with systemd. ``` chmod +x kanata # may be downloaded without executable permissions sudo ./kanata --cfg ` ``` To avoid requiring `sudo`, [follow the instructions here](https://github.com/jtroo/kanata/wiki/Avoid-using-sudo-on-Linux).
## macOS
Instructions **WARNING**: feature support on macOS [is limited](https://github.com/jtroo/kanata/blob/main/docs/platform-known-issues.adoc#macos). ### Instructions for macOS 11 and newer Please read through this issue comment: https://github.com/jtroo/kanata/issues/1264#issuecomment-2763085239 Also have a read through this discussion: https://github.com/jtroo/kanata/discussions/1537 ### Old instructions for macOS 11 and newer
Click to expand First install Karabiner driver for macOS 11 and newer: - Install the [V5 Karabiner VirtualHiDDevice Driver](https://github.com/pqrs-org/Karabiner-DriverKit-VirtualHIDDevice/blob/main/dist/Karabiner-DriverKit-VirtualHIDDevice-5.0.0.pkg). To activate it: ``` sudo /Applications/.Karabiner-VirtualHIDDevice-Manager.app/Contents/MacOS/Karabiner-VirtualHIDDevice-Manager activate ``` Then you need to run the daemon. You should run this in the background somehow or leave the terminal window where you run this command open. ``` sudo '/Library/Application Support/org.pqrs/Karabiner-DriverKit-VirtualHIDDevice/Applications/Karabiner-VirtualHIDDevice-Daemon.app/Contents/MacOS/Karabiner-VirtualHIDDevice-Daemon' ```
### Install Karabiner driver for macOS 10 and older: - Install the [Karabiner kernel extension](https://github.com/pqrs-org/Karabiner-VirtualHIDDevice). ### After installing the appropriate driver for your OS (both macOS <=10 and >=11) Download a `kanata_macos` variant. Run it in a terminal and point it to a valid configuration file. Kanata does not start a background process, so the window needs to stay open after startup. Example ``` chmod +x kanata_macos_arm64 # may be downloaded without executable permissions sudo ./kanata_macos_arm64 --cfg ` ``` ### Add permissions If Kanata is not behaving correctly, you may need to add permissions. Please see this issue: [link to macOS permissions issue](https://github.com/jtroo/kanata/issues/1211).
## cmd\_allowed variants
Explanation The binaries with the name `cmd_allowed` are conditionally compiled with the `cmd` action enabled. Using the regular binaries, there is no way to get the `cmd` action to work. This action is restricted behind conditional compilation because I consider the action to be a security risk that should be explicitly opted into and completely forbidden by default.
## wintercept variants
Explanation and instructions ### Warning: known issue This issue in the Interception driver exists: https://github.com/oblitum/Interception/issues/25. This will affect you if you put your PC to sleep instead of shutting it down, or if you frequently plug/unplug USB devices. ### Description These variants use the [Interception driver](https://github.com/oblitum/Interception) instead of Windows hooks. You will need to install the driver using the release or from the [copy in this repo](https://github.com/jtroo/kanata/tree/main/assets). The benefit of using this driver is that it is a lower-level mechanism than Windows hooks. This means `kanata` will work in more applications. ### Steps to install the driver - extract the `.zip` - run a shell with administrator privilege - run the script `"command line installer/install-interception.exe"` - reboot ### Additional installation steps The above steps are those recommended by the interception driver author. However, I have found that those steps work inconsistently and sometimes the dll stops being able to be loaded. I think it has something to do with being installed in the privileged location of `system32\drivers`. To help with the dll issue, you can copy the following file in the zip archive to the directory that kanata starts from: `Interception\library\x64\interception.dll`. E.g. if you start kanata from your `Documents` folder, put the file there: ``` C:\Users\my_user\Documents\ kanata_wintercept.exe kanata.kbd interception.dll ```
## kanata\_passthru.dll
Explanation and instructions The Windows `kanata_passthru.dll` file allows using Kanata as a library within AutoHotkey to avoid conflicts between keyboard hooks installed by both. You can channel keyboard input events received by AutoHotkey into Kanata's keyboard engine and get the transformed keyboard output events (per your Kanata config) that AutoHotkey can then send to the OS. To make use of this, download `kanata_passthru.dll`, then the [simulated_passthru_ahk](https://github.com/jtroo/kanata/blob/main/docs/simulated_passthru_ahk) folder with a brief example, place the dll there, open `kanata_passthru.ahk` to read what the example does and then double-click to launch it.
## sha256 checksums
Sums ``` TODO: fill this out ```
kanata-1.9.0/docs/sequence-adding-chords-ideas.md000064400000000000000000000126461046102023000177540ustar 00000000000000# Sequence improvement: sequence chords ## Preface This document is a record of designing/braindumping for the improvement to the sequences feature to add chord support. It is left in an informal and disorganized state — as opposed to a being presentable design doc — out of laziness. Apologies ahead of time if you read this and it's hard to follow, feel free to contribute a PR to create a new and more polished doc. ## Motivation The desire is to be able to add output chords to a sequence. The effect of this is that: `(S-(a b))` can be differentiated from `(S-a b)`. Today, chords are not supported at all. The two sequences above could be written as `(lsft a b)`; however, the code today has no way to decide the difference between `lsft` being applied to only `a` or to both `a` and `b`. The feature will codenamed "seqchords" for brevity in this document. ## An exploration of an idea: track releases? Today, the sequence `(lsft a b c)` doesn't care when the `lsft`, or even `a` or `b` are released relative to when the subsequent keys are pressed. However, with seqchords, the code could be potentially changed to make sequences release-aware. It seems a little difficult to integrate this into the trie structure used to track sequences though. With an implementation that is release-aware, it seems like the code would need to figure out how to conditionally add release events to the trie, depending if the seq was `(lsft a)` or `(S-a)` For now, I think a different approach would be better. ## A different idea: modify presses held with mod keys. The current sequence type is `Vec` since keys don't fit into `u8`. However, there are fewer than 1024 (2^10) keys total. That means there are 6 bits to play with. 6 bits are enough for the types of modifiers (of which there are 4), but differentiating both sides (increases to 8). Perhaps one only cares to use both left and right shifts though, and maybe both left and right alts. One could also use a `u32` instead, but that seems unnecessary for now. I see no backwards compatibility issues if one desired that change in the future. With this in mind, while modifiers are held, set the upper unused bits of the stored `u16` values. ### Backwards compatibility? This does mean that `(lsft a b)` behaves differently with vs. without seqchords. Unless maybe the code automatically generates the various permutations of this type of sequence, but that seems complicated. Or maybe have a `u16` with a special bit pattern that could be used to differentiate between `(S-(a b))` and `(lsft a b)`. For now, let's say that the bit pattern is `0xFFFF`. If a modifier is pressed and the sequence `[..., , 0xFFFF]` exists in the trie: continue processing the sequence in mod-aware mode. OR for simplicity, just say "screw backwards compatibility" and force users to be clear about what they mean and define the extra permutations, if they want them. I prefer this. ### Data format examples Let's begin the description of the new data format. Since shifted keys seem like they will be the main use case for seqchords, only that will be described in this document for now. Here are the numerical key values relevant to the examples. - `a: 0x001E` - `b: 0x0030`. - `lsft: 0x002A`. This differs by OS, but that's not important. The transformation of `(lsft a b)` to a sequence in the trie today looks like: - `[0x002A, 0x001E, 0x0030]` This will remain unchanged with seqchords. Let's say that chorded keys using `lsft` will have the otherwise-unused MSB (bit 15) set. The transformation of some sequences using chords will be: 1. `(S-(a b)) => [0x802A, 0x801E, 0x8030]` 2. ` (S-a b) => [0x802A, 0x801E, 0x0030]` 3. `(S-a S-b) => [0x802A, 0x801E, 0x802A, 0x8030]` Notably, `lsft` is modifying its own upper bits. This should simplify the implementation logic so that the code does not need to add a special-case check that the newly-pressed key is itself a modifier. One may need to define different sequences if one wishes to use both left and right shifts to be able to trigger these shifted sequences. The syntax does not exist today, but maybe `(S-(a b))` and `(RS-(a b))` as an example for left and right shifts. The reason different sequences would be required is because the sequence->trie check operates on the integers that correspond to the keycodes. Consideration: maybe there could be transformations for the right modifier keys to ensure they get translated to the left modifier keys. This seems like it could be a sensible default to start with. If a change is desired in the future to **not** do this transformation, it doesn't seem too difficult to add a configuration item to do so. For now that will be left out, deferring to the YAGNI principle. ### Backwards compatibility revisited Thinking back on the topic of backwards compatibility, I'm scrapping that idea of special bit patterns. I thought of a probably-better way: backtracking with modifier cancellation. By default when seqchords gets added, the modified bit patterns will be used to check in the trie for valid sequences. However, with a `defcfg` item `sequence-backtrack-modcancel` — which should be `yes` by default for back-compat reasons — if the code encounters an invalid sequence with the modded bit pattern, it will try again with the unmodded bit pattern, and only if that does not match will sequence-mode end with an invalid termination. This backtracking can be turned off if desired, e.g. if it behaves badly in some future seqchords use cases. kanata-1.9.0/docs/setup-linux.md000064400000000000000000000071111046102023000146410ustar 00000000000000# Instructions In Linux, kanata needs to be able to access the input and uinput subsystem to inject events. To do this, your user needs to have permissions. Follow the steps in this page to obtain user permissions. ### 1. If the uinput group does not exist, create a new group ```bash sudo groupadd uinput ``` ### 2. Add your user to the input and the uinput group ```bash sudo usermod -aG input $USER sudo usermod -aG uinput $USER ``` Make sure that it's effective by running `groups`. You might have to logout and login. ### 3. Make sure the uinput device file has the right permissions. #### Create a new file: `/etc/udev/rules.d/99-input.rules` #### Insert the following in the code ```bash KERNEL=="uinput", MODE="0660", GROUP="uinput", OPTIONS+="static_node=uinput" ``` #### Machine reboot or run this to reload ```bash sudo udevadm control --reload-rules && sudo udevadm trigger ``` #### Verify settings by following command: ```bash ls -l /dev/uinput ``` #### Output: ```bash crw-rw---- 1 root date uinput /dev/uinput ``` ### 4. Make sure the uinput drivers are loaded You may need to run this command whenever you start kanata for the first time: ``` sudo modprobe uinput ``` ### 5a. To create and enable a systemd daemon service Run this command first: ```bash mkdir -p ~/.config/systemd/user ``` Then add this to: `~/.config/systemd/user/kanata.service`: ```bash [Unit] Description=Kanata keyboard remapper Documentation=https://github.com/jtroo/kanata [Service] Environment=PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/bin # Uncomment the 4 lines beneath this to increase process priority # of Kanata in case you encounter lagginess when resource constrained. # WARNING: doing so will require the service to run as an elevated user such as root. # Implementing least privilege access is an exercise left to the reader. # # CPUSchedulingPolicy=rr # CPUSchedulingPriority=99 # IOSchedulingClass=realtime # Nice=-20 Type=simple ExecStart=/usr/bin/sh -c 'exec $$(which kanata) --cfg $${HOME}/.config/kanata/config.kbd' Restart=no [Install] WantedBy=default.target ``` Make sure to update the executable location for sh in the snippet above. This would be the line starting with `ExecStart=/usr/bin/sh -c`. You can check the executable path with: ```bash which sh ``` Also, verify if the path to kanata is included in the line `Environment=PATH=[...]`. For example, if executing `which kanata` returns `/home/[user]/.cargo/bin/kanata`, the `PATH` line should be appended with `/home/[user]/.cargo/bin` or `:%h/.cargo/bin`. `%h` is one of the specifiers allowed in systemd, more can be found in https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#Specifiers Then run: ```bash systemctl --user daemon-reload systemctl --user enable kanata.service systemctl --user start kanata.service systemctl --user status kanata.service # check whether the service is running ``` ### 5b. To create and enable an OpenRC daemon service Edit new file `/etc/init.d/kanata` as root, replacing \ as appropriate: ```bash #!/sbin/openrc-run command="/home//.cargo/bin/kanata" #command_args="--config=/home//.config/kanata/kanata.kbd" command_background=true pidfile="/run/${RC_SVCNAME}.pid" command_user="" ``` Then run: ``` sudo chmod +x /etc/init.d/kanata # script must be executable sudo rc-service kanata start rc-status # check that kanata isn't listed as [ crashed ] sudo rc-update add kanata default # start the service automatically at boot ``` # Credits The original text was taken and adapted from: https://github.com/kmonad/kmonad/blob/master/doc/faq.md#linux kanata-1.9.0/docs/simulated_output/sim.kbd000064400000000000000000000033441046102023000167070ustar 00000000000000(defcfg process-unmapped-keys yes ;;|no| enable processing of keys that are not in defsrc, useful if mapping a few keys in defsrc instead of most of the keys on your keyboard. Without this, the tap-hold-release and tap-hold-press actions will not activate for keys that are not in defsrc. Disabled because some keys may not work correctly if they are intercepted. E.g. rctl/altgr on Windows; see the windows-altgr configuration item above for context. log-layer-changes yes ;;|no| overhead ) (defvar ;; declare commonly-used values. prefix with $ to call them. They are refered with `$` tap-repress-timeout 1000 ;;|500| hold-timeout 1500 ;;|500| 🕐↕ $tap-repress-timeout 🕐🠿 $hold-timeout ) (defalias ;; home row mods ↕tap 🠿hold ;; pinky ring middle index | index middle ring pinky ;; timeout ↕tap 🠿hold¦↕tap 🠿hold action ⌂‹◆ (tap-hold-release $🕐↕ $🕐🠿 a ‹◆) ;; ⌂‹⎇ (tap-hold-release $🕐↕ $🕐🠿 s ‹⎇) ;; ⌂‹⎈ (tap-hold-release $🕐↕ $🕐🠿 d ‹⎈) ;; ⌂‹⇧ (tap-hold-release $🕐↕ $🕐🠿 f ‹⇧) ;; ⌂⇧› (tap-hold-release $🕐↕ $🕐🠿 j ⇧›) ;; same actions for the right side ⌂⎈› (tap-hold-release $🕐↕ $🕐🠿 k ⎈›) ;; ⌂⎇› (tap-hold-release $🕐↕ $🕐🠿 l ⎇›) ;; ⌂◆› (tap-hold-release $🕐↕ $🕐🠿 ; ◆›) ;; ) (defsrc ` 1 2 a s d f j k l ;) (deflayer ⌂ ;; modtap layer for home row mods and 1 printing a 🤲🏿 char (will appear as 🤲 until kanata's unicode feature is extended) ‗ 🔣🤲🏿 ‗ @⌂‹◆ @⌂‹⎇ @⌂‹⎈ @⌂‹⇧ @⌂⇧› @⌂⎈› @⌂⎇› @⌂◆›) kanata-1.9.0/docs/simulated_output/sim.txt000064400000000000000000000001451046102023000167620ustar 00000000000000↓j 🕐1600 ↓l 🕐5000 ↓1 🕐50 ↑1 🕐50 ↓1 🕐50 ↑1 🕐50 ↑j 🕐50 ↑l 🕐50 kanata-1.9.0/docs/simulated_output/sim_out.txt000064400000000000000000000034461046102023000176600ustar 00000000000000🕐Δms│ 1500 100 1500 3500 50 50 50 50 50 In───┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── k↑ │ 1 1 J L k↓ │ J L 1 1 k⟳ │ Σin │ ↓J 🕐1600 ↓L 🕐5000 ↓1 🕐50 ↑1 🕐50 ↓1 🕐50 ↑1 🕐50 ↑J 🕐50 ↑L 🕐50 Out──┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── k↑ │ ⇧› ⎇› k↓ │ ⇧› ⎇› 🖰↑ │ 🖰↓ │ 🖰 │ 🔣 │ 🤲 🤲 code│ raw↑│ raw↓│ Σout │ ↓⇧› ↓⎇› 🤲 🤲 ↑⇧› ↑⎇› kanata-1.9.0/docs/simulated_passthru_ahk/[COPY HERE] kanata_passthru.dll _000064400000000000000000000000001046102023000243660ustar 00000000000000kanata-1.9.0/docs/simulated_passthru_ahk/kanata_dll.kbd000064400000000000000000000011631046102023000213420ustar 00000000000000;;Test config for kanata.dll use by AutoHotkey, only maps two keys (f,j) to left/right modtap home row mod Shifts (defcfg process-unmapped-keys yes ;;|no| enable processing of keys that are not in defsrc log-layer-changes no ;;|no| overhead ) (defvar 🕐↕ 1000 ;;|500| tap-repress-timeout 🕐🠿 1500 ;;|500| hold-timeout ) (defalias ;; timeout→ tap hold ¦ tap hold ←action f⌂‹⇧ (tap-hold-release $🕐↕ $🕐🠿 f ‹⇧) j⌂⇧› (tap-hold-release $🕐↕ $🕐🠿 j ⇧›) ) (defsrc f j ) (deflayer ⌂ @f⌂‹⇧ @j⌂⇧›) kanata-1.9.0/docs/simulated_passthru_ahk/kanata_passthru.ahk000064400000000000000000000255421046102023000224520ustar 00000000000000#Requires AutoHotKey 2.1-alpha.4 /* A short example of using Kanata as a library with AutoHotkey, first F8 press will load the library, second F8 press will activate Kanata for a few (ihDuration) seconds with 'f'/'j' turned into home row Shift mods. The more useful script would not use F8, but f/j directly as hotkeys, but this owl hasn't been drawn yet... Both Kanata and this script mainly output to Windows debug log, use github.com/smourier/TraceSpy to view it Dependencies and config: */ libPath := "./" ; kanata_passthru.dll @ this folder kanata_cfg := "./kanata_dll.kbd" ; kanata config @ this file location ihDuration := 10 ; seconds of activity after pressing F8 dbg := 1 ; script's debug level (0 to silence some of its output) dbg_dll := 1 ; kanata's debug level (Err=1 Warn=2 Inf=3 Dbg=4 Trace=5) /* Brief overview of the architecture: Setup: - AHK: configures cbKanataOut callback to Send keys out and shares its address with Kanata - Kanata: exports 4 functions - fnKanata_main: set up paths to config and initialize - fnKanata_in_ev: get input key events - K_output_ev_check: check if output key events exist - fnKanata_reset: reset Kanata's state without exiting ! AHK enables inputhook, so intercepts all keyboard input ← Redirects all intercepted input to Kanata, where we hit 2 limitations ✗ Kanata can't send keys out itself as it'll be intercepted by AHK's inputhook ✗ Kanata's thread that processes input can't call AHK function in the main thread since AHK is single-threaded ✓ Kanata opens an async channel from the input processing thread (with Keyberon state machine) to its main ← sends key out data back to the main thread → our script after sending input keys calls Kanata to read this channel until it's empty, and then Sends these keys out */ get_thread_id() { return DllCall("GetCurrentThreadId", "UInt") } F8::kanata_dll('vk77') kanata_dll(vkC) { ; static K := keyConstant , vk := K._map, sc := K._mapsc ; various key name constants, gets vk code to avoid issues with another layout ; , s := helperString ; K.▼ = vk['▼'] static is_init := false ,lErr:=1, lWarn:=2, lInf:=3, lDbg:=4, lTrace:=5, log_lvl := dbg_dll ; Kanata's ,last↓ := [0,0] ,id_thread := get_thread_id() ,Cvk_d := GetKeyVK(vkC), Csc_d := GetKeySC(vkC), token1 := 1, ih0 := 0 ; decimal value ,C↑ := false, cleanup := false ; track whether the trigger key has been released to not release it twice on kanata cleanup ; set up machinery for AHK and Kanata to communicate ,libNm := "kanata_passthru" ,lib𝑓 := libNm '\' 'lib_kanata_passthru' ; receives AHK's address of AHK's cb KanataOut that accepts simulated output events ,lib𝑓input_ev := libNm '\' 'input_ev_listener' ; receives key input and uses event_loop's input event handler callback (which will in turn communicate via the internal kanata's channels to keyberon state machine etc.) ,lib𝑓output_ev := libNm '\' 'output_ev_check' ; checks if output event is ready (it's sent to our callback if it is) ,lib𝑓reset := libNm '\' 'reset_kanata_state' ; reset kanata's state ,hModule := DllCall("LoadLibrary", "Str",libPath libNm '.dll', "Ptr") ; Avoids the need for DllCall in the loop to load the library ,fnKanata_main := DllCall.Bind(lib𝑓, 'Ptr',unset, 'Str',unset, 'Int',unset) ,fnKanata_in_ev := DllCall.Bind(lib𝑓input_ev, 'Int',unset , 'Int',unset, 'Int',unset) ,K_output_ev_check := DllCall.Bind(lib𝑓output_ev) ,fnKanata_reset := DllCall.Bind(lib𝑓reset, 'Int',unset) static ih := InputHook("T" ihDuration " I1") , 🕐k_pre := A_TickCount , 🕐k_now := A_TickCount hooks := "hooks#: " gethookcount() addr_cbKanataOut := CallbackCreate(cbKanataOut) if not is_init { is_init := true ; setup inputhook callback functions ih.KeyOpt( '{All}','NSI') ; N: Notify. OnKeyDown/OnKeyUp callbacks to be called each time the key is pressed ih.OnKeyDown := cbK↓.Bind(1) ; ih.OnKeyUp := cbK↑.Bind(0) ; OutputDebug('¦' id_thread "¦registered inputhook with VisibleText=" ih.VisibleText " VisibleNonText=" ih.VisibleNonText "`nIlevel=" ih.MinSendLevel ' hooks#: ' gethookcount() ' →kanata addr#' addr_cbKanataOut) fnKanata_main(addr_cbKanataOut,kanata_cfg,log_lvl) ; setup kanata, passign ahk callback to accept out key events return } cbK↓(token, ih,vk,sc) { static _d := 1, isUp := false, dir := (isUp?'↑':'↓') 🕐k_pre := 🕐k_now 🕐k_now := A_TickCount if (dbg>=_d) { dbgtxt := '' vk_hex := Format("vk{:x}",vk) key_name := GetKeyName(Format("vk{:x}",vk)) ; bugs with layouts, not english even if english is active dbgtxt .= "ih" dir (isSet(key_name)?key_name:'') " 🢥🄺: vk=" vk "¦" vk_hex " sc=" sc ' l' A_SendLevel " ¦" id_thread "¦" OutputDebug(dbgtxt) } isH := fnKanata_in_ev(vk,sc,isUp) dbgOut := '' for i in [4,4,4,5,5,5] { ; poll a key out channel@kanata) a few times to see if there are key events sleep(i) isOut := K_output_ev_check(), dbgOut.=isOut if (isOut < 0) { ; get as many keys as are available untill reception errors out break } } ;🔚∎🏁 (dbg<_d+1)?'':(dbgtxt:='🏁ih' dir ' pos isH=' isH ' isOut=' dbgOut ' ' format(" 🕐Δ{:.3f}",A_TickCount - 🕐k_now) ' ' A_ThisFunc ' ¦' id_thread '¦', OutputDebug(dbgtxt)) } cbK↑(token, ih,vk,sc) { static _d := 1, isUp := true, dir := (isUp?'↑':'↓') 🕐k_pre := 🕐k_now 🕐k_now := A_TickCount if (dbg>=_d) { dbgtxt := '' vk_hex := Format("vk{:x}",vk) key_name := GetKeyName(Format("vk{:x}",vk)) ; bugs with layouts, not english even if english is active dbgtxt .= "ih" dir (isSet(key_name)?key_name:'') " 🢥🄺: vk=" vk "¦" vk_hex " sc=" sc ' l' A_SendLevel " ¦" id_thread "¦" OutputDebug(dbgtxt) } isH := fnKanata_in_ev(vk,sc,isUp) dbgOut := '' for i in [4,4,4,5,5,5] { ; poll a key out channel@kanata) a few times to see if there are key events sleep(i) isOut := K_output_ev_check(), dbgOut.=isOut if (isOut < 0) { ; get as many keys as are available until reception errors out break } } (dbg<_d+1)?'':(dbgtxt:='🏁ih' dir ' pos isH=' isH ' isOut=' dbgOut ' ' format(" 🕐Δ{:.3f}",A_TickCount - 🕐k_now) ' ' A_ThisFunc ' ¦' id_thread '¦', OutputDebug(dbgtxt)) } ; set up machinery for AHK to receive data from kanata cbKanataOut(kvk,ksc,up) { ; static K := keyConstant, vk:=K._map, vkr:=K._mapr, vkl:=K._maplng, vkrl:=K._maprlng, vk→en:=vkrl['en'], sc:=K._mapsc ; various key name constants, gets vk code to avoid issues with another layout static _d := 1, lvl_to := 0 🕐1 := preciseTΔ() vk_hex := Format("vk{:x}",kvk) if not C↑ && up && (kvk=Cvk_d) { C↑ := true , (dbg<_d)?'':(OutputDebug('trigger key released')) } if cleanup && C↑ && up && (kvk=Cvk_d) { ; todo: check for physical position before excluding? (dbg<_d)?'':(OutputDebug("dupe release of trigger key on kanata's cleanup, ignore")) C↑ := false return } ; Critical ; todo: needed??? avoid being interrupted by itself (or any other thread) if (dbg>=_d) { dbgtxt := '' dir := (up?'↑':'↓') key_name := GetKeyName(vk_hex) ; bugs with layouts, not english even if english is active ; key_name := vk→en.Get(vk_hex,key_name_cur) hooks := "hooks#: " gethookcount() dbgtxt .= dir } if isSet(vk_hex) { (dbg<_d)?'':(dbgtxt .= key_name " 🄷🢦 : vk=" kvk '¦' vk_hex ' @l' A_SendLevel ' → ' lvl_to ' ' hooks ' ¦' id_thread '¦ ' A_ThisFunc, OutputDebug(dbgtxt)) if up { ; SendEvent('{' vk_hex ' up}') SendInput('{' vk_hex ' up}') } else { ; SendEvent('{' vk_hex ' down}') SendInput('{' vk_hex ' down}') } } else { (dbg<_d)?'':(dbgtxt .= '✗name' " 🄷🢦 : vk=" kvk '¦' vk_hex ' @l' A_SendLevel ' → ' lvl_to ' ' hooks ' ¦' id_thread '¦ ' A_ThisFunc, OutputDebug(dbgtxt)) } 🕐2 := preciseTΔ(), 🕐Δ := 🕐2-🕐1 if 🕐Δ > 0.5 { (dbg<_d+1)?'':(OutputDebug('🐢🏁 ' format(" 🕐Δ{:.3f}",🕐Δ) ' ¦' id_thread '¦ ' A_ThisFunc)) } else { (dbg<_d+1)?'':(OutputDebug('🐇🏁 ' format(" 🕐Δ{:.3f}",🕐Δ) ' ¦' id_thread '¦ ' A_ThisFunc)) } return 1 } ; CallbackFree(cbKanataOut) if (Cvk_d) { ; modtap; send the activating hotkey to Kanata so it can take it into acount cbK↓(token1,ih0,Cvk_d,Csc_d) } ih.Start() ; ih.Wait() ; Waits until the Input is terminated (InProgress is false) if (ih.EndReason = "Timeout") { ; cleanup kanata's state ; key_name := GetKeyName(Format("vk{:x}",last↓[1])) OutputDebug('—`n`n——————————————— Timeout') 🕐k_now := A_TickCount, 🕐Δ := 🕐k_now - 🕐k_pre cleanup := true res := fnKanata_reset(🕐Δ) ; reset kanata's state, progressing time to catch up, release held keys (even those physically held since reset is reset, so from kanata's perspective they should be released) cleanup := false dbgtxt := '' dbgtxt .= 'ih¦' 🕐Δ '🕐Δ timeout A_TimeSinceThisHotkey ' A_TimeSinceThisHotkey dbgOut := '' loop 10 { ; get the remaining out keys from kanata isOut := K_output_ev_check(), dbgOut.=isOut if (isOut < 0) { break } } OutputDebug(dbgtxt '`n`n——————————————— isOut=' dbgOut ' ') } ; DllCall("FreeLibrary", "Ptr",hModule) ; to conserve memory, the DLL may be unloaded after using it hModule:=0 } gethookcount() { if (A_KeybdHookInstalled = 0) { return "_¦_" } else if (A_KeybdHookInstalled = 3) { return "✓¦✓" } else if (A_KeybdHookInstalled = 2) { return "_¦✓" } else if (A_KeybdHookInstalled = 1) { return "✓¦_" } else { return "?" } } preciseTΔ(n:=3) { static start := nativeFunc.GetSystemTimePreciseAsFileTime() t := round( nativeFunc.GetSystemTimePreciseAsFileTime() - start,n) return t } class nativeFunc { static GetSystemTimePreciseAsFileTime() { /* learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemtimepreciseasfiletime retrieves the current system date and time with the highest possible level of precision (<1us) FILETIME structure contains a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC) 100 ns -> 0.1 µs -> 0.001 ms -> 0.00001 s 1 sec -> 1000 ms -> 1000000 µs 0.1 sec -> 100 ms -> 100000 µs 0.001 sec -> 10 ms -> 10000 µs */ static interval2sec := (10 * 1000 * 1000) ; 100ns * 10 → µs * 1000 → ms * 1000 → sec DllCall("GetSystemTimePreciseAsFileTime", "int64*",&ft:=0) return ft / interval2sec } } kanata-1.9.0/docs/switch-design000064400000000000000000000060241046102023000145170ustar 00000000000000# Preface: This document is a scratch space for the design of the switch action. It may be out of date and is kept around for posterity. .syntax: ---- (switch (or a b c) (cmd ) break (and a b (or c d)) (cmd ) fallthrough (and a b (or c d) (or e f)) fallthrough () ) ---- .opcode format examples: ---- (or a b c) OR-4 a b c (and a b (or c d)) AND-6 a b OR-6 c d (and a b (or c d) (or e f)) AND-9 a b OR-6 c d OR-9 e f ---- .opcodes: ---- key: all values < 1024 OR/AND: OP & 0xF000 OR : 0x1000 AND: 0x2000 length: OP & 0x0FFF ---- .Rough algorithm for opcodes: ---- value=true push first opcode WHILE stack is not empty WHILE index <= ending_index switch opcode: push, continue key(OR): value=true: skip to index, pop value=false: continue key(AND): value=true: continue value=false: skip to index, pop pop switch current_value(OR): value=true: skip to index, pop value=false: continue current_value(AND): value=true: continue value=false: skip to index, pop return value ---- .statestruct: ---- value current_index current_end_index current_op stack (op, ending_index) ---- .rough sequence 1: ---- pressed: y y y y y y opcodes: AND-9 a b OR-6 c d OR-9 e f index: 0 push: AND-9 stack: AND-9 index: 1 val: true index: 2 val: true index: 3 push: OR-6 stack: AND-9 OR-6 index: 4 val: true skip to 6 pop stack: AND-9 index: 6 push: OR-9 stack: AND-9 OR-9 index: 7 val: true skip to 9 pop stack: AND-9-true index 9: pop stack: empty return val: true ---- .rough sequence 2: ---- pressed: y y n n y y opcodes: AND-9 a b OR-6 c d OR-9 e f index: 0 push: AND-9 stack: AND-9 index: 1 val: true index: 2 val: true index: 3 push: OR-6 stack: AND-9 OR-6 val: true index: 4 val: false index: 5 val: false index: 6 val: false pop stack: AND-9 skip to 9 pop stack: empty return val: false ---- .rough sequence 3: ---- pressed: n y n n y y opcodes: AND-9 a b OR-6 c d OR-9 e f index: 0 push: AND-9 stack: AND-9 index: 1 val: false skip to 9 pop stack: empty return val: false ---- .pseudo code again: ---- let mut value = true let mut current_index = 1 let mut current_end_index = first_opcode - end_index let mut current_op = OR while current_index < slice_length { if index >= current_end_index: if stack is empty: break else: pop stack to current_op and current_end_index switch current_value(OR): value=true: skip to current_end_index; continue current_value(AND): value=false: skip to current_end_index; continue switch opcode: push (current_end_index,current_op) update (current_end_index,current_op) with opcode key(OR): value=true: skip to current_end_index; continue value=false key(AND): value=true value=false: skip to current_end_index; continue current_index++; } return value ---- kanata-1.9.0/docs/win-tray/win-tray-layer-change.gif000064400000000000000000000127031046102023000203750ustar 00000000000000GIF89a!#  "#$&#$(#'*(+/*.2-126.57<9<><=?A9AGHCGHHHKLDKM?KNOJNOOOQAOQUOTVITWRVXXXYOW]M\^K[^K]^Y]_L^_aO`cPbdTcePceQdhReiafjm[jpjnt_rvw{fy|lx|tymo~uǪ˦Ϫ'#29=FMPP\gwPV{  (,:BDE\blt~! NETSCAPE2.0!,!#  "#$&#$(#'*(+/*.2-126.57<9<><=?A9AGHCGHHHKLDKM?KNOJNOOOQAOQUOTVITWRVXXXYOW]M\^K[^K]^Y]_L^_aO`cPbdTcePceQdhReiafjm[jpjnt_rvw{fy|lx|tymo~uǪ˦Ϫ'#29=FMPP\gwPV{  (,:BDE\blt~H*\ȰÇ#JP hAʐ 4T,l1 h$P={ԩCNK NcFBl&|R˞|Y'ɠi$Ԃ 5>>D*;Z4*jQdꩨ !, "!, "!, "!, "!, "!, "!, "!, "!, "!, "!, "!, "!, "!, "!, "!, "!, "!, "!, "!, "!,H* ɓU8p`T]` "?9@0QDQ) (`RQ &) Qd' `\t)ʤMLp 0P"K&́"8]1TL@\ڂ^JZj?"*OИ<,/HJ5!PY߉. 0yk0jIkd;$p .\<@@!, "!, "!, "!, "!, "!, "!, "!,0@p!/@h X0b+( IRB{SGp;4-褳;xN; E#9x΍88#\!F;)E@䌹ƒPʈl&|R˖\vY'ɘi`C 8调=p:Ý)'A矀)@@!, "!, "!, "!, "!, "!, "!,HA)\dȠ 毢U~ D*i Eh$-$0DYVt9&(r4iuvHeTS LD8BChKx =-*s?HZU+PEVV@B,>֒颁,ZWhxa@Zv>4YQ-S._]Z0?Axo!, "!, "!, "!, "!,0@p!/@h X0b+( IRB{SGp;4-褳;xN; E#9x΍88#\!F;)E@䌹ƒPʈl&|R˖\vY'ɘi`C 8调=p:Ý)'A矀)@@!, "!, "!, "!, "!, "!,@Gp;4-褳;xN; E#9x΍88#\!F;)E@䌹ƒPʈl&|R˖\vY'ɘi`C 8调=p:Ý)'A矀)@@!, "!, "!, "!, "!, "!, "!, "!, "!, "!, ";kanata-1.9.0/docs/win-tray/win-tray-screen.png000064400000000000000000000333031046102023000173330ustar 00000000000000PNG  IHDR`V pHYs%%IR$ IDATx{|[ՙ-8NRހ@ 4Aks L=$|Ңvzj%&0Tfcw5E)%4@rFXIeY%jv֗ ZkZϳV8p@B Xn]4GL 9rr!:- fxJx&,տҡP(m@pAp{&b @P"֣@DR˛R m iEY=r] ()wORQYC"- %(Q`vDW/`nRr |^%J[8F2K XX5,_Hq] [C1{&8w ZmiӉ%,\r^H}CHrWO ;%#EKF/`aF+-٧实@0w"ʢ  DwGSbkNwњ]3t&/6_P)ѿ3to_/!i9ꬦms5mr#(9R%*A\qNPz.(yzkQAu>۹_,D$ f{ܹ?HqVf}s6Ns'VVAtbӚn(s?.rے?_[ >$q^<EsMd%~`ū//D/RR7-:Z ^V/AwI_k#ȇm -_{#L?Y-xz ϱ^0:}>rS8 {}zNO^PY9G9H"+MVo8LZZ-Zwمk|Q6nyLW7Xřؗ9)- '.2/s;Ϯ.sfh,w{uޜO䓔`ŝ]e'mVtG8IT0nbb}߲uC'QȪd{\(Gn+.:wyyr:6ʡScHvF'>|؂ɜ_E[?N>SISs{\(]vyYI2r@U7.:F8st[hߚSNm_oR o/ǦNc 46{{D U+}SL0^q sи9My/:syB-&(:E\Qt/C)r[ C=-6^6ME0쐃L<|9Hkks\[٣5̍U?I0znۛw&$GlhԆ7p_hٽe2l.o{s\~imw(nhiB֭[4Lb烱a-hN^%Bp?yt{mk" 'C,%ȃww ,Vt 1 VT /L J,]@ JDrp6-b &G*r 1dD^nse""gܼD}! @%э5ϟMk%4L:>x 0A0JJjkkh46O IޢT;SK+̡Cp\<,&i/v,4 #~Ê; c,xFƊ4Ǣ} ؖ#i Ă^tmyAկR{سg^ziB+W/akF8.5F6kǬGUϾnXϐSr]M篕SwTytm;htbX#P1O2 C188ȧ>)vܙU.n|Y0ڎ8ɫ /H{tvt+a=eFg0L(lzb0^J7 ===|K_FWWWIl-@P r}oOO6yl0T0q~cA~tӃ'f.kӢ[Sv$:r޸6퐮fRLjdng[1^u6mڄNۿett4;`G`eK*R'9D4S0Q}k>{M{ymhz,X Jt&6:9ק}TiM!Zڻ_Oɍa~y,۠s2ۊǢOf(`91Vw4ݐIuʶ6)&mtN|N|~fFw՛p?ᬒO[`0?̃>ȃ>H(bhhGyݻwsΜ9Ë/UW]{^>[ooB#0`m5M¬ӗT S_`zv[2W52k>B.f7` ua@^˟_6ꅸSqy#Gqo{fO'A#8?aN#8OY|zm-YJm(Vw~]kn+8:{,xN| ؀=@1/ɣ-KFٳu]ڵ W_};3.Rkll{{( [nڵ:}In9j౺v+8:Ҏ46_`4zɭzͧ;'{GY !/_RXV w[<eiS;c e:$Q̢dkӢ590:}I7B!(oM}ۚi -+iÅImY&/?Lccc|?<>ŋyYЀm)%q3@FrlD>&JI1))dB {Yzщ/ڑG.:n!ee׋M t3.H h2P(馛x'я~Dmmm1Qp>a9mYւf7{5o!>q!aRDXOc0عs'ַ뮻ضmv0.+OJ !#pwi31zk]A924D/At[Z@ ][^r])@ (1V#(:iE7@'@ #@ (Bt@ (Bt@ (Bt@ (Bt@ (Bt@ ( iPB;O([ExFkj+@  kp?7;p8μ;o 2^7Xh/E+J^7MG #GS{PUe yNؾkڻ]'`Qn~NwޝSՐ40tI*h !<$4dt8B@,]DEibQ)U ~ DBwU. w὏%>{ݻ2X>#OV#H82<E"ƈ aIqg*<OSx?84K`ˌ_%eF-L^lZ-fBmA^7P +Qpp3^'w9 齏R]9fq!݆2nhkϡ<Rb$i|H %Q'4 թmPү:ȑqp^>Jꡗ8܉r[ $񛭰Gf`kHHY_M\ 8?Sok͢g|P,cږCYIxzmAIBt'5(\D"DMh !ƇAټ*Q, @BY5UW32`|DAbi]d/xpωa»{~ D> gj`4ÔZb39NFkJO; ka6pfl/ⓖm^ɕ+CDN27HAc0 8^D *j΢ PbB蘂qFTHuƳֳDڅݐN=N>wqEJH@+Qu5Y'N#8:G^ F;,-+@ {5wa1uCK#F--l@7Gx(Ş=8omm{`ٞ|ғN0A|t. Vy93r-20vл+PA j "a%RJ#X[\ .8tk7sw~ InhmB4ENtA9Ӂ^kƬGNa--ػ701:%<,˱,9*rt:t×QtUռ|Ǜ>JFn¿7״p7qru ]>"գ#hTFPk$Tj@f42A:V*j%6m)EF~tU3i-x)xf;_:]D٦[x-D,voʁR(;n+x,m~\mʱltQ G(^z 8;*wr2NxD`wcŃE1 u7~k30:٧Jg_9L8?z=>|Сcu t1wp O*`йm, -t촶r^6kY\O3xb.a$zμ;~1vP"*f֯܆fP#K㝣xU^%fV.kbŊE12G+q`O(۬ޗt31Y=J@# }Yި+]8,mE~p1qot8b\ϾnOccѣ7uC@W;Vtvko=Zͬx.+n3q<31moO).iEWTDXBID0c#!FcԜ&@%a4UT+R=.KukJ:^?Żg1<<\F:+w!?1s\6L#F̂.N,F~Lىݐx_ZGG% m6o=&;Vvf v|l9nk{iE[ Xں*&JFF%FÌRNyZY*+R=Af!4 R0@u0 *h4ű`>e&?NJЯrț~|ѧLE(O!{{~ xn,RdZ;{W /HSx ,Vc8 v+S4-kJtm'bcXhMNLZJ8ԋ2{GSĴ7853.>F'`i gޥ"R( $U0>!!ljgj9XU,ZxGߠ LёM|0צɣlˊK!d7+R/>f%o=uLx0昞xMkG/6H6t(l_|1)x%`YQwjZٝt7E3ak3uF_9 zY.1o2݈;JC0(Ԁ=[ΣGSzҊ1C};uaN0[zAlM CT*TJTTWVGBc iiXV@&YS,xٗO7۲EY;o+n_3 -+ `zd6 =G1m}~M:}Isn~ѬU?WF^;{(ȖɇBcX4gtRD/GƖ{0씯3V`^3jQFﺺQc ꛕҺuD# eԨG֠2Wy.`/ ,HIrXʺ~ p+y9_§>emR~9͑#Gx'ijjb455QSSJ~ݼff( Wo 9KP~ش4Ҿ\8rH~zΞ?Š+hhh@ ج3o.#}0n i BBRiW3دᠿ,H\ VU@ PnS1{,sd.\9 n*mC(gH+UUUZ˗VcOjݚKYt19R8ʺqF+9}?\V@R|ݻ/ 9}od.>10P g:RR BBհfTha}=7L(`xDMXΥm`]`FnW uX+WriΜ9„QթhZWd[*$<`ѝd,_`"@ (Bt@ (E^3P(bRmAx@ J %ٳ(JZt [__saR8D)B &T+lX<-@.r LF_'#bIW^De%s}4ʫ6enk'?fV"*A"߽|eO]ξ>ː]_@ F#C}* ŸΜ^fpQ_TCdY Fz|w42/mD>qx .j\ACT"?S&_0>u$֝._ȉԭj$)t\K$@ j[ US#)wQ.3Z 4Z}:܋MC@[t oBQW28?//QR ?r׻qf0khmIF@ c-{VzJ~fcSPG:ϙW^bΟ/dy$3cSn#DAyRyP*ȺXl &BHB@]Uɧaw/7 U+3cr1tvh-]t8mh@ H&سOFͷQZ$ILA(, ZRS~̢J.W^^'P֔ ?6 88E@ ē>3@S?"c(JT?0]?g~C͍ڴhäEڢ~UMF}ڸ-nn:oٲզ.cO+{?$]mX=`m ~fFwyc d׉/v%r}DѰ$ E}? م˟݉ CahѮ ʊZmS.E fV[NW.۬އaNKa*_7<=z,{&5KfyJcu'7{.sTphMau'׵v>bA/m}Nz ˾Axe_9 N9M__#a BD dqVTUVcqbV4`ŗVw9~宒@ ȂEwc I(W,# (Nx-/eͷD{'F{W!06zBяˬbK^!:: ]XqR8N7BOe `la.1ڍ@1dxT.3&;RpʶEi6(Luӭe#ЛuQh RN>!Ipp$L$A$"P"$JDZZFV~Sj Y7Ⱦ} C]]UqSZHsZf4"20t4U(ZWJ^AyN.ԾD׎x{p`S" ֭#9`V7W%䒦5l5\ZFRM8#p ) Iz%Xutld:rWM d S;*.kf*7i$FDB(*QՠQE#C|6ǥ]tو՝L&:!Cy{`VXcA?^&5Vu7 nP(Ftx!wgڰcCГz$zUWJm^`0c/ C{ϷYsя]J$EQuhJpMeFT&TuaĹ#OmtwxY=RFL((<H|rДqkR o/ǦNw 4uj UQڠTpIV}L &U f쨆ASՕ\w͇T] EWd)F 8_ Kc>])b80QTTCd__I,FdѴ(R5i`1YLd 1q[o'F}2Or!2(ӘFx=XKǂ>~fxSe+R0tM[ KwNƭgٲe实@ @Q߷9{&&H(0AGƏSuGfdLrԬ1:jhᶦJ b-[r_&pZ{liTX$'qko%DIZb›(Vܾf:u&3e]uo΄%S.KɃpFalP p\rWO d@qiݺuDK_C)&r |Q\ٿ;'PE55%X2TjeFo٘&*Y ȟ#GpW?}keOƠ}Sp./TPh8J "̍|_{b_w9HVy7UMΝC PP( |; >Rͦ9Ȥ]oFTfdVX/ӦMlڴemJeT֠R /,1U@*`nxxOG54$IDM*xLT*>|Fr}\Pi*\M]m3 E1Pk4Fȯ=%¨O@E|a"ٽwժUlݺ[opme| /^杷^j(G$z`GkEb CUX:LCB'IENDB`kanata-1.9.0/justfile000064400000000000000000000111231046102023000126400ustar 00000000000000set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] # Build the release binaries for Linux and put the binaries+cfg in the output directory build_release_linux output_dir: cargo build --release cp target/release/kanata "{{output_dir}}/kanata" strip "{{output_dir}}/kanata" cargo build --release --features cmd cp target/release/kanata "{{output_dir}}/kanata_cmd_allowed" strip "{{output_dir}}/kanata_cmd_allowed" cp cfg_samples/kanata.kbd "{{output_dir}}" # Build the release binaries for Windows and put the binaries+cfg in the output directory. build_release_windows output_dir: cargo build --release --no-default-features --features tcp_server,win_manifest; cp target/release/kanata.exe "{{output_dir}}\kanata_legacy_output.exe" cargo build --release --features win_manifest,interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_wintercept.exe" cargo build --release --features win_manifest,win_sendinput_send_scancodes; cp target/release/kanata.exe "{{output_dir}}\kanata.exe" cargo build --release --features win_manifest,win_sendinput_send_scancodes,win_llhook_read_scancodes; cp target/release/kanata.exe "{{output_dir}}\kanata_winIOv2.exe" cargo build --release --features win_manifest,cmd,win_sendinput_send_scancodes; cp target/release/kanata.exe "{{output_dir}}\kanata_cmd_allowed.exe" cargo build --release --features win_manifest,cmd,interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_wintercept_cmd_allowed.exe" cargo build --release --features passthru_ahk --package=simulated_passthru; cp target/release/kanata_passthru.dll "{{output_dir}}\kanata_passthru.dll" cargo build --release --features win_manifest,gui ; cp target/release/kanata.exe "{{output_dir}}\kanata_gui.exe" cargo build --release --features win_manifest,gui,cmd; cp target/release/kanata.exe "{{output_dir}}\kanata_gui_cmd_allowed.exe" cargo build --release --features win_manifest,gui,interception_driver ; cp target/release/kanata.exe "{{output_dir}}\kanata_gui_wintercept.exe" cargo build --release --features win_manifest,gui,cmd,interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_gui_wintercept_cmd_allowed.exe" cp cfg_samples/kanata.kbd "{{output_dir}}" # Generate the sha256sums for all files in the output directory sha256sums output_dir: rm -f {{output_dir}}/sha256sums cd {{output_dir}}; sha256sum * > sha256sums test: cargo test -p kanata -p kanata-parser -p kanata-keyberon -- --nocapture cargo test --features=simulated_output sim_tests cargo clippy --all fmt: cargo fmt --all guic: cargo check --features=gui guif: cargo fmt --all cargo clippy --all --fix --features=gui -- -D warnings ahkc: cargo check --features=passthru_ahk ahkf: cargo fmt --all cargo clippy --all --fix --features=passthru_ahk -- -D warnings use_cratesio_deps: sed -i 's/^# \(kanata-\(keyberon\|parser\|tcp-protocol\) = ".*\)$/\1/' Cargo.toml parser/Cargo.toml sed -i 's/^\(kanata-\(keyberon\|parser\|tcp-protocol\) = .*path.*\)$/# \1/' Cargo.toml parser/Cargo.toml use_local_deps: sed -i 's/^\(kanata-\(keyberon\|parser\|tcp-protocol\) = ".*\)$/# \1/' Cargo.toml parser/Cargo.toml sed -i 's/^# \(kanata-\(keyberon\|parser\|tcp-protocol\) = .*path.*\)$/\1/' Cargo.toml parser/Cargo.toml change_subcrate_versions version: sed -i 's/^version = ".*"$/version = "{{version}}"/' parser/Cargo.toml tcp_protocol/Cargo.toml keyberon/Cargo.toml sed -i 's/^\(#\? \?kanata-\(keyberon\|parser\|tcp-protocol\).*version\) = "[0-9.]*"/\1 = "{{version}}"/' Cargo.toml parser/Cargo.toml cov: cargo llvm-cov clean --workspace cargo llvm-cov --no-report --workspace --no-default-features cargo llvm-cov --no-report --workspace cargo llvm-cov --no-report --workspace --features=cmd,win_llhook_read_scancodes,win_sendinput_send_scancodes cargo llvm-cov --no-report --workspace --features=cmd,interception_driver,win_sendinput_send_scancodes cargo llvm-cov --no-report --features=simulated_output -- sim_tests cargo llvm-cov report --html publish: cd keyberon && cargo publish cd tcp_protocol && cargo publish cd parser && cargo publish # Include the trailing `\` or `/` in the output_dir parameter. The parameter should be an absolute path. cfg_to_html output_dir: cd docs ; asciidoctor config.adoc cd docs ; cp config.html "{{output_dir}}config.html"; rm config.html # Include the trailing `\` or `/` in the output_dir parameter. The parameter should be an absolute path. wasm_pack output_dir: cd wasm; wasm-pack build --target web; cd pkg; cp kanata_wasm_bg.wasm "{{output_dir}}"; cp kanata_wasm.js "{{output_dir}}" kanata-1.9.0/src/gui/mod.rs000064400000000000000000000013761046102023000136010ustar 00000000000000pub mod win; pub use win::*; pub mod win_dbg_logger; pub mod win_nwg_ext; pub use win_dbg_logger as log_win; pub use win_dbg_logger::WINDBG_LOGGER; pub use win_nwg_ext::*; use crate::*; use parking_lot::Mutex; use std::sync::mpsc::Sender as ASender; use std::sync::{Arc, OnceLock}; pub static CFG: OnceLock>> = OnceLock::new(); pub static GUI_TX: OnceLock = OnceLock::new(); pub static GUI_CFG_TX: OnceLock = OnceLock::new(); pub static GUI_ERR_TX: OnceLock = OnceLock::new(); pub static GUI_ERR_MSG_TX: OnceLock> = OnceLock::new(); pub static GUI_EXIT_TX: OnceLock = OnceLock::new(); kanata-1.9.0/src/gui/win.rs000064400000000000000000002144021046102023000136130ustar 00000000000000use crate::Kanata; use anyhow::{bail, Result}; use core::cell::RefCell; use kanata_parser::cfg::CfgOptionsGui; use log::Level::*; use std::cell::Cell; use winapi::shared::windef::HWND; use core::ffi::c_void; use native_windows_gui as nwg; use parking_lot::Mutex; use parking_lot::MutexGuard; use std::collections::HashMap; use std::env::{current_exe, var_os}; use std::ffi::OsStr; use std::iter::once; use std::os::windows::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::sync::mpsc::{Receiver, Sender as ASender, TryRecvError}; use std::sync::OnceLock; use std::time::Duration; use winapi::shared::minwindef::{BYTE, DWORD}; use winapi::shared::windef::COLORREF; use windows_sys::Wdk::System::SystemServices::RtlGetVersion; use windows_sys::Win32::Foundation::{ERROR_FILE_NOT_FOUND, ERROR_SUCCESS, POINT, RECT, SIZE}; use windows_sys::Win32::System::Registry::{RegGetValueW, HKEY_CURRENT_USER, RRF_RT_REG_DWORD}; use windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW; use windows_sys::Win32::UI::HiDpi::GetSystemMetricsForDpi; use windows_sys::Win32::UI::WindowsAndMessaging::{ CalculatePopupWindowPosition, SM_CXCURSOR, SM_CYCURSOR, TPM_WORKAREA, }; use crate::gui::win_nwg_ext::{BitmapEx, MenuEx, MenuItemEx}; use kanata_parser::cfg; use nwg::{ControlHandle, NativeUi}; use std::sync::Arc; trait PathExt { fn add_ext(&mut self, ext_o: impl AsRef); } impl PathExt for PathBuf { fn add_ext(&mut self, ext_o: impl AsRef) { match self.extension() { Some(ext) => { let mut ext = ext.to_os_string(); ext.push("."); ext.push(ext_o.as_ref()); self.set_extension(ext) } None => self.set_extension(ext_o.as_ref()), }; } } #[derive(Default, Debug, Clone)] pub struct SystemTrayData { pub tooltip: String, pub cfg_p: Vec, pub cfg_icon: Option, pub layer0_name: String, pub layer0_icon: Option, pub gui_opts: CfgOptionsGui, pub tt_duration_pre: u16, pub tt_size_pre: (u16, u16), } #[derive(Default)] pub struct Icn { pub tray: nwg::Bitmap, // uses an image of different size to fit the menu items pub tooltip: nwg::Bitmap, // uses an image of different size to fit the tooltip pub icon: nwg::Icon, } #[derive(Default)] pub struct SystemTray { pub app_data: RefCell, /// Store dynamically created tray menu items pub tray_item_dyn: RefCell>, /// Store dynamically created tray menu items' handlers pub handlers_dyn: RefCell>, /// Store dynamically created icons to not load them from a file every time /// (icon format for tray icon, bitmap for tray MenuItem icons and tooltips) pub img_dyn: RefCell>>, /// Store 'img_dyn' hashmap key for the currently active icon ('cfg_path:🗍layer_name' format) pub icon_act_key: RefCell>, /// Store 'img_dyn' hashmap key for the first deflayer to allow skipping it in tooltips pub icon_0_key: RefCell>, /// Store embedded-in-the-binary resources like icons not to load them from a file pub embed: nwg::EmbedResource, pub icon: nwg::Icon, decoder: nwg::ImageDecoder, pub window: nwg::MessageWindow, /// A tooltip-like (no title/resize/focus/taskbar/clickthru) window to show notifications /// (e.g., layer change messages) pub win_tt: nwg::Window, win_tt_ifr: nwg::ImageFrame, win_tt_timer: nwg::AnimationTimer, pub layer_notice: nwg::Notice, pub cfg_notice: nwg::Notice, pub err_notice: nwg::Notice, pub exit_notice: nwg::Notice, pub tt_notice: nwg::Notice, /// Receiver of error message content sent from other threads /// (e.g., from key event thread via WinDbgLogger that will also notify our GUI /// (but not pass data) after sending data to this receiver) pub err_recv: Option>, pub tt2m_channel: Option<(ASender, Receiver)>, // receiver will be created before a thread is spawned and moved there pub m2tt_sender: RefCell>>, pub m_ptr_wh: RefCell<(u32, u32)>, pub tray: nwg::TrayNotification, pub tray_menu: nwg::Menu, pub tray_1cfg_m: nwg::Menu, pub tray_2reload: nwg::MenuItem, pub tray_3exit: nwg::MenuItem, pub img_reload: nwg::Bitmap, pub img_exit: nwg::Bitmap, } pub fn get_appdata() -> Option { var_os("APPDATA").map(PathBuf::from) } pub fn get_user_home() -> Option { var_os("USERPROFILE").map(PathBuf::from) } pub fn get_xdg_home() -> Option { var_os("XDG_CONFIG_HOME").map(PathBuf::from) } const CFG_FD: [&str; 3] = ["", "kanata", "kanata-tray"]; // blank "" allow checking directly for // user passed values const ASSET_FD: [&str; 4] = ["", "icon", "img", "icons"]; const IMG_EXT: [&str; 7] = ["ico", "jpg", "jpeg", "png", "bmp", "dds", "tiff"]; const PRE_LAYER: &str = "\n🗍: "; // : invalid path marker, so should be safe to use as a separator const TTTIMER_L: u16 = 9; // lifetime delta to duration for a tooltip timer use crate::gui::{CFG, GUI_CFG_TX, GUI_ERR_MSG_TX, GUI_ERR_TX, GUI_EXIT_TX, GUI_TX}; pub fn send_gui_notice() { if let Some(gui_tx) = GUI_TX.get() { gui_tx.notice(); } else { error!("no GUI_TX to notify GUI thread of layer changes"); } } pub fn send_gui_cfg_notice() { if let Some(gui_tx) = GUI_CFG_TX.get() { gui_tx.notice(); } else { error!("no GUI_CFG_TX to notify GUI thread of layer changes"); } } pub fn send_gui_err_notice() { if let Some(gui_tx) = GUI_ERR_TX.get() { gui_tx.notice(); } else { error!("no GUI_ERR_TX to notify GUI thread of errors"); } } pub fn send_gui_exit_notice() { if let Some(gui_tx) = GUI_EXIT_TX.get() { gui_tx.notice(); } else { error!("no GUI_EXIT_TX to ask GUI thread to exit"); } } pub fn show_err_msg_nofail(title: String, msg: String) { // log gets insalized before gui, so some errors might have no target to log to, ignore them if let Some(gui_msg_tx) = GUI_ERR_MSG_TX.get() { if gui_msg_tx.send((title, msg)).is_err() { warn!("send_gui_err_msg_notice failed to use OS notifications") } else { // can't Error to avoid an ∞ error loop ↑ if let Some(gui_tx) = GUI_ERR_TX.get() { gui_tx.notice(); } } } } /// Find an icon file that matches a given config icon name for a layer `lyr_icn` or a layer name /// `lyr_nm` (if `match_name` is `true`) or a given config icon name for the whole config `cfg_p` /// or a config file name at various locations (where config file is, where executable is, /// in user config folders) fn get_icon_p( lyr_icn: S1, lyr_nm: S2, cfg_icn: S3, cfg_p: P, match_name: &bool, ) -> Option where S1: AsRef, S2: AsRef, S3: AsRef, P: AsRef, { get_icon_p_impl( lyr_icn.as_ref(), lyr_nm.as_ref(), cfg_icn.as_ref(), cfg_p.as_ref(), match_name, ) } fn get_icon_p_impl( lyr_icn: &str, lyr_nm: &str, cfg_icn: &str, p: &Path, match_name: &bool, ) -> Option { trace!( "lyr_icn={lyr_icn} lyr_nm={lyr_nm} cfg_icn={cfg_icn} cfg_p={p:?} match_name={match_name}" ); let mut icon_file = PathBuf::new(); let blank_p = Path::new(""); let lyr_icn_p = Path::new(&lyr_icn); let lyr_nm_p = Path::new(&lyr_nm); let cfg_icn_p = Path::new(&cfg_icn); let cfg_stem = &p.file_stem().unwrap_or_else(|| OsStr::new("")); let cfg_name = &p.file_name().unwrap_or_else(|| OsStr::new("")); let f_name = [ lyr_icn_p.as_os_str(), if *match_name { lyr_nm_p.as_os_str() } else { OsStr::new("") }, cfg_icn_p.as_os_str(), cfg_stem, cfg_name, ] .into_iter(); let f_ext = [ lyr_icn_p.extension(), if *match_name { lyr_nm_p.extension() } else { None }, cfg_icn_p.extension(), None, None, ]; let pre_p = p.parent().unwrap_or_else(|| Path::new("")); let cur_exe = current_exe().unwrap_or_else(|_| PathBuf::new()); let xdg_cfg = get_xdg_home().unwrap_or_default(); let app_data = get_appdata().unwrap_or_default(); let mut user_cfg = get_user_home().unwrap_or_default(); user_cfg.push(".config"); let parents = [ Path::new(""), pre_p, &cur_exe, &xdg_cfg, &app_data, &user_cfg, ]; // empty path to allow no prefixes when icon path is explictily set in case it's a full // path already for (i, nm) in f_name.enumerate() { trace!("{}nm={:?}", "", nm); if nm.is_empty() { trace!("no file name to test, skip"); continue; } let mut is_full_p = false; if nm == lyr_icn_p { is_full_p = true }; // user configs can have full paths, so test them even if all parent folders are emtpy if nm == cfg_icn_p { is_full_p = true }; let icn_ext = &f_ext[i] .unwrap_or_else(|| OsStr::new("")) .to_string_lossy() .to_string(); let is_icn_ext_valid = if f_ext[i].is_some() { if IMG_EXT.iter().any(|&i| i == icn_ext) { trace!("icn_ext={:?}", icn_ext); true } else { warn!( "user icon extension \"{}\" might be invalid (or just not an extension)!", icn_ext ); false } } else { false }; 'p: for p_par in parents { trace!("{}p_par={:?}", " ", p_par); if p_par == blank_p && !is_full_p { trace!("blank parent for non-user, skip"); continue; } for p_kan in CFG_FD { trace!("{}p_kan={:?}", " ", p_kan); for p_icn in ASSET_FD { trace!("{}p_icn={:?}", " ", p_icn); for ext in IMG_EXT { trace!("{} ext={:?}", " ", ext); if p_par != blank_p { icon_file.push(p_par); } // folders if !p_kan.is_empty() { icon_file.push(p_kan); } if !p_icn.is_empty() { icon_file.push(p_icn); } if !nm.is_empty() { icon_file.push(nm); } if !is_full_p { icon_file.set_extension(ext); // no icon name passed, iterate extensions } else if !is_icn_ext_valid { icon_file.add_ext(ext); } else { trace!("skip ext"); } // replace invalid icon extension trace!("testing icon file {:?}", icon_file); if !icon_file.is_file() { icon_file.clear(); if p_par == blank_p && p_kan.is_empty() && p_icn.is_empty() && is_full_p { trace!("skipping further sub-iters on an empty parent with user config {:?}",nm); continue 'p; } } else { debug!("✓ found icon file: {}", icon_file.display().to_string()); return Some(icon_file.display().to_string()); } } } } } } debug!("✗ no icon file found"); None } pub const ICN_SZ_MENU: [u32; 2] = [24, 24]; // size for menu icons pub const ICN_SZ_TT: [u32; 2] = [36, 36]; // size for tooltip icons pub const ICN_SZ_MENU_I: [i32; 2] = [24, 24]; // for the builder, which needs i32 pub const ICN_SZ_TT_I: [i32; 2] = [36, 36]; // for the builder, which needs i32 macro_rules! win_ver { () => {{ static WIN_VER: OnceLock<(u32, u32, u32)> = OnceLock::new(); *WIN_VER.get_or_init(|| { let os_ver_i: *mut OSVERSIONINFOW = &mut OSVERSIONINFOW { dwOSVersionInfoSize: 0, //u32 dwMajorVersion: 0, //u32 dwMinorVersion: 0, //u32 dwBuildNumber: 0, //u32 dwPlatformId: 0, //u32 szCSDVersion: [0; 128], //[u16; 128] }; unsafe { if 0 == RtlGetVersion(os_ver_i) { return ( (*os_ver_i).dwMajorVersion, (*os_ver_i).dwMinorVersion, (*os_ver_i).dwBuildNumber, ); } } (0, 0, 0) }) }}; } /// Convert string to wide array and append null pub fn to_wide_str(s: &str) -> Vec { OsStr::new(s).encode_wide().chain(once(0)).collect() } macro_rules! mouse_scale_factor { // screen size = dpi⋅size⋅scaleF () => {{ //TODO: track changes by subscribing via RegNotifyChangeKeyValue and reset value static MOUSE_PTR_SCALE_F: OnceLock = OnceLock::new(); *MOUSE_PTR_SCALE_F.get_or_init(|| { // 3. pointer scale factor @ Settings/Ease of Access/Mouse pointer let key_root = HKEY_CURRENT_USER; let key_path_s = r"SOFTWARE\Microsoft\Accessibility"; let key_name_s = "CursorSize"; let key_path = to_wide_str(key_path_s); let key_name = to_wide_str(key_name_s); let mut mouse_scale: DWORD = 0; let mouse_scale_p: *mut c_void = &mut mouse_scale as *mut u32 as *mut std::ffi::c_void; let mut mouse_scale_sz: DWORD = std::mem::size_of::() as DWORD; let res = unsafe { RegGetValueW( key_root, key_path.as_ptr(), key_name.as_ptr(), RRF_RT_REG_DWORD, //restrict type to REG_DWORD std::ptr::null_mut(), //pdwType mouse_scale_p, &mut mouse_scale_sz, ) }; match res as DWORD { ERROR_SUCCESS => {} ERROR_FILE_NOT_FOUND => { log::error!(r"Registry '{}\{}' not found", key_path_s, key_name_s); mouse_scale = 1; } _ => { log::error!( r"Registry '{}\{}' couldn't be read as DWORD {}", key_path_s, key_name_s, res ); mouse_scale = 1; } } mouse_scale }) }}; } pub fn get_mouse_ptr_size(dpi_scale: bool) -> (u32, u32) { // 1. get monitor DPI let dpi = if dpi_scale { unsafe { nwg::dpi() } } else { 96 }; // 2. icon size @ dpi let cur_w = SM_CXCURSOR; let cur_h = SM_CYCURSOR; let width = unsafe { GetSystemMetricsForDpi(cur_w, dpi as u32) } as u32; let height = unsafe { GetSystemMetricsForDpi(cur_h, dpi as u32) } as u32; let mouse_scale = mouse_scale_factor!(); (mouse_scale * width, mouse_scale * height) } // stores old mouse pointer position to avoid refreshing tooltips if mouse doesn't move thread_local! {static MXY:Cell<(i32,i32)> = Cell::default();} impl SystemTray { /// Read an image from a file, convert it to various formats: tray, tooltip, icon fn get_icon_from_file

(&self, ico_p: P) -> Result where P: AsRef, { self.get_icon_from_file_impl(ico_p.as_ref()) } fn get_icon_from_file_impl(&self, ico_p: &str) -> Result { let app_data = self.app_data.borrow(); let icn_sz_tt = [ app_data.gui_opts.tooltip_size.0 as u32, app_data.gui_opts.tooltip_size.1 as u32, ]; if let Ok(img_data) = self .decoder .from_filename(ico_p) .and_then(|img_src| img_src.frame(0)) { if let Ok(cfg_img_menu) = self.decoder.resize_image(&img_data, ICN_SZ_MENU) { let cfg_icon_bmp_tray = cfg_img_menu.as_bitmap()?; let cfg_icon_bmp_icon = cfg_icon_bmp_tray.copy_as_icon(); if let Ok(cfg_img_menu) = self.decoder.resize_image(&img_data, icn_sz_tt) { let cfg_icon_bmp_tt = cfg_img_menu.as_bitmap()?; return Ok(Icn { tray: cfg_icon_bmp_tray, tooltip: cfg_icon_bmp_tt, icon: cfg_icon_bmp_icon, }); } else { debug!("✓ main ✗ icon resize Tray for {:?}", ico_p); } } else { debug!("✓ main ✗ icon resize TTip for {:?}", ico_p); } } else { debug!("✗ main 0 icon ✓ icon path for {:?}", ico_p); } bail!("✗ couldn't get a valid icon at {:?}", ico_p) } /// Read an image from a file, convert it to a menu-sized icon, /// assign to a menu and return the image in various formats (tray, tooltip, icon) fn set_menu_item_cfg_icon( &self, menu_item: &mut nwg::MenuItem, cfg_icon_s: &str, cfg_p: &PathBuf, ) -> Result { if let Some(ico_p) = get_icon_p("", "", cfg_icon_s, cfg_p, &false) { if let Ok(icn) = self.get_icon_from_file(ico_p) { menu_item.set_bitmap(Some(&icn.tray)); return Ok(icn); } else { debug!( "✗ main 0 icon ✓ icon path, will be using DEFAULT icon for {:?}", cfg_p ); } } menu_item.set_bitmap(None); bail!("✗couldn't get a valid icon for {:?}", cfg_p) } /// Move tooltip to the current mouse pointer position fn update_tooltip_pos(&self) { let app_data = self.app_data.borrow(); let mut x = 0; let mut y = 0; let mut is_same = false; MXY.with(|mxy| { (x, y) = nwg::GlobalCursor::position(); let (mx, my) = mxy.get(); if mx == x && my == y { is_same = true; return; } mxy.set((x, y)); }); if is_same { return; }; let win_ver = win_ver!(); // image width/height to take it into account when calculating overlaps let w = app_data.gui_opts.tooltip_size.0 as i32; let h = app_data.gui_opts.tooltip_size.1 as i32; let flags = if (win_ver.0 >= 6 && win_ver.1 >= 1) || win_ver.0 > 6 { TPM_WORKAREA } else { 0 }; // 🖰 pointer size to make sure tooltip doesn't overlap, // don't adjust for dpi in internal calculations // tooltip offset vs. 🖰 pointer by 25% its size let (mouse_ptr_w, mouse_ptr_h) = *self.m_ptr_wh.borrow(); let tt_off_x = (mouse_ptr_w as f64 * 0.25).round() as i32; let tt_off_y = (mouse_ptr_h as f64 * 0.25).round() as i32; // let (mouse_ptr_w, mouse_ptr_h) = (mouse_ptr_w as i32, mouse_ptr_h as i32); let anchorpoint = &POINT { x: x + tt_off_x, y: y + tt_off_y, }; let tt_win_sz = &SIZE { cx: w, cy: h }; let excluderect = &RECT { left: x.saturating_sub(mouse_ptr_w), right: x.saturating_add(mouse_ptr_w), // assuming ~top-left hotspot top: y.saturating_sub(mouse_ptr_h), bottom: y.saturating_add(mouse_ptr_h), }; //Avoid ~ mouse pointer area let out_rect = &mut RECT { left: 0, right: 0, top: 0, bottom: 0, }; let ret = unsafe { CalculatePopupWindowPosition(anchorpoint, tt_win_sz, flags, excluderect, out_rect) }; if ret != 0 { x = out_rect.left; y = out_rect.top; } let dpi = unsafe { nwg::dpi() }; let xx = (x as f64 / (dpi as f64 / 96_f64)).round() as i32; // adjust dpi for layout let yy = (y as f64 / (dpi as f64 / 96_f64)).round() as i32; self.win_tt.set_position(xx, yy); // TODO: somehow still shown a bit too far off from the pointer if log_enabled!(Trace) { let (mx, my) = MXY.get(); trace!("🖰 @{mx}⋅{my} ↔{mouse_ptr_w}↕{mouse_ptr_h} (upd={}) {x}⋅{y} @ dpi={dpi} → {xx}⋅{yy} {win_ver:?} flags={flags} ex←{}→{}↑{}↓{}" ,ret != 0,excluderect.left,excluderect.right,excluderect.top,excluderect.bottom); } } /// Spawn a thread with a new 🖰 pointer watcher /// (that sends a signal back to GUI which in turn moves the tooltip to the new position) fn update_mouse_watcher(&self, tt2m_sndr: ASender, ticks: u16, poll_time: Duration) { debug!(" ✓ update_mouse_watcher"); let gui_tx = self.tt_notice.sender(); // allows notifying GUI on tooltip move updates let (m2tt_sndr0, m2tt_rcvr) = std::sync::mpsc::channel::(); { let mut m2tt_sender = self.m2tt_sender.borrow_mut(); *m2tt_sender = Some(m2tt_sndr0.clone()); } let _handler = std::thread::spawn(move || -> Result<()> { debug!(" ✓ Starting polling for a 🖰 pointer position"); let mut i = 0; while i <= ticks { i += 1; std::thread::sleep(poll_time); match m2tt_rcvr.try_recv() { Ok(_) => { debug!("extending tooltip watcher instead of launching +1"); i = 0; } Err(TryRecvError::Empty) => { trace!("send signal to reposition"); gui_tx.notice(); } Err(TryRecvError::Disconnected) => { debug!("internal: m2tt_sender disconnected, no more 🖰 pointer tracking"); break; } } } debug!(" ✗ Stopped polling for a 🖰 pointer position"); tt2m_sndr.send(true)?; Ok(()) }); } /// Show our tooltip-like notification window fn show_tooltip(&self, img: Option<&nwg::Bitmap>) { let app_data = self.app_data.borrow(); if !app_data.gui_opts.tooltip_layer_changes { return; }; if img.is_none() && !app_data.gui_opts.tooltip_show_blank { self.win_tt.set_visible(false); return; }; static IS_INIT: OnceLock = OnceLock::new(); if IS_INIT.get().is_none() { // layered win needs a special call after being initialized to appear let _ = IS_INIT.set(true); debug!("win_tt hasn't been shown as a layered window"); let win_id = self .win_tt .handle .hwnd() .expect("win_tt should be a valid/existing window!"); show_layered_win(win_id); } else { debug!("win_tt has been shown as a layered window"); } self.win_tt_ifr.set_bitmap(img); { let mut m_ptr_wh = self.m_ptr_wh.borrow_mut(); *m_ptr_wh = get_mouse_ptr_size(false); } // 🖰 pointer size so tooltip doesn't overlap // don't adjust for dpi in internal calculations self.update_tooltip_pos(); self.win_tt.set_visible(true); if app_data.gui_opts.tooltip_duration != 0 { self.win_tt_timer.start() }; if let Some((tt2m_sndr, tt2m_rcvr)) = &self.tt2m_channel { let mut start = false; match tt2m_rcvr.try_recv() { Ok(_) => { debug!("launch a new thread"); start = true; } Err(TryRecvError::Empty) => { if let Some(m2tt_sender) = self.m2tt_sender.borrow().as_ref() { trace!("send signal to extend"); m2tt_sender.send(true).unwrap_or_else(|_| { error!("internal: couldn't send a signal to the 🖰 pointer watcher!") }); } else { debug!("no message and no m2tt_sender_o, so no thread should be running, launch a new thread!"); start = true; } } Err(TryRecvError::Disconnected) => { error!("internal: tt2m_channel disconnected, no more 🖰 pointer tracking") } } let duration = 16; let poll_time = Duration::from_millis(duration); let ticks = (app_data.gui_opts.tooltip_duration as f64 / duration as f64).round() as u16; debug!( "will tick for {ticks} every {duration} ms to match user {}", app_data.gui_opts.tooltip_duration ); if start { self.update_mouse_watcher(tt2m_sndr.clone(), ticks, poll_time); } } else { error!("internal: m2tt_sender doesn't exist can't track 🖰 pointer without it!"); } } /// Hide our tooltip-like notification window fn hide_tooltip(&self) { self.win_tt.set_visible(false) } fn show_menu(&self) { self.update_tray_icon_cfg_group(false); let (x, y) = nwg::GlobalCursor::position(); self.tray_menu.popup(x, y); } /// Add a ✓ (or highlight the icon) to the currently active config. /// Runs on opening of the list of configs menu fn update_tray_icon_cfg( &self, menu_item_cfg: &mut nwg::MenuItem, cfg_p: &PathBuf, is_active: bool, ) -> Result<()> { let mut img_dyn = self.img_dyn.borrow_mut(); if img_dyn.contains_key(cfg_p) { // check if menu group icon needs to be updated to match active if is_active { if let Some(icn) = img_dyn.get(cfg_p).and_then(|maybe_icn| maybe_icn.as_ref()) { self.tray_1cfg_m.set_bitmap(Some(&icn.tray)) } } } else { trace!("config menu item icon missing, read config and add it (or nothing) {cfg_p:?}"); if let Ok(cfg) = cfg::new_from_file(cfg_p) { if let Some(cfg_icon_s) = cfg.options.gui_opts.tray_icon { debug!("loaded config without a tray icon {cfg_p:?}"); if let Ok(icn) = self.set_menu_item_cfg_icon(menu_item_cfg, &cfg_icon_s, cfg_p) { if is_active { self.tray_1cfg_m.set_bitmap(Some(&icn.tray)); } // update currently active config's icon in the combo menu debug!("✓set icon {cfg_p:?}"); let _ = img_dyn.insert(cfg_p.clone(), Some(icn)); } else { bail!("✗couldn't get a valid icon") } } else { bail!("✗icon not configured") } } else { bail!("✗couldn't load config") } } Ok(()) } fn update_tray_icon_cfg_group(&self, force: bool) { if let Some(cfg) = CFG.get() { if let Some(k) = cfg.try_lock() { let idx_cfg = k.cur_cfg_idx; let mut tray_item_dyn = self.tray_item_dyn.borrow_mut(); let h_cfg_i = &mut tray_item_dyn[idx_cfg]; let is_check = h_cfg_i.checked(); if !is_check || force { let cfg_p = &k.cfg_paths[idx_cfg]; debug!( "✗ mismatch idx_cfg={idx_cfg:?} {} {:?} cfg_p={cfg_p:?}", if is_check { "✓" } else { "✗" }, h_cfg_i.handle ); h_cfg_i.set_checked(true); if let Err(e) = self.update_tray_icon_cfg(h_cfg_i, cfg_p, true) { debug!("{e:?} {cfg_p:?}"); let mut img_dyn = self.img_dyn.borrow_mut(); img_dyn.insert(cfg_p.clone(), None); self.tray_1cfg_m.set_bitmap(None); // can't update menu, so remove combo // menu icon }; } else { debug!("gui cfg selection matches active config"); }; } else { debug!("✗ kanata config is locked, can't get current config (likely the gui changed the layer and is still holding the lock, it will update the icon)"); } }; } fn check_active(&self) { if let Some(cfg) = CFG.get() { let k = cfg.lock(); let idx_cfg = k.cur_cfg_idx; let mut tray_item_dyn = self.tray_item_dyn.borrow_mut(); for (i, h_cfg_i) in tray_item_dyn.iter_mut().enumerate() { // 1 if missing an icon, read config to get one let cfg_p = &k.cfg_paths[i]; trace!(" →→→→ i={i:?} {:?} cfg_p={cfg_p:?}", h_cfg_i.handle); let is_active = i == idx_cfg; if let Err(e) = self.update_tray_icon_cfg(h_cfg_i, cfg_p, is_active) { debug!("{e:?} {cfg_p:?}"); let mut img_dyn = self.img_dyn.borrow_mut(); img_dyn.insert(cfg_p.clone(), None); if is_active { self.tray_1cfg_m.set_bitmap(None); } // update currently active config's icon in the combo menu }; // 2 if wrong GUI checkmark, correct it if h_cfg_i.checked() && !is_active { debug!("uncheck i{} act{}", i, idx_cfg); h_cfg_i.set_checked(false); } if !h_cfg_i.checked() && is_active { debug!(" check i{} act{}", i, idx_cfg); h_cfg_i.set_checked(true); } } } else { error!("no CFG var that contains active kanata config"); }; } /// Check if tooltip data is changed, and update tooltip window size / timer duration fn update_tooltip_data(&self, k: &Kanata) -> bool { let mut app_data = self.app_data.borrow_mut(); let mut clear = false; if app_data.tt_duration_pre != k.gui_opts.tooltip_duration { app_data.gui_opts.tooltip_duration = k.gui_opts.tooltip_duration; clear = true; app_data.tt_duration_pre = k.gui_opts.tooltip_duration; trace!("timer duration changed, updating"); self.win_tt_timer.set_interval(Duration::from_millis( (k.gui_opts.tooltip_duration.saturating_add(1)).into(), )); self.win_tt_timer.set_lifetime(Some(Duration::from_millis( (k.gui_opts.tooltip_duration.saturating_add(TTTIMER_L)).into(), ))); } if !(app_data.tt_size_pre.0 == k.gui_opts.tooltip_size.0 && app_data.tt_size_pre.1 == k.gui_opts.tooltip_size.1) { app_data.tt_size_pre = k.gui_opts.tooltip_size; clear = true; app_data.gui_opts.tooltip_size = k.gui_opts.tooltip_size; trace!("tooltip_size duration changed, updating"); let dpi = unsafe { nwg::dpi() }; let icn_sz_tt_i = (k.gui_opts.tooltip_size.0, k.gui_opts.tooltip_size.1); let w = (icn_sz_tt_i.0 as f64 / (dpi as f64 / 96_f64)).round() as u32; let h = (icn_sz_tt_i.1 as f64 / (dpi as f64 / 96_f64)).round() as u32; self.win_tt.set_size(w, h); let icn_sz_tt_i = ( k.gui_opts.tooltip_size.0 as u32, k.gui_opts.tooltip_size.1 as u32, ); // todo: replace with a no-margin NWG config when it's available let padx = (k.gui_opts.tooltip_size.0 as f64 / 6_f64).round() as i32; let pady = (k.gui_opts.tooltip_size.1 as f64 / 6_f64).round() as i32; trace!( "kanata tooltip size = {icn_sz_tt_i:?}, ttsize = {w}⋅{h} offset = {padx}⋅{pady}" ); self.win_tt_ifr.set_size(icn_sz_tt_i.0, icn_sz_tt_i.1); self.win_tt_ifr.set_position(-padx, -pady); } clear } /// Reload config file, currently active (`i=None`) or matching a given `i` index fn reload_cfg(&self, i: Option) -> Result<()> { use nwg::TrayNotificationFlags as f_tray; let mut msg_title = "".to_string(); let mut msg_content = "".to_string(); let mut flags = f_tray::empty(); if let Some(cfg) = CFG.get() { let mut k = cfg.lock(); let paths = &k.cfg_paths; let idx_cfg = match i { Some(idx) => { if idx < paths.len() { idx } else { error!( "Invalid config index {} while kanata has only {} configs loaded", idx + 1, paths.len() ); k.cur_cfg_idx } } None => k.cur_cfg_idx, }; let path_cur = &paths[idx_cfg]; let path_cur_s = path_cur.display().to_string(); let path_cur_cc = path_cur.clone(); msg_content += &path_cur_s; let cfg_name = &path_cur .file_name() .unwrap_or_else(|| OsStr::new("")) .to_string_lossy() .to_string(); if log_enabled!(Debug) { let cfg_icon = &k.gui_opts.tray_icon; let cfg_icon_s = cfg_icon.clone().unwrap_or("✗".to_string()); let layer_id = k.layout.b().current_layer(); let layer_name = &k.layer_info[layer_id].name; let layer_icon = &k.layer_info[layer_id].icon; let layer_icon_s = layer_icon.clone().unwrap_or("✗".to_string()); debug!( "pre reload tray_icon={} layer_name={} layer_icon={}", cfg_icon_s, layer_name, layer_icon_s ); } match i { Some(idx) => { if let Ok(()) = k.live_reload_n(idx) { msg_title += &("🔄 \"".to_owned() + cfg_name + "\" loaded"); flags |= f_tray::USER_ICON; } else { msg_title += &("🔄 \"".to_owned() + cfg_name + "\" NOT loaded"); flags |= f_tray::ERROR_ICON | f_tray::LARGE_ICON; { let app_data = self.app_data.borrow(); if app_data.gui_opts.notify_cfg_reload_silent { flags |= f_tray::SILENT; } } self.tray.show( &msg_content, Some(&msg_title), Some(flags), Some(&self.icon), ); bail!("{msg_content}"); } } None => { if let Ok(()) = k.live_reload() { msg_title += &("🔄 \"".to_owned() + cfg_name + "\" reloaded"); flags |= f_tray::USER_ICON; } else { msg_title += &("🔄 \"".to_owned() + cfg_name + "\" NOT reloaded"); flags |= f_tray::ERROR_ICON | f_tray::LARGE_ICON; { let app_data = self.app_data.borrow(); if app_data.gui_opts.notify_cfg_reload_silent { flags |= f_tray::SILENT; } } self.tray.show( &msg_content, Some(&msg_title), Some(flags), Some(&self.icon), ); bail!("{msg_content}"); } } }; let cfg_icon = &k.gui_opts.tray_icon; let layer_id = k.layout.b().current_layer(); let layer_name = &k.layer_info[layer_id].name; let layer_icon = &k.layer_info[layer_id].icon; let mut cfg_layer_pkey = PathBuf::new(); // path key cfg_layer_pkey.push(path_cur_cc.clone()); cfg_layer_pkey.push(PRE_LAYER.to_owned() + layer_name); //:invalid path marker, // so should be safe to use as // a separator let cfg_layer_pkey_s = cfg_layer_pkey.display().to_string(); if log_enabled!(Debug) { let layer_icon_s = layer_icon.clone().unwrap_or("✗".to_string()); debug!( "pos reload tray_icon={:?} layer_name={:?} layer_icon={:?}", cfg_icon, layer_name, layer_icon_s ); } let _ = self.update_tooltip_data(&k); // check for changes before they're overwritten ↓ { *self.app_data.borrow_mut() = update_app_data(&k)?; } self.tray.set_tip(&cfg_layer_pkey_s); // update tooltip to point to the newer config let clear = i.is_none(); self.update_tray_icon( cfg_layer_pkey, &cfg_layer_pkey_s, layer_name, layer_icon, path_cur_cc, clear, ) } else { msg_title += "✗ Config NOT reloaded, no CFG"; warn!("{}", msg_title); flags |= f_tray::ERROR_ICON; }; flags |= f_tray::LARGE_ICON; // todo: fails without this, must have SM_CXICON x SM_CYICON? { let app_data = self.app_data.borrow(); if app_data.gui_opts.notify_cfg_reload_silent { flags |= f_tray::SILENT; } } self.tray.show( &msg_content, Some(&msg_title), Some(flags), Some(&self.icon), ); Ok(()) } fn reload_layer_icon(&self) { let _ = self.reload_cfg_or_layer_icon(false); } /// Show OS notification message with an error coming from WinDbgLogger fn notify_error(&self) { let app_data = self.app_data.borrow(); if !app_data.gui_opts.notify_error { return; }; use nwg::TrayNotificationFlags as f_tray; let mut msg_title = "".to_string(); let mut msg_content = "".to_string(); let mut flags = f_tray::empty(); if let Some(gui_msg_rx) = &self.err_recv { match gui_msg_rx.try_recv() { Ok((title, msg)) => { msg_title += &title; msg_content += &msg; } Err(TryRecvError::Empty) => { msg_title += "internal"; msg_content += "channel to receive errors is Empty"; } Err(TryRecvError::Disconnected) => { msg_title += "internal"; msg_content += "channel to receive errors is Disconnected"; } } } else { msg_title += "internal"; msg_content += "SystemTray is supposed to have a valid 'err_recv' field value" } flags |= f_tray::ERROR_ICON; if app_data.gui_opts.notify_cfg_reload_silent { flags |= f_tray::SILENT; } let msg_title = strip_ansi_escapes::strip_str(&msg_title); let msg_content = strip_ansi_escapes::strip_str(&msg_content); self.tray.show( &msg_content, Some(&msg_title), Some(flags), Some(&self.icon), ); } /// Update tray icon data on config reload fn reload_cfg_icon(&self) { let _ = self.reload_cfg_or_layer_icon(true); } /// Update tray icon data on layer change (and config reload) fn reload_cfg_or_layer_icon(&self, is_cfg: bool) -> Result<()> { if let Some(cfg) = CFG.get() { if let Some(k) = cfg.try_lock() { let paths = &k.cfg_paths; let idx_cfg = k.cur_cfg_idx; let path_cur = &paths[idx_cfg]; let path_cur_cc = path_cur.clone(); let cfg_icon = &k.gui_opts.tray_icon; let layer_id = k.layout.b().current_layer(); let layer_name = &k.layer_info[layer_id].name; let layer_icon = &k.layer_info[layer_id].icon; let mut cfg_layer_pkey = PathBuf::new(); // path key cfg_layer_pkey.push(path_cur_cc.clone()); cfg_layer_pkey.push(PRE_LAYER.to_owned() + layer_name); //:invalid path marker, // so should be safe // to use as a separator let cfg_layer_pkey_s = cfg_layer_pkey.display().to_string(); if log_enabled!(Debug) { let cfg_name = &path_cur .file_name() .unwrap_or_else(|| OsStr::new("")) .to_string_lossy() .to_string(); let cfg_icon_s = layer_icon.clone().unwrap_or("✗".to_string()); let layer_icon_s = cfg_icon.clone().unwrap_or("✗".to_string()); debug!( "✓ layer changed to ‘{}’ with icon ‘{}’ @ ‘{}’ tray_icon ‘{}’", layer_name, layer_icon_s, cfg_name, cfg_icon_s ); } let clear = self.update_tooltip_data(&k); if is_cfg { *self.app_data.borrow_mut() = update_app_data(&k)?; } if is_cfg { let app_data = self.app_data.borrow(); if app_data.gui_opts.notify_cfg_reload { use nwg::TrayNotificationFlags as f_tray; let cfg_name = &path_cur .file_name() .unwrap_or_else(|| OsStr::new("")) .to_string_lossy() .to_string(); let msg_title = "🔄 \"".to_owned() + cfg_name + "\" re-loaded"; let msg_content = &path_cur.display().to_string(); let mut flags = f_tray::empty() | f_tray::USER_ICON | f_tray::LARGE_ICON; if app_data.gui_opts.notify_cfg_reload_silent { flags |= f_tray::SILENT; } self.tray.show( msg_content, Some(&msg_title), Some(flags), Some(&self.icon), ); } } self.tray.set_tip(&cfg_layer_pkey_s); self.update_tray_icon( cfg_layer_pkey, &cfg_layer_pkey_s, layer_name, layer_icon, path_cur_cc, clear, ) } else { debug!("✗ kanata config is locked, can't get current layer (likely the gui changed the layer and is still holding the lock, it will update the icon)"); } } else { warn!("✗ Layer indicator NOT changed, no CFG"); }; Ok(()) } /// Update tray icon data given various config/layer info /// * `cfg_layer_pkey` - "path␤🗍: layer_name" unique icon id /// * `path_cur_cc` - "path" without the layer name /// * `clear` - reset stored icon cached paths/files fn update_tray_icon( &self, cfg_layer_pkey: PathBuf, cfg_layer_pkey_s: &str, layer_name: &str, layer_icon: &Option, path_cur_cc: PathBuf, clear: bool, ) { let mut img_dyn = self.img_dyn.borrow_mut(); // update the tray icons let mut icon_act_key = self.icon_act_key.borrow_mut(); // update the tray icon active path let mut icon_0_key = self.icon_0_key.borrow_mut(); // update the tray tooltip layer0 path if clear { *img_dyn = Default::default(); *icon_act_key = Default::default(); *icon_0_key = Some(cfg_layer_pkey.clone()); debug!("reloading active config, clearing img_dyn/_active cache"); } let app_data = self.app_data.borrow(); let skip_tt = app_data.gui_opts.tooltip_no_base && icon_0_key .as_ref() .filter(|p| **p == cfg_layer_pkey) .is_some(); if icon_0_key.is_none() { warn!("internal bug?: icon_0_key should never be empty?") } if let Some(icn_opt) = img_dyn.get(&cfg_layer_pkey) { // 1a config+layer path has already been checked if let Some(icn) = icn_opt { self.tray.set_icon(&icn.icon); *icon_act_key = Some(cfg_layer_pkey); if !skip_tt { self.show_tooltip(Some(&icn.tooltip)); } } else { info!( "no icon found, using default for config+layer = {}", cfg_layer_pkey_s ); self.tray.set_icon(&self.icon); *icon_act_key = Some(cfg_layer_pkey); self.show_tooltip(None); } } else if layer_icon.is_some() || app_data.gui_opts.icon_match_layer_name { let layer_icon = match layer_icon { Some(layer_icon_inner) => { trace!("configured layer icon = {}", layer_icon_inner); layer_icon_inner } None => { trace!( "no configured layer icon, checking its name = {}", layer_name ); layer_name } }; // 1b cfg+layer path hasn't been checked, but layer has an icon configured... // or configured to check its name, so check it if let Some(ico_p) = get_icon_p( layer_icon, layer_name, "", &path_cur_cc, &app_data.gui_opts.icon_match_layer_name, ) { if let Ok(icn) = self.get_icon_from_file(ico_p) { info!( "✓ Using an icon from this config+layer: {}", cfg_layer_pkey_s ); self.tray.set_icon(&icn.icon); if !skip_tt { self.show_tooltip(Some(&icn.tooltip)); } let _ = img_dyn.insert(cfg_layer_pkey.clone(), Some(icn)); *icon_act_key = Some(cfg_layer_pkey); } else { warn!( "✗ Invalid icon file \"{layer_icon}\" from this config+layer: {}", cfg_layer_pkey_s ); let _ = img_dyn.insert(cfg_layer_pkey.clone(), None); self.tray.set_icon(&self.icon); *icon_act_key = Some(cfg_layer_pkey); self.show_tooltip(None); } } else { warn!( "✗ Invalid icon path \"{layer_icon}\" from this config+layer: {}", cfg_layer_pkey_s ); let _ = img_dyn.insert(cfg_layer_pkey.clone(), None); self.tray.set_icon(&self.icon); *icon_act_key = Some(cfg_layer_pkey); self.show_tooltip(None); } } else if img_dyn.contains_key(&path_cur_cc) { // 2a no layer icon configured, but config icon exists, use it if let Some(icn) = img_dyn.get(&path_cur_cc).unwrap() { self.tray.set_icon(&icn.icon); *icon_act_key = Some(path_cur_cc); self.show_tooltip(None); } else { info!( "no icon found, using default for config: {}", path_cur_cc.display().to_string() ); self.tray.set_icon(&self.icon); *icon_act_key = Some(path_cur_cc); self.show_tooltip(None); } } else { // 2b no layer icon configured, no config icon, use config path let cfg_icon_p = if let Some(cfg_icon) = &app_data.cfg_icon { cfg_icon } else { "" }; if let Some(ico_p) = get_icon_p( "", layer_name, cfg_icon_p, &path_cur_cc, &app_data.gui_opts.icon_match_layer_name, ) { if let Ok(icn) = self.get_icon_from_file(ico_p) { info!( "✓ Using an icon from this config: {}", path_cur_cc.display().to_string() ); self.tray.set_icon(&icn.icon); if !skip_tt { self.show_tooltip(Some(&icn.tooltip)); } let _ = img_dyn.insert(cfg_layer_pkey.clone(), Some(icn)); *icon_act_key = Some(cfg_layer_pkey); } else { warn!( "✗ Invalid icon file \"{cfg_icon_p}\" from this config: {}", cfg_layer_pkey.display().to_string() ); let _ = img_dyn.insert(cfg_layer_pkey.clone(), None); *icon_act_key = Some(cfg_layer_pkey); self.tray.set_icon(&self.icon); self.show_tooltip(None); } } else { warn!( "✗ Invalid icon path \"{cfg_icon_p}\" from this config: {}", cfg_layer_pkey.display().to_string() ); let _ = img_dyn.insert(cfg_layer_pkey.clone(), None); *icon_act_key = Some(cfg_layer_pkey); self.tray.set_icon(&self.icon); self.show_tooltip(None); } } } fn exit(&self) { let handlers = self.handlers_dyn.borrow(); for handler in handlers.iter() { nwg::unbind_event_handler(handler); } nwg::stop_thread_dispatch(); } fn build_win_tt(&self) -> Result { let f_style = wf::POPUP; let f_ex = WS_CLICK_THRU | WS_EX_NOACTIVATE //0x8000000L top-level win doesn't become foreground win on user click | WS_EX_TOOLWINDOW // remove from the taskbar (floating toolbar) ; let mut window: nwg::Window = Default::default(); let dpi = unsafe { nwg::dpi() }; let app_data = self.app_data.borrow(); let icn_sz_tt_i = ( app_data.gui_opts.tooltip_size.0, app_data.gui_opts.tooltip_size.1, ); let w = (icn_sz_tt_i.0 as f64 / (dpi as f64 / 96_f64)).round() as i32; let h = (icn_sz_tt_i.1 as f64 / (dpi as f64 / 96_f64)).round() as i32; trace!("Active Kanata Layer win size = {w}⋅{h}"); nwg::Window::builder() .title("Active Kanata Layer") .size((w, h)) .position((0, 0)) .center(false) .topmost(true) .maximized(false) .minimized(false) .flags(f_style) .ex_flags(f_ex) .icon(None) .accept_files(false) .build(&mut window)?; Ok(window) } } pub mod system_tray_ui { use super::*; use native_windows_gui::{self as nwg, MousePressEvent}; use std::cell::RefCell; use std::ops::Deref; use std::rc::Rc; use windows_sys::Win32::UI::Shell::SIID_DELETE; pub struct SystemTrayUi { inner: Rc, handler_def: RefCell>, } impl nwg::NativeUi for SystemTray { fn build_ui(mut d: SystemTray) -> Result { use nwg::Event as E; let app_data = d.app_data.borrow().clone(); d.tray_item_dyn = RefCell::new(Default::default()); d.handlers_dyn = RefCell::new(Default::default()); d.decoder = Default::default(); nwg::ImageDecoder::builder().build(&mut d.decoder)?; // Resources d.embed = Default::default(); d.embed = nwg::EmbedResource::load(None)?; nwg::Icon::builder() .source_embed(Some(&d.embed)) .source_embed_str(Some("iconMain")) .strict(true) /*use sys, not panic, if missing*/ .build(&mut d.icon)?; let (sndr, rcvr) = std::sync::mpsc::channel(); d.tt2m_channel = Some((sndr, rcvr)); let (sndr, rcvr) = std::sync::mpsc::channel(); d.err_recv = Some(rcvr); if GUI_ERR_MSG_TX.set(sndr).is_err() { warn!("Someone else set our ‘GUI_ERR_MSG_TX’"); }; // Controls nwg::MessageWindow::builder().build(&mut d.window)?; nwg::Notice::builder() .parent(&d.window) .build(&mut d.layer_notice)?; nwg::Notice::builder() .parent(&d.window) .build(&mut d.cfg_notice)?; nwg::Menu::builder() .parent(&d.window) .popup(true) /*context menu*/ // .build(&mut d.tray_menu)?; nwg::Notice::builder() .parent(&d.window) .build(&mut d.tt_notice)?; nwg::Notice::builder() .parent(&d.window) .build(&mut d.err_notice)?; nwg::Notice::builder() .parent(&d.window) .build(&mut d.exit_notice)?; nwg::Menu::builder() .parent(&d.tray_menu) .text("&F Load config") // .build(&mut d.tray_1cfg_m)?; nwg::MenuItem::builder() .parent(&d.tray_menu) .text("&R Reload config") // .build(&mut d.tray_2reload)?; nwg::MenuItem::builder() .parent(&d.tray_menu) .text("&X Exit\t‹⎈␠⎋") // .build(&mut d.tray_3exit)?; if app_data.gui_opts.tooltip_layer_changes { d.win_tt = d.build_win_tt().expect("Tooltip window"); nwg::AnimationTimer::builder() .parent(&d.window) .interval(Duration::from_millis( (app_data.gui_opts.tooltip_duration.saturating_add(1)).into(), )) .lifetime(Some(Duration::from_millis( (app_data.gui_opts.tooltip_duration + TTTIMER_L).into(), ))) .max_tick(None) .active(false) .build(&mut d.win_tt_timer)?; let icn_sz_tt_i = ( app_data.gui_opts.tooltip_size.0 as i32, app_data.gui_opts.tooltip_size.1 as i32, ); // todo: replace with a no-margin NWG config when it's available let padx = (app_data.gui_opts.tooltip_size.0 as f64 / 6_f64).round() as i32; let pady = (app_data.gui_opts.tooltip_size.1 as f64 / 6_f64).round() as i32; let pad = (-padx, -pady); trace!("kanata tooltip size = {icn_sz_tt_i:?}, offset = {padx}⋅{pady}"); let mut cfg_icon_bmp_tray = Default::default(); nwg::Bitmap::builder() .source_embed(Some(&d.embed)) .source_embed_str(Some("imgMain")) .strict(true) .size(Some(ICN_SZ_MENU.into())) .build(&mut cfg_icon_bmp_tray)?; nwg::ImageFrame::builder() .parent(&d.win_tt) .size(icn_sz_tt_i) .position(pad) .build(&mut d.win_tt_ifr)?; } let mut tmp_bitmap = Default::default(); nwg::Bitmap::builder() .source_embed(Some(&d.embed)) .source_embed_str(Some("imgReload")) .strict(true) .size(Some(ICN_SZ_MENU.into())) .build(&mut tmp_bitmap)?; let img_exit = nwg::Bitmap::from_system_icon(SIID_DELETE); d.tray_2reload.set_bitmap(Some(&tmp_bitmap)); d.tray_3exit.set_bitmap(Some(&img_exit)); d.img_reload = tmp_bitmap; d.img_exit = img_exit; let mut main_tray_icon_l = Default::default(); let mut main_tray_icon_is = false; { let mut tray_item_dyn = d.tray_item_dyn.borrow_mut(); let mut img_dyn = d.img_dyn.borrow_mut(); let mut icon_act_key = d.icon_act_key.borrow_mut(); let mut icon_0_key = d.icon_0_key.borrow_mut(); const MENU_ACC: &str = "1234567890ASDFGQWERTZXCVBYUIOPHJKLNM"; const M_E: usize = MENU_ACC.len() - 1; let layer0_icon_s = &app_data.layer0_icon.clone().unwrap_or("".to_string()); let cfg_icon_s = &app_data.cfg_icon.clone().unwrap_or("".to_string()); if !(app_data.cfg_p).is_empty() { for (i, cfg_p) in app_data.cfg_p.iter().enumerate() { let i_acc = match i { // accelerators from 1–0, A–Z starting from home row for easier presses 0..=M_E => format!("&{} ", &MENU_ACC[i..i + 1]), _ => " ".to_string(), }; let cfg_name = &cfg_p .file_name() .unwrap_or_else(|| OsStr::new("")) .to_string_lossy() .to_string(); //kanata.kbd let menu_text = format!("{cfg_name}\t{i_acc}"); // kanata.kbd &1 let mut menu_item = Default::default(); if i == 0 { nwg::MenuItem::builder() .parent(&d.tray_1cfg_m) .text(&menu_text) .check(true) .build(&mut menu_item)?; } else { nwg::MenuItem::builder() .parent(&d.tray_1cfg_m) .text(&menu_text) .build(&mut menu_item)?; } if i == 0 { // add icons if exists, hashed by config path // (for active config, others will create on load) if let Some(ico_p) = get_icon_p( layer0_icon_s, &app_data.layer0_name, cfg_icon_s, cfg_p, &app_data.gui_opts.icon_match_layer_name, ) { let mut cfg_layer_pkey = PathBuf::new(); // path key cfg_layer_pkey.push(cfg_p.clone()); cfg_layer_pkey.push(PRE_LAYER.to_owned() + &app_data.layer0_name); let cfg_layer_pkey_s = cfg_layer_pkey.display().to_string(); *icon_0_key = Some(cfg_layer_pkey.clone()); if let Ok(icn) = d.get_icon_from_file(&ico_p) { debug!("✓ main 0 config: using icon for {}", cfg_layer_pkey_s); main_tray_icon_l = icn.tray.copy_as_icon(); main_tray_icon_is = true; let _ = img_dyn.insert(cfg_layer_pkey, Some(icn)); } else { info!("✗ main 0 icon ✓ icon path, will be using DEFAULT icon for {:?}",cfg_p); let _ = img_dyn.insert(cfg_layer_pkey, None); } } else { debug!("✗ main 0 config: using DEFAULT icon for {:?}", cfg_p); let mut cfg_icon_bmp_tray = Default::default(); let mut cfg_icon_bmp_tt = Default::default(); let mut cfg_icon_bmp_icon = Default::default(); nwg::Bitmap::builder() .source_embed(Some(&d.embed)) .source_embed_str(Some("imgMain")) .strict(true) .size(Some(ICN_SZ_MENU.into())) .build(&mut cfg_icon_bmp_tray)?; nwg::Bitmap::builder() .source_embed(Some(&d.embed)) .source_embed_str(Some("imgMain")) .strict(true) .size(Some(ICN_SZ_TT.into())) .build(&mut cfg_icon_bmp_tt)?; nwg::Icon::builder() .source_embed(Some(&d.embed)) .source_embed_str(Some("iconMain")) .strict(true) .build(&mut cfg_icon_bmp_icon)?; let _ = img_dyn.insert( cfg_p.clone(), Some(Icn { tray: cfg_icon_bmp_tray, tooltip: cfg_icon_bmp_tt, icon: cfg_icon_bmp_icon, }), ); *icon_act_key = Some(cfg_p.clone()); } // Set tray menu config item icons, ignores layers since these // are per config if let Ok(icn) = d.set_menu_item_cfg_icon(&mut menu_item, cfg_icon_s, cfg_p) { // show currently active config's icon in the combo menu d.tray_1cfg_m.set_bitmap(Some(&icn.tray)); let _ = img_dyn.insert(cfg_p.clone(), Some(icn)); } else { let _ = img_dyn.insert(cfg_p.clone(), None); } } tray_item_dyn.push(menu_item); } } else { warn!("Didn't get any config paths from Kanata!") } } let main_tray_icon = match main_tray_icon_is { true => Some(&main_tray_icon_l), false => Some(&d.icon), }; nwg::TrayNotification::builder() .parent(&d.window) .icon(main_tray_icon) .tip(Some(&app_data.tooltip)) .build(&mut d.tray)?; let ui = SystemTrayUi { // Wrap-up inner: Rc::new(d), handler_def: Default::default(), }; let evt_ui = Rc::downgrade(&ui.inner); // Events let handle_events = move |evt, _evt_data, handle| { if let Some(evt_ui) = evt_ui.upgrade() { match evt { E::OnNotice => if handle == evt_ui.layer_notice { SystemTray::reload_layer_icon(&evt_ui); } else if handle == evt_ui.cfg_notice { SystemTray::reload_cfg_icon(&evt_ui); } else if handle == evt_ui.err_notice { SystemTray::notify_error(&evt_ui); } else if handle == evt_ui.exit_notice { SystemTray::exit(&evt_ui); } else if handle == evt_ui.tt_notice { SystemTray::update_tooltip_pos(&evt_ui);} E::OnWindowClose => if handle == evt_ui.window {SystemTray::exit (&evt_ui);} E::OnMousePress(MousePressEvent::MousePressLeftUp) => if handle == evt_ui.tray {SystemTray::show_menu(&evt_ui);} E::OnContextMenu/*🖰›*/ => if handle == evt_ui.tray {SystemTray::show_menu(&evt_ui);} E::OnTimerStop/*🕐*/ => {SystemTray::hide_tooltip(&evt_ui);} E::OnMenuHover => if handle == evt_ui.tray_1cfg_m { SystemTray::check_active(&evt_ui);} E::OnMenuItemSelected => if handle == evt_ui.tray_2reload { let _ = SystemTray::reload_cfg(&evt_ui,None); SystemTray::update_tray_icon_cfg_group(&evt_ui,true); } else if handle == evt_ui.tray_3exit {SystemTray::exit (&evt_ui); } else if let ControlHandle::MenuItem(_parent, _id) = handle { {let tray_item_dyn = &evt_ui.tray_item_dyn.borrow(); // for (i, h_cfg) in tray_item_dyn.iter().enumerate() { if &handle == h_cfg { for h_cfg_j in tray_item_dyn.iter() { if h_cfg_j.checked() {h_cfg_j.set_checked(false);} } // uncheck others h_cfg.set_checked(true); // check self let _ = SystemTray::reload_cfg(&evt_ui,Some(i)); // depends } } } } _ => {} } } }; ui.handler_def .borrow_mut() .push(nwg::full_bind_event_handler( &ui.window.handle, handle_events, )); Ok(ui) } } impl Drop for SystemTrayUi { /// To make sure that everything is freed without issues, the default handler /// must be unbound. fn drop(&mut self) { let mut handlers = self.handler_def.borrow_mut(); for handler in handlers.drain(0..) { nwg::unbind_event_handler(&handler); } } } impl Deref for SystemTrayUi { type Target = SystemTray; fn deref(&self) -> &Self::Target { &self.inner } } } use winapi::um::winuser::{ SetLayeredWindowAttributes, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, }; pub const WS_CLICK_THRU: u32 = WS_EX_LAYERED | WS_EX_TRANSPARENT; use nwg::WindowFlags as wf; /// Build a tooltip-like window to notify of user events fn show_layered_win(win_id: HWND) { use winapi::um::wingdi::RGB; use winapi::um::winuser::LWA_ALPHA; let cr_key: COLORREF = RGB(0, 0, 0); let b_alpha: BYTE = 255; let dw_flags: DWORD = LWA_ALPHA; unsafe { SetLayeredWindowAttributes(win_id, cr_key, b_alpha, dw_flags); } // layered window doesn't appear w/o this call } pub fn update_app_data(k: &MutexGuard) -> Result { let paths = &k.cfg_paths; let path_cur = &paths[0]; let layer0_id = k.layout.b().current_layer(); let layer0_name = &k.layer_info[layer0_id].name; let layer0_icon = &k.layer_info[layer0_id].icon; Ok(SystemTrayData { tooltip: path_cur.display().to_string(), cfg_p: paths.clone(), cfg_icon: k.gui_opts.tray_icon.clone(), layer0_name: layer0_name.clone(), layer0_icon: layer0_icon.clone(), gui_opts: k.gui_opts.clone(), tt_duration_pre: k.gui_opts.tooltip_duration, tt_size_pre: k.gui_opts.tooltip_size, }) } pub fn build_tray(cfg: &Arc>) -> Result { let k = cfg.lock(); let app_data = update_app_data(&k)?; let app = SystemTray { app_data: RefCell::new(app_data), ..Default::default() }; Ok(SystemTray::build_ui(app)?) } pub use log::*; pub use std::io::{stdout, IsTerminal}; pub use winapi::shared::minwindef::BOOL; pub use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; use once_cell::sync::Lazy; pub static IS_TERM: Lazy = Lazy::new(|| stdout().is_terminal()); pub static IS_CONSOLE: Lazy = Lazy::new(|| unsafe { AttachConsole(ATTACH_PARENT_PROCESS) != 0i32 }); kanata-1.9.0/src/gui/win_dbg_logger/mod.rs000064400000000000000000000244761046102023000165570ustar 00000000000000#![allow(non_upper_case_globals)] //! A logger for use with Windows debuggers. //! //! This crate integrates with the ubiquitous [`log`] crate and can be used with the [`simplelog`] crate. //! //! Windows allows applications to output a string directly to debuggers. This is very useful in //! situations where other forms of logging are not available. //! For example, stderr is not available for GUI apps. //! //! Windows provides the `OutputDebugString` entry point, which allows apps to print a debug string. //! Internally, `OutputDebugString` is implemented by raising an SEH exception, which the debugger //! catches and handles. //! //! Raising an exception has a significant cost, when run under a debugger, because the debugger //! halts all threads in the target process. So you should avoid using this logger for high rates //! of output, because doing so will slow down your app. //! //! Like many Windows entry points, `OutputDebugString` is actually two entry points: //! `OutputDebugStringA` (multi-byte encodings) and //! `OutputDebugStringW` (UTF-16). In most cases, the `*A` version is implemented using a "thunk" //! which converts its arguments to UTF-16 and then calls the `*W` version. However, //! `OutputDebugStringA` is one of the few entry points where the opposite is true. //! //! This crate can be compiled and used on non-Windows platforms, but it does nothing. //! This is intended to minimize the impact on code that takes a dependency on this crate. //! //! # Example //! //! ```rust //! use log::{debug, info}; //! //! fn do_cool_stuff() { //! info!("Hello, world!"); //! debug!("Hello, world, in detail!"); //! } //! //! fn main() { //! log::set_logger(&kanata_state_machine::gui::WINDBG_LOGGER).unwrap(); //! log::set_max_level(log::LevelFilter::Debug); //! //! do_cool_stuff(); //! } //! ``` use log::{Level, LevelFilter, Metadata, Record}; /// This implements `log::Log`, and so can be used as a logging provider. /// It forwards log messages to the Windows `OutputDebugString` API. #[derive(Copy, Clone)] pub struct WinDbgLogger { level: LevelFilter, /// Allow for `WinDbgLogger` to possibly have more fields in the future _priv: (), } /// This is a static instance of `WinDbgLogger`. Since `WinDbgLogger` contains no state, /// this can be directly registered using `log::set_logger`, e.g.: /// /// ``` /// log::set_logger(&kanata_state_machine::gui::WINDBG_LOGGER).unwrap(); // Initialize /// log::set_max_level(log::LevelFilter::Debug); /// /// use log::{info, debug}; // Import /// /// info!("Hello, world!"); debug!("Hello, world, in detail!"); // Use to log /// ``` pub static WINDBG_LOGGER: WinDbgLogger = WinDbgLogger { level: LevelFilter::Trace, _priv: (), }; pub static WINDBG_L1: WinDbgLogger = WinDbgLogger { level: LevelFilter::Error, _priv: (), }; pub static WINDBG_L2: WinDbgLogger = WinDbgLogger { level: LevelFilter::Warn, _priv: (), }; pub static WINDBG_L3: WinDbgLogger = WinDbgLogger { level: LevelFilter::Info, _priv: (), }; pub static WINDBG_L4: WinDbgLogger = WinDbgLogger { level: LevelFilter::Debug, _priv: (), }; pub static WINDBG_L5: WinDbgLogger = WinDbgLogger { level: LevelFilter::Trace, _priv: (), }; pub static WINDBG_L0: WinDbgLogger = WinDbgLogger { level: LevelFilter::Off, _priv: (), }; #[cfg(all(target_os = "windows", feature = "gui"))] pub fn windbg_simple_combo( log_lvl: LevelFilter, noti_lvl: LevelFilter, ) -> Box { set_noti_lvl(noti_lvl); match log_lvl { LevelFilter::Error => Box::new(WINDBG_L1), LevelFilter::Warn => Box::new(WINDBG_L2), LevelFilter::Info => Box::new(WINDBG_L3), LevelFilter::Debug => Box::new(WINDBG_L4), LevelFilter::Trace => Box::new(WINDBG_L5), LevelFilter::Off => Box::new(WINDBG_L0), } } #[cfg(all(target_os = "windows", feature = "gui"))] impl simplelog::SharedLogger for WinDbgLogger { // allows using with simplelog's CombinedLogger fn level(&self) -> LevelFilter { self.level } fn config(&self) -> Option<&simplelog::Config> { None } fn as_log(self: Box) -> Box { Box::new(*self) } } /// Convert logging levels to shorter and more visible icons pub fn iconify(lvl: log::Level) -> char { match lvl { Level::Error => '❗', Level::Warn => '⚠', Level::Info => 'ⓘ', Level::Debug => 'ⓓ', Level::Trace => 'ⓣ', } } use std::sync::OnceLock; pub fn is_thread_state() -> &'static bool { set_thread_state(false) } pub fn set_thread_state(is: bool) -> &'static bool { // accessor function to avoid get_or_init on every call // (lazycell allows doing that without an extra function) static CELL: OnceLock = OnceLock::new(); CELL.get_or_init(|| is) } pub fn get_noti_lvl() -> &'static LevelFilter { set_noti_lvl(LevelFilter::Off) } pub fn set_noti_lvl(lvl: LevelFilter) -> &'static LevelFilter { static CELL: OnceLock = OnceLock::new(); CELL.get_or_init(|| lvl) } use regex::Regex; macro_rules! regex { ($re:literal $(,)?) => {{ static RE: OnceLock = OnceLock::new(); RE.get_or_init(|| regex::Regex::new($re).unwrap()) }}; } fn clean_name(path: Option<&str>) -> String { let re_ext: &Regex = regex!(r"\..*$"); // shorten source file name, no src/ no .rs ext let re_src: &Regex = regex!(r"src[\\/]"); // remove extension and src paths if let Some(p) = path { re_src.replace(&re_ext.replace(p, ""), "").to_string() } else { "?".to_string() } } #[cfg(target_os = "windows")] use winapi::um::processthreadsapi::GetCurrentThreadId; impl log::Log for WinDbgLogger { fn enabled(&self, metadata: &Metadata) -> bool { metadata.level() <= self.level } fn log(&self, record: &Record) { #[cfg(not(target_os = "windows"))] let thread_id = ""; #[cfg(target_os = "windows")] let thread_id = if *is_thread_state() { format!("{}¦", unsafe { GetCurrentThreadId() }) } else { "".to_string() }; if self.enabled(record.metadata()) { let s = format!( "{}{}{}:{} {}", thread_id, iconify(record.level()), clean_name(record.file()), record.line().unwrap_or(0), record.args() ); #[cfg(all(target_os = "windows", feature = "gui"))] { use crate::gui::win::*; let title = format!( "{}{}:{}", thread_id, clean_name(record.file()), record.line().unwrap_or(0) ); let msg = format!("{}", record.args()); if record.level() <= *get_noti_lvl() { show_err_msg_nofail(title, msg); } } output_debug_string(&s); } } fn flush(&self) {} } /// Calls the `OutputDebugString` API to log a string. /// /// On non-Windows platforms, this function does nothing. /// /// See [`OutputDebugStringW`](https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-outputdebugstringw). pub fn output_debug_string(s: &str) { #[cfg(windows)] { let len = s.encode_utf16().count() + 1; let mut s_utf16: Vec = Vec::with_capacity(len); s_utf16.extend(s.encode_utf16()); s_utf16.push(0); unsafe { OutputDebugStringW(&s_utf16[0]); } } #[cfg(not(windows))] { let _ = s; } } #[cfg(windows)] extern "stdcall" { fn OutputDebugStringW(chars: *const u16); fn IsDebuggerPresent() -> i32; } /// Checks whether a debugger is attached to the current process. /// /// On non-Windows platforms, this function always returns `false`. /// /// See [`IsDebuggerPresent`](https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-isdebuggerpresent). pub fn is_debugger_present() -> bool { #[cfg(windows)] { unsafe { IsDebuggerPresent() != 0 } } #[cfg(not(windows))] { false } } /// Sets the `WinDbgLogger` as the currently-active logger. /// /// If an error occurs when registering `WinDbgLogger` as the current logger, this function will /// output a warning and will return normally. It will not panic. /// This behavior was chosen because `WinDbgLogger` is intended for use in debugging. /// Panicking would disrupt debugging and introduce new failure modes. It would also create /// problems for mixed-mode debugging, where Rust code is linked with C/C++ code. pub fn init() { match log::set_logger(&WINDBG_LOGGER) { Ok(()) => {} //↓ there's really nothing we can do about it. Err(_) => { output_debug_string( "Warning: Failed to register WinDbgLogger as the current Rust logger.\r\n", ); } } } macro_rules! define_init_at_level { ($func:ident, $level:ident) => { /// This can be called from C/C++ code to register the debug logger. /// /// For Windows DLLs that have statically linked an instance of `win_dbg_logger` into /// them, `DllMain` should call `win_dbg_logger_init_()` from the `DLL_PROCESS_ATTACH` /// handler, e.g.: /// /// ```ignore /// extern "C" void __cdecl rust_win_dbg_logger_init_debug(); // Calls into Rust code /// BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD reason, LPVOID reserved) { /// switch (reason) { /// case DLL_PROCESS_ATTACH: /// rust_win_dbg_logger_init_debug(); /// // ... /// } /// // ... /// } /// ``` /// /// For Windows executables that have statically linked an instance of `win_dbg_logger` /// into them, call `win_dbg_logger_init_()` during app startup. #[no_mangle] pub extern "C" fn $func() { init(); log::set_max_level(LevelFilter::$level); } }; } define_init_at_level!(rust_win_dbg_logger_init_trace, Trace); define_init_at_level!(rust_win_dbg_logger_init_info, Info); define_init_at_level!(rust_win_dbg_logger_init_debug, Debug); define_init_at_level!(rust_win_dbg_logger_init_warn, Warn); define_init_at_level!(rust_win_dbg_logger_init_error, Error); kanata-1.9.0/src/gui/win_dbg_logger/win_dbg_logger.toml000064400000000000000000000011061046102023000212600ustar 00000000000000[package] name = "win_dbg_logger" version = "0.1.0" authors = ["Arlie Davis "] edition = "2018" license = "MIT OR Apache-2.0" repository = "https://github.com/sivadeilra/win_dbg_logger" description = "A logger for use with Windows debuggers." # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] log = "0.4.*" winapi = {version="0.3.9", features=["processthreadsapi",]} regex = {version="1.10.4"} simplelog = {version="0.12.0", optional=true} [features] simple_shared = ["simplelog"] kanata-1.9.0/src/gui/win_nwg_ext/license-MIT000064400000000000000000000021331046102023000167700ustar 00000000000000The MIT License (MIT) ===================== Copyright © `2024` `Niccolò Betto` Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. kanata-1.9.0/src/gui/win_nwg_ext/license-nwg-MIT000064400000000000000000000020551046102023000175640ustar 00000000000000MIT License Copyright (c) 2019 Gabriel Dube Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. kanata-1.9.0/src/gui/win_nwg_ext/mod.rs000064400000000000000000000172121046102023000161250ustar 00000000000000// based on https://github.com/lynxnb/wsl-usb-manager/blob/master/src/gui/nwg_ext.rs use native_windows_gui as nwg; use native_windows_gui::ControlHandle; use std::{mem::size_of, ptr}; use winapi::ctypes::c_int; use winapi::shared::windef::HWND; use windows_sys::Win32::Foundation::HANDLE; use windows_sys::Win32::Graphics::Gdi::DeleteObject; use windows_sys::Win32::UI::Shell::{ SHGetStockIconInfo, SHGSI_ICON, SHGSI_SMALLICON, SHSTOCKICONID, SHSTOCKICONINFO, }; use windows_sys::Win32::UI::WindowsAndMessaging::{ CopyImage, DestroyIcon, GetIconInfoExW, SetMenuItemInfoW, HMENU, ICONINFOEXW, IMAGE_BITMAP, LR_CREATEDIBSECTION, MENUITEMINFOW, MF_BYCOMMAND, MIIM_BITMAP, }; /// Extends [`nwg::Bitmap`] with additional functionality. pub trait BitmapEx { fn from_system_icon(icon: SHSTOCKICONID) -> nwg::Bitmap; } impl BitmapEx for nwg::Bitmap { /// Creates a bitmap from a [`SHSTOCKICONID`] system icon ID. fn from_system_icon(icon: SHSTOCKICONID) -> nwg::Bitmap { // Retrieve the icon let mut stock_icon_info = SHSTOCKICONINFO { cbSize: std::mem::size_of::() as u32, hIcon: 0, iSysImageIndex: 0, iIcon: 0, szPath: [0; 260], }; unsafe { SHGetStockIconInfo( icon, SHGSI_ICON | SHGSI_SMALLICON, &mut stock_icon_info as *mut _, ); } // Retrieve the bitmap for the icon let mut icon_info = ICONINFOEXW { cbSize: std::mem::size_of::() as u32, fIcon: 0, xHotspot: 0, yHotspot: 0, hbmMask: 0, hbmColor: 0, wResID: 0, szModName: [0; 260], szResName: [0; 260], }; unsafe { GetIconInfoExW(stock_icon_info.hIcon, &mut icon_info as *mut _); } // Create a copy of the bitmap with transparent background from the icon bitmap let hbitmap = unsafe { CopyImage( icon_info.hbmColor as HANDLE, IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION, ) }; // Delete the unused icon and bitmaps unsafe { DeleteObject(icon_info.hbmMask); DeleteObject(icon_info.hbmColor); DestroyIcon(stock_icon_info.hIcon); }; if hbitmap == 0 { panic!("Failed to create bitmap from system icon"); } else { #[allow(unused)] struct Bitmap { handle: HANDLE, owned: bool, } let bitmap = Bitmap { handle: hbitmap as HANDLE, owned: true, }; // Ugly hack to set the private `owned` field inside nwg::Bitmap to true #[allow(clippy::missing_transmute_annotations)] unsafe { std::mem::transmute(bitmap) } } } } /// Extends [`nwg::Menu`] with additional functionality. pub trait MenuEx { fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>); } impl MenuEx for nwg::Menu { /// Sets a bitmap to be displayed on a menu. Pass `None` to remove the bitmap fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>) { let (hmenu_par, hmenu) = self.handle.hmenu().unwrap(); let hbitmap = match bitmap { Some(b) => b.handle as HANDLE, None => 0, }; let menu_item_info = MENUITEMINFOW { cbSize: size_of::() as u32, fMask: MIIM_BITMAP, hbmpItem: hbitmap, fType: 0, fState: 0, hSubMenu: 0, hbmpChecked: 0, hbmpUnchecked: 0, dwTypeData: ptr::null_mut(), wID: 0, dwItemData: 0, cch: 0, }; unsafe { SetMenuItemInfoW( hmenu_par as HMENU, hmenu as u32, MF_BYCOMMAND as i32, &menu_item_info as *const _, ); } } } /// Extends [`nwg::MenuItem`] with additional functionality. pub trait MenuItemEx { fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>); } impl MenuItemEx for nwg::MenuItem { /// Sets a bitmap to be displayed on a menu item. Pass `None` to remove the bitmap. fn set_bitmap(&self, bitmap: Option<&nwg::Bitmap>) { let (hmenu, item_id) = self.handle.hmenu_item().unwrap(); let hbitmap = match bitmap { Some(b) => b.handle as HANDLE, None => 0, }; let menu_item_info = MENUITEMINFOW { cbSize: std::mem::size_of::() as u32, fMask: MIIM_BITMAP, fType: 0, fState: 0, wID: 0, hSubMenu: 0, hbmpChecked: 0, hbmpUnchecked: 0, dwItemData: 0, dwTypeData: std::ptr::null_mut(), cch: 0, hbmpItem: hbitmap, }; unsafe { SetMenuItemInfoW( hmenu as HMENU, item_id, MF_BYCOMMAND as i32, &menu_item_info as *const _, ); } } } pub trait WindowEx { fn set_position_ex(&self, x: i32, y: i32); } pub fn dpi() -> i32 { // prevents GDI DC resource leak use winapi::um::wingdi::GetDeviceCaps; use winapi::um::wingdi::LOGPIXELSX; use winapi::um::winuser::{GetDC, ReleaseDC}; let screen = unsafe { GetDC(std::ptr::null_mut()) }; let dpi = unsafe { GetDeviceCaps(screen, LOGPIXELSX) }; let _ = unsafe { ReleaseDC(std::ptr::null_mut(), screen) }; dpi } pub fn logical_to_physical(x: i32, y: i32) -> (i32, i32) { use muldiv::MulDiv; use winapi::um::winuser::USER_DEFAULT_SCREEN_DPI; let dpi = dpi(); let x = x.mul_div_round(dpi, USER_DEFAULT_SCREEN_DPI).unwrap_or(x); let y = y.mul_div_round(dpi, USER_DEFAULT_SCREEN_DPI).unwrap_or(y); (x, y) } /// # Safety /// The `handle` param must be a valid pointer to a window handle returned by some winapi call. /// Failure to do so probably won't be UB because the handle is passed to a WinAPI call /// which is expected to handle these cases safely, but seems worth noting anyway. pub unsafe fn set_window_position(handle: HWND, x: i32, y: i32) { use winapi::um::winuser::SetWindowPos; use winapi::um::winuser::{SWP_NOACTIVATE, SWP_NOOWNERZORDER, SWP_NOSIZE, SWP_NOZORDER}; let (x, y) = logical_to_physical(x, y); unsafe { SetWindowPos( handle, ptr::null_mut(), x as c_int, y as c_int, 0, 0, SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER, ); } } const NOT_BOUND: &str = "Window is not yet bound to a winapi object"; const BAD_HANDLE: &str = "INTERNAL ERROR: Window handle is not HWND!"; pub fn check_hwnd(handle: &ControlHandle, not_bound: &str, bad_handle: &str) -> HWND { use winapi::um::winuser::IsWindow; if handle.blank() { panic!("{}", not_bound); } match handle.hwnd() { Some(hwnd) => match unsafe { IsWindow(hwnd) } { 0 => { panic!("The window handle is no longer valid. This usually means the control was freed by the OS"); } _ => hwnd, }, None => { panic!("{}", bad_handle); } } } impl WindowEx for nwg::Window { /// Set the position of the button in the parent window fn set_position_ex(&self, x: i32, y: i32) { let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE); unsafe { set_window_position(handle, x, y) } } } kanata-1.9.0/src/kanata/caps_word.rs000064400000000000000000000044001046102023000154450ustar 00000000000000use kanata_keyberon::key_code::KeyCode; use rustc_hash::FxHashSet as HashSet; use kanata_parser::custom_action::CapsWordCfg; #[derive(Debug)] pub struct CapsWordState { /// Keys that will trigger an `lsft` key to be added to the active keys if present in the /// currently active keys. pub keys_to_capitalize: HashSet, /// An extra list of keys that should **not** terminate the caps_word state, in addition to /// keys_to_capitalize, but which don't trigger a capitalization. pub keys_nonterminal: HashSet, /// The configured timeout for caps_word. pub timeout: u16, /// The number of ticks remaining for caps_word, after which its state should be cleared. The /// number of ticks gets reset back to `timeout` when `maybe_add_lsft` is called. The reason /// for having this timeout at all is in case somebody was in the middle of typing a word, had /// to go do something, and forgot that caps_word was active. Having this timeout means that /// shift won't be active for their next keypress. pub timeout_ticks: u16, } #[derive(PartialEq, Eq, Debug, Clone, Copy)] pub enum CapsWordNextState { Active, End, } use CapsWordNextState::*; impl CapsWordState { pub(crate) fn new(cfg: &CapsWordCfg) -> Self { Self { keys_to_capitalize: cfg.keys_to_capitalize.iter().copied().collect(), keys_nonterminal: cfg.keys_nonterminal.iter().copied().collect(), timeout: cfg.timeout, timeout_ticks: cfg.timeout, } } pub(crate) fn maybe_add_lsft(&mut self, active_keys: &mut Vec) -> CapsWordNextState { if self.timeout_ticks == 0 { return End; } for kc in active_keys.iter() { if !self.keys_to_capitalize.contains(kc) && !self.keys_nonterminal.contains(kc) { return End; } } if active_keys .last() .map(|kc| self.keys_to_capitalize.contains(kc)) .unwrap_or(false) { active_keys.insert(0, KeyCode::LShift); } if !active_keys.is_empty() { self.timeout_ticks = self.timeout; } self.timeout_ticks = self.timeout_ticks.saturating_sub(1); Active } } kanata-1.9.0/src/kanata/cfg_forced.rs000064400000000000000000000012701046102023000155470ustar 00000000000000//! Options in the configuration file that are overidden/forced to some value other than what's in //! the configuration file, with the primary example being CLI arguments. use std::sync::OnceLock; static LOG_LAYER_CHANGES: OnceLock = OnceLock::new(); /// Force the log_layer_changes configuration to some value. /// This can only be called up to once. Panics if called a second time. pub fn force_log_layer_changes(v: bool) { LOG_LAYER_CHANGES .set(v) .expect("force cfg fns can only be called once"); } /// Get the forced log_layer_changes configuration if it was set. pub fn get_forced_log_layer_changes() -> Option { LOG_LAYER_CHANGES.get().copied() } kanata-1.9.0/src/kanata/clipboard.rs000064400000000000000000000244341046102023000154340ustar 00000000000000use super::*; #[cfg(not(target_arch = "wasm32"))] pub use real::*; #[cfg(not(target_arch = "wasm32"))] mod real { use super::*; use std::sync::LazyLock; use parking_lot::Mutex; pub type SavedClipboardData = HashMap; #[derive(Debug, Clone)] pub enum ClipboardData { Text(String), Image(arboard::ImageData<'static>), } use ClipboardData::*; static CLIPBOARD: LazyLock> = LazyLock::new(|| { for _ in 0..10 { let c = arboard::Clipboard::new(); if let Ok(goodclip) = c { log::trace!("clipboard init"); return Mutex::new(goodclip); } std::thread::sleep(std::time::Duration::from_millis(25)); } panic!("could not initialize clipboard"); }); pub(crate) fn clpb_set(clipboard_string: &str) { for _ in 0..10 { match CLIPBOARD.lock().set_text(clipboard_string) { Ok(()) => { log::trace!("clipboard set to {clipboard_string}"); return; } Err(e) => { log::error!("error setting clipboard: {e:?}"); } } std::thread::sleep(std::time::Duration::from_millis(25)); } } pub(crate) fn clpb_cmd_set(cmd_and_args: &[String]) { let mut newclip = None; for _ in 0..10 { match CLIPBOARD.lock().get_text() { Ok(cliptext) => { newclip = Some(run_cmd_get_stdout(cmd_and_args, cliptext.as_str())); break; } Err(e) => { if matches!(e, arboard::Error::ContentNotAvailable) { log::warn!("clipboard is unset or is image data; no-op for cmd-set"); return; } log::error!("error setting clipboard: {e:?}"); } } std::thread::sleep(std::time::Duration::from_millis(25)); } if let Some(nc) = newclip { clpb_set(&nc); } } fn run_cmd_get_stdout(cmd_and_args: &[String], stdin: &str) -> String { use std::io::Write; use std::process::{Command, Stdio}; let mut args = cmd_and_args.iter(); let executable = args .next() .expect("parsing should have forbidden empty cmd"); log::trace!("executing {executable}"); let mut cmd = Command::new(executable); cmd.stdin(Stdio::piped()).stdout(Stdio::piped()); for arg in args { log::trace!("arg is {arg}"); cmd.arg(arg); } let mut child = match cmd.spawn() { Ok(c) => c, Err(e) => { log::warn!("failed to spawn cmd, returning empty string for cmd-set: {e:?}"); return String::new(); } }; let child_stdin = child.stdin.as_mut().unwrap(); if let Err(e) = child_stdin.write_all(stdin.as_bytes()) { log::warn!("failed to write to stdin: {e:?}"); } child .wait_with_output() .map(|out| String::from_utf8_lossy(&out.stdout).to_string()) .unwrap_or_else(|e| { log::error!("failed to execute cmd: {e:?}"); String::new() }) } pub(crate) fn clpb_save(id: u16, save_data: &mut SavedClipboardData) { for _ in 0..10 { match CLIPBOARD.lock().get_text() { Ok(cliptext) => { log::trace!("saving to id {id}: {cliptext}"); save_data.insert(id, Text(cliptext)); return; } Err(e) => { if matches!(e, arboard::Error::ContentNotAvailable) { // ContentNotAvailable could be an image or missing data break; } log::error!("error setting clipboard: {e:?}"); } } std::thread::sleep(std::time::Duration::from_millis(25)); } for _ in 0..10 { match CLIPBOARD.lock().get_image() { Ok(clipimg) => { log::trace!("saving to id {id}: "); save_data.insert(id, Image(clipimg)); } Err(e) => { if matches!(e, arboard::Error::ContentNotAvailable) { break; } log::error!("error setting clipboard: {e:?}"); } } std::thread::sleep(std::time::Duration::from_millis(25)); } } pub(crate) fn clpb_restore(id: u16, save_data: &SavedClipboardData) { let Some(restore_data) = save_data.get(&id) else { log::warn!("tried to set clipboard with missing data in id {id}, doing nothing"); return; }; for _ in 0..10 { let e = match restore_data { Text(s) => match CLIPBOARD.lock().set_text(s) { Ok(()) => { log::trace!("restored clipboard with id {id}: {s}"); return; } Err(e) => e, }, Image(img) => match CLIPBOARD.lock().set_image(img.clone()) { Ok(()) => { log::trace!("restored clipboard with id {id}: "); return; } Err(e) => e, }, }; log::error!("error setting clipboard: {e:?}"); std::thread::sleep(std::time::Duration::from_millis(25)); } } pub(crate) fn clpb_save_set(id: u16, content: &str, save_data: &mut SavedClipboardData) { log::trace!("setting save id {id} with {content}"); save_data.insert(id, Text(content.into())); } #[test] fn test_set() { let mut sd = SavedClipboardData::default(); clpb_save_set(1, "hi", &mut sd); if let Text(s) = sd.get(&1).unwrap() { assert_eq!(s.as_str(), "hi"); } else { panic!("did not expect image data"); } assert!(!sd.contains_key(&2)); } pub(crate) fn clpb_save_cmd_set( id: u16, cmd_and_args: &[String], save_data: &mut SavedClipboardData, ) { let stdin_content = match save_data.get(&id) { Some(slot_data) => match slot_data { Text(s) => s.as_str(), Image(_) => "", }, None => "", }; let content = run_cmd_get_stdout(cmd_and_args, stdin_content); log::trace!("setting save id {id} with {content}"); save_data.insert(id, Text(content)); } #[test] #[cfg(target_os = "windows")] fn test_save_cmd_set() { let mut sd = SavedClipboardData::default(); sd.insert(1, Text("one".into())); clpb_save_cmd_set( 1, &[ "powershell.exe".into(), "-c".into(), "$v = ($Input | Select-Object -First 1); Write-Host -NoNewLine \"$v $v\"".into(), ], &mut sd, ); if let Text(s) = sd.get(&1).unwrap() { assert_eq!("one one", s.as_str()); } else { panic!("did not expect image data"); } assert!(!sd.contains_key(&2)); clpb_save_cmd_set( 3, &[ "powershell.exe".into(), "-c".into(), "Write-Host -NoNewLine 'wat'".into(), ], &mut sd, ); if let Text(s) = sd.get(&3).unwrap() { assert_eq!("wat", s.as_str()); } else { panic!("did not expect image data"); } } pub(crate) fn clpb_save_swap(id1: u16, id2: u16, save_data: &mut SavedClipboardData) { let data1 = save_data.remove(&id1); let data2 = save_data.remove(&id2); if let Some(d) = data1 { save_data.insert(id2, d); } if let Some(d) = data2 { save_data.insert(id1, d); } } #[test] fn test_swap() { let mut sd = SavedClipboardData::default(); sd.insert(1, Text("one".into())); sd.insert(2, Text("two".into())); clpb_save_swap(1, 2, &mut sd); if let Text(s) = sd.get(&1).unwrap() { assert_eq!(s.as_str(), "two"); } else { panic!("did not expect image data"); } if let Text(s) = sd.get(&2).unwrap() { assert_eq!(s.as_str(), "one"); } else { panic!("did not expect image data"); } sd.insert(3, Text("three".into())); clpb_save_swap(3, 4, &mut sd); assert!(!sd.contains_key(&3)); if let Text(s) = sd.get(&4).unwrap() { assert_eq!(s.as_str(), "three"); } else { panic!("did not expect image data"); } sd.insert(6, Text("six".into())); clpb_save_swap(5, 6, &mut sd); if let Text(s) = sd.get(&5).unwrap() { assert_eq!(s.as_str(), "six"); } else { panic!("did not expect image data"); } assert!(!sd.contains_key(&6)); clpb_save_swap(7, 8, &mut sd); assert!(!sd.contains_key(&7)); assert!(!sd.contains_key(&8)); } } #[cfg(target_arch = "wasm32")] pub use fake::*; #[cfg(target_arch = "wasm32")] mod fake { #![allow(unused)] use super::*; pub type SavedClipboardData = HashMap; #[derive(Debug, Clone)] pub enum ClipboardData { Text(String), Text2(String), } pub(crate) fn clpb_set(clipboard_string: &str) {} pub(crate) fn clpb_cmd_set(cmd_and_args: &[String]) {} pub(crate) fn clpb_save(id: u16, save_data: &mut SavedClipboardData) {} pub(crate) fn clpb_restore(id: u16, save_data: &SavedClipboardData) {} pub(crate) fn clpb_save_set(id: u16, content: &str, save_data: &mut SavedClipboardData) {} pub(crate) fn clpb_save_cmd_set( id: u16, cmd_and_args: &[String], save_data: &mut SavedClipboardData, ) { } pub(crate) fn clpb_save_swap(id1: u16, id2: u16, save_data: &mut SavedClipboardData) {} } kanata-1.9.0/src/kanata/cmd.rs000064400000000000000000000165671046102023000142500ustar 00000000000000#![cfg_attr(feature = "simulated_output", allow(dead_code, unused_imports))] use std::fmt::Write; use kanata_parser::cfg::parse_mod_prefix; use kanata_parser::cfg::sexpr::*; use kanata_parser::keys::*; // local log prefix const LP: &str = "cmd-out:"; #[cfg(not(feature = "simulated_output"))] pub(super) fn run_cmd_in_thread( cmd_and_args: Vec, log_level: Option, error_log_level: Option, ) -> std::thread::JoinHandle<()> { std::thread::spawn(move || { let mut args = cmd_and_args.iter(); let mut printable_cmd = String::new(); let executable = args .next() .expect("parsing should have forbidden empty cmd"); write!( printable_cmd, "Program: {}, Arguments:", executable.as_str() ) .expect("write to string should succeed"); let mut cmd = std::process::Command::new(executable); for arg in args { cmd.arg(arg); printable_cmd.push(' '); printable_cmd.push_str(arg.as_str()); } if let Some(level) = log_level { log::log!(level, "Running cmd: {}", printable_cmd); } match cmd.output() { Ok(output) => { if let Some(level) = log_level { log::log!( level, "Successfully ran cmd: {}\nstdout:\n{}\nstderr:\n{}", printable_cmd, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); }; } Err(e) => { if let Some(level) = error_log_level { log::log!( level, "Failed to execute program {:?}: {}", cmd.get_program(), e ) } } }; }) } pub(super) type Item = KeyAction; #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(super) enum KeyAction { Press(OsCode), Release(OsCode), Delay(u16), } use kanata_keyberon::key_code::KeyCode; use KeyAction::*; fn empty() -> std::vec::IntoIter { vec![].into_iter() } fn from_sexpr(sexpr: Vec) -> std::vec::IntoIter { let mut items = vec![]; let mut remainder = sexpr.as_slice(); while !remainder.is_empty() { remainder = parse_items(remainder, &mut items); } items.into_iter() } fn parse_items<'a>(exprs: &'a [SExpr], items: &mut Vec) -> &'a [SExpr] { match &exprs[0] { SExpr::Atom(osc) => match str_to_oscode(&osc.t) { Some(osc) => { items.push(Press(osc)); items.push(Release(osc)); &exprs[1..] } None => { use std::str::FromStr; match u16::from_str(&osc.t) { Ok(delay) => { items.push(Delay(delay)); &exprs[1..] } Err(_) => try_parse_chord(&osc.t, exprs, items), } } }, SExpr::List(sexprs) => { let mut remainder = sexprs.t.as_slice(); while !remainder.is_empty() { remainder = parse_items(remainder, items); } &exprs[1..] } } } fn try_parse_chord<'a>(chord: &str, exprs: &'a [SExpr], items: &mut Vec) -> &'a [SExpr] { match parse_mod_prefix(chord) { Ok((mods, osc)) => match osc.is_empty() { true => try_parse_chorded_list(&mods, chord, &exprs[1..], items), false => { try_parse_chorded_key(&mods, osc, chord, items); &exprs[1..] } }, Err(e) => { log::warn!("{LP} found invalid chord {chord}: {}", e.msg); &exprs[1..] } } } fn try_parse_chorded_key(mods: &[KeyCode], osc: &str, chord: &str, items: &mut Vec) { if mods.is_empty() { log::warn!("{LP} found invalid key: {osc}"); return; } match str_to_oscode(osc) { Some(osc) => { for mod_kc in mods.iter().copied() { items.push(Press(mod_kc.into())); } items.push(Press(osc)); for mod_kc in mods.iter().copied() { items.push(Release(mod_kc.into())); } items.push(Release(osc)); } None => { log::warn!("{LP} found chord {chord} with invalid key: {osc}"); } }; } fn try_parse_chorded_list<'a>( mods: &[KeyCode], chord: &str, exprs: &'a [SExpr], items: &mut Vec, ) -> &'a [SExpr] { if exprs.is_empty() { log::warn!( "{LP} found chord modifiers with no attached key or list - ignoring it: {chord}" ); return exprs; } match &exprs[0] { SExpr::Atom(osc) => { log::warn!("{LP} expected list after {chord}, got string {}", &osc.t); exprs } SExpr::List(subexprs) => { for mod_kc in mods.iter().copied() { items.push(Press(mod_kc.into())); } let mut remainder = subexprs.t.as_slice(); while !remainder.is_empty() { remainder = parse_items(remainder, items); } for mod_kc in mods.iter().copied() { items.push(Release(mod_kc.into())); } &exprs[1..] } } } #[cfg(not(feature = "simulated_output"))] pub(super) fn keys_for_cmd_output(cmd_and_args: &[String]) -> impl Iterator { let mut args = cmd_and_args.iter(); let mut cmd = std::process::Command::new( args.next() .expect("parsing should have forbidden empty cmd"), ); for arg in args { cmd.arg(arg); } let output = match cmd.output() { Ok(o) => o, Err(e) => { log::error!("Failed to execute cmd: {e}"); return empty(); } }; log::debug!("{LP} stderr: {}", String::from_utf8_lossy(&output.stderr)); let stdout = String::from_utf8_lossy(&output.stdout); match parse(&stdout, "cmd") { Ok(lists) => match lists.len() { 0 => { log::warn!("{LP} got zero top-level S-expression from cmd, expected 1:\n{stdout}"); empty() } 1 => from_sexpr(lists.into_iter().next().expect("len 1").t), _ => { log::warn!( "{LP} got multiple top-level S-expression from cmd, expected 1:\n{stdout}" ); empty() } }, Err(e) => { log::warn!( "{LP} could not parse an S-expression from cmd:\n{stdout}\n{}", e.msg ); empty() } } } #[cfg(feature = "simulated_output")] pub(super) fn keys_for_cmd_output(cmd_and_args: &[String]) -> impl Iterator { println!("cmd-keys:{cmd_and_args:?}"); [].iter().copied() } #[cfg(feature = "simulated_output")] pub(super) fn run_cmd_in_thread( cmd_and_args: Vec, _log_level: Option, _error_log_level: Option, ) -> std::thread::JoinHandle<()> { std::thread::spawn(move || { println!("cmd:{cmd_and_args:?}"); }) } kanata-1.9.0/src/kanata/dynamic_macro.rs000064400000000000000000000264551046102023000163070ustar 00000000000000use std::collections::VecDeque; use kanata_keyberon::layout::Event; use kanata_parser::cfg::ReplayDelayBehaviour; use kanata_parser::keys::OsCode; use rustc_hash::FxHashMap as HashMap; use rustc_hash::FxHashSet as HashSet; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum DynamicMacroItem { Press((OsCode, u16)), Release((OsCode, u16)), EndMacro(u16), } pub struct DynamicMacroReplayState { active_macros: HashSet, delay_remaining: u16, macro_items: VecDeque, } pub struct DynamicMacroRecordState { starting_macro_id: u16, waiting_event: Option<(OsCode, WaitingEventType)>, macro_items: Vec, current_delay: u16, } enum WaitingEventType { Press, Release, } impl DynamicMacroRecordState { fn new(macro_id: u16) -> Self { Self { starting_macro_id: macro_id, waiting_event: None, macro_items: vec![], current_delay: 0, } } fn add_release_for_all_unreleased_presses(&mut self) { let mut pressed_oscs = HashSet::default(); for item in self.macro_items.iter() { match item { DynamicMacroItem::Press((osc, _)) => { pressed_oscs.insert(*osc); } DynamicMacroItem::Release((osc, _)) => { pressed_oscs.remove(osc); } DynamicMacroItem::EndMacro(_) => {} }; } // Hopefully release order doesn't matter here. A HashSet is being used, meaning release order is arbitrary. for osc in pressed_oscs.into_iter() { self.macro_items.push(DynamicMacroItem::Release((osc, 0))); } } fn add_event(&mut self, osc: OsCode, evtype: WaitingEventType) { if let Some(pending_event) = self.waiting_event.take() { match pending_event.1 { WaitingEventType::Press => self.macro_items.push(DynamicMacroItem::Press(( pending_event.0, self.current_delay, ))), WaitingEventType::Release => self.macro_items.push(DynamicMacroItem::Release(( pending_event.0, self.current_delay, ))), }; } self.current_delay = 0; self.waiting_event = Some((osc, evtype)); } } /// A replay event for a dynamically recorded macro. /// Note that the key event and the subsequent delay must be processed together. /// Otherwise there will be real-world time gap between event and the delay, /// which results in an inaccurate simulation of the keyberon state machine. #[derive(Clone, Copy, PartialEq, Eq)] pub struct ReplayEvent(Event, u16); impl ReplayEvent { pub fn key_event(self) -> Event { self.0 } pub fn delay(self) -> u16 { self.1 } } pub fn tick_record_state(record_state: &mut Option) { if let Some(state) = record_state { state.current_delay = state.current_delay.saturating_add(1); } } #[derive(Clone, Copy, PartialEq, Eq)] pub struct ReplayBehaviour { pub delay: ReplayDelayBehaviour, } pub fn tick_replay_state( replay_state: &mut Option, replay_behaviour: ReplayBehaviour, ) -> Option { if let Some(state) = replay_state { state.delay_remaining = state.delay_remaining.saturating_sub(1); if state.delay_remaining == 0 { state.delay_remaining = 5; match state.macro_items.pop_front() { None => { *replay_state = None; log::debug!("finished macro replay"); None } Some(i) => match i { DynamicMacroItem::Press((key, delay)) => { let event = Event::Press(0, key.into()); let delay = match replay_behaviour.delay { ReplayDelayBehaviour::Constant => 0, ReplayDelayBehaviour::Recorded => { state.delay_remaining = delay; delay } }; Some(ReplayEvent(event, delay)) } DynamicMacroItem::Release((key, delay)) => { let event = Event::Release(0, key.into()); let delay = match replay_behaviour.delay { ReplayDelayBehaviour::Constant => 0, ReplayDelayBehaviour::Recorded => { state.delay_remaining = delay; delay } }; Some(ReplayEvent(event, delay)) } DynamicMacroItem::EndMacro(macro_id) => { state.active_macros.remove(¯o_id); None } }, } } else { None } } else { None } } pub fn begin_record_macro( macro_id: u16, record_state: &mut Option, ) -> Option<(u16, Vec)> { match record_state.take() { None => { log::info!("starting dynamic macro {macro_id} recording"); *record_state = Some(DynamicMacroRecordState::new(macro_id)); None } Some(mut state) => { if let Some(pending_event) = state.waiting_event.take() { match pending_event.1 { WaitingEventType::Press => state.macro_items.push(DynamicMacroItem::Press(( pending_event.0, state.current_delay, ))), WaitingEventType::Release => state.macro_items.push(DynamicMacroItem::Release( (pending_event.0, state.current_delay), )), }; } // remove the last item, since it's almost certainly a "macro // record" key press action which we don't want to keep. state.macro_items.remove(state.macro_items.len() - 1); state.add_release_for_all_unreleased_presses(); if state.starting_macro_id == macro_id { log::info!( "same macro id pressed. saving and stopping dynamic macro {} recording", state.starting_macro_id ); *record_state = None; } else { log::info!( "saving dynamic macro {} recording then starting new macro recording {macro_id}", state.starting_macro_id, ); *record_state = Some(DynamicMacroRecordState::new(macro_id)); } Some((state.starting_macro_id, state.macro_items)) } } } pub fn record_press( record_state: &mut Option, osc: OsCode, max_presses: u16, ) -> Option<(u16, Vec)> { if let Some(state) = record_state { // This is not 100% accurate since there may be multiple presses before any of // their relesease are received. But it's probably good enough in practice. // // The presses are defined so that a user cares about the number of keys rather // than events. So rather than the user multiplying by 2 in their config after // considering the number of keys they want, kanata does the multiplication // instead. if state.macro_items.len() > usize::from(max_presses) * 2 { log::warn!( "saving and stopping dynamic macro {} recording due to exceeding limit", state.starting_macro_id, ); state.add_release_for_all_unreleased_presses(); let state = record_state.take().unwrap(); Some((state.starting_macro_id, state.macro_items)) } else { log::debug!("delay to press: {}", state.current_delay); state.add_event(osc, WaitingEventType::Press); None } } else { None } } pub fn record_release(record_state: &mut Option, osc: OsCode) { if let Some(state) = record_state { log::debug!("delay to release: {}", state.current_delay); state.add_event(osc, WaitingEventType::Release); } } pub fn stop_macro( record_state: &mut Option, num_actions_to_remove: u16, ) -> Option<(u16, Vec)> { if let Some(mut state) = record_state.take() { if let Some(pending_event) = state.waiting_event.take() { match pending_event.1 { WaitingEventType::Press => state.macro_items.push(DynamicMacroItem::Press(( pending_event.0, state.current_delay, ))), WaitingEventType::Release => state.macro_items.push(DynamicMacroItem::Release(( pending_event.0, state.current_delay, ))), }; } // remove the last item independently of `num_actions_to_remove` // since it's almost certainly a "macro record stop" key press // action which we don't want to keep. state.macro_items.remove(state.macro_items.len() - 1); log::info!( "saving and stopping dynamic macro {} recording with {num_actions_to_remove} actions at the end removed", state.starting_macro_id, ); state.macro_items.truncate( state .macro_items .len() .saturating_sub(usize::from(num_actions_to_remove)), ); state.add_release_for_all_unreleased_presses(); Some((state.starting_macro_id, state.macro_items)) } else { None } } pub fn play_macro( macro_id: u16, replay_state: &mut Option, recorded_macros: &HashMap>, ) { match replay_state { None => { log::info!("replaying macro {macro_id}"); *replay_state = recorded_macros.get(¯o_id).map(|macro_items| { let mut active_macros = HashSet::default(); active_macros.insert(macro_id); log::debug!("playing macro {macro_items:?}"); DynamicMacroReplayState { active_macros, delay_remaining: 0, macro_items: macro_items.clone().into(), } }); } Some(state) => { if state.active_macros.contains(¯o_id) { log::warn!("refusing to recurse into macro {macro_id}"); } else if let Some(items) = recorded_macros.get(¯o_id) { log::debug!("prepending macro {macro_id} items to current replay"); log::debug!("playing macro {items:?}"); state.active_macros.insert(macro_id); state .macro_items .push_front(DynamicMacroItem::EndMacro(macro_id)); for item in items.iter().copied().rev() { state.macro_items.push_front(item); } } } } } kanata-1.9.0/src/kanata/key_repeat.rs000064400000000000000000000116071046102023000156230ustar 00000000000000use super::*; impl Kanata { /// This compares the active keys in the keyberon layout against the potential key outputs for /// corresponding physical key in the configuration. If any of keyberon active keys match any /// potential physical key output, write the repeat event to the OS. pub(super) fn handle_repeat(&mut self, event: &KeyEvent) -> Result<()> { let ret = self.handle_repeat_actual(event); // The cur_keys Vec is re-used for processing, for efficiency reasons to avoid allocation. // Unlike prev_keys which has useful info for the next call to handle_time_ticks, cur_keys // can be reused and cleared — it just needs to be empty for the next handle_time_ticks // call. self.cur_keys.clear(); ret } pub(super) fn handle_repeat_actual(&mut self, event: &KeyEvent) -> Result<()> { if let Some(state) = self.sequence_state.get_active() { // While in non-visible sequence mode, don't send key repeats. I can't imagine it's a // helpful use case for someone trying to type in a sequence that they want to rely on // key repeats to finish a sequence. I suppose one might want to do repeat in order to // try and cancel an input sequence... I'll wait for a user created issue to deal with // this. // // It should be noted that even with visible-backspaced, key repeat does not interact // with the sequence; the key is output with repeat as normal. Which might be // surprising/unexpected. It's technically fixable but I don't want to add the code to // do that if nobody needs it. if state.sequence_input_mode != SequenceInputMode::VisibleBackspaced { return Ok(()); } } self.cur_keys.extend(self.layout.bm().keycodes()); self.overrides .override_keys(&mut self.cur_keys, &mut self.override_states); // Prioritize checking the active layer in case a layer-while-held is active. let active_held_layers = self.layout.bm().trans_resolution_layer_order(); let mut held_layer_active = false; for layer in active_held_layers { held_layer_active = true; if let Some(outputs_for_key) = self.key_outputs[usize::from(layer)].get(&event.code) { log::debug!("key outs for active layer-while-held: {outputs_for_key:?};"); for osc in outputs_for_key.iter().rev().copied() { let kc = osc.into(); if self.cur_keys.contains(&kc) || self.unshifted_keys.contains(&kc) || self.unmodded_keys.contains(&kc) { log::debug!("repeat {:?}", KeyCode::from(osc)); if let Err(e) = write_key(&mut self.kbd_out, osc, KeyValue::Repeat) { bail!("could not write key {e:?}") } return Ok(()); } } } } if held_layer_active { log::debug!("empty layer-while-held outputs, probably transparent"); } if let Some(outputs_for_key) = self.key_outputs[self.layout.bm().default_layer].get(&event.code) { // Try matching a key on the default layer. // // This code executes in two cases: // 1. current layer is the default layer // 2. current layer is layer-while-held but did not find a match in the code above, e.g. a // transparent key was pressed. log::debug!("key outs for default layer: {outputs_for_key:?};"); for osc in outputs_for_key.iter().rev().copied() { let kc = osc.into(); if self.cur_keys.contains(&kc) || self.unshifted_keys.contains(&kc) || self.unmodded_keys.contains(&kc) { log::debug!("repeat {:?}", KeyCode::from(osc)); if let Err(e) = write_key(&mut self.kbd_out, osc, KeyValue::Repeat) { bail!("could not write key {e:?}") } return Ok(()); } } } // Reached here and have not exited yet. // Check the standard key output itself because default layer might also be transparent // and have delegated to defsrc handling. log::debug!("checking defsrc output"); let kc = event.code.into(); if self.cur_keys.contains(&kc) || self.unshifted_keys.contains(&kc) || self.unmodded_keys.contains(&kc) { if let Err(e) = write_key(&mut self.kbd_out, event.code, KeyValue::Repeat) { bail!("could not write key {e:?}"); } } Ok(()) } } kanata-1.9.0/src/kanata/linux.rs000064400000000000000000000200141046102023000146220ustar 00000000000000#![cfg_attr( feature = "simulated_output", allow(dead_code, unused_imports, unused_variables, unused_mut) )] use anyhow::{anyhow, bail, Result}; use evdev::{InputEvent, InputEventKind, RelativeAxisType}; use log::info; use parking_lot::Mutex; use std::convert::TryFrom; use std::sync::mpsc::SyncSender as Sender; use std::sync::Arc; use super::*; impl Kanata { /// Enter an infinite loop that listens for OS key events and sends them to the processing /// thread. pub fn event_loop(kanata: Arc>, tx: Sender) -> Result<()> { info!("entering the event loop"); let k = kanata.lock(); let allow_hardware_repeat = k.allow_hardware_repeat; let mouse_movement_key = k.mouse_movement_key.clone(); let mut kbd_in = match KbdIn::new( &k.kbd_in_paths, k.continue_if_no_devices, k.include_names.clone(), k.exclude_names.clone(), k.device_detect_mode, ) { Ok(kbd_in) => kbd_in, Err(e) => { bail!("failed to open keyboard device(s): {}", e) } }; // In some environments, this needs to be done after the input device grab otherwise it // does not work on kanata startup. Kanata::set_repeat_rate(k.x11_repeat_rate)?; drop(k); loop { let events = kbd_in.read().map_err(|e| anyhow!("failed read: {}", e))?; log::trace!("event count: {}\nevents:\n{events:?}", events.len()); for in_event in events.iter().copied() { if let Some(ms_mvmt_key) = *mouse_movement_key.lock() { if let InputEventKind::RelAxis(_) = in_event.kind() { let fake_event = KeyEvent::new(ms_mvmt_key, KeyValue::Tap); if let Err(e) = tx.try_send(fake_event) { bail!("failed to send on channel: {}", e) } } } let key_event = match KeyEvent::try_from(in_event) { Ok(ev) => ev, _ => { // Pass-through non-key and non-scroll events let mut kanata = kanata.lock(); #[cfg(not(feature = "simulated_output"))] kanata .kbd_out .write_raw(in_event) .map_err(|e| anyhow!("failed write: {}", e))?; continue; } }; check_for_exit(&key_event); if key_event.value == KeyValue::Repeat && !allow_hardware_repeat { continue; } if key_event.value == KeyValue::Tap { // Scroll event for sure. Only scroll events produce Tap. if !handle_scroll(&kanata, in_event, key_event.code, &events)? { continue; } } else { // Handle normal keypresses. // Check if this keycode is mapped in the configuration. // If it hasn't been mapped, send it immediately. if !MAPPED_KEYS.lock().contains(&key_event.code) { let mut kanata = kanata.lock(); #[cfg(not(feature = "simulated_output"))] kanata .kbd_out .write_raw(in_event) .map_err(|e| anyhow!("failed write: {}", e))?; continue; }; } // Send key events to the processing loop if let Err(e) = tx.try_send(key_event) { bail!("failed to send on channel: {}", e) } } } } pub fn check_release_non_physical_shift(&mut self) -> Result<()> { Ok(()) } pub fn set_repeat_rate(s: Option) -> Result<()> { if let Some(s) = s { log::info!( "Using xset to set X11 repeat delay to {} and repeat rate to {}", s.delay, s.rate, ); let cmd_output = std::process::Command::new("xset") .args([ "r", "rate", s.delay.to_string().as_str(), s.rate.to_string().as_str(), ]) .output() .map_err(|e| { log::error!("failed to run xset: {e:?}"); e })?; log::info!( "xset stdout: {}", String::from_utf8_lossy(&cmd_output.stdout) ); log::info!( "xset stderr: {}", String::from_utf8_lossy(&cmd_output.stderr) ); } Ok(()) } } /// Returns true if the scroll event should be sent to the processing loop, otherwise returns /// false. fn handle_scroll( kanata: &Mutex, in_event: InputEvent, code: OsCode, all_events: &[InputEvent], ) -> Result { let direction: MWheelDirection = code.try_into().unwrap(); let scroll_distance = in_event.value().unsigned_abs() as u16; match in_event.kind() { InputEventKind::RelAxis(axis_type) => { match axis_type { RelativeAxisType::REL_WHEEL | RelativeAxisType::REL_HWHEEL => { if MAPPED_KEYS.lock().contains(&code) { return Ok(true); } // If we just used `write_raw` here, some of the scrolls issued by kanata would be // REL_WHEEL_HI_RES + REL_WHEEL and some just REL_WHEEL and an issue like this one // would happen: https://github.com/jtroo/kanata/issues/395 // // So to fix this case, we need to use `scroll` which will also send hi-res scrolls // along normal scrolls. // // However, if this is a normal scroll event, it may be sent alongside a hi-res // scroll event. In this scenario, the hi-res event should be used to call // scroll, and not the normal event. Otherwise, too much scrolling will happen. let mut kanata = kanata.lock(); if !all_events.iter().any(|ev| { matches!( ev.kind(), InputEventKind::RelAxis( RelativeAxisType::REL_WHEEL_HI_RES | RelativeAxisType::REL_HWHEEL_HI_RES ) ) }) { kanata .kbd_out .scroll(direction, scroll_distance * HI_RES_SCROLL_UNITS_IN_LO_RES) .map_err(|e| anyhow!("failed write: {}", e))?; } Ok(false) } RelativeAxisType::REL_WHEEL_HI_RES | RelativeAxisType::REL_HWHEEL_HI_RES => { if !MAPPED_KEYS.lock().contains(&code) { // Passthrough if the scroll wheel event is not mapped // in the configuration. let mut kanata = kanata.lock(); kanata .kbd_out .scroll(direction, scroll_distance) .map_err(|e| anyhow!("failed write: {}", e))?; } // Kanata will not handle high resolution scroll events for now. // Full notch scrolling only. Ok(false) } _ => unreachable!("expect to be handling a wheel event"), } } _ => unreachable!("expect to be handling a wheel event"), } } kanata-1.9.0/src/kanata/macos.rs000064400000000000000000000053211046102023000145710ustar 00000000000000use super::*; use anyhow::{anyhow, bail, Result}; use log::info; use parking_lot::Mutex; use std::convert::TryFrom; use std::sync::mpsc::SyncSender as Sender; use std::sync::Arc; pub(crate) static PRESSED_KEYS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::default())); impl Kanata { /// Enter an infinite loop that listens for OS key events and sends them to the processing thread. pub fn event_loop(kanata: Arc>, tx: Sender) -> Result<()> { info!("entering the event loop"); let k = kanata.lock(); let allow_hardware_repeat = k.allow_hardware_repeat; let mut kb = match KbdIn::new(k.include_names.clone(), k.exclude_names.clone()) { Ok(kbd_in) => kbd_in, Err(e) => bail!("failed to open keyboard device(s): {}", e), }; drop(k); loop { let event = kb.read().map_err(|e| anyhow!("failed read: {}", e))?; let mut key_event = match KeyEvent::try_from(event) { Ok(ev) => ev, _ => { // Pass-through unrecognized keys log::debug!("{event:?} is unrecognized!"); let mut kanata = kanata.lock(); kanata .kbd_out .write(event) .map_err(|e| anyhow!("failed write: {}", e))?; continue; } }; check_for_exit(&key_event); if key_event.value == KeyValue::Repeat && !allow_hardware_repeat { continue; } if !MAPPED_KEYS.lock().contains(&key_event.code) { log::debug!("{key_event:?} is not mapped"); let mut kanata = kanata.lock(); kanata .kbd_out .write(event) .map_err(|e| anyhow!("failed write: {}", e))?; continue; } log::debug!("sending {key_event:?} to processing loop"); match key_event.value { KeyValue::Release => { PRESSED_KEYS.lock().remove(&key_event.code); } KeyValue::Press => { let mut pressed_keys = PRESSED_KEYS.lock(); if pressed_keys.contains(&key_event.code) { key_event.value = KeyValue::Repeat; } else { pressed_keys.insert(key_event.code); } } _ => {} } tx.try_send(key_event)?; } } pub fn check_release_non_physical_shift(&mut self) -> Result<()> { Ok(()) } } kanata-1.9.0/src/kanata/millisecond_counting.rs000064400000000000000000000041521046102023000177000ustar 00000000000000pub struct MillisecondCountResult { pub last_tick: instant::Instant, pub ms_elapsed: u128, pub ms_remainder_in_ns: u128, } pub fn count_ms_elapsed( last_tick: instant::Instant, now: instant::Instant, prev_ms_remainder_in_ns: u128, ) -> MillisecondCountResult { const NS_IN_MS: u128 = 1_000_000; let ns_elapsed = now.duration_since(last_tick).as_nanos(); let ns_elapsed_with_rem = ns_elapsed + prev_ms_remainder_in_ns; let ms_elapsed = ns_elapsed_with_rem / NS_IN_MS; let ms_remainder_in_ns = ns_elapsed_with_rem % NS_IN_MS; let last_tick = match ms_elapsed { 0 => last_tick, _ => now, }; MillisecondCountResult { last_tick, ms_elapsed, ms_remainder_in_ns, } } #[test] fn ms_counts_0_elapsed_correctly() { use std::time::Duration; let last_tick = instant::Instant::now(); let now = last_tick + Duration::from_nanos(999999); let result = count_ms_elapsed(last_tick, now, 0); assert_eq!(0, result.ms_elapsed); assert_eq!(last_tick, result.last_tick); assert_eq!(999999, result.ms_remainder_in_ns); } #[test] fn ms_counts_1_elapsed_correctly() { use std::time::Duration; let last_tick = instant::Instant::now(); let now = last_tick + Duration::from_nanos(1234567); let result = count_ms_elapsed(last_tick, now, 0); assert_eq!(1, result.ms_elapsed); assert_eq!(now, result.last_tick); assert_eq!(234567, result.ms_remainder_in_ns); } #[test] fn ms_counts_1_then_2_elapsed_correctly() { use std::time::Duration; let last_tick = instant::Instant::now(); let now = last_tick + Duration::from_micros(1750); let result = count_ms_elapsed(last_tick, now, 0); assert_eq!(1, result.ms_elapsed); assert_eq!(now, result.last_tick); assert_eq!(750000, result.ms_remainder_in_ns); let last_tick = result.last_tick; let now = last_tick + Duration::from_micros(1750); let result = count_ms_elapsed(last_tick, now, result.ms_remainder_in_ns); assert_eq!(2, result.ms_elapsed); assert_eq!(now, result.last_tick); assert_eq!(500000, result.ms_remainder_in_ns); } kanata-1.9.0/src/kanata/mod.rs000075500000000000000000003317051046102023000142610ustar 00000000000000//! Implements the glue between OS input/output and keyberon state management. #[cfg(all(target_os = "windows", feature = "gui"))] use crate::gui::win::*; use anyhow::{bail, Result}; use kanata_parser::sequences::*; use log::{error, info}; use parking_lot::Mutex; use std::sync::mpsc::{Receiver, SyncSender as Sender, TryRecvError}; #[cfg(feature = "passthru_ahk")] use std::sync::mpsc::Sender as ASender; use kanata_keyberon::action::ReleasableState; use kanata_keyberon::key_code::*; use kanata_keyberon::layout::{CustomEvent, Event, Layout, State}; use std::path::PathBuf; use std::sync::Arc; use std::time; use crate::oskbd::{KeyEvent, *}; #[cfg(feature = "tcp_server")] use crate::tcp_server::simple_sexpr_to_json_array; #[cfg(feature = "tcp_server")] use crate::SocketAddrWrapper; use crate::ValidatedArgs; use kanata_parser::cfg; use kanata_parser::cfg::list_actions::*; use kanata_parser::cfg::*; use kanata_parser::custom_action::*; pub use kanata_parser::keys::*; use kanata_tcp_protocol::ServerMessage; mod clipboard; use clipboard::*; mod dynamic_macro; use dynamic_macro::*; mod key_repeat; mod millisecond_counting; pub use millisecond_counting::*; mod sequences; use sequences::*; pub mod cfg_forced; use cfg_forced::*; #[cfg(feature = "cmd")] mod cmd; #[cfg(feature = "cmd")] use cmd::*; #[cfg(target_os = "windows")] mod windows; #[cfg(target_os = "windows")] pub use windows::*; #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "macos")] use macos::*; mod output_logic; use output_logic::*; #[cfg(target_os = "unknown")] mod unknown; #[cfg(target_os = "unknown")] use unknown::*; mod caps_word; pub use caps_word::*; type HashSet = rustc_hash::FxHashSet; type HashMap = rustc_hash::FxHashMap; pub struct Kanata { /// Handle to some OS keyboard output mechanism. pub kbd_out: KbdOut, /// Paths to one or more configuration files that define kanata's behaviour. pub cfg_paths: Vec, /// Index into `cfg_paths`, used to know which file to live reload. Changes when cycling /// through the configuration files. pub cur_cfg_idx: usize, /// The potential key outputs of every key input. Used for managing key repeat. pub key_outputs: cfg::KeyOutputs, /// Handle to the keyberon library layout. pub layout: cfg::KanataLayout, /// Reusable vec (to save on allocations) that stores the currently active output keys. /// This can be cleared and reused in various procedures as buffer space. pub cur_keys: Vec, /// Reusable vec (to save on allocations) that stores the active output keys from the previous /// tick. This must only be updated once per tick and must not be modified outside of the one /// procedure that updates it. pub prev_keys: Vec, /// Used for printing layer info to the info log when changing layers. pub layer_info: Vec, /// Used to track when a layer change occurs. pub prev_layer: usize, /// Vertical scrolling state tracker. Is Some(...) when a vertical scrolling action is active /// and None otherwise. pub scroll_state: Option, /// Horizontal scrolling state. Is Some(...) when a horizontal scrolling action is active and /// None otherwise. pub hscroll_state: Option, /// Vertical mouse movement state. Is Some(...) when vertical mouse movement is active and None /// otherwise. pub move_mouse_state_vertical: Option, /// Horizontal mouse movement state. Is Some(...) when horizontal mouse movement is active and /// None otherwise. pub move_mouse_state_horizontal: Option, /// A list of mouse speed modifiers in percentages by which mouse travel distance is scaled. pub move_mouse_speed_modifiers: Vec, /// The user configuration for backtracking to find valid sequences. See /// <../../docs/sequence-adding-chords-ideas.md> for more info. pub sequence_backtrack_modcancel: bool, /// The user configuration for sequences be permanently on. pub sequence_always_on: bool, /// Default sequence input mode for use with always-on. pub sequence_input_mode: SequenceInputMode, /// Default sequence timeout for use with always-on. pub sequence_timeout: u16, /// Tracks sequence progress. Is Some(...) when in sequence mode and None otherwise. pub sequence_state: SequenceState, /// Valid sequences defined in the user configuration. pub sequences: cfg::KeySeqsToFKeys, /// Stores the user recored dynamic macros. pub dynamic_macros: HashMap>, /// Tracks the progress of an active dynamic macro. Is Some(...) when a dynamic macro is being /// replayed and None otherwise. pub dynamic_macro_replay_state: Option, /// Tracks the inputs for a dynamic macro recording. Is Some(...) when a dynamic macro is /// being recorded and None otherwise. pub dynamic_macro_record_state: Option, /// Global overrides defined in the user configuration. pub overrides: Overrides, /// Reusable allocations to help with computing whether overrides are active based on key /// outputs. pub override_states: OverrideStates, /// Time of the last tick to know how many tick iterations to run, to achieve a 1ms tick /// interval more closely. last_tick: instant::Instant, /// Tracks the non-whole-millisecond gaps between ticks to know when to do another tick /// iteration without sleeping, to achive a 1ms tick interval more closely. time_remainder: u128, /// Is true if a live reload was requested by the user and false otherwise. live_reload_requested: bool, #[cfg(target_os = "linux")] /// Linux input paths in the user configuration. pub kbd_in_paths: Vec, #[cfg(target_os = "linux")] /// Tracks the Linux user configuration to continue or abort if no devices are found. continue_if_no_devices: bool, #[cfg(any(target_os = "linux", target_os = "macos"))] /// Tracks the Linux/Macos user configuration for device names (instead of paths) that should be /// included for interception and processing by kanata. pub include_names: Option>, #[cfg(any(target_os = "linux", target_os = "macos"))] /// Tracks the Linux/Macos user configuration for device names (instead of paths) that should be /// excluded for interception and processing by kanata. pub exclude_names: Option>, #[cfg(target_os = "windows")] /// Tracks whether Kanata should try to synchronize keystates with the Windows OS. /// Has no effect on Interception. Fixes some use cases related to admin window permissions and /// potentially locking via Win+L. pub windows_sync_keystates: bool, #[cfg(all(feature = "interception_driver", target_os = "windows"))] /// Used to know which input device to treat as a mouse for intercepting and processing inputs /// by kanata. intercept_mouse_hwids: Option>, #[cfg(all(feature = "interception_driver", target_os = "windows"))] /// Used to know which mouse input devices to exclude from processing inputs by kanata. This is /// mutually exclusive from `intercept_mouse_hwids` and kanata will panic if both are included. intercept_mouse_hwids_exclude: Option>, #[cfg(all(feature = "interception_driver", target_os = "windows"))] /// Used to know which input device to treat as a keyboard for intercepting and processing inputs /// by kanata. intercept_kb_hwids: Option>, #[cfg(all(feature = "interception_driver", target_os = "windows"))] /// Used to know which keyboard input devices to exclude from processing inputs by kanata. This /// is mutually exclusive from `intercept_kb_hwids` and kanata will panic if both are included. intercept_kb_hwids_exclude: Option>, /// User configuration to do logging of layer changes or not. log_layer_changes: bool, /// Tracks the caps-word state. Is Some(...) if caps-word is active and None otherwise. pub caps_word: Option, /// Config items from `defcfg`. #[cfg(target_os = "linux")] pub x11_repeat_rate: Option, /// Determines what types of devices to grab based on autodetection mode. #[cfg(target_os = "linux")] pub device_detect_mode: DeviceDetectMode, /// Fake key actions that are waiting for a certain duration of keyboard idling. pub waiting_for_idle: HashSet, /// Fake key actions that are being held and are pending release. /// The key is the coordinate and the value is the number of ticks until release should be /// done. pub vkeys_pending_release: HashMap, /// Number of ticks since kanata was idle. pub ticks_since_idle: u16, /// If a mousemove action is active and another mousemove action is activated, /// reuse the acceleration state. movemouse_inherit_accel_state: bool, /// Removes jaggedneess of vertical and horizontal mouse movements when used /// simultaneously at the cost of increased mousemove actions latency. movemouse_smooth_diagonals: bool, /// If movemouse_smooth_diagonals is enabled, the previous mouse actions /// gets stored in this buffer and if the next movemouse action is opposite axis /// than the one stored in the buffer, both events are outputted at the same time. movemouse_buffer: Option<(Axis, CalculatedMouseMove)>, override_release_on_activation: bool, /// Configured maximum for dynamic macro recording, to protect users from themselves if they /// have accidentally left it on. dynamic_macro_max_presses: u16, /// Determines behaviour of replayed dynamic macros. dynamic_macro_replay_behaviour: ReplayBehaviour, /// Keys that should be unmodded. If non-empty, any modifier should be cleared. unmodded_keys: Vec, /// Modifiers to be cleared in case the above is non-empty. unmodded_mods: UnmodMods, /// Keys that should be unshifted. If non-empty, left+right shift keys should be cleared. unshifted_keys: Vec, /// Keep track of last pressed key for [`CustomAction::Repeat`]. last_pressed_key: KeyCode, #[cfg(feature = "tcp_server")] /// Names of fake keys mapped to their index in the fake keys row pub virtual_keys: HashMap, /// The maximum value of switch's key-timing item in the configuration. pub switch_max_key_timing: u16, #[cfg(feature = "tcp_server")] tcp_server_address: Option, #[cfg(all(target_os = "windows", feature = "gui"))] /// Various GUI-related options. pub gui_opts: CfgOptionsGui, pub allow_hardware_repeat: bool, /// When > 0, it means macros should be cancelled on the next press. /// Upon cancelling this should be set to 0. pub macro_on_press_cancel_duration: u32, /// Stores user's saved clipboard contents. pub saved_clipboard_content: SavedClipboardData, // if set, key taps of this code are sent whenever mouse movement events are passed through #[cfg(any( all(target_os = "windows", feature = "interception_driver"), target_os = "linux", target_os = "unknown" ))] mouse_movement_key: Arc>>, } #[derive(PartialEq, Clone, Copy)] pub enum Axis { Vertical, Horizontal, } impl From for Axis { fn from(val: MoveDirection) -> Axis { match val { MoveDirection::Up | MoveDirection::Down => Axis::Vertical, MoveDirection::Left | MoveDirection::Right => Axis::Horizontal, } } } #[derive(Clone, Copy)] pub struct CalculatedMouseMove { pub direction: MoveDirection, pub distance: u16, } pub struct ScrollState { pub direction: MWheelDirection, pub interval: u16, pub ticks_until_scroll: u16, pub distance: u16, } pub struct MoveMouseState { pub direction: MoveDirection, pub interval: u16, pub ticks_until_move: u16, pub distance: u16, pub move_mouse_accel_state: Option, } #[derive(Clone, Copy)] pub struct MoveMouseAccelState { pub accel_ticks_from_min: u16, pub accel_ticks_until_max: u16, pub accel_increment: f64, pub min_distance: u16, pub max_distance: u16, } use once_cell::sync::Lazy; static MAPPED_KEYS: Lazy> = Lazy::new(|| Mutex::new(cfg::MappedKeys::default())); impl Kanata { pub fn new(args: &ValidatedArgs) -> Result { let cfg = match cfg::new_from_file(&args.paths[0]) { Ok(c) => c, Err(e) => { log::error!("{e:?}"); bail!("failed to parse file"); } }; let kbd_out = match KbdOut::new( #[cfg(target_os = "linux")] &args.symlink_path, #[cfg(target_os = "linux")] cfg.options.linux_opts.linux_use_trackpoint_property, #[cfg(target_os = "linux")] &cfg.options.linux_opts.linux_output_name, #[cfg(target_os = "linux")] match cfg.options.linux_opts.linux_output_bus_type { LinuxCfgOutputBusType::BusUsb => evdev::BusType::BUS_USB, LinuxCfgOutputBusType::BusI8042 => evdev::BusType::BUS_I8042, }, ) { Ok(kbd_out) => kbd_out, Err(err) => { error!("Failed to open the output uinput device. Make sure you've added the user executing kanata to the `uinput` group"); bail!(err) } }; #[cfg(target_os = "windows")] unsafe { log::info!("Asking Windows to improve timer precision"); if winapi::um::timeapi::timeBeginPeriod(1) == winapi::um::mmsystem::TIMERR_NOCANDO { bail!("failed to improve timer precision"); } } #[cfg(target_os = "windows")] unsafe { log::info!("Asking Windows to increase process priority"); winapi::um::processthreadsapi::SetPriorityClass( winapi::um::processthreadsapi::GetCurrentProcess(), winapi::um::winbase::REALTIME_PRIORITY_CLASS, ); } update_kbd_out(&cfg.options, &kbd_out)?; #[cfg(target_os = "windows")] set_win_altgr_behaviour(cfg.options.windows_opts.windows_altgr); *MAPPED_KEYS.lock() = cfg.mapped_keys; #[cfg(feature = "zippychord")] { zch().zch_configure(cfg.zippy.unwrap_or_default()); } Ok(Self { kbd_out, cfg_paths: args.paths.clone(), cur_cfg_idx: 0, key_outputs: cfg.key_outputs, layout: cfg.layout, layer_info: cfg.layer_info, cur_keys: Vec::new(), prev_keys: Vec::new(), prev_layer: 0, scroll_state: None, hscroll_state: None, move_mouse_state_vertical: None, move_mouse_state_horizontal: None, move_mouse_speed_modifiers: Vec::new(), sequence_backtrack_modcancel: cfg.options.sequence_backtrack_modcancel, sequence_always_on: cfg.options.sequence_always_on, sequence_input_mode: cfg.options.sequence_input_mode, sequence_timeout: cfg.options.sequence_timeout, sequence_state: SequenceState::new(), sequences: cfg.sequences, last_tick: instant::Instant::now(), time_remainder: 0, live_reload_requested: false, overrides: cfg.overrides, override_states: OverrideStates::new(), #[cfg(target_os = "macos")] include_names: cfg.options.macos_opts.macos_dev_names_include, #[cfg(target_os = "macos")] exclude_names: cfg.options.macos_opts.macos_dev_names_exclude, #[cfg(target_os = "linux")] kbd_in_paths: cfg.options.linux_opts.linux_dev, #[cfg(target_os = "linux")] continue_if_no_devices: cfg.options.linux_opts.linux_continue_if_no_devs_found, #[cfg(target_os = "linux")] include_names: cfg.options.linux_opts.linux_dev_names_include, #[cfg(target_os = "linux")] exclude_names: cfg.options.linux_opts.linux_dev_names_exclude, #[cfg(target_os = "windows")] windows_sync_keystates: cfg.options.windows_opts.sync_keystates, #[cfg(all(feature = "interception_driver", target_os = "windows"))] intercept_mouse_hwids: cfg.options.wintercept_opts.windows_interception_mouse_hwids, #[cfg(all(feature = "interception_driver", target_os = "windows"))] intercept_mouse_hwids_exclude: cfg .options .wintercept_opts .windows_interception_mouse_hwids_exclude, #[cfg(all(feature = "interception_driver", target_os = "windows"))] intercept_kb_hwids: cfg .options .wintercept_opts .windows_interception_keyboard_hwids, #[cfg(all(feature = "interception_driver", target_os = "windows"))] intercept_kb_hwids_exclude: cfg .options .wintercept_opts .windows_interception_keyboard_hwids_exclude, dynamic_macro_replay_state: None, dynamic_macro_record_state: None, dynamic_macros: Default::default(), log_layer_changes: get_forced_log_layer_changes() .unwrap_or(cfg.options.log_layer_changes), caps_word: None, movemouse_smooth_diagonals: cfg.options.movemouse_smooth_diagonals, override_release_on_activation: cfg.options.override_release_on_activation, movemouse_inherit_accel_state: cfg.options.movemouse_inherit_accel_state, dynamic_macro_max_presses: cfg.options.dynamic_macro_max_presses, dynamic_macro_replay_behaviour: ReplayBehaviour { delay: cfg.options.dynamic_macro_replay_delay_behaviour, }, #[cfg(target_os = "linux")] x11_repeat_rate: cfg.options.linux_opts.linux_x11_repeat_delay_rate, #[cfg(target_os = "linux")] device_detect_mode: cfg .options .linux_opts .linux_device_detect_mode .expect("parser should default to some"), waiting_for_idle: HashSet::default(), vkeys_pending_release: HashMap::default(), ticks_since_idle: 0, movemouse_buffer: None, unmodded_keys: vec![], unmodded_mods: UnmodMods::empty(), unshifted_keys: vec![], last_pressed_key: KeyCode::No, #[cfg(feature = "tcp_server")] virtual_keys: cfg.fake_keys, switch_max_key_timing: cfg.switch_max_key_timing, #[cfg(feature = "tcp_server")] tcp_server_address: args.tcp_server_address.clone(), #[cfg(all(target_os = "windows", feature = "gui"))] gui_opts: cfg.options.gui_opts, allow_hardware_repeat: cfg.options.allow_hardware_repeat, macro_on_press_cancel_duration: 0, saved_clipboard_content: Default::default(), #[cfg(any( all(target_os = "windows", feature = "interception_driver"), target_os = "linux", target_os = "unknown" ))] mouse_movement_key: Arc::new(Mutex::new(cfg.options.mouse_movement_key)), }) } /// Create a new configuration from a file, wrapped in an Arc> pub fn new_arc(args: &ValidatedArgs) -> Result>> { Ok(Arc::new(Mutex::new(Self::new(args)?))) } pub fn new_from_str(cfg: &str, file_content: HashMap) -> Result { let cfg = match cfg::new_from_str(cfg, file_content) { Ok(c) => c, Err(e) => { bail!("{e:?}"); } }; let kbd_out = match KbdOut::new( #[cfg(target_os = "linux")] &None, #[cfg(target_os = "linux")] cfg.options.linux_opts.linux_use_trackpoint_property, #[cfg(target_os = "linux")] &cfg.options.linux_opts.linux_output_name, #[cfg(target_os = "linux")] match cfg.options.linux_opts.linux_output_bus_type { LinuxCfgOutputBusType::BusUsb => evdev::BusType::BUS_USB, LinuxCfgOutputBusType::BusI8042 => evdev::BusType::BUS_I8042, }, ) { Ok(kbd_out) => kbd_out, Err(err) => { error!("Failed to open the output uinput device. Make sure you've added the user executing kanata to the `uinput` group"); bail!(err) } }; *MAPPED_KEYS.lock() = cfg.mapped_keys; #[cfg(feature = "zippychord")] { zch().zch_configure(cfg.zippy.unwrap_or_default()); } Ok(Self { kbd_out, cfg_paths: vec!["config string".into()], cur_cfg_idx: 0, key_outputs: cfg.key_outputs, layout: cfg.layout, layer_info: cfg.layer_info, cur_keys: Vec::new(), prev_keys: Vec::new(), prev_layer: 0, scroll_state: None, hscroll_state: None, move_mouse_state_vertical: None, move_mouse_state_horizontal: None, move_mouse_speed_modifiers: Vec::new(), sequence_backtrack_modcancel: cfg.options.sequence_backtrack_modcancel, sequence_always_on: cfg.options.sequence_always_on, sequence_input_mode: cfg.options.sequence_input_mode, sequence_timeout: cfg.options.sequence_timeout, sequence_state: SequenceState::new(), sequences: cfg.sequences, last_tick: instant::Instant::now(), time_remainder: 0, live_reload_requested: false, overrides: cfg.overrides, override_states: OverrideStates::new(), #[cfg(target_os = "macos")] include_names: cfg.options.macos_opts.macos_dev_names_include, #[cfg(target_os = "macos")] exclude_names: cfg.options.macos_opts.macos_dev_names_exclude, #[cfg(target_os = "linux")] kbd_in_paths: cfg.options.linux_opts.linux_dev, #[cfg(target_os = "linux")] continue_if_no_devices: cfg.options.linux_opts.linux_continue_if_no_devs_found, #[cfg(target_os = "linux")] include_names: cfg.options.linux_opts.linux_dev_names_include, #[cfg(target_os = "linux")] exclude_names: cfg.options.linux_opts.linux_dev_names_exclude, #[cfg(target_os = "windows")] windows_sync_keystates: cfg.options.windows_opts.sync_keystates, #[cfg(all(feature = "interception_driver", target_os = "windows"))] intercept_mouse_hwids: cfg.options.wintercept_opts.windows_interception_mouse_hwids, #[cfg(all(feature = "interception_driver", target_os = "windows"))] intercept_mouse_hwids_exclude: cfg .options .wintercept_opts .windows_interception_mouse_hwids_exclude, #[cfg(all(feature = "interception_driver", target_os = "windows"))] intercept_kb_hwids: cfg .options .wintercept_opts .windows_interception_keyboard_hwids, #[cfg(all(feature = "interception_driver", target_os = "windows"))] intercept_kb_hwids_exclude: cfg .options .wintercept_opts .windows_interception_keyboard_hwids_exclude, dynamic_macro_replay_state: None, dynamic_macro_record_state: None, dynamic_macros: Default::default(), log_layer_changes: get_forced_log_layer_changes() .unwrap_or(cfg.options.log_layer_changes), caps_word: None, movemouse_smooth_diagonals: cfg.options.movemouse_smooth_diagonals, override_release_on_activation: cfg.options.override_release_on_activation, movemouse_inherit_accel_state: cfg.options.movemouse_inherit_accel_state, dynamic_macro_max_presses: cfg.options.dynamic_macro_max_presses, dynamic_macro_replay_behaviour: ReplayBehaviour { delay: cfg.options.dynamic_macro_replay_delay_behaviour, }, #[cfg(target_os = "linux")] x11_repeat_rate: cfg.options.linux_opts.linux_x11_repeat_delay_rate, #[cfg(target_os = "linux")] device_detect_mode: cfg .options .linux_opts .linux_device_detect_mode .expect("parser should default to some"), waiting_for_idle: HashSet::default(), vkeys_pending_release: HashMap::default(), ticks_since_idle: 0, movemouse_buffer: None, unmodded_keys: vec![], unmodded_mods: UnmodMods::empty(), unshifted_keys: vec![], last_pressed_key: KeyCode::No, #[cfg(feature = "tcp_server")] virtual_keys: cfg.fake_keys, switch_max_key_timing: cfg.switch_max_key_timing, #[cfg(feature = "tcp_server")] tcp_server_address: None, #[cfg(all(target_os = "windows", feature = "gui"))] gui_opts: cfg.options.gui_opts, allow_hardware_repeat: cfg.options.allow_hardware_repeat, macro_on_press_cancel_duration: 0, saved_clipboard_content: Default::default(), #[cfg(any( all(target_os = "windows", feature = "interception_driver"), target_os = "linux", target_os = "unknown" ))] mouse_movement_key: Arc::new(Mutex::new(cfg.options.mouse_movement_key)), }) } #[cfg(feature = "passthru_ahk")] pub fn new_with_output_channel( args: &ValidatedArgs, tx: Option>, ) -> Result>> { let mut k = Self::new(args)?; k.kbd_out.tx_kout = tx; Ok(Arc::new(Mutex::new(k))) } fn do_live_reload(&mut self, _tx: &Option>) -> Result<()> { let cfg = match cfg::new_from_file(&self.cfg_paths[self.cur_cfg_idx]) { Ok(c) => c, Err(e) => { log::error!("{e:?}"); bail!("failed to parse config file"); } }; update_kbd_out(&cfg.options, &self.kbd_out)?; #[cfg(target_os = "windows")] set_win_altgr_behaviour(cfg.options.windows_opts.windows_altgr); self.sequence_backtrack_modcancel = cfg.options.sequence_backtrack_modcancel; self.sequence_always_on = cfg.options.sequence_always_on; self.sequence_input_mode = cfg.options.sequence_input_mode; self.sequence_timeout = cfg.options.sequence_timeout; self.layout = cfg.layout; self.key_outputs = cfg.key_outputs; self.layer_info = cfg.layer_info; self.sequences = cfg.sequences; self.overrides = cfg.overrides; self.log_layer_changes = get_forced_log_layer_changes().unwrap_or(cfg.options.log_layer_changes); self.movemouse_smooth_diagonals = cfg.options.movemouse_smooth_diagonals; self.override_release_on_activation = cfg.options.override_release_on_activation; self.movemouse_inherit_accel_state = cfg.options.movemouse_inherit_accel_state; self.dynamic_macro_max_presses = cfg.options.dynamic_macro_max_presses; self.dynamic_macro_replay_behaviour = ReplayBehaviour { delay: cfg.options.dynamic_macro_replay_delay_behaviour, }; self.switch_max_key_timing = cfg.switch_max_key_timing; #[cfg(feature = "tcp_server")] { self.virtual_keys = cfg.fake_keys; } #[cfg(target_os = "windows")] { self.windows_sync_keystates = cfg.options.windows_opts.sync_keystates; } #[cfg(all(target_os = "windows", feature = "gui"))] { self.gui_opts.tray_icon = cfg.options.gui_opts.tray_icon; self.gui_opts.icon_match_layer_name = cfg.options.gui_opts.icon_match_layer_name; self.gui_opts.tooltip_layer_changes = cfg.options.gui_opts.tooltip_layer_changes; self.gui_opts.tooltip_no_base = cfg.options.gui_opts.tooltip_no_base; self.gui_opts.tooltip_show_blank = cfg.options.gui_opts.tooltip_show_blank; self.gui_opts.tooltip_duration = cfg.options.gui_opts.tooltip_duration; self.gui_opts.notify_cfg_reload = cfg.options.gui_opts.notify_cfg_reload; self.gui_opts.notify_cfg_reload_silent = cfg.options.gui_opts.notify_cfg_reload_silent; self.gui_opts.notify_error = cfg.options.gui_opts.notify_error; self.gui_opts.tooltip_size = cfg.options.gui_opts.tooltip_size; } #[cfg(feature = "zippychord")] { zch().zch_configure(cfg.zippy.unwrap_or_default()); } *MAPPED_KEYS.lock() = cfg.mapped_keys; #[cfg(target_os = "linux")] Kanata::set_repeat_rate(cfg.options.linux_opts.linux_x11_repeat_delay_rate)?; log::info!("Live reload successful"); #[cfg(feature = "tcp_server")] if let Some(tx) = _tx { match tx.try_send(ServerMessage::ConfigFileReload { new: self.cfg_paths[self.cur_cfg_idx] .to_str() .unwrap() .to_string(), }) { Ok(_) => {} Err(error) => { log::error!( "could not send ConfigFileReload event notification: {}", error ); } } } let cur_layer = self.layout.bm().current_layer(); self.prev_layer = cur_layer; self.print_layer(cur_layer); self.macro_on_press_cancel_duration = 0; #[cfg(any( all(target_os = "windows", feature = "interception_driver"), target_os = "linux", target_os = "unknown" ))] { #[cfg(all(target_os = "windows", feature = "interception_driver"))] { if self.mouse_movement_key.lock().is_none() && cfg.options.mouse_movement_key.is_some() { log::warn!( "defcfg option mouse-movement-key will not take effect until kanata is restarted!" ); } } *self.mouse_movement_key.lock() = cfg.options.mouse_movement_key; } #[cfg(not(target_os = "linux"))] { PRESSED_KEYS.lock().clear(); } #[cfg(feature = "tcp_server")] if let Some(tx) = _tx { let new = self.layer_info[cur_layer].name.clone(); match tx.try_send(ServerMessage::LayerChange { new }) { Ok(_) => {} Err(error) => { log::error!("could not send LayerChange event notification: {}", error); } } } #[cfg(all(target_os = "windows", feature = "gui"))] send_gui_cfg_notice(); Ok(()) } /// Update keyberon layout state for press/release, handle repeat separately pub fn handle_input_event(&mut self, event: &KeyEvent) -> Result<()> { log::debug!("process recv ev {event:?}"); let evc: u16 = event.code.into(); self.ticks_since_idle = 0; let kbrn_ev = match event.value { KeyValue::Press => { if let Some((macro_id, recorded_macro)) = record_press( &mut self.dynamic_macro_record_state, event.code, self.dynamic_macro_max_presses, ) { self.dynamic_macros.insert(macro_id, recorded_macro); } if self.macro_on_press_cancel_duration > 0 { log::debug!("cancelling all macros: other press"); self.macro_on_press_cancel_duration = 0; let layout = self.layout.bm(); layout.active_sequences.clear(); layout.states.retain(|s| { !matches!(s, State::FakeKey { .. } | State::RepeatingSequence { .. }) }); } Event::Press(0, evc) } KeyValue::Release => { record_release(&mut self.dynamic_macro_record_state, event.code); Event::Release(0, evc) } KeyValue::Repeat => { let ret = self.handle_repeat(event); return ret; } KeyValue::Tap => { self.layout.bm().event(Event::Press(0, evc)); self.layout.bm().event(Event::Release(0, evc)); return Ok(()); } KeyValue::WakeUp => { return Ok(()); } }; self.layout.bm().event(kbrn_ev); Ok(()) } /// Returns the number of ms elapsed for the procesing loop according to current monotonic time /// and stored internal state. Mutates the internal time-tracking state. pub fn get_ms_elapsed(&mut self) -> u128 { let now = instant::Instant::now(); let ms_count_result = count_ms_elapsed(self.last_tick, now, self.time_remainder); let ms_elapsed = ms_count_result.ms_elapsed; self.time_remainder = ms_count_result.ms_remainder_in_ns; self.last_tick = match ms_elapsed { 0..=10 => ms_count_result.last_tick, // If too many ms elapsed, probably doing a tight loop of something that's quite // expensive, e.g. click spamming. To avoid a growing ms_elapsed due to trying and // failing to catch up, reset last_tick to the "actual now" instead the "past now" // even though that means ticks will be missed - meaning there will be fewer than // 1000 ticks in 1ms on average. In practice, there will already be fewer than 1000 // ticks in 1ms when running expensive operations, this just avoids having tens to // thousands of ticks all happening as soon as the expensive operations end. _ => instant::Instant::now(), }; ms_elapsed } /// Advance keyberon layout state and send events based on changes to its state. /// Returns the number of ticks that elapsed. fn handle_time_ticks(&mut self, tx: &Option>) -> Result { let ms_elapsed = self.get_ms_elapsed(); self.tick_ms(ms_elapsed, tx)?; self.check_handle_layer_change(tx); if self.live_reload_requested && ((self.prev_keys.is_empty() && self.cur_keys.is_empty()) || self.ticks_since_idle > 1000) { // Note regarding the ticks_since_idle check above: // After 1 second if live reload is still not done, there might be a key in a stuck // state. One known instance where this happens is Win+L to lock the screen in // Windows with the LLHOOK mechanism. The release of Win and L keys will not be // caught by the kanata process when on the lock screen. However, the OS knows that // these keys have released - only the kanata state is wrong. And since kanata has // a key in a stuck state, without this 1s fallback, live reload would never // activate. Having this fallback allows live reload to happen which resets the // kanata states. self.live_reload_requested = false; if let Err(e) = self.do_live_reload(tx) { log::error!("live reload failed {e}"); } } #[cfg(feature = "perf_logging")] log::info!("ms elapsed: {ms_elapsed}"); // Note regarding `as` casting. It doesn't really matter if the result would truncate and // end up being wrong. Prefer to do the cheaper operation, as compared to doing the min of // u16::MAX and ms_elapsed. Ok(ms_elapsed as u16) } pub fn tick_ms(&mut self, ms_elapsed: u128, _tx: &Option>) -> Result<()> { let mut extra_ticks: u16 = 0; for _ in 0..ms_elapsed { self.tick_states(_tx)?; if let Some(event) = tick_replay_state( &mut self.dynamic_macro_replay_state, self.dynamic_macro_replay_behaviour, ) { self.layout.bm().event(event.key_event()); extra_ticks = extra_ticks.saturating_add(event.delay()); log::debug!("dyn macro extra ticks: {extra_ticks}, ms_elapsed: {ms_elapsed}"); } } for i in 0..(extra_ticks.saturating_sub(ms_elapsed as u16)) { self.tick_states(_tx)?; if tick_replay_state( &mut self.dynamic_macro_replay_state, self.dynamic_macro_replay_behaviour, ) .is_some() { log::error!("overshot to next event at iteration #{i}, the code is broken!"); break; } } Ok(()) } fn tick_held_vkeys(&mut self) { if self.vkeys_pending_release.is_empty() { return; } let layout = self.layout.bm(); self.vkeys_pending_release.retain(|coord, deadline| { *deadline = deadline.saturating_sub(1); match deadline { 0 => { layout.event(Event::Release(coord.x, coord.y)); false } _ => true, } }); } fn tick_states(&mut self, _tx: &Option>) -> Result<()> { self.live_reload_requested |= self.handle_keystate_changes(_tx)?; self.handle_scrolling()?; self.handle_move_mouse()?; self.tick_sequence_state()?; self.tick_idle_timeout(); self.macro_on_press_cancel_duration = self.macro_on_press_cancel_duration.saturating_sub(1); tick_record_state(&mut self.dynamic_macro_record_state); zippy_tick(self.caps_word.is_some()); self.prev_keys.clear(); self.prev_keys.append(&mut self.cur_keys); self.tick_held_vkeys(); #[cfg(feature = "simulated_output")] { self.kbd_out.tick(); } Ok(()) } fn handle_scrolling(&mut self) -> Result<()> { if let Some(scroll_state) = &mut self.scroll_state { if scroll_state.ticks_until_scroll == 0 { scroll_state.ticks_until_scroll = scroll_state.interval - 1; self.kbd_out .scroll(scroll_state.direction, scroll_state.distance)?; } else { scroll_state.ticks_until_scroll -= 1; } } if let Some(hscroll_state) = &mut self.hscroll_state { if hscroll_state.ticks_until_scroll == 0 { hscroll_state.ticks_until_scroll = hscroll_state.interval - 1; self.kbd_out .scroll(hscroll_state.direction, hscroll_state.distance)?; } else { hscroll_state.ticks_until_scroll -= 1; } } Ok(()) } fn handle_move_mouse(&mut self) -> Result<()> { if let Some(mmsv) = &mut self.move_mouse_state_vertical { if let Some(mmas) = &mut mmsv.move_mouse_accel_state { if mmas.accel_ticks_until_max != 0 { let increment = (mmas.accel_increment * f64::from(mmas.accel_ticks_from_min)) as u16; mmsv.distance = mmas.min_distance + increment; mmas.accel_ticks_from_min += 1; mmas.accel_ticks_until_max -= 1; } else { mmsv.distance = mmas.max_distance; } } if mmsv.ticks_until_move == 0 { mmsv.ticks_until_move = mmsv.interval - 1; let scaled_distance = apply_mouse_distance_modifiers(mmsv.distance, &self.move_mouse_speed_modifiers); log::debug!("handle_move_mouse: scaled vdistance: {}", scaled_distance); let current_move = CalculatedMouseMove { direction: mmsv.direction, distance: scaled_distance, }; if self.movemouse_smooth_diagonals { let axis: Axis = current_move.direction.into(); match &self.movemouse_buffer { Some((previous_axis, previous_move)) => { if axis == *previous_axis { self.kbd_out.move_mouse(*previous_move)?; self.movemouse_buffer = Some((axis, current_move)); } else { self.kbd_out .move_mouse_many(&[*previous_move, current_move])?; self.movemouse_buffer = None; } } None => { self.movemouse_buffer = Some((axis, current_move)); } } } else { self.kbd_out.move_mouse(current_move)?; } } else { mmsv.ticks_until_move -= 1; } } if let Some(mmsh) = &mut self.move_mouse_state_horizontal { if let Some(mmas) = &mut mmsh.move_mouse_accel_state { if mmas.accel_ticks_until_max != 0 { let increment = (mmas.accel_increment * f64::from(mmas.accel_ticks_from_min)) as u16; mmsh.distance = mmas.min_distance + increment; mmas.accel_ticks_from_min += 1; mmas.accel_ticks_until_max -= 1; } else { mmsh.distance = mmas.max_distance; } } if mmsh.ticks_until_move == 0 { mmsh.ticks_until_move = mmsh.interval - 1; let scaled_distance = apply_mouse_distance_modifiers(mmsh.distance, &self.move_mouse_speed_modifiers); log::debug!("handle_move_mouse: scaled hdistance: {}", scaled_distance); let current_move = CalculatedMouseMove { direction: mmsh.direction, distance: scaled_distance, }; if self.movemouse_smooth_diagonals { let axis: Axis = current_move.direction.into(); match &self.movemouse_buffer { Some((previous_axis, previous_move)) => { if axis == *previous_axis { self.kbd_out.move_mouse(*previous_move)?; self.movemouse_buffer = Some((axis, current_move)); } else { self.kbd_out .move_mouse_many(&[*previous_move, current_move])?; self.movemouse_buffer = None; } } None => { self.movemouse_buffer = Some((axis, current_move)); } } } else { self.kbd_out.move_mouse(current_move)?; } } else { mmsh.ticks_until_move -= 1; } } Ok(()) } fn tick_sequence_state(&mut self) -> Result<()> { if let Some(state) = self.sequence_state.get_active() { state.ticks_until_timeout -= 1; if state.ticks_until_timeout == 0 { log::debug!("sequence timeout; exiting sequence state"); cancel_sequence(state, &mut self.kbd_out)?; } } Ok(()) } fn tick_idle_timeout(&mut self) { if self.waiting_for_idle.is_empty() { return; } self.waiting_for_idle.retain(|wfd| { if self.ticks_since_idle >= wfd.idle_duration { // Process this and return false so that it is not retained. let layout = self.layout.bm(); let Coord { x, y } = wfd.coord; handle_fakekey_action(wfd.action, layout, x, y); false } else { true } }) } /// Sends OS key events according to the change in key state between the current and the /// previous keyberon keystate. Also processes any custom actions. /// /// Updates self.cur_keys. /// /// Returns whether live reload was requested. fn handle_keystate_changes(&mut self, _tx: &Option>) -> Result { let layout = self.layout.bm(); let custom_event = layout.tick(); let mut live_reload_requested = false; let cur_keys = &mut self.cur_keys; cur_keys.extend(layout.keycodes()); let mut reverse_release_order = false; // Deal with unmodded. Unlike other custom actions, this should come before key presses and // releases. I don't quite remember why custom actions come after the key processing, but I // remember that it is intentional. However, since unmodded needs to modify the key lists, // it should come before. match custom_event { CustomEvent::Press(custacts) => { for custact in custacts.iter() { match custact { CustomAction::Unmodded { keys, mods } => { self.unmodded_keys.extend(keys.iter()); self.unmodded_mods = *mods; } CustomAction::Unshifted { keys } => { self.unshifted_keys.extend(keys.iter()); } _ => {} } } } CustomEvent::Release(custacts) => { for custact in custacts.iter() { match custact { CustomAction::Unmodded { keys, mods: _ } => { self.unmodded_keys.retain(|k| !keys.contains(k)); } CustomAction::Unshifted { keys } => { self.unshifted_keys.retain(|k| !keys.contains(k)); } CustomAction::ReverseReleaseOrder => { reverse_release_order = true; } _ => {} } } } _ => {} } if !self.unmodded_keys.is_empty() { for mod_key in self.unmodded_mods.iter() { let kc = match mod_key { UnmodMods::LSft => KeyCode::LShift, UnmodMods::RSft => KeyCode::RShift, UnmodMods::LAlt => KeyCode::LAlt, UnmodMods::RAlt => KeyCode::RAlt, UnmodMods::LCtl => KeyCode::LCtrl, UnmodMods::RCtl => KeyCode::RCtrl, UnmodMods::LMet => KeyCode::LGui, UnmodMods::RMet => KeyCode::RGui, _ => unreachable!("all bits of u8 should be covered"), // test_unmodmods_bits }; cur_keys.retain(|k| *k != kc); } cur_keys.extend(self.unmodded_keys.iter()); } if !self.unshifted_keys.is_empty() { cur_keys.retain(|k| !matches!(k, KeyCode::LShift | KeyCode::RShift)); cur_keys.extend(self.unshifted_keys.iter()); } self.overrides .override_keys(cur_keys, &mut self.override_states); mark_overridden_nonmodkeys_for_eager_erasure(&self.override_states, &mut layout.states); if self.override_release_on_activation { for removed in self.override_states.removed_oscs() { if !removed.is_modifier() { layout.states.retain(|s| { s.release_state(ReleasableState::KeyCode(removed.into())) .is_some() }); } } } if let Some(caps_word) = &mut self.caps_word { if caps_word.maybe_add_lsft(cur_keys) == CapsWordNextState::End { self.caps_word = None; } } // Release keys that do not exist in the current state but exist in the previous state. // This used to use a HashSet but it was changed to a Vec because the order of operations // matters. // // BUG(sequences): // // With hidden-delay-type or hidden-suppressed, // sequences will unexpectedly send releases // for the presses that would otherwise have happened. // This is because the press is skipped but the keys make it // into `self.prev_keys` and the OS release event is sent in the code below. // // There haven't been any reports of negative consequences of this behaviour, // but it is unusual and ideally wouldn't happen, so I tried to fix it anyway. // But I was unsuccessful. Approach tried: // // - clear `self.cur_keys` and `layout.states` of outputted keys // when a sequence is active, for the impacted sequence modes. // // This approach fails because it keeping `layout.states` intact // is necessary to complete chorded sequences, e.g. `S-(a b c)`. // Clearing the `lsft` means the above sequence is impossible to complete. // // Another approach that might work, which has not been attempted, // is to keep track of oskbd events that have actually been sent. // Then, a release can only be sent if an un-released corresponding press // has been pressed in the past. // However, this doesn't seem worth the: // // - runtime cost // - work involved to add the code // - ongoing burden of maintaining that code // // Given that there appears to be no practical negative consequences for this bug // remaining. log::trace!("{:?}", &self.prev_keys); let mut fwd_release = self.prev_keys.iter(); let mut rev_release = self.prev_keys.iter().rev(); let keys: &mut dyn Iterator = match reverse_release_order { false => &mut fwd_release, true => &mut rev_release, }; for k in keys { if cur_keys.contains(k) { continue; } log::debug!("key release {:?}", k); if let Err(e) = release_key(&mut self.kbd_out, k.into()) { bail!("failed to release key: {:?}", e); } } if cur_keys.is_empty() && !self.prev_keys.is_empty() { if let Some(state) = self.sequence_state.get_active() { use kanata_parser::trie::GetOrDescendentExistsResult::*; state.overlapped_sequence.push(KEY_OVERLAP_MARKER); match self .sequences .get_or_descendant_exists(&state.overlapped_sequence) { HasValue((i, j)) => { do_successful_sequence_termination( &mut self.kbd_out, state, layout, i, j, EndSequenceType::Overlap, )?; } NotInTrie => { // Overwrite overlapped with non-overlapped tracking state.overlapped_sequence.clear(); state .overlapped_sequence .extend(state.sequence.iter().copied()); } InTrie => {} } } } // Press keys that exist in the current state but are missing from the previous state. // Comment above regarding Vec/HashSet also applies here. log::trace!("{cur_keys:?}"); for k in cur_keys.iter() { if self.prev_keys.contains(k) { log::trace!("{k:?} is old press"); continue; } // Note - keyberon can return duplicates of a key in the keycodes() // iterator. Instead of trying to fix it in the keyberon library, It // seems better to fix it in the kanata logic. Keyberon iterates over // its internal state array with very simple filtering logic when // calling keycodes(). It would be troublesome to add deduplication // logic there and is easier to add here since we already have // allocations and logic. self.prev_keys.push(*k); self.last_pressed_key = *k; if self.sequence_always_on && self.sequence_state.is_inactive() { self.sequence_state .activate(self.sequence_input_mode, self.sequence_timeout); } if let Some(state) = self.sequence_state.get_active() { do_sequence_press_logic( state, k, get_mod_mask_for_cur_keys(cur_keys), &mut self.kbd_out, &self.sequences, self.sequence_backtrack_modcancel, layout, )?; } else { log::debug!("key press {:?}", k); if let Err(e) = press_key(&mut self.kbd_out, k.into()) { bail!("failed to press key: {:?}", e); } } } // Handle custom events. This used to be in a separate function but lifetime issues cause // it to now be here. match custom_event { CustomEvent::Press(custacts) => { #[cfg(feature = "cmd")] let mut cmds = vec![]; let mut prev_mouse_btn = None; for custact in custacts.iter() { match custact { // For unicode, only send on the press. No repeat action is supported for this for // now. CustomAction::Unicode(c) => self.kbd_out.send_unicode(*c)?, CustomAction::LiveReload => { live_reload_requested = true; log::info!( "Requested live reload of file: {}", self.cfg_paths[self.cur_cfg_idx].display() ); } CustomAction::LiveReloadNext => { live_reload_requested = true; self.cur_cfg_idx = if self.cur_cfg_idx == self.cfg_paths.len() - 1 { 0 } else { self.cur_cfg_idx + 1 }; log::info!( "Requested live reload of next file: {}", self.cfg_paths[self.cur_cfg_idx].display() ); } CustomAction::LiveReloadPrev => { live_reload_requested = true; self.cur_cfg_idx = match self.cur_cfg_idx { 0 => self.cfg_paths.len() - 1, i => i - 1, }; log::info!( "Requested live reload of prev file: {}", self.cfg_paths[self.cur_cfg_idx].display() ); } CustomAction::LiveReloadNum(n) => { let n = usize::from(*n); live_reload_requested = true; match self.cfg_paths.get(n) { Some(path) => { self.cur_cfg_idx = n; log::info!("Requested live reload of file: {}", path.display(),); } None => { log::error!("Requested live reload of config file number {}, but only {} config files were passed", n+1, self.cfg_paths.len()); } } } CustomAction::LiveReloadFile(path) => { let path = PathBuf::from(path); let result = self .cfg_paths .iter() .enumerate() .find(|(_idx, fpath)| **fpath == path); match result { Some((index, _path)) => { log::info!( "Requested live reload of file with path: {}", path.display(), ); live_reload_requested = true; self.cur_cfg_idx = index; } None => { log::error!("Requested live reload of file with path {}, but no such path was passed as an argument to Kanata", path.display()); } } } CustomAction::Mouse(btn) => { log::debug!("click {:?}", btn); if let Some(pbtn) = prev_mouse_btn { log::debug!("unclick {:?}", pbtn); self.kbd_out.release_btn(pbtn)?; } self.kbd_out.click_btn(*btn)?; prev_mouse_btn = Some(*btn); } CustomAction::MouseTap(btn) => { log::debug!("click {:?}", btn); self.kbd_out.click_btn(*btn)?; log::debug!("unclick {:?}", btn); self.kbd_out.release_btn(*btn)?; } CustomAction::MWheel { direction, interval, distance, } => match direction { MWheelDirection::Up | MWheelDirection::Down => { self.scroll_state = Some(ScrollState { direction: *direction, distance: *distance, ticks_until_scroll: 0, interval: *interval, }) } MWheelDirection::Left | MWheelDirection::Right => { self.hscroll_state = Some(ScrollState { direction: *direction, distance: *distance, ticks_until_scroll: 0, interval: *interval, }) } }, CustomAction::MWheelNotch { direction } => { self.kbd_out .scroll(*direction, HI_RES_SCROLL_UNITS_IN_LO_RES)?; } CustomAction::MoveMouse { direction, interval, distance, } => match direction { MoveDirection::Up | MoveDirection::Down => { self.move_mouse_state_vertical = Some(MoveMouseState { direction: *direction, distance: *distance, ticks_until_move: 0, interval: *interval, move_mouse_accel_state: None, }) } MoveDirection::Left | MoveDirection::Right => { self.move_mouse_state_horizontal = Some(MoveMouseState { direction: *direction, distance: *distance, ticks_until_move: 0, interval: *interval, move_mouse_accel_state: None, }) } }, CustomAction::MoveMouseAccel { direction, interval, accel_time, min_distance, max_distance, } => { let move_mouse_accel_state = match ( self.movemouse_inherit_accel_state, &self.move_mouse_state_horizontal, &self.move_mouse_state_vertical, ) { ( true, Some(MoveMouseState { move_mouse_accel_state: Some(s), .. }), _, ) | ( true, _, Some(MoveMouseState { move_mouse_accel_state: Some(s), .. }), ) => *s, _ => { let f_max_distance: f64 = *max_distance as f64; let f_min_distance: f64 = *min_distance as f64; let f_accel_time: f64 = *accel_time as f64; let increment = (f_max_distance - f_min_distance) / f_accel_time; MoveMouseAccelState { accel_ticks_from_min: 0, accel_ticks_until_max: *accel_time, accel_increment: increment, min_distance: *min_distance, max_distance: *max_distance, } } }; match direction { MoveDirection::Up | MoveDirection::Down => { self.move_mouse_state_vertical = Some(MoveMouseState { direction: *direction, distance: *min_distance, ticks_until_move: 0, interval: *interval, move_mouse_accel_state: Some(move_mouse_accel_state), }) } MoveDirection::Left | MoveDirection::Right => { self.move_mouse_state_horizontal = Some(MoveMouseState { direction: *direction, distance: *min_distance, ticks_until_move: 0, interval: *interval, move_mouse_accel_state: Some(move_mouse_accel_state), }) } } } CustomAction::MoveMouseSpeed { speed } => { self.move_mouse_speed_modifiers.push(*speed); log::debug!( "movemousespeed modifiers: {:?}", self.move_mouse_speed_modifiers ); } CustomAction::Cmd(_cmd) => { #[cfg(feature = "cmd")] cmds.push(( Some(log::Level::Info), Some(log::Level::Error), _cmd.clone(), )); } CustomAction::CmdLog(_log_level, _error_log_level, _cmd) => { #[cfg(feature = "cmd")] cmds.push(( _log_level.get_level(), _error_log_level.get_level(), _cmd.clone(), )); } CustomAction::CmdOutputKeys(_cmd) => { #[cfg(feature = "cmd")] { let cmd = _cmd.clone(); // Maybe improvement in the future: // A delay here, as in KeyAction::Delay, will pause the entire // state machine loop. That is _probably_ OK, but ideally this // would be done in a separate thread or somehow for key_action in keys_for_cmd_output(&cmd) { match key_action { KeyAction::Press(osc) => press_key(&mut self.kbd_out, osc)?, KeyAction::Release(osc) => { release_key(&mut self.kbd_out, osc)? } KeyAction::Delay(delay) => std::thread::sleep( std::time::Duration::from_millis(u64::from(delay)), ), } } } } CustomAction::PushMessage(_message) => { log::debug!("Action push-msg"); #[cfg(feature = "tcp_server")] if let Some(tx) = _tx { let message = simple_sexpr_to_json_array(_message); log::debug!("Action push-msg message: {}", message); match tx.try_send(ServerMessage::MessagePush { message }) { Ok(_) => {} Err(error) => { log::error!( "could not send {} event notification: {}", PUSH_MESSAGE, error ); } } } #[cfg(feature = "tcp_server")] if self.tcp_server_address.is_none() { log::warn!("{} was used, but TCP server is not running. did you specify a port?", PUSH_MESSAGE); } #[cfg(not(feature = "tcp_server"))] log::warn!( "{} was used, but Kanata was compiled with TCP server disabled.", PUSH_MESSAGE ); } CustomAction::FakeKey { coord, action } => { let (x, y) = (coord.x, coord.y); log::debug!( "fake key on press {action:?} {:?},{x:?},{y:?} {:?}", layout.default_layer, layout.layers[layout.default_layer][x as usize][y as usize] ); handle_fakekey_action(*action, layout, x, y); } CustomAction::Delay(delay) => { log::debug!("on-press: sleeping for {delay} ms"); std::thread::sleep(time::Duration::from_millis((*delay).into())); } CustomAction::SequenceCancel => { if let Some(state) = self.sequence_state.get_active() { log::debug!("pressed cancel sequence key"); cancel_sequence(state, &mut self.kbd_out)?; } } CustomAction::SequenceLeader(timeout, input_mode) => { if self.sequence_state.is_inactive() { log::debug!("entering sequence mode"); self.sequence_state.activate(*input_mode, *timeout); } else if *input_mode == SequenceInputMode::HiddenSuppressed { log::debug!("retriggering sequence mode"); self.sequence_state.activate(*input_mode, *timeout); } } CustomAction::SequenceNoerase(noerase_count) => { if let Some(state) = self.sequence_state.get_active() { log::debug!("pressed cancel sequence key"); add_noerase(state, *noerase_count); } } CustomAction::Repeat => { let keycode = self.last_pressed_key; let osc: OsCode = keycode.into(); log::debug!("repeating a keypress {osc:?}"); let mut do_caps_word = false; if !cur_keys.contains(&KeyCode::LShift) { if let Some(ref mut cw) = self.caps_word { cur_keys.push(keycode); let prev_len = cur_keys.len(); cw.maybe_add_lsft(cur_keys); if cur_keys.len() > prev_len { do_caps_word = true; press_key(&mut self.kbd_out, OsCode::KEY_LEFTSHIFT)?; } } } // Release key in case the most recently pressed key is still pressed. release_key(&mut self.kbd_out, osc)?; press_key(&mut self.kbd_out, osc)?; release_key(&mut self.kbd_out, osc)?; if do_caps_word { self.kbd_out.release_key(OsCode::KEY_LEFTSHIFT)?; } } CustomAction::DynamicMacroRecord(macro_id) => { if let Some((macro_id, prev_recorded_macro)) = begin_record_macro(*macro_id, &mut self.dynamic_macro_record_state) { log::debug!("saving macro {prev_recorded_macro:?}"); self.dynamic_macros.insert(macro_id, prev_recorded_macro); } } CustomAction::DynamicMacroRecordStop(num_actions_to_remove) => { if let Some((macro_id, prev_recorded_macro)) = stop_macro( &mut self.dynamic_macro_record_state, *num_actions_to_remove, ) { log::debug!("saving macro {prev_recorded_macro:?}"); self.dynamic_macros.insert(macro_id, prev_recorded_macro); } } CustomAction::DynamicMacroPlay(macro_id) => { play_macro( *macro_id, &mut self.dynamic_macro_replay_state, &self.dynamic_macros, ); } CustomAction::CancelMacroOnNextPress(duration) => { self.macro_on_press_cancel_duration = *duration; } CustomAction::SendArbitraryCode(code) => { #[cfg(all(not(feature = "simulated_output"), target_os = "windows"))] { self.kbd_out.write_code_raw(*code, KeyValue::Press)?; } #[cfg(any(feature = "simulated_output", not(target_os = "windows")))] { self.kbd_out.write_code(*code as u32, KeyValue::Press)?; } } CustomAction::CapsWord(cfg) => match cfg.repress_behaviour { CapsWordRepressBehaviour::Overwrite => { self.caps_word = Some(CapsWordState::new(cfg)); } CapsWordRepressBehaviour::Toggle => { self.caps_word = match self.caps_word { Some(_) => None, None => Some(CapsWordState::new(cfg)), }; } }, CustomAction::SetMouse { x, y } => { self.kbd_out.set_mouse(*x, *y)?; } CustomAction::FakeKeyOnIdle(fkd) => { self.ticks_since_idle = 0; self.waiting_for_idle.insert(*fkd); } CustomAction::FakeKeyHoldForDuration(fk_hfd) => { let duration = fk_hfd.hold_duration; self.vkeys_pending_release.entry(fk_hfd.coord) .and_modify(|d| *d = duration) .or_insert_with(|| { let Coord { x, y } = fk_hfd.coord; layout.event(Event::Press(x, y)); duration }); } CustomAction::ClipboardSet(clipboard_string) => { clpb_set(clipboard_string); } CustomAction::ClipboardCmdSet(cmd_params) => { clpb_cmd_set(cmd_params); } CustomAction::ClipboardSave(id) => { clpb_save(*id, &mut self.saved_clipboard_content); } CustomAction::ClipboardRestore(id) => { clpb_restore(*id, &self.saved_clipboard_content); } CustomAction::ClipboardSaveSet(id, clipboard_string) => { clpb_save_set(*id, clipboard_string, &mut self.saved_clipboard_content); } CustomAction::ClipboardSaveCmdSet(id, cmd_params) => { clpb_save_cmd_set(*id, cmd_params, &mut self.saved_clipboard_content); } CustomAction::ClipboardSaveSwap(id1, id2) => { clpb_save_swap(*id1, *id2, &mut self.saved_clipboard_content); } CustomAction::FakeKeyOnRelease { .. } | CustomAction::DelayOnRelease(_) | CustomAction::Unmodded { .. } | CustomAction::Unshifted { .. } // Note: ReverseReleaseOrder is already handled earlier on. | CustomAction::ReverseReleaseOrder | CustomAction::CancelMacroOnRelease => {} } } #[cfg(feature = "cmd")] run_multi_cmd(cmds); } CustomEvent::Release(custacts) => { // Unclick only the last mouse button if let Some(Err(e)) = custacts .iter() .fold(None, |pbtn, ac| match ac { CustomAction::Mouse(btn) => Some(btn), CustomAction::MWheel { direction, .. } => { match direction { MWheelDirection::Up | MWheelDirection::Down => { if let Some(ss) = &self.scroll_state { if ss.direction == *direction { self.scroll_state = None; } } } MWheelDirection::Left | MWheelDirection::Right => { if let Some(ss) = &self.hscroll_state { if ss.direction == *direction { self.hscroll_state = None; } } } } pbtn } CustomAction::MoveMouse { direction, .. } | CustomAction::MoveMouseAccel { direction, .. } => { match direction { MoveDirection::Up | MoveDirection::Down => { if let Some(move_mouse_state_vertical) = &self.move_mouse_state_vertical { if move_mouse_state_vertical.direction == *direction { self.move_mouse_state_vertical = None; } } } MoveDirection::Left | MoveDirection::Right => { if let Some(move_mouse_state_horizontal) = &self.move_mouse_state_horizontal { if move_mouse_state_horizontal.direction == *direction { self.move_mouse_state_horizontal = None; } } } } if self.movemouse_smooth_diagonals { self.movemouse_buffer = None } pbtn } CustomAction::MoveMouseSpeed { speed, .. } => { if let Some(idx) = self .move_mouse_speed_modifiers .iter() .position(|s| *s == *speed) { self.move_mouse_speed_modifiers.remove(idx); } log::debug!( "movemousespeed modifiers: {:?}", self.move_mouse_speed_modifiers ); pbtn } CustomAction::DelayOnRelease(delay) => { log::debug!("on-release: sleeping for {delay} ms"); std::thread::sleep(time::Duration::from_millis((*delay).into())); pbtn } CustomAction::FakeKeyOnRelease { coord, action } => { let (x, y) = (coord.x, coord.y); log::debug!("fake key on release {action:?} {x:?},{y:?}"); handle_fakekey_action(*action, layout, x, y); pbtn } CustomAction::CancelMacroOnRelease => { log::debug!("cancelling all macros: releasable macro"); layout.active_sequences.clear(); self.macro_on_press_cancel_duration = 0; layout.states.retain(|s| { !matches!( s, State::FakeKey { .. } | State::RepeatingSequence { .. } ) }); pbtn } CustomAction::SendArbitraryCode(code) => { if let Err(e) = { #[cfg(all( not(feature = "simulated_output"), target_os = "windows" ))] { self.kbd_out.write_code_raw(*code, KeyValue::Release) } #[cfg(any( feature = "simulated_output", not(target_os = "windows") ))] { self.kbd_out.write_code(*code as u32, KeyValue::Release) } } { log::error!("failed to release arbitrary code {e:?}"); } pbtn } _ => pbtn, }) .map(|btn| { log::debug!("unclick {:?}", btn); self.kbd_out.release_btn(*btn) }) { bail!(e); } } _ => {} }; self.check_release_non_physical_shift()?; Ok(live_reload_requested) } #[cfg(feature = "tcp_server")] pub fn change_layer(&mut self, layer_name: String) { for (i, l) in self.layer_info.iter().enumerate() { if l.name == layer_name { self.layout.bm().set_default_layer(i); return; } } } #[allow(unused_variables)] /// Prints the layer. If the TCP server is enabled, then this will also send a notification to /// all connected clients. fn check_handle_layer_change(&mut self, tx: &Option>) { let cur_layer = self.layout.bm().current_layer(); if cur_layer != self.prev_layer { let new = self.layer_info[cur_layer].name.clone(); self.prev_layer = cur_layer; self.print_layer(cur_layer); #[cfg(feature = "tcp_server")] if let Some(tx) = tx { match tx.try_send(ServerMessage::LayerChange { new }) { Ok(_) => {} Err(error) => { log::error!("could not send event notification: {}", error); } } } #[cfg(all(target_os = "windows", feature = "gui"))] send_gui_notice(); } } fn print_layer(&self, layer: usize) { if self.log_layer_changes { log::info!("Entered layer:\n\n{}", self.layer_info[layer].cfg_text); } } #[cfg(feature = "tcp_server")] pub fn start_notification_loop( rx: Receiver, clients: crate::tcp_server::Connections, ) { use std::io::Write; info!("listening for event notifications to relay to connected clients"); std::thread::spawn(move || { loop { match rx.recv() { Err(_) => { panic!("channel disconnected") } Ok(event) => { let notification = event.as_bytes(); let mut clients = clients.lock(); let mut stale_clients = vec![]; for (id, client) in &mut *clients { match client.write_all(¬ification) { Ok(_) => { log::debug!("layer change notification sent"); } Err(e) => { log::warn!( "removing tcp client where write failed: {id}, {e:?}" ); // the client is no longer connected, let's remove them stale_clients.push(id.clone()); } } } for id in &stale_clients { log::warn!("removing disconnected tcp client: {id}"); clients.remove(id); } } } } }); } #[cfg(not(feature = "tcp_server"))] pub fn start_notification_loop( _rx: Receiver, _clients: crate::tcp_server::Connections, ) { } /// Starts a new thread that processes OS key events and advances the keyberon layout's state. pub fn start_processing_loop( kanata: Arc>, rx: Receiver, tx: Option>, nodelay: bool, ) { info!("entering the processing loop"); std::thread::spawn(move || { if !nodelay { info!("Init: catching only releases and sending immediately"); for _ in 0..500 { if let Ok(kev) = rx.try_recv() { if kev.value == KeyValue::Release { let mut k = kanata.lock(); info!("Init: releasing {:?}", kev.code); k.kbd_out.release_key(kev.code).expect("key released"); } } std::thread::sleep(time::Duration::from_millis(1)); } } let mut ms_elapsed = 0; info!("Starting kanata proper"); #[cfg(not(feature = "passthru_ahk"))] info!( "You may forcefully exit kanata by pressing lctl+spc+esc at any time. \ These keys refer to defsrc input, meaning BEFORE kanata remaps keys." ); #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] let mut idle_clear_happened = false; #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] let mut last_input_time = instant::Instant::now(); let err = loop { let can_block = { let mut k = kanata.lock(); k.can_block_update_idle_waiting(ms_elapsed) }; if can_block { #[cfg(all( target_os = "windows", not(feature = "interception_driver"), not(feature = "simulated_input"), ))] kanata.lock().win_synchronize_keystates(); log::trace!("blocking on channel"); match rx.recv() { Ok(kev) => { let mut k = kanata.lock(); let now = instant::Instant::now() .checked_sub(time::Duration::from_millis(1)) .expect("subtract 1ms from current time"); #[cfg(all( not(feature = "interception_driver"), target_os = "windows" ))] { // If kanata has been inactive for long enough, clear all states. // This won't trigger if there are macros running, or if a key is // held down for a long time and is sending OS repeats. The reason // for this code is in cases like Win+L which locks the Windows // desktop. When this happens, the Win key and L key will be stuck // as pressed in the kanata state because LLHOOK kanata cannot read // keys in the lock screen or administrator applications. So this // is heuristic to detect such an issue and clear states assuming // that's what happened. // // Only states in the normal key row are cleared, since those are // the states that might be stuck. A real use case might be to have // a fake key pressed for a long period of time, so make sure those // are not cleared. if (now - last_input_time) > time::Duration::from_secs(LLHOOK_IDLE_TIME_SECS_CLEAR_INPUTS) { log::debug!( "clearing keyberon normal key states due to inactivity" ); let layout = k.layout.bm(); release_normalkey_states(layout); PRESSED_KEYS.lock().clear(); } } k.last_tick = now; #[cfg(feature = "perf_logging")] let start = instant::Instant::now(); if let Err(e) = k.handle_input_event(&kev) { break e; } #[cfg(all( not(feature = "interception_driver"), target_os = "windows" ))] { last_input_time = now; } #[cfg(all( not(feature = "interception_driver"), target_os = "windows" ))] { idle_clear_happened = false; } #[cfg(feature = "perf_logging")] log::info!( "[PERF]: handle key event: {} ns", (start.elapsed()).as_nanos() ); #[cfg(feature = "perf_logging")] let start = instant::Instant::now(); match k.handle_time_ticks(&tx) { Ok(ms) => ms_elapsed = ms, Err(e) => break e, }; #[cfg(feature = "perf_logging")] log::info!( "[PERF]: handle time ticks: {} ns", (start.elapsed()).as_nanos() ); } Err(_) => { log::error!("channel disconnected"); return; } } } else { let mut k = kanata.lock(); match rx.try_recv() { Ok(kev) => { #[cfg(feature = "perf_logging")] let start = instant::Instant::now(); if let Err(e) = k.handle_input_event(&kev) { break e; } #[cfg(all( not(feature = "interception_driver"), target_os = "windows" ))] { last_input_time = instant::Instant::now(); } #[cfg(all( not(feature = "interception_driver"), target_os = "windows" ))] { idle_clear_happened = false; } #[cfg(feature = "perf_logging")] log::info!( "[PERF]: handle key event: {} ns", (start.elapsed()).as_nanos() ); #[cfg(feature = "perf_logging")] let start = instant::Instant::now(); match k.handle_time_ticks(&tx) { Ok(ms) => ms_elapsed = ms, Err(e) => break e, }; #[cfg(feature = "perf_logging")] log::info!( "[PERF]: handle time ticks: {} ns", (start.elapsed()).as_nanos() ); } Err(TryRecvError::Empty) => { #[cfg(feature = "perf_logging")] let start = instant::Instant::now(); match k.handle_time_ticks(&tx) { Ok(ms) => ms_elapsed = ms, Err(e) => break e, }; #[cfg(feature = "perf_logging")] log::info!( "[PERF]: handle time ticks: {} ns", (start.elapsed()).as_nanos() ); #[cfg(all( not(feature = "interception_driver"), target_os = "windows" ))] { // If kanata has been inactive for long enough, clear all states. // This won't trigger if there are macros running, or if a key is // held down for a long time and is sending OS repeats. The reason // for this code is in case like Win+L which locks the Windows // desktop. When this happens, the Win key and L key will be stuck // as pressed in the kanata state because LLHOOK kanata cannot read // keys in the lock screen or administrator applications. So this // is heuristic to detect such an issue and clear states assuming // that's what happened. // // Only states in the normal key row are cleared, since those are // the states that might be stuck. A real use case might be to have // a fake key pressed for a long period of time, so make sure those // are not cleared. if (instant::Instant::now() - (last_input_time)) > time::Duration::from_secs(LLHOOK_IDLE_TIME_SECS_CLEAR_INPUTS) && !idle_clear_happened { idle_clear_happened = true; log::debug!( "clearing keyberon normal key states due to inactivity" ); let layout = k.layout.bm(); release_normalkey_states(layout); PRESSED_KEYS.lock().clear(); } } drop(k); std::thread::sleep(time::Duration::from_millis(1)); } Err(TryRecvError::Disconnected) => { log::error!("channel disconnected"); return; } } } }; panic!("processing loop encountered error {err:?}") }); } /// Returns `true` if kanata's processing thread loop can block on the channel instead of doing /// a non-blocking channel read and then sleeping for ~1ms. /// /// In addition to doing the logic for the above, this mutates the `waiting_for_idle` state /// used by the `on-idle` action for virtual keys. pub fn can_block_update_idle_waiting(&mut self, ms_elapsed: u16) -> bool { let k = self; let is_idle = k.is_idle(); // Note: checking waiting_for_idle can not be part of the computation for // is_idle() since incrementing ticks_since_idle is dependent on the return // value of is_idle(). let counting_idle_ticks = !k.waiting_for_idle.is_empty() || k.live_reload_requested; if !is_idle { k.ticks_since_idle = 0; } else if is_idle && counting_idle_ticks { k.ticks_since_idle = k.ticks_since_idle.saturating_add(ms_elapsed); #[cfg(feature = "perf_logging")] log::info!("ticks since idle: {}", k.ticks_since_idle); } // NOTE: this check must not be part of `is_idle` because its falsiness // does not mean that kanata is in a non-idle state, just that we // haven't done enough ticks yet to properly compute key-timing. let passed_max_switch_timing_check = k .layout .b() .historical_keys .iter_hevents() .next() .map(|he| he.ticks_since_occurrence >= k.switch_max_key_timing) .unwrap_or(true); let chordsv2_accepts_chords = k .layout .b() .chords_v2 .as_ref() .map(|cv2| cv2.accepts_chords_chv2()) .unwrap_or(true); is_idle && !counting_idle_ticks && passed_max_switch_timing_check && chordsv2_accepts_chords } pub fn is_idle(&self) -> bool { let pressed_keys_means_not_idle = !self.waiting_for_idle.is_empty() || self.live_reload_requested; self.layout.b().queue.is_empty() && zippy_is_idle() && self.layout.b().waiting.is_none() && self.layout.b().last_press_tracker.tap_hold_timeout == 0 && (self.layout.b().oneshot.timeout == 0 || self.layout.b().oneshot.keys.is_empty()) && self.layout.b().active_sequences.is_empty() && self.layout.b().tap_dance_eager.is_none() && self.layout.b().action_queue.is_empty() && self.sequence_state.is_inactive() && self.scroll_state.is_none() && self.hscroll_state.is_none() && self.move_mouse_state_vertical.is_none() && self.macro_on_press_cancel_duration == 0 && self.move_mouse_state_horizontal.is_none() && self.dynamic_macro_replay_state.is_none() && self.caps_word.is_none() && self.vkeys_pending_release.is_empty() && !self.layout.b().states.iter().any(|s| { matches!(s, State::SeqCustomPending(_) | State::SeqCustomActive(_)) || (pressed_keys_means_not_idle && matches!(s, State::NormalKey { .. })) }) && self .layout .b() .chords_v2 .as_ref() .map(|cv2| cv2.is_idle_chv2()) .unwrap_or(true) } } #[test] fn test_unmodmods_bits() { assert_eq!(UnmodMods::empty().bits(), 0u8); assert_eq!(UnmodMods::all().bits(), 255u8); } #[cfg(feature = "cmd")] fn run_multi_cmd(cmds: Vec<(Option, Option, Vec)>) { std::thread::spawn(move || { for (cmd_log_level, cmd_error_log_level, cmd) in cmds { if let Err(e) = run_cmd_in_thread(cmd, cmd_log_level, cmd_error_log_level).join() { log::error!("problem joining thread {:?}", e); } } }); } fn apply_mouse_distance_modifiers(initial_distance: u16, mods: &Vec) -> u16 { let mut scaled_distance = initial_distance; for &modifier in mods { scaled_distance = u16::max( 1, f32::min( scaled_distance as f32 * (modifier as f32 / 100f32), u16::MAX as f32, ) .round() as u16, ); } scaled_distance } #[test] fn apply_speed_modifiers() { assert_eq!(apply_mouse_distance_modifiers(15, &vec![]), 15); assert_eq!(apply_mouse_distance_modifiers(10, &vec![200u16]), 20); assert_eq!(apply_mouse_distance_modifiers(20, &vec![50u16]), 10); assert_eq!(apply_mouse_distance_modifiers(5, &vec![33u16]), 2); // 1.65 assert_eq!(apply_mouse_distance_modifiers(100, &vec![99u16]), 99); // Clamping assert_eq!( apply_mouse_distance_modifiers(65535, &vec![65535u16]), 65535 ); assert_eq!(apply_mouse_distance_modifiers(1, &vec![1u16]), 1); // Nice, round calculations equal themselves assert_eq!( apply_mouse_distance_modifiers(10, &vec![50u16, 200u16]), apply_mouse_distance_modifiers(10, &vec![200u16, 50u16]) ); // 33% of 20 assert_eq!(apply_mouse_distance_modifiers(10, &vec![200u16, 33u16]), 7); // 200% of 3 assert_eq!(apply_mouse_distance_modifiers(10, &vec![33u16, 200u16]), 6); } #[cfg(feature = "passthru_ahk")] /// Clean kanata's state without exiting pub fn clean_state(kanata: &Arc>, tick: u128) -> Result<()> { let mut k = kanata.lock(); #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] let layout = k.layout.bm(); #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] release_normalkey_states(layout); k.tick_ms(tick, &None)?; #[cfg(not(target_os = "linux"))] { let mut k_pressed = PRESSED_KEYS.lock(); for key_os in k_pressed.clone() { k.kbd_out.release_key(key_os)?; } k_pressed.clear(); } Ok(()) } /// Checks if kanata should exit based on the fixed key combination of: /// Lctl+Spc+Esc fn check_for_exit(_event: &KeyEvent) { #[cfg(not(feature = "passthru_ahk"))] { use std::sync::atomic::{AtomicBool, Ordering::SeqCst}; static IS_LCL_PRESSED: AtomicBool = AtomicBool::new(false); static IS_SPC_PRESSED: AtomicBool = AtomicBool::new(false); static IS_ESC_PRESSED: AtomicBool = AtomicBool::new(false); let is_pressed = match _event.value { KeyValue::Press => true, KeyValue::Release => false, _ => return, }; match _event.code { OsCode::KEY_ESC => IS_ESC_PRESSED.store(is_pressed, SeqCst), OsCode::KEY_SPACE => IS_SPC_PRESSED.store(is_pressed, SeqCst), OsCode::KEY_LEFTCTRL => IS_LCL_PRESSED.store(is_pressed, SeqCst), _ => return, } const EXIT_MSG: &str = "pressed LControl+Space+Escape, exiting"; if IS_ESC_PRESSED.load(SeqCst) && IS_SPC_PRESSED.load(SeqCst) && IS_LCL_PRESSED.load(SeqCst) { log::info!("{EXIT_MSG}"); #[cfg(all(target_os = "windows", feature = "gui"))] { #[cfg(not(feature = "interception_driver"))] native_windows_gui::stop_thread_dispatch(); #[cfg(feature = "interception_driver")] send_gui_exit_notice(); // interception driver is running in another thread to allow // GUI take the main one, so it's calling check_for_exit // from a thread that has no access to the main one, so // can't stop main thread's dispatch } #[cfg(all( not(target_os = "linux"), not(all(target_os = "windows", feature = "gui")) ))] { panic!("{EXIT_MSG}"); } #[cfg(target_os = "linux")] { signal_hook::low_level::raise(signal_hook::consts::SIGTERM).expect("raise signal"); } } } } fn update_kbd_out(_cfg: &CfgOptions, _kbd_out: &KbdOut) -> Result<()> { #[cfg(all(not(feature = "simulated_output"), target_os = "linux"))] { _kbd_out.update_unicode_termination(_cfg.linux_opts.linux_unicode_termination); _kbd_out.update_unicode_u_code(_cfg.linux_opts.linux_unicode_u_code); } Ok(()) } pub fn handle_fakekey_action<'a, const C: usize, const R: usize, T>( action: FakeKeyAction, layout: &mut Layout<'a, C, R, T>, x: u8, y: u16, ) where T: 'a + std::fmt::Debug + Copy, { match action { FakeKeyAction::Press => layout.event(Event::Press(x, y)), FakeKeyAction::Release => layout.event(Event::Release(x, y)), FakeKeyAction::Tap => { layout.event(Event::Press(x, y)); layout.event(Event::Release(x, y)); } FakeKeyAction::Toggle => { match states_has_coord(&layout.states, x, y) { true => layout.event(Event::Release(x, y)), false => layout.event(Event::Press(x, y)), }; } }; } fn states_has_coord(states: &[State], x: u8, y: u16) -> bool { states.iter().any(|s| match s { State::NormalKey { coord, .. } | State::LayerModifier { coord, .. } | State::Custom { coord, .. } | State::RepeatingSequence { coord, .. } => *coord == (x, y), _ => false, }) } #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] fn release_normalkey_states<'a, const C: usize, const R: usize, T>(layout: &mut Layout<'a, C, R, T>) where T: 'a + std::fmt::Debug + Copy, { let mut coords_to_release = vec![]; for state in layout.states.iter().copied() { match state { State::NormalKey { coord: (NORMAL_KEY_ROW, y), .. } | State::LayerModifier { coord: (NORMAL_KEY_ROW, y), .. } | State::Custom { coord: (NORMAL_KEY_ROW, y), .. } | State::RepeatingSequence { coord: (NORMAL_KEY_ROW, y), .. } => { coords_to_release.push((NORMAL_KEY_ROW, y)); } _ => {} } } for coord in coords_to_release.into_iter() { layout.event(Event::Release(coord.0, coord.1)); } } kanata-1.9.0/src/kanata/output_logic/zippychord.rs000064400000000000000000000645241046102023000204110ustar 00000000000000use super::*; use kanata_parser::subset::GetOrIsSubsetOfKnownKey::*; use std::sync::Arc; use std::sync::Mutex; use std::sync::MutexGuard; // Maybe-todos: // --- // Feature-parity: suffixes - only active while disabled, to complete a word. // Feature-parity: prefix vs. non-prefix. Assuming smart spacing is implemented and enabled, // standard activations would output space one outputs space, but not prefixes. // I guess can be done in parser. static ZCH: Lazy> = Lazy::new(|| Mutex::new(Default::default())); pub(crate) fn zch() -> MutexGuard<'static, ZchState> { match ZCH.lock() { Ok(guard) => guard, Err(poisoned) => { let mut inner = poisoned.into_inner(); inner.zchd.zchd_reset(); inner } } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] enum ZchEnabledState { #[default] Enabled, WaitEnable, Disabled, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] enum ZchLastPressClassification { #[default] IsChord, NotChord, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] enum ZchSmartSpaceState { #[default] Inactive, Sent, } #[derive(Debug, Default)] struct ZchDynamicState { /// Input to compare against configured available chords to output. zchd_input_keys: ZchInputKeys, /// Whether chording should be enabled or disabled. /// Chording will be disabled if: /// - further presses cannot possibly activate a chord /// - a release happens with no chord having been activated /// /// Once disabled, chording will be enabled when: /// - all keys have been released /// - zchd_ticks_until_enabled shrinks to 0 zchd_enabled_state: ZchEnabledState, /// Is Some when a chord has been activated which has possible follow-up chords. /// E.g. dy -> day /// dy 1 -> Monday /// dy 2 -> Tuesday /// Using the example above, when dy has been activated, the `1` and `2` activations will be /// contained within `zchd_prioritized_chords`. This is cleared if the input is such that an /// activation is no longer possible. zchd_prioritized_chords: Option>>, /// Tracks the prior output character count /// because it may need to be erased (see `zchd_prioritized_chords). zchd_prior_activation_output_count: i16, /// Tracks the number of characters typed to complete an activation, which will be erased if an /// activation completes succesfully. zchd_characters_to_delete_on_next_activation: i16, /// Tracks past activation for additional computation. zchd_prior_activation: Option>, /// Tracker for time until prior state change to know if potential stale data should be /// cleared. This is a contingency in case of bugs or weirdness with OS interactions, e.g. /// Windows lock screen weirdness. /// /// This counts upwards to a "reset state" number. zchd_ticks_since_state_change: u16, /// Zch has a time delay between being disabled->pending-enabled->truly-enabled to mitigate /// against unintended activations. This counts downwards from a configured number until 0, and /// at 0 the state transitions from pending-enabled to truly-enabled if applicable. zchd_ticks_until_enabled: u16, /// There is a deadline between the first press happening and a chord activation being /// possible; after which if a chord has not been activated, zippychording is disabled. This /// state is the counter for this deadline. zchd_ticks_until_disable: u16, /// Track number of activations within the same hold. zchd_same_hold_activation_count: u16, /// Current state of caps-word, which is a factor in handling capitalization. zchd_is_caps_word_active: bool, /// Current state of lsft which is a factor in handling capitalization. zchd_is_lsft_active: bool, /// Current state of rsft which is a factor in handling capitalization. zchd_is_rsft_active: bool, /// Current state of altgr which is a factor in smart space erasure. zchd_is_altgr_active: bool, /// Tracks whether last press was part of a chord or not. /// Upon releasing keys, this state determines if zippychording should remain enabled or /// disabled. zchd_last_press: ZchLastPressClassification, /// Tracks smart spacing state so punctuation characters /// can know whether a space needs to be erased or not. zchd_smart_space_state: ZchSmartSpaceState, } impl ZchDynamicState { fn zchd_tick(&mut self, is_caps_word_active: bool) { const TICKS_UNTIL_FORCE_STATE_RESET: u16 = 10000; self.zchd_ticks_since_state_change += 1; self.zchd_is_caps_word_active = is_caps_word_active; match self.zchd_enabled_state { ZchEnabledState::WaitEnable => { self.zchd_ticks_until_enabled = self.zchd_ticks_until_enabled.saturating_sub(1); if self.zchd_ticks_until_enabled == 0 { log::debug!("zippy wait enable->enable"); self.zchd_enabled_state = ZchEnabledState::Enabled; self.zchd_ticks_until_disable = 0; } } ZchEnabledState::Enabled => { // Only run disable-check logic if ticks is already greater than zero, because zero // means deadline has never been triggered by any press. if self.zchd_ticks_until_disable > 0 { self.zchd_ticks_until_disable = self.zchd_ticks_until_disable.saturating_sub(1); if self.zchd_ticks_until_disable == 0 { log::debug!("zippy enable->disable"); self.zchd_soft_reset(); } } } ZchEnabledState::Disabled => {} } if self.zchd_ticks_since_state_change > TICKS_UNTIL_FORCE_STATE_RESET { self.zchd_reset(); } } fn zchd_state_change(&mut self, cfg: &ZchConfig) { self.zchd_ticks_since_state_change = 0; self.zchd_ticks_until_enabled = cfg.zch_cfg_ticks_wait_enable; } fn zchd_activate_chord_deadline(&mut self, deadline_ticks: u16) { if self.zchd_ticks_until_disable == 0 { self.zchd_ticks_until_disable = deadline_ticks; } } fn zchd_restart_deadline(&mut self, deadline_ticks: u16) { self.zchd_ticks_until_disable = deadline_ticks; } /// Clean up the state, potentially causing inaccuracies with regards to what the user is /// currently still pressing. fn zchd_reset(&mut self) { log::debug!("zchd reset state"); self.zchd_soft_reset(); self.zchd_is_caps_word_active = false; self.zchd_is_lsft_active = false; self.zchd_is_rsft_active = false; self.zchd_is_altgr_active = false; self.zchd_last_press = ZchLastPressClassification::IsChord; self.zchd_enabled_state = ZchEnabledState::Enabled; } fn zchd_soft_reset(&mut self) { log::debug!("zchd soft reset state"); self.zchd_last_press = ZchLastPressClassification::NotChord; self.zchd_enabled_state = ZchEnabledState::Disabled; self.zchd_input_keys.zchik_clear(); self.zchd_ticks_since_state_change = 0; self.zchd_ticks_until_disable = 0; self.zchd_ticks_until_enabled = 0; self.zchd_smart_space_state = ZchSmartSpaceState::Inactive; self.zchd_clear_history(); } fn zchd_clear_history(&mut self) { log::debug!("zchd clear historical data"); self.zchd_characters_to_delete_on_next_activation = 0; self.zchd_prioritized_chords = None; self.zchd_prior_activation = None; self.zchd_prior_activation_output_count = 0; } /// Returns true if dynamic zch state is such that idling optimization can activate. fn zchd_is_idle(&self) -> bool { let is_idle = self.zchd_enabled_state == ZchEnabledState::Enabled && self.zchd_input_keys.zchik_is_empty(); log::trace!("zch is idle: {is_idle}"); is_idle } fn zchd_press_key(&mut self, osc: OsCode) { self.zchd_input_keys.zchik_insert(osc); } fn zchd_release_key(&mut self, osc: OsCode) { self.zchd_input_keys.zchik_remove(osc); match (self.zchd_last_press, self.zchd_input_keys.zchik_is_empty()) { (ZchLastPressClassification::NotChord, true) => { log::debug!("all released->zippy wait enable"); self.zchd_enabled_state = ZchEnabledState::WaitEnable; self.zchd_clear_history(); } (ZchLastPressClassification::NotChord, false) => { log::debug!("release but not all->zippy disable"); self.zchd_soft_reset(); } (ZchLastPressClassification::IsChord, true) => { log::debug!("all released->zippy enabled"); if self.zchd_prioritized_chords.is_none() { log::debug!("no continuation->zippy clear key erase state"); self.zchd_clear_history(); } self.zchd_characters_to_delete_on_next_activation = 0; self.zchd_ticks_until_disable = 0; self.zchd_enabled_state = ZchEnabledState::Enabled; self.zchd_same_hold_activation_count = 0; } (ZchLastPressClassification::IsChord, false) => { log::debug!("some released->zippy enabled"); self.zchd_ticks_until_disable = 0; } } } } #[derive(Debug, Default)] pub(crate) struct ZchState { /// Dynamic state. Maybe doesn't make sense to separate this from zch_chords and to instead /// just flatten the structures. zchd: ZchDynamicState, /// Chords configured by the user. This is fixed at runtime other than live-reloads replacing /// the state. zch_chords: ZchPossibleChords, /// Options to configure behaviour. zch_cfg: ZchConfig, } impl ZchState { /// Configure zippychord behaviour. pub(crate) fn zch_configure(&mut self, cfg: (ZchPossibleChords, ZchConfig)) { self.zch_chords = cfg.0; self.zch_cfg = cfg.1; self.zchd.zchd_reset(); } /// Zch handling for key presses. pub(crate) fn zch_press_key( &mut self, kb: &mut KbdOut, osc: OsCode, ) -> Result<(), std::io::Error> { if self.zch_chords.is_empty() { return kb.press_key(osc); } match osc { OsCode::KEY_LEFTSHIFT => { self.zchd.zchd_is_lsft_active = true; return kb.press_key(osc); } OsCode::KEY_RIGHTSHIFT => { self.zchd.zchd_is_rsft_active = true; return kb.press_key(osc); } OsCode::KEY_RIGHTALT => { self.zchd.zchd_is_altgr_active = true; return kb.press_key(osc); } osc if osc.is_zippy_ignored() => { return kb.press_key(osc); } _ => {} } if self.zchd.zchd_smart_space_state == ZchSmartSpaceState::Sent && self .zch_cfg .zch_cfg_smart_space_punctuation .contains(&match ( self.zchd.zchd_is_lsft_active | self.zchd.zchd_is_rsft_active, self.zchd.zchd_is_altgr_active, ) { (false, false) => ZchOutput::Lowercase(osc), (true, false) => ZchOutput::Uppercase(osc), (false, true) => ZchOutput::AltGr(osc), (true, true) => ZchOutput::ShiftAltGr(osc), }) { self.zchd.zchd_characters_to_delete_on_next_activation -= 1; kb.press_key(OsCode::KEY_BACKSPACE)?; kb.release_key(OsCode::KEY_BACKSPACE)?; } self.zchd.zchd_smart_space_state = ZchSmartSpaceState::Inactive; if self.zchd.zchd_enabled_state != ZchEnabledState::Enabled { return kb.press_key(osc); } // Zippychording is enabled. Ensure the deadline to disable it if no chord activates is // active. self.zchd .zchd_activate_chord_deadline(self.zch_cfg.zch_cfg_ticks_chord_deadline); self.zchd.zchd_state_change(&self.zch_cfg); self.zchd.zchd_press_key(osc); // There might be an activation. // - delete typed keys // - output activation // // Key deletion needs to remove typed keys as well as past activations that need to be // cleaned up, e.g. either the antecedent in a "combo chord" or an eagerly-activated // chord using fewer keys, but user has still held that chord and pressed further keys, // activating a chord with the same+extra keys. let mut activation = Neither; if let Some(pchords) = &self.zchd.zchd_prioritized_chords { activation = pchords .lock() .0 .ssm_get_or_is_subset_ksorted(self.zchd.zchd_input_keys.zchik_keys()); } let mut is_prioritized_activation = false; if !matches!(activation, HasValue(..)) { activation = self .zch_chords .0 .ssm_get_or_is_subset_ksorted(self.zchd.zchd_input_keys.zchik_keys()); } else { is_prioritized_activation = true; } match activation { HasValue(a) => { // Find the longest common prefix length between the prior activation and the new // activation. This value affects both: // - the number of backspaces that need to be done // - the number of characters that actually need to be typed by the activation let common_prefix_len_from_past_activation = if !is_prioritized_activation && self.zchd.zchd_same_hold_activation_count == 0 { 0 } else { self.zchd .zchd_prior_activation .as_ref() .map(|prior_activation| { let current_activation_output = &a.zch_output; let mut len: i16 = 0; for (past, current) in prior_activation .zch_output .iter() .copied() .zip(current_activation_output.iter().copied()) { if past.osc() == OsCode::KEY_BACKSPACE || current.osc() == OsCode::KEY_BACKSPACE || past != current { break; } len += 1; } len }) .unwrap_or(0) }; self.zchd.zchd_prior_activation = Some(a.clone()); self.zchd.zchd_same_hold_activation_count += 1; self.zchd .zchd_restart_deadline(self.zch_cfg.zch_cfg_ticks_chord_deadline); if !a.zch_output.is_empty() { // Zippychording eagerly types characters that form a chord and also eagerly // outputs chords that are of a maybe-to-be-activated-later chord with more // participating keys. This procedure erases both classes of typed characters // in order to have the correct typed output for this chord activation. for _ in 0..(self.zchd.zchd_characters_to_delete_on_next_activation + if is_prioritized_activation { self.zchd.zchd_prior_activation_output_count } else { 0 } - common_prefix_len_from_past_activation) { kb.press_key(OsCode::KEY_BACKSPACE)?; kb.release_key(OsCode::KEY_BACKSPACE)?; } self.zchd.zchd_characters_to_delete_on_next_activation = 0; self.zchd.zchd_prior_activation_output_count = ZchOutput::display_len(&a.zch_output); } else { // Followup chords may consist of an empty output; eventually in the followup // chain has an activation output that is not empty. For empty outputs, do not // do any backspacing. self.zchd.zchd_characters_to_delete_on_next_activation += 1; self.zchd.zchd_prior_activation_output_count += self.zchd.zchd_input_keys.zchik_keys().len() as i16; kb.press_key(osc)?; } self.zchd .zchd_prioritized_chords .clone_from(&a.zch_followups); let mut released_sft = false; #[cfg(feature = "interception_driver")] let mut send_count = 0; if self.zchd.zchd_is_altgr_active && !a.zch_output.is_empty() { kb.release_key(OsCode::KEY_RIGHTALT)?; } for key_to_send in a .zch_output .iter() .copied() .skip(common_prefix_len_from_past_activation as usize) { #[cfg(feature = "interception_driver")] { // Note: every 5 keys on Windows Interception, do a sleep because // sending too quickly apparently causes weird behaviour... // I guess there's some buffer in the Interception code that is filling up. send_count += 1; if send_count % 5 == 0 { std::thread::sleep(std::time::Duration::from_millis(1)); } } match key_to_send { ZchOutput::Lowercase(osc) | ZchOutput::NoEraseLowercase(osc) => { type_osc(osc, kb, &self.zchd)?; } ZchOutput::Uppercase(osc) | ZchOutput::NoEraseUppercase(osc) => { maybe_press_sft_during_activation(released_sft, kb, &self.zchd)?; type_osc(osc, kb, &self.zchd)?; maybe_release_sft_during_activation(released_sft, kb, &self.zchd)?; } ZchOutput::AltGr(osc) | ZchOutput::NoEraseAltGr(osc) => { // A note regarding maybe_press|release_sft // in contrast to always pressing|releasing altgr: // // The maybe-logic is valuable with Shift to capitalize the first // typed output during activation. // However, altgr - if already held - // does not seem useful to keep held on the first typed output so it is // always released at the beginning and pressed at the end if it was // previously being held. kb.press_key(OsCode::KEY_RIGHTALT)?; type_osc(osc, kb, &self.zchd)?; kb.release_key(OsCode::KEY_RIGHTALT)?; } ZchOutput::ShiftAltGr(osc) | ZchOutput::NoEraseShiftAltGr(osc) => { kb.press_key(OsCode::KEY_RIGHTALT)?; maybe_press_sft_during_activation(released_sft, kb, &self.zchd)?; type_osc(osc, kb, &self.zchd)?; maybe_release_sft_during_activation(released_sft, kb, &self.zchd)?; kb.release_key(OsCode::KEY_RIGHTALT)?; } }; self.zchd.zchd_characters_to_delete_on_next_activation += key_to_send.output_char_count(); if !released_sft && !self.zchd.zchd_is_caps_word_active { released_sft = true; if self.zchd.zchd_is_lsft_active { kb.release_key(OsCode::KEY_LEFTSHIFT)?; } if self.zchd.zchd_is_rsft_active { kb.release_key(OsCode::KEY_RIGHTSHIFT)?; } } } if self.zch_cfg.zch_cfg_smart_space != ZchSmartSpaceCfg::Disabled && a.zch_output .last() .map(|out| !matches!(out.osc(), OsCode::KEY_SPACE | OsCode::KEY_BACKSPACE)) .unwrap_or(false /* if output is empty, don't do smart spacing */) { if self.zch_cfg.zch_cfg_smart_space == ZchSmartSpaceCfg::Full { self.zchd.zchd_smart_space_state = ZchSmartSpaceState::Sent; } // It might look unusual to add to both. // This is correct to do. // zchd_prior_activation_output_count only applies to followup activations, // which should only occur after a full release+repress of a new chord. // The full release will set zchd_characters_to_delete_on_next_activation to 0. // Overlapping chords do not use zchd_prior_activation_output_count but // instead keep track of characters to delete via // zchd_characters_to_delete_on_next_activation, // which is incremented both by typing characters // to achieve a chord in the first place, // as well as by chord activations that are overlapped // by the intended final chord. self.zchd.zchd_prior_activation_output_count += 1; self.zchd.zchd_characters_to_delete_on_next_activation += 1; kb.press_key(OsCode::KEY_SPACE)?; kb.release_key(OsCode::KEY_SPACE)?; } if !self.zchd.zchd_is_caps_word_active { // When expanding, lsft/rsft will be released after the first press. if self.zchd.zchd_is_lsft_active { kb.press_key(OsCode::KEY_LEFTSHIFT)?; } if self.zchd.zchd_is_rsft_active { kb.press_key(OsCode::KEY_RIGHTSHIFT)?; } } if self.zchd.zchd_is_altgr_active && !a.zch_output.is_empty() { kb.press_key(OsCode::KEY_RIGHTALT)?; } // Note: it is incorrect to clear input keys. // Zippychord will eagerly output chords even if there is an overlapping chord that // may be activated later by an additional keypress before any releases happen. // E.g. // ab => Abba // abc => Alphabet // // If (b a) are typed, "Abba" is outputted. // If (b a) are continued to be held and (c) is subsequently pressed, // "Abba" gets erased and "Alphabet" is outputted. // // WRONG: // self.zchd.zchd_input_keys.zchik_clear() self.zchd.zchd_last_press = ZchLastPressClassification::IsChord; Ok(()) } IsSubset => { self.zchd.zchd_last_press = ZchLastPressClassification::NotChord; self.zchd.zchd_characters_to_delete_on_next_activation += 1; kb.press_key(osc) } Neither => { self.zchd.zchd_soft_reset(); kb.press_key(osc) } } } // Zch handling for key releases. pub(crate) fn zch_release_key( &mut self, kb: &mut KbdOut, osc: OsCode, ) -> Result<(), std::io::Error> { if self.zch_chords.is_empty() { return kb.release_key(osc); } match osc { OsCode::KEY_LEFTSHIFT => { self.zchd.zchd_is_lsft_active = false; } OsCode::KEY_RIGHTSHIFT => { self.zchd.zchd_is_rsft_active = false; } OsCode::KEY_RIGHTALT => { self.zchd.zchd_is_altgr_active = false; } _ => {} } if osc.is_zippy_ignored() { return kb.release_key(osc); } self.zchd.zchd_state_change(&self.zch_cfg); self.zchd.zchd_release_key(osc); kb.release_key(osc) } /// Tick the zch output state. pub(crate) fn zch_tick(&mut self, is_caps_word_active: bool) { self.zchd.zchd_tick(is_caps_word_active); } /// Returns true if zch state has no further processing so the idling optimization can /// activate. pub(crate) fn zch_is_idle(&self) -> bool { self.zchd.zchd_is_idle() } } fn type_osc(osc: OsCode, kb: &mut KbdOut, zchd: &ZchDynamicState) -> Result<(), std::io::Error> { if zchd.zchd_input_keys.zchik_contains(osc) { kb.release_key(osc)?; kb.press_key(osc)?; } else { kb.press_key(osc)?; kb.release_key(osc)?; } Ok(()) } fn maybe_press_sft_during_activation( sft_already_released: bool, kb: &mut KbdOut, zchd: &ZchDynamicState, ) -> Result<(), std::io::Error> { if !zchd.zchd_is_caps_word_active && (sft_already_released || !zchd.zchd_is_lsft_active && !zchd.zchd_is_rsft_active) { kb.press_key(OsCode::KEY_LEFTSHIFT)?; } Ok(()) } fn maybe_release_sft_during_activation( sft_already_released: bool, kb: &mut KbdOut, zchd: &ZchDynamicState, ) -> Result<(), std::io::Error> { if !zchd.zchd_is_caps_word_active && (sft_already_released || !zchd.zchd_is_lsft_active && !zchd.zchd_is_rsft_active) { kb.release_key(OsCode::KEY_LEFTSHIFT)?; } Ok(()) } kanata-1.9.0/src/kanata/output_logic.rs000064400000000000000000000105241046102023000162050ustar 00000000000000use super::*; #[cfg(feature = "zippychord")] mod zippychord; #[cfg(feature = "zippychord")] pub(crate) use zippychord::*; // Functions to send keys except those that fall in the ignorable range. // And also have been repurposed to have additional logic to send mouse events, out of convenience. // // POTENTIAL PROBLEM - G-keys: // Some keys are ignored because they are *probably* unused, // or otherwise are probably in an unergonomic, far away key position, // so if you're using kanata, you can now stop using those keys and // do something better! // // I should probably let people turn this off if they really want to, // but I don't like how that would require extra code. // I'll defer to YAGNI and add docs, and let people report problems if // they want a fix 🐝. // // The keys ignored are intentionally the upper numbers of KEY_MACROX. // The Linux input-event-codes.h file mentions G1-G18 and S1-S30 // as keys that might use these codes. // // Logitech still makes devices with G-keys // but the S-keys are apparently from the // "Microsoft SideWinder X6 Keyboard" // which appears to no longer be in production. // // Thus based on my reading, 18 is the highest macro key // that can be assumed to be used by devices still in production. pub(super) const KEY_IGNORE_MIN: u16 = 0x2a4; // KEY_MACRO21 pub(super) const KEY_IGNORE_MAX: u16 = 0x2ad; // KEY_MACRO30 pub(super) fn write_key(kb: &mut KbdOut, osc: OsCode, val: KeyValue) -> Result<(), std::io::Error> { match u16::from(osc) { KEY_IGNORE_MIN..=KEY_IGNORE_MAX => Ok(()), _ => kb.write_key(osc, val), } } pub(super) fn press_key(kb: &mut KbdOut, osc: OsCode) -> Result<(), std::io::Error> { use OsCode::*; match u16::from(osc) { KEY_IGNORE_MIN..=KEY_IGNORE_MAX => Ok(()), _ => match osc { BTN_LEFT | BTN_RIGHT | BTN_MIDDLE | BTN_SIDE | BTN_EXTRA => { let btn = osc_to_btn(osc); kb.click_btn(btn) } MouseWheelUp | MouseWheelDown | MouseWheelLeft | MouseWheelRight => { let direction = osc_to_wheel_direction(osc); kb.scroll(direction, HI_RES_SCROLL_UNITS_IN_LO_RES) } _ => post_filter_press(kb, osc), }, } } pub(super) fn release_key(kb: &mut KbdOut, osc: OsCode) -> Result<(), std::io::Error> { use OsCode::*; match u16::from(osc) { KEY_IGNORE_MIN..=KEY_IGNORE_MAX => Ok(()), _ => match osc { BTN_LEFT | BTN_RIGHT | BTN_MIDDLE | BTN_SIDE | BTN_EXTRA => { let btn = osc_to_btn(osc); kb.release_btn(btn) } MouseWheelUp | MouseWheelDown | MouseWheelLeft | MouseWheelRight => { // no-op: these are handled as scroll events in the press but scroll has no notion // of release. Ok(()) } _ => post_filter_release(kb, osc), }, } } fn osc_to_btn(osc: OsCode) -> Btn { use Btn::*; use OsCode::*; match osc { BTN_LEFT => Left, BTN_RIGHT => Right, BTN_MIDDLE => Mid, BTN_EXTRA => Forward, BTN_SIDE => Backward, _ => unreachable!("called osc_to_btn with bad value {osc}"), } } fn osc_to_wheel_direction(osc: OsCode) -> MWheelDirection { use MWheelDirection::*; use OsCode::*; match osc { MouseWheelUp => Up, MouseWheelDown => Down, MouseWheelLeft => Left, MouseWheelRight => Right, _ => unreachable!("called osc_to_wheel_direction with bad value {osc}"), } } fn post_filter_press(kb: &mut KbdOut, osc: OsCode) -> Result<(), std::io::Error> { #[cfg(not(feature = "zippychord"))] { kb.press_key(osc) } #[cfg(feature = "zippychord")] { zch().zch_press_key(kb, osc) } } fn post_filter_release(kb: &mut KbdOut, osc: OsCode) -> Result<(), std::io::Error> { #[cfg(not(feature = "zippychord"))] { kb.release_key(osc) } #[cfg(feature = "zippychord")] { zch().zch_release_key(kb, osc) } } pub(super) fn zippy_is_idle() -> bool { #[cfg(not(feature = "zippychord"))] { true } #[cfg(feature = "zippychord")] { zch().zch_is_idle() } } pub(super) fn zippy_tick(_caps_word_is_active: bool) { #[cfg(feature = "zippychord")] { zch().zch_tick(_caps_word_is_active) } } kanata-1.9.0/src/kanata/sequences.rs000064400000000000000000000344261046102023000154720ustar 00000000000000use super::*; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum SequenceActivity { Inactive, Active, } use SequenceActivity::*; pub struct SequenceState { /// Unmangled sequence of keys pressed for hidden-delay-type. pub raw_oscs: Vec, /// Keeps track of standard sequence state. /// This includes regular keys, e.g. `a b c` /// and chorded keys, e.g. `S-(d e f)`. pub sequence: Vec, /// Keeps track of overlapping sequence state. /// E.g. able to detect `O-(g h i)` pub overlapped_sequence: Vec, /// Determines the handling of keys while sequence state is in progress. pub sequence_input_mode: SequenceInputMode, /// Starts from `sequence_timeout` and ticks down /// approximately every millisecond. /// At 0 the sequence state terminates. pub ticks_until_timeout: u16, /// User-configured sequence timeout setting. pub sequence_timeout: u16, /// Whether the sequence is active or not. pub activity: SequenceActivity, /// Counter to reduce number of backspaces typed. noerase_count: u16, } impl SequenceState { pub fn new() -> Self { Self { raw_oscs: vec![], sequence: vec![], overlapped_sequence: vec![], sequence_input_mode: SequenceInputMode::HiddenSuppressed, ticks_until_timeout: 0, sequence_timeout: 0, activity: Inactive, noerase_count: 0, } } /// Updates the sequence state parameters, clears buffers, and sets the state to active. pub fn activate(&mut self, input_mode: SequenceInputMode, timeout: u16) { self.sequence_input_mode = input_mode; self.sequence_timeout = timeout; self.ticks_until_timeout = timeout; self.raw_oscs.clear(); self.sequence.clear(); self.overlapped_sequence.clear(); self.activity = Active; self.noerase_count = 0; } pub fn is_active(&self) -> bool { self.activity == Active } pub fn get_active(&mut self) -> Option<&mut Self> { match self.activity { Active => Some(self), Inactive => None, } } pub fn is_inactive(&self) -> bool { self.activity == Inactive } } impl Default for SequenceState { fn default() -> Self { Self::new() } } pub(super) fn get_mod_mask_for_cur_keys(cur_keys: &[KeyCode]) -> u16 { cur_keys .iter() .copied() .fold(0, |a, v| a | mod_mask_for_keycode(v)) } pub(super) enum EndSequenceType { Standard, Overlap, } pub(super) fn do_sequence_press_logic( state: &mut SequenceState, k: &KeyCode, mod_mask: u16, kbd_out: &mut KbdOut, sequences: &kanata_parser::trie::Trie<(u8, u16)>, sequence_backtrack_modcancel: bool, layout: &mut BorrowedKLayout, ) -> Result<(), anyhow::Error> { state.ticks_until_timeout = state.sequence_timeout; let osc = OsCode::from(*k); state.raw_oscs.push(osc); use kanata_parser::trie::GetOrDescendentExistsResult::*; let pushed_into_seq = { // Transform to OsCode and convert modifiers other than altgr/ralt // (same key different names) to the left version, since that's // how chords get transformed when building up sequences. let base = u16::from(match osc { OsCode::KEY_RIGHTSHIFT => OsCode::KEY_LEFTSHIFT, OsCode::KEY_RIGHTMETA => OsCode::KEY_LEFTMETA, OsCode::KEY_RIGHTCTRL => OsCode::KEY_LEFTCTRL, osc => osc, }); base | mod_mask }; match state.sequence_input_mode { SequenceInputMode::VisibleBackspaced => { press_key(kbd_out, osc)?; } SequenceInputMode::HiddenSuppressed | SequenceInputMode::HiddenDelayType => {} } log::debug!("sequence got {k:?}"); state.sequence.push(pushed_into_seq); let pushed_into_overlap_seq = (pushed_into_seq & MASK_KEYCODES) | KEY_OVERLAP_MARKER; state.overlapped_sequence.push(pushed_into_overlap_seq); let mut res = sequences.get_or_descendant_exists(&state.sequence); // Check for invalid termination of standard variant of sequence state. // Can potentially backtrack and overwrite modded keystates as well as overlap keystates, which // might exist in the sequence because of an earlier invalid termination where the standard // sequence got filled in with overlap sequence data. let mut is_invalid_termination_standard = false; if res == NotInTrie { is_invalid_termination_standard = { let mut no_valid_seqs = true; // If applicable, check again with modifier bits unset. for i in (0..state.sequence.len()).rev() { // Note: proper bounds are immediately above. // Can't use iter_mut due to borrowing issues. if state.sequence[i] == KEY_OVERLAP_MARKER { state.sequence.remove(i); } else if sequence_backtrack_modcancel { state.sequence[i] &= MASK_KEYCODES; } else { state.sequence[i] &= !KEY_OVERLAP_MARKER; } res = sequences.get_or_descendant_exists(&state.sequence); if res != NotInTrie { no_valid_seqs = false; break; } } no_valid_seqs }; } // Check for invalid termination of overlap variant of sequence state. // This variant does not backtrack today because I haven't figured out how to do that easily. // It does do some attempts to stay valid by modifying the tail of the sequence though. let mut res_overlapped = sequences.get_or_descendant_exists(&state.overlapped_sequence); let is_invalid_termination_overlapped = if res_overlapped == NotInTrie { // Try ending the overlapping and push overlapping seq again. let index_of_last = state.overlapped_sequence.len() - 1; state.overlapped_sequence[index_of_last] = KEY_OVERLAP_MARKER; state.overlapped_sequence.push(pushed_into_overlap_seq); res_overlapped = sequences.get_or_descendant_exists(&state.overlapped_sequence); let index_of_last = index_of_last + 1; if res_overlapped == NotInTrie { // Try checking the trie after setting the latest key to not have the overlapping // marker. state.overlapped_sequence[index_of_last] = pushed_into_seq; res_overlapped = sequences.get_or_descendant_exists(&state.overlapped_sequence); if res_overlapped == NotInTrie { if pushed_into_seq & MASK_KEYCODES == pushed_into_seq { // Avoid calling get_or_descendant_exists if there is no difference, to save on // doing work checking in the trie. true } else { // Try unmodded `pushed_into_seq`. state.overlapped_sequence[index_of_last] = pushed_into_seq & MASK_KEYCODES; res_overlapped = sequences.get_or_descendant_exists(&state.overlapped_sequence); res_overlapped == NotInTrie } } else { false } } else { false } } else { false }; match ( is_invalid_termination_standard, is_invalid_termination_overlapped, ) { (false, false) => {} (false, true) => { log::debug!("overlap seq is invalid; filling with standard seq"); // Overwrite overlapped with non-overlapped tracking state.overlapped_sequence.clear(); state .overlapped_sequence .extend(state.sequence.iter().copied()); res_overlapped = sequences.get_or_descendant_exists(&state.overlapped_sequence); } (true, false) => { log::debug!("standard seq is invalid; filling with overlap seq"); state.sequence.clear(); state .sequence .extend(state.overlapped_sequence.iter().copied()); if state.sequence.last().copied().unwrap_or(0) != KEY_OVERLAP_MARKER && state.overlapped_sequence.last().copied().unwrap_or(0) >= KEY_OVERLAP_MARKER { // Always treat non-overlapping sequence as if overlap state has // ended; if overlapped_sequence itself has an overlap state. state.sequence.push(KEY_OVERLAP_MARKER); } res = sequences.get_or_descendant_exists(&state.sequence); } (true, true) => { // One more try for backtracking: check for validity by removing from the front. while res == NotInTrie && !state.sequence.is_empty() { state.sequence.remove(0); res = sequences.get_or_descendant_exists(&state.sequence); } if res == NotInTrie || state.sequence.is_empty() { log::debug!("invalid keys for seq"); cancel_sequence(state, kbd_out)?; } } } // Check for successful sequence termination. if let HasValue((i, j)) = res_overlapped { // First, check for a valid simultaneous completion. // Simultaneous completion should take priority. do_successful_sequence_termination(kbd_out, state, layout, i, j, EndSequenceType::Overlap)?; } else if let HasValue((i, j)) = res { // Try terminating the overlapping and check if simultaneous termination worked. // Simultaneous completion should take priority. state.overlapped_sequence.push(KEY_OVERLAP_MARKER); if let HasValue((oi, oj)) = sequences.get_or_descendant_exists(&state.overlapped_sequence) { do_successful_sequence_termination( kbd_out, state, layout, oi, oj, EndSequenceType::Overlap, )?; } else { do_successful_sequence_termination( kbd_out, state, layout, i, j, EndSequenceType::Standard, )?; } } Ok(()) } use kanata_keyberon::key_code::KeyCode::*; pub(super) fn do_successful_sequence_termination( kbd_out: &mut KbdOut, state: &mut SequenceState, layout: &mut Layout<'_, 767, 2, &&[&CustomAction]>, i: u8, j: u16, seq_type: EndSequenceType, ) -> Result<(), anyhow::Error> { log::debug!("sequence complete; tapping fake key"); state.activity = Inactive; let sequence = match seq_type { EndSequenceType::Standard => &state.sequence, EndSequenceType::Overlap => &state.overlapped_sequence, }; match state.sequence_input_mode { SequenceInputMode::HiddenSuppressed | SequenceInputMode::HiddenDelayType => {} SequenceInputMode::VisibleBackspaced => { // Release mod keys and backspace because they can cause backspaces to mess up. layout.states.retain(|s| match s { State::NormalKey { keycode, .. } => { if matches!(keycode, LCtrl | RCtrl | LAlt | RAlt | LGui | RGui) { // Ignore the error, ugly to return it from retain, and // this is very unlikely to happen anyway. let _ = release_key(kbd_out, keycode.into()); false } else { true } } _ => true, }); for k in sequence.iter().copied() { // Check for pressed modifiers and don't input backspaces for // those since they don't output characters that can be // backspaced. if k == KEY_OVERLAP_MARKER { continue; }; let osc = OsCode::from(k & MASK_KEYCODES); match osc { // Known bug: most non-characters-outputting keys are not // listed. I'm too lazy to list them all. Just use // character-outputting keys (and modifiers) in sequences // please! Or switch to a different input mode? It doesn't // really make sense to use non-typing characters other // than modifiers does it? Since those would probably be // further away from the home row, so why use them? If one // desired to fix this, a shorter list of keys would // probably be the list of keys that **do** output // characters than those that don't. osc if osc.is_modifier() => continue, osc if matches!(u16::from(osc), KEY_IGNORE_MIN..=KEY_IGNORE_MAX) => continue, _ => { if state.noerase_count > 0 { state.noerase_count -= 1; } else { kbd_out.press_key(OsCode::KEY_BACKSPACE)?; kbd_out.release_key(OsCode::KEY_BACKSPACE)?; } } } } } } for k in sequence.iter().copied() { if k == KEY_OVERLAP_MARKER { continue; }; let kc = KeyCode::from(OsCode::from(k & MASK_KEYCODES)); layout.states.retain(|s| match s { State::NormalKey { keycode, .. } => kc != *keycode, _ => true, }); } layout.event(Event::Press(i, j)); layout.event(Event::Release(i, j)); Ok(()) } pub(super) fn cancel_sequence(state: &mut SequenceState, kbd_out: &mut KbdOut) -> Result<()> { state.activity = Inactive; log::debug!("sequence cancelled"); match state.sequence_input_mode { SequenceInputMode::HiddenDelayType => { for osc in state.raw_oscs.iter().copied() { // BUG: chorded_hidden_delay_type press_key(kbd_out, osc)?; release_key(kbd_out, osc)?; } } SequenceInputMode::HiddenSuppressed | SequenceInputMode::VisibleBackspaced => {} } Ok(()) } pub(super) fn add_noerase(state: &mut SequenceState, noerase_count: u16) { state.noerase_count += noerase_count; } kanata-1.9.0/src/kanata/unknown.rs000064400000000000000000000005211046102023000151630ustar 00000000000000use super::*; pub static PRESSED_KEYS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::default())); impl Kanata { pub fn check_release_non_physical_shift(&mut self) -> Result<()> { // Silence warning check_for_exit(&KeyEvent::new(OsCode::KEY_UNKNOWN, KeyValue::Release)); Ok(()) } } kanata-1.9.0/src/kanata/windows/exthook.rs000064400000000000000000000201451046102023000166430ustar 00000000000000use parking_lot::Mutex; use std::convert::TryFrom; use std::sync::mpsc::{sync_channel, Receiver, SyncSender as Sender, TryRecvError}; use std::sync::Arc; use std::time; use super::PRESSED_KEYS; use crate::kanata::*; impl Kanata { /// Initialize the callback that is passed to the Windows low level hook to receive key events and run the native_windows_gui event loop. pub fn event_loop(_cfg: Arc>, tx: Sender) -> Result<()> { let (preprocess_tx, preprocess_rx) = sync_channel(100); start_event_preprocessor(preprocess_rx, tx); let _ = KeyboardHook::set_input_cb(move |input_event| { // →true if input event was handled, false otherwise, informs input_ev_listener whether to look for the output key event let mut key_event = match KeyEvent::try_from(input_event) { // InputEvent{code:u32 , up :bool} Ok(ev) => ev, // KeyEvent {code:OsCode , value:KeyValue} _ => return false, }; // Some(OsCode::KEY_0)←0x30 Release0 Press1 Repeat2 Tap WakeUp check_for_exit(&key_event); //noop let oscode = OsCode::from(input_event.code); if !MAPPED_KEYS.lock().contains(&oscode) { return false; } log::debug!("event loop: {}", key_event); match key_event.value { // Unlike Linux, Windows does not use a separate value for repeat. However, our code needs to differentiate between initial press and repeat press. KeyValue::Release => { PRESSED_KEYS.lock().remove(&key_event.code); } KeyValue::Press => { let mut pressed_keys = PRESSED_KEYS.lock(); if pressed_keys.contains(&key_event.code) { key_event.value = KeyValue::Repeat; } else { pressed_keys.insert(key_event.code); } } _ => {} } try_send_panic(&preprocess_tx, key_event); // Send input_events to the preprocessing loop. Panic if channel somehow gets full or if channel disconnects. Typing input should never trigger a panic based on the channel getting full, assuming regular operation of the program and some other bug isn't the problem. I've tried to crash the program by pressing as many keys on my keyboard at the same time as I could, but was unable to. #[cfg(feature = "perf_logging")] debug!(" 🕐{}μs sent msg to tx→rx@start_processing_loop from event loop@KeyboardHook::set_input_cb",(start.elapsed()).as_micros()); true }); Ok(()) } } fn try_send_panic(tx: &Sender, kev: KeyEvent) { if let Err(e) = tx.try_send(kev) { panic!("failed to send on channel: {e:?}") } } fn start_event_preprocessor(preprocess_rx: Receiver, process_tx: Sender) { #[derive(Debug, Clone, Copy, PartialEq)] enum LctlState { Pressed, Released, Pending, PendingReleased, None, } std::thread::spawn(move || { let mut lctl_state = LctlState::None; loop { match preprocess_rx.try_recv() { Ok(kev) => match (*ALTGR_BEHAVIOUR.lock(), kev) { (AltGrBehaviour::DoNothing, _) => try_send_panic(&process_tx, kev), ( AltGrBehaviour::AddLctlRelease, KeyEvent { value: KeyValue::Release, code: OsCode::KEY_RIGHTALT, .. }, ) => { log::debug!("altgr add: adding lctl release"); try_send_panic(&process_tx, kev); try_send_panic( &process_tx, KeyEvent::new(OsCode::KEY_LEFTCTRL, KeyValue::Release), ); PRESSED_KEYS.lock().remove(&OsCode::KEY_LEFTCTRL); } ( AltGrBehaviour::CancelLctlPress, KeyEvent { value: KeyValue::Press, code: OsCode::KEY_LEFTCTRL, .. }, ) => { log::debug!("altgr cancel: lctl state->pressed"); lctl_state = LctlState::Pressed; } ( AltGrBehaviour::CancelLctlPress, KeyEvent { value: KeyValue::Release, code: OsCode::KEY_LEFTCTRL, .. }, ) => match lctl_state { LctlState::Pressed => { log::debug!("altgr cancel: lctl state->released"); lctl_state = LctlState::Released; } LctlState::Pending => { log::debug!("altgr cancel: lctl state->pending-released"); lctl_state = LctlState::PendingReleased; } LctlState::None => try_send_panic(&process_tx, kev), _ => {} }, ( AltGrBehaviour::CancelLctlPress, KeyEvent { value: KeyValue::Press, code: OsCode::KEY_RIGHTALT, .. }, ) => { log::debug!("altgr cancel: lctl state->none"); lctl_state = LctlState::None; try_send_panic(&process_tx, kev); } (_, _) => try_send_panic(&process_tx, kev), }, Err(TryRecvError::Empty) => { if *ALTGR_BEHAVIOUR.lock() == AltGrBehaviour::CancelLctlPress { match lctl_state { LctlState::Pressed => { log::debug!("altgr cancel: lctl state->pending"); lctl_state = LctlState::Pending; } LctlState::Released => { log::debug!("altgr cancel: lctl state->pending-released"); lctl_state = LctlState::PendingReleased; } LctlState::Pending => { log::debug!("altgr cancel: lctl state->send"); try_send_panic( &process_tx, KeyEvent::new(OsCode::KEY_LEFTCTRL, KeyValue::Press), ); lctl_state = LctlState::None; } LctlState::PendingReleased => { log::debug!("altgr cancel: lctl state->send+release"); try_send_panic( &process_tx, KeyEvent::new(OsCode::KEY_LEFTCTRL, KeyValue::Press), ); try_send_panic( &process_tx, KeyEvent::new(OsCode::KEY_LEFTCTRL, KeyValue::Release), ); lctl_state = LctlState::None; } _ => {} } } std::thread::sleep(time::Duration::from_millis(1)); } Err(TryRecvError::Disconnected) => { panic!("channel disconnected (exthook event_preproces)") } } } }); } kanata-1.9.0/src/kanata/windows/interception.rs000064400000000000000000000257231046102023000176740ustar 00000000000000use anyhow::{anyhow, Result}; use kanata_interception as ic; use parking_lot::Mutex; use std::sync::mpsc::SyncSender as Sender; use std::sync::Arc; use super::PRESSED_KEYS; use crate::kanata::*; use crate::oskbd::KeyValue; use kanata_parser::keys::OsCode; impl Kanata { pub fn event_loop_inner(kanata: Arc>, tx: Sender) -> Result<()> { let intrcptn = ic::Interception::new().ok_or_else(|| anyhow!("interception driver should init: have you completed the interception driver installation?"))?; intrcptn.set_filter(ic::is_keyboard, ic::Filter::KeyFilter(ic::KeyFilter::all())); let mut strokes = [ic::Stroke::Keyboard { code: ic::ScanCode::Esc, state: ic::KeyState::empty(), information: 0, }; 32]; let keyboards_to_intercept_hwids = kanata.lock().intercept_kb_hwids.clone(); let keyboards_to_intercept_hwids_exclude = kanata.lock().intercept_kb_hwids_exclude.clone(); let mouse_to_intercept_hwids: Option> = kanata.lock().intercept_mouse_hwids.clone(); let mouse_to_intercept_excluded_hwids: Option> = kanata.lock().intercept_mouse_hwids_exclude.clone(); let mouse_movement_key = kanata.lock().mouse_movement_key.clone(); if mouse_to_intercept_hwids.is_some() || mouse_to_intercept_excluded_hwids.is_some() { if mouse_movement_key.lock().is_some() { intrcptn.set_filter(ic::is_mouse, ic::Filter::MouseFilter(ic::MouseState::all())); } else { intrcptn.set_filter( ic::is_mouse, ic::Filter::MouseFilter(ic::MouseState::all() & (!ic::MouseState::MOVE)), ); } } let mut is_dev_interceptable: HashMap = HashMap::default(); loop { let dev = intrcptn.wait(); if dev > 0 { let num_strokes = intrcptn.receive(dev, &mut strokes) as usize; for i in 0..num_strokes { let mut key_event = match strokes[i] { ic::Stroke::Keyboard { state, .. } => { if !is_device_interceptable( dev, &intrcptn, &keyboards_to_intercept_hwids, &keyboards_to_intercept_hwids_exclude, &mut is_dev_interceptable, ) { log::debug!("stroke {:?} is from undesired device", strokes[i]); intrcptn.send(dev, &strokes[i..i + 1]); continue; } log::debug!("got stroke {:?}", strokes[i]); let code = match OsCodeWrapper::try_from(strokes[i]) { Ok(c) => c.0, _ => { log::debug!("could not map code to oscode"); intrcptn.send(dev, &strokes[i..i + 1]); continue; } }; let value = match state.contains(ic::KeyState::UP) { false => KeyValue::Press, true => KeyValue::Release, }; KeyEvent { code, value } } ic::Stroke::Mouse { state, rolling, flags, .. } => { let allow_this_dev = is_device_interceptable( dev, &intrcptn, &mouse_to_intercept_hwids, &mouse_to_intercept_excluded_hwids, &mut is_dev_interceptable, ); if allow_this_dev { log::trace!("checking mouse stroke {:?}", strokes[i]); if let Some(ms_mvmt_key) = *mouse_movement_key.lock() { if flags.contains(ic::MouseFlags::MOVE_RELATIVE) { tx.try_send(KeyEvent::new(ms_mvmt_key, KeyValue::Tap))?; } }; } if let (true, Some(event)) = (allow_this_dev, mouse_state_to_event(state, rolling)) { event } else { intrcptn.send(dev, &strokes[i..i + 1]); continue; } } }; check_for_exit(&key_event); if !MAPPED_KEYS.lock().contains(&key_event.code) { log::debug!("{key_event:?} is not mapped"); intrcptn.send(dev, &strokes[i..i + 1]); continue; } log::debug!("sending {key_event:?} to processing loop"); match key_event.value { KeyValue::Release => { PRESSED_KEYS.lock().remove(&key_event.code); } KeyValue::Press => { let mut pressed_keys = PRESSED_KEYS.lock(); if pressed_keys.contains(&key_event.code) { key_event.value = KeyValue::Repeat; } else { pressed_keys.insert(key_event.code); } } _ => {} } tx.try_send(key_event)?; } } } } pub fn event_loop( kanata: Arc>, tx: Sender, #[cfg(feature = "gui")] ui: crate::gui::system_tray_ui::SystemTrayUi, ) -> Result<()> { #[cfg(not(feature = "gui"))] { Self::event_loop_inner(kanata, tx) } #[cfg(feature = "gui")] { std::thread::spawn(move || -> Result<()> { Self::event_loop_inner(kanata, tx) }); let _ui = ui; // prevents thread from panicking on exiting via a GUI native_windows_gui::dispatch_thread_events(); Ok(()) } } } fn is_device_interceptable( input_dev: ic::Device, intrcptn: &ic::Interception, allowed_hwids: &Option>, excluded_hwids: &Option>, cache: &mut HashMap, ) -> bool { match (allowed_hwids, excluded_hwids) { (None, None) => true, (Some(allowed), None) => match cache.get(&input_dev) { Some(v) => *v, None => { let mut hwid = [0u8; HWID_ARR_SZ]; log::trace!("getting hardware id for input dev: {input_dev}"); let res = intrcptn.get_hardware_id(input_dev, &mut hwid); let dev_is_interceptable = allowed.contains(&hwid); log::info!("include check - res {res}; device #{input_dev} is intercepted: {dev_is_interceptable}; hwid {hwid:?} "); cache.insert(input_dev, dev_is_interceptable); dev_is_interceptable } }, (None, Some(excluded)) => match cache.get(&input_dev) { Some(v) => *v, None => { let mut hwid = [0u8; HWID_ARR_SZ]; log::trace!("getting hardware id for input dev: {input_dev}"); let res = intrcptn.get_hardware_id(input_dev, &mut hwid); let dev_is_interceptable = !excluded.contains(&hwid); log::info!("exclude check - res {res}; device #{input_dev} is intercepted: {dev_is_interceptable}; hwid {hwid:?} "); cache.insert(input_dev, dev_is_interceptable); dev_is_interceptable } }, _ => unreachable!("excluded and allowed should be mutually exclusive"), } } fn mouse_state_to_event(state: ic::MouseState, rolling: i16) -> Option { if state.contains(ic::MouseState::RIGHT_BUTTON_DOWN) { Some(KeyEvent { code: OsCode::BTN_RIGHT, value: KeyValue::Press, }) } else if state.contains(ic::MouseState::RIGHT_BUTTON_UP) { Some(KeyEvent { code: OsCode::BTN_RIGHT, value: KeyValue::Release, }) } else if state.contains(ic::MouseState::LEFT_BUTTON_DOWN) { Some(KeyEvent { code: OsCode::BTN_LEFT, value: KeyValue::Press, }) } else if state.contains(ic::MouseState::LEFT_BUTTON_UP) { Some(KeyEvent { code: OsCode::BTN_LEFT, value: KeyValue::Release, }) } else if state.contains(ic::MouseState::MIDDLE_BUTTON_DOWN) { Some(KeyEvent { code: OsCode::BTN_MIDDLE, value: KeyValue::Press, }) } else if state.contains(ic::MouseState::MIDDLE_BUTTON_UP) { Some(KeyEvent { code: OsCode::BTN_MIDDLE, value: KeyValue::Release, }) } else if state.contains(ic::MouseState::BUTTON_4_DOWN) { Some(KeyEvent { code: OsCode::BTN_SIDE, value: KeyValue::Press, }) } else if state.contains(ic::MouseState::BUTTON_4_UP) { Some(KeyEvent { code: OsCode::BTN_SIDE, value: KeyValue::Release, }) } else if state.contains(ic::MouseState::BUTTON_5_DOWN) { Some(KeyEvent { code: OsCode::BTN_EXTRA, value: KeyValue::Press, }) } else if state.contains(ic::MouseState::BUTTON_5_UP) { Some(KeyEvent { code: OsCode::BTN_EXTRA, value: KeyValue::Release, }) } else if state.contains(ic::MouseState::WHEEL) { let osc = if rolling >= 0 { OsCode::MouseWheelUp } else { OsCode::MouseWheelDown }; if MAPPED_KEYS.lock().contains(&osc) { Some(KeyEvent { code: osc, value: KeyValue::Tap, }) } else { None } } else if state.contains(ic::MouseState::HWHEEL) { let osc = if rolling >= 0 { OsCode::MouseWheelRight } else { OsCode::MouseWheelLeft }; if MAPPED_KEYS.lock().contains(&osc) { Some(KeyEvent { code: osc, value: KeyValue::Tap, }) } else { None } } else { None } } kanata-1.9.0/src/kanata/windows/llhook.rs000064400000000000000000000366361046102023000164660ustar 00000000000000use parking_lot::Mutex; use std::convert::TryFrom; use std::sync::mpsc::{sync_channel, Receiver, SyncSender as Sender, TryRecvError}; use std::sync::Arc; use std::time; use super::PRESSED_KEYS; use crate::kanata::*; impl Kanata { /// Initialize the callback that is passed to the Windows low level hook to receive key events /// and run the native_windows_gui event loop. pub fn event_loop( _cfg: Arc>, tx: Sender, #[cfg(all(target_os = "windows", feature = "gui"))] ui: crate::gui::system_tray_ui::SystemTrayUi, ) -> Result<()> { // Display debug and panic output when launched from a terminal. #[cfg(not(feature = "gui"))] unsafe { use winapi::um::wincon::*; if AttachConsole(ATTACH_PARENT_PROCESS) != 0 { panic!("Could not attach to console"); } }; let (preprocess_tx, preprocess_rx) = sync_channel(100); start_event_preprocessor(preprocess_rx, tx); // This callback should return `false` if the input event is **not** handled by the // callback and `true` if the input event **is** handled by the callback. Returning false // informs the callback caller that the input event should be handed back to the OS for // normal processing. let _kbhook = KeyboardHook::set_input_cb(move |input_event| { let mut key_event = match KeyEvent::try_from(input_event) { Ok(ev) => ev, _ => return false, }; check_for_exit(&key_event); let oscode = OsCode::from(input_event.code); if !MAPPED_KEYS.lock().contains(&oscode) { return false; } // Unlike Linux, Windows does not use a separate value for repeat. However, our code // needs to differentiate between initial press and repeat press. log::debug!("event loop: {:?}", key_event); match key_event.value { KeyValue::Release => { PRESSED_KEYS.lock().remove(&key_event.code); } KeyValue::Press => { let mut pressed_keys = PRESSED_KEYS.lock(); if pressed_keys.contains(&key_event.code) { key_event.value = KeyValue::Repeat; } else { pressed_keys.insert(key_event.code); } } _ => {} } // Send input_events to the preprocessing loop. Panic if channel somehow gets full or if // channel disconnects. Typing input should never trigger a panic based on the channel // getting full, assuming regular operation of the program and some other bug isn't the // problem. I've tried to crash the program by pressing as many keys on my keyboard at // the same time as I could, but was unable to. try_send_panic(&preprocess_tx, key_event); true }); #[cfg(all(target_os = "windows", feature = "gui"))] let _ui = ui; // prevents thread from panicking on exiting via a GUI // The event loop is also required for the low-level keyboard hook to work. native_windows_gui::dispatch_thread_events(); Ok(()) } /// # Note /// /// This is disabled by default due to known issues. /// Under some use cases this works just fine and can be enabled. /// /// ## Known issues /// /// - Ralt/lctl may have some issues with AltGr layouts /// - Some software may have a later-stage remapping that changes which VK is active for a /// given VK that Kanata outputs. E.g. changing to roya/loya modifiers. /// /// # Description /// /// On Windows with LLHOOK/SendInput APIs, /// Kanata does not have as much control /// over the full system's keystates as one would want; /// unlike in Linux or with the Interception driver. /// Sometimes Kanata can miss events; e.g. a release is /// missed and a keystate remains pressed within Kanata (1), /// or a press is missed in Kanata but the release is caught, /// and thus the keystate remains pressed within the Windows system /// because Kanata consumed the release and didn't know what to do about it (2). /// /// For (1), `release_normalkey_states` theoretically fixes the issue /// after 60s of Kanata being idle, /// but that is a long time and doesn't seem to work consistently. /// Unfortunately this does not seem to be easily fixable in all cases. /// For example, a press consumed by Kanata could result in /// **only** a `(layer-while-held ...)` action as the output; /// if the corresponding release were missed, /// Kanata has no information available from the larger Windows system /// to confirm that the physical key is actually released /// but that the process didn't see the event. /// E.g. there is the `GetAsyncKeyState` API /// and this will be useful when the missed release has a key output, /// but not with the layer example. /// There does not appear to be any "raw input" mechanism /// to see the snapshot of the current state of physical keyboard keys. /// /// For (2), consider that this might be fixed purely within Kanata's /// event handling and processing, by checking Kanata's active action states, /// and if there are no active states corresponding to a released event, /// to send a release of the original input. /// This would result in extra release events though; /// for example if the `A` key action is `(macro a)`, /// the above logic will result in a second SendInput release event of `A`. /// Instead, this function checks against the outside Windows state. /// /// The solution makes use of the following states: /// - `MAPPED_KEYS` (MK) /// - `GetAsyncKeyState` WinAPI (GKS) /// - `PRESSED_KEYS` (PK) /// - `self.prev_keys` (SPV) /// /// If a discrepancy is detected, /// this procedure releases Windows keys via SendInput /// and/or clears internal Kanata states. /// /// The checks are: /// 1. For all of SPV, check that it is pressed in GKS. /// If a key is not pressed, find the coordinate of this state. /// Clear in PK and clear all states with the same coordinate as key output. /// 2. For all keys in MK and active in GKS, check it is in SPV. /// If not in SPV, call SendInput to release in Windows. #[cfg(not(feature = "simulated_input"))] pub(crate) fn win_synchronize_keystates(&mut self) { use kanata_keyberon::layout::*; use winapi::um::winuser::*; if !self.windows_sync_keystates { return; } log::debug!("synchronizing win keystates"); for pvk in self.prev_keys.iter() { // Check 1 : each pvk is expected to be pressed. let osc: OsCode = pvk.into(); let vk = i32::from(osc); if vk > 254 { // 254 should be highest valid VK number in Windows OS. continue; } let vk_state = unsafe { GetAsyncKeyState(vk) } as u32; let is_pressed_in_windows = vk_state >= 0b1000000; if is_pressed_in_windows { continue; } log::error!("Unexpected keycode is pressed in kanata but not in Windows. Clearing kanata states: {pvk}"); // Need to clear internal state about this key. // find coordinate(s) in keyberon associated with pvk let mut coords_to_clear = Vec::::new(); let layout = self.layout.bm(); layout.states.retain(|s| { let retain = match s.keycode() { Some(k) => k != *pvk, _ => true, }; if !retain { if let Some(coord) = s.coord() { coords_to_clear.push(coord); } } retain }); // Clear other states other than keycode associated with a keycode that needs to be // cleaned up. layout.states.retain(|s| match s.coord() { Some(c) => !coords_to_clear.contains(&c), None => true, }); // Clear PRESSED_KEYS for coordinates associated with real and not virtual keys let mut pressed_keys = PRESSED_KEYS.lock(); for osc in coords_to_clear.iter().copied().filter_map(|c| match c { (FAKE_KEY_ROW, _) => None, (_, kc) => Some(OsCode::from(kc)), }) { pressed_keys.remove(&osc); } drop(pressed_keys); } let mapped_keys = MAPPED_KEYS.lock(); for mapped_osc in mapped_keys.iter().copied() { // Check 2: each active win vk mapped in Kanata should have a value in pvk if matches!( mapped_osc, OsCode::BTN_LEFT | OsCode::BTN_RIGHT | OsCode::BTN_MIDDLE | OsCode::BTN_SIDE | OsCode::BTN_EXTRA ) { // Skip mouse. Probably not under primary control of Kanata. continue; } let vk = i32::from(mapped_osc); if vk >= 256 { continue; } let vk_state = unsafe { GetAsyncKeyState(vk) } as u32; let is_pressed_in_windows = vk_state >= 0b1000000; if !is_pressed_in_windows { continue; } let vk = vk as u16; let Some(osc) = OsCode::from_u16(vk) else { continue; }; if self.prev_keys.contains(&osc.into()) { continue; } log::error!("Unexpected keycode is pressed in Windows but not Kanata. Releasing in Windows: {osc}"); let _ = release_key(&mut self.kbd_out, osc); } drop(mapped_keys); } } fn try_send_panic(tx: &Sender, kev: KeyEvent) { if let Err(e) = tx.try_send(kev) { panic!("failed to send on channel: {e:?}") } } fn start_event_preprocessor(preprocess_rx: Receiver, process_tx: Sender) { #[derive(Debug, Clone, Copy, PartialEq)] enum LctlState { Pressed, Released, Pending, PendingReleased, None, } std::thread::spawn(move || { let mut lctl_state = LctlState::None; loop { match preprocess_rx.try_recv() { Ok(kev) => match (*ALTGR_BEHAVIOUR.lock(), kev) { (AltGrBehaviour::DoNothing, _) => try_send_panic(&process_tx, kev), ( AltGrBehaviour::AddLctlRelease, KeyEvent { value: KeyValue::Release, code: OsCode::KEY_RIGHTALT, .. }, ) => { log::debug!("altgr add: adding lctl release"); try_send_panic(&process_tx, kev); try_send_panic( &process_tx, KeyEvent::new(OsCode::KEY_LEFTCTRL, KeyValue::Release), ); PRESSED_KEYS.lock().remove(&OsCode::KEY_LEFTCTRL); } ( AltGrBehaviour::CancelLctlPress, KeyEvent { value: KeyValue::Press, code: OsCode::KEY_LEFTCTRL, .. }, ) => { log::debug!("altgr cancel: lctl state->pressed"); lctl_state = LctlState::Pressed; } ( AltGrBehaviour::CancelLctlPress, KeyEvent { value: KeyValue::Release, code: OsCode::KEY_LEFTCTRL, .. }, ) => match lctl_state { LctlState::Pressed => { log::debug!("altgr cancel: lctl state->released"); lctl_state = LctlState::Released; } LctlState::Pending => { log::debug!("altgr cancel: lctl state->pending-released"); lctl_state = LctlState::PendingReleased; } LctlState::None => try_send_panic(&process_tx, kev), _ => {} }, ( AltGrBehaviour::CancelLctlPress, KeyEvent { value: KeyValue::Press, code: OsCode::KEY_RIGHTALT, .. }, ) => { log::debug!("altgr cancel: lctl state->none"); lctl_state = LctlState::None; try_send_panic(&process_tx, kev); } (_, _) => try_send_panic(&process_tx, kev), }, Err(TryRecvError::Empty) => { if *ALTGR_BEHAVIOUR.lock() == AltGrBehaviour::CancelLctlPress { match lctl_state { LctlState::Pressed => { log::debug!("altgr cancel: lctl state->pending"); lctl_state = LctlState::Pending; } LctlState::Released => { log::debug!("altgr cancel: lctl state->pending-released"); lctl_state = LctlState::PendingReleased; } LctlState::Pending => { log::debug!("altgr cancel: lctl state->send"); try_send_panic( &process_tx, KeyEvent::new(OsCode::KEY_LEFTCTRL, KeyValue::Press), ); lctl_state = LctlState::None; } LctlState::PendingReleased => { log::debug!("altgr cancel: lctl state->send+release"); try_send_panic( &process_tx, KeyEvent::new(OsCode::KEY_LEFTCTRL, KeyValue::Press), ); try_send_panic( &process_tx, KeyEvent::new(OsCode::KEY_LEFTCTRL, KeyValue::Release), ); lctl_state = LctlState::None; } _ => {} } } std::thread::sleep(time::Duration::from_millis(1)); } Err(TryRecvError::Disconnected) => { panic!("channel disconnected") } } } }); } kanata-1.9.0/src/kanata/windows/mod.rs000064400000000000000000000142461046102023000157460ustar 00000000000000use anyhow::Result; use parking_lot::Mutex; use crate::kanata::*; #[cfg(all(feature = "simulated_input", not(feature = "interception_driver")))] mod exthook; #[cfg(all(not(feature = "simulated_input"), feature = "interception_driver"))] mod interception; #[cfg(all(not(feature = "simulated_input"), not(feature = "interception_driver")))] mod llhook; pub static PRESSED_KEYS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::default())); pub static ALTGR_BEHAVIOUR: Lazy> = Lazy::new(|| Mutex::new(AltGrBehaviour::default())); pub fn set_win_altgr_behaviour(b: AltGrBehaviour) { *ALTGR_BEHAVIOUR.lock() = b; } impl Kanata { #[cfg(all( not(feature = "interception_driver"), not(feature = "simulated_output"), not(feature = "win_sendinput_send_scancodes"), ))] pub fn check_release_non_physical_shift(&mut self) -> Result<()> { fn state_filter(v: &State<'_, &&[&CustomAction]>) -> Option> { match v { State::NormalKey { keycode, coord, flags, } => Some(State::NormalKey::<()> { keycode: *keycode, coord: *coord, flags: *flags, }), State::FakeKey { keycode } => Some(State::FakeKey::<()> { keycode: *keycode }), _ => None, } } static PREV_STATES: Lazy>>> = Lazy::new(|| Mutex::new(vec![])); let mut prev_states = PREV_STATES.lock(); if prev_states.is_empty() { prev_states.extend( self.layout .bm() .states .as_slice() .iter() .filter_map(state_filter), ); return Ok(()); } // This is an n^2 loop, but realistically there should be <= 5 states at a given time so // this should not be a problem. State does not implement Hash so can't use a HashSet. A // HashSet might perform worse anyway. for prev_state in prev_states.iter() { let keycode = match prev_state { State::NormalKey { keycode, coord, .. } => { // Goal of this conditional: // // Do not process state if: // - keycode is neither shift // - keycode is at the position of either shift // - state has not yet been released if !matches!(keycode, KeyCode::LShift | KeyCode::RShift) || *coord == (NORMAL_KEY_ROW, u16::from(OsCode::KEY_LEFTSHIFT)) || *coord == (NORMAL_KEY_ROW, u16::from(OsCode::KEY_RIGHTSHIFT)) || self .layout .bm() .states .iter() .filter_map(state_filter) .any(|s| s == *prev_state) { continue; } else { keycode } } State::FakeKey { keycode } => { // Goal of this conditional: // // Do not process state if: // - keycode is neither shift // - state has not yet been released if !matches!(keycode, KeyCode::LShift | KeyCode::RShift) || self .layout .bm() .states .iter() .filter_map(state_filter) .any(|s| s == *prev_state) { continue; } else { keycode } } _ => continue, }; log::debug!("lsft-arrowkey workaround: removing {keycode:?} at its typical coordinate"); self.layout.bm().states.retain(|s| match s { State::LayerModifier { coord, .. } | State::Custom { coord, .. } | State::RepeatingSequence { coord, .. } | State::NormalKey { coord, .. } => { *coord != (NORMAL_KEY_ROW, u16::from(OsCode::from(keycode))) } _ => true, }); log::debug!("removing {keycode:?} from pressed keys"); PRESSED_KEYS.lock().remove(&keycode.into()); } prev_states.clear(); prev_states.extend(self.layout.bm().states.iter().filter_map(state_filter)); Ok(()) } #[cfg(any( feature = "interception_driver", feature = "simulated_output", feature = "win_sendinput_send_scancodes" ))] pub fn check_release_non_physical_shift(&mut self) -> Result<()> { Ok(()) } #[cfg(feature = "gui")] pub fn live_reload(&mut self) -> Result<()> { self.live_reload_requested = true; self.do_live_reload(&None)?; Ok(()) } #[cfg(feature = "gui")] pub fn live_reload_n(&mut self, n: usize) -> Result<()> { // can't use in CustomAction::LiveReloadNum(n) due to 2nd mut borrow self.live_reload_requested = true; // let backup_cfg_idx = self.cur_cfg_idx; match self.cfg_paths.get(n) { Some(path) => { self.cur_cfg_idx = n; log::info!("Requested live reload of file: {}", path.display(),); } None => { log::error!("Requested live reload of config file number {}, but only {} config files were passed", n+1, self.cfg_paths.len()); } } // if let Err(e) = self.do_live_reload(&None) { // self.cur_cfg_idx = backup_cfg_idx; // restore index on fail when. TODO: add when a similar reversion is added to other custom actions // return Err(e) // } self.do_live_reload(&None)?; Ok(()) } } kanata-1.9.0/src/kanata.exe.manifest.rc000064400000000000000000000002721046102023000160340ustar 00000000000000#define RT_MANIFEST 24 1 RT_MANIFEST "./target/kanata.exe.manifest" iconMain ICON "../assets/kanata.ico" imgMain IMAGE "../assets/kanata.ico" imgReload IMAGE "../assets/reload_32px.png" kanata-1.9.0/src/lib.rs000064400000000000000000000032141046102023000127750ustar 00000000000000use anyhow::{anyhow, Error, Result}; use std::net::SocketAddr; use std::path::PathBuf; use std::str::FromStr; #[cfg(all(target_os = "windows", feature = "gui"))] pub mod gui; pub mod kanata; pub mod oskbd; pub mod tcp_server; #[cfg(test)] pub mod tests; pub use kanata::*; pub use tcp_server::TcpServer; type CfgPath = PathBuf; pub struct ValidatedArgs { pub paths: Vec, #[cfg(feature = "tcp_server")] pub tcp_server_address: Option, #[cfg(target_os = "linux")] pub symlink_path: Option, pub nodelay: bool, } pub fn default_cfg() -> Vec { let mut cfgs = Vec::new(); let default = PathBuf::from("kanata.kbd"); if default.is_file() { cfgs.push(default); } if let Some(config_dir) = dirs::config_dir() { let fallback = config_dir.join("kanata").join("kanata.kbd"); if fallback.is_file() { cfgs.push(fallback); } } cfgs } #[derive(Debug, Clone)] pub struct SocketAddrWrapper(SocketAddr); impl FromStr for SocketAddrWrapper { type Err = Error; fn from_str(s: &str) -> Result { let mut address = s.to_string(); if let Ok(port) = s.parse::() { address = format!("127.0.0.1:{}", port); } address .parse::() .map(SocketAddrWrapper) .map_err(|e| anyhow!("Please specify either a port number, e.g. 8081 or an address, e.g. 127.0.0.1:8081.\n{e}")) } } impl SocketAddrWrapper { pub fn into_inner(self) -> SocketAddr { self.0 } pub fn get_ref(&self) -> &SocketAddr { &self.0 } } kanata-1.9.0/src/main.rs000064400000000000000000000213741046102023000131620ustar 00000000000000#![cfg_attr(feature = "gui", windows_subsystem = "windows")] // disable default console for a Windows GUI app mod main_lib; use anyhow::{bail, Result}; use clap::Parser; use kanata_parser::cfg; use kanata_state_machine::*; use simplelog::{format_description, *}; use std::path::PathBuf; #[derive(Parser, Debug)] #[command(author, version, verbatim_doc_comment)] /// kanata: an advanced software key remapper /// /// kanata remaps key presses to other keys or complex actions depending on the /// configuration for that key. You can find the guide for creating a config /// file here: https://github.com/jtroo/kanata/blob/main/docs/config.adoc /// /// If you need help, please feel welcome to create an issue or discussion in /// the kanata repository: https://github.com/jtroo/kanata struct Args { // Display different platform specific paths based on the target OS #[cfg_attr( target_os = "windows", doc = r"Configuration file(s) to use with kanata. If not specified, defaults to kanata.kbd in the current working directory and 'C:\Users\user\AppData\Roaming\kanata\kanata.kbd'." )] #[cfg_attr( target_os = "macos", doc = "Configuration file(s) to use with kanata. If not specified, defaults to kanata.kbd in the current working directory and '$HOME/Library/Application Support/kanata/kanata.kbd'." )] #[cfg_attr( not(any(target_os = "macos", target_os = "windows")), doc = "Configuration file(s) to use with kanata. If not specified, defaults to kanata.kbd in the current working directory and '$XDG_CONFIG_HOME/kanata/kanata.kbd'." )] #[arg(short, long, verbatim_doc_comment)] cfg: Option>, /// Port or full address (IP:PORT) to run the optional TCP server on. If blank, /// no TCP port will be listened on. #[cfg(feature = "tcp_server")] #[arg( short = 'p', long = "port", value_name = "PORT or IP:PORT", verbatim_doc_comment )] tcp_server_address: Option, /// Path for the symlink pointing to the newly-created device. If blank, no /// symlink will be created. #[cfg(target_os = "linux")] #[arg(short, long, verbatim_doc_comment)] symlink_path: Option, /// List the keyboards available for grabbing and exit. #[cfg(target_os = "macos")] #[arg(short, long)] list: bool, /// Disable logging, except for errors. Takes precedent over debug and trace. #[arg(short, long)] quiet: bool, /// Enable debug logging. #[arg(short, long)] debug: bool, /// Enable trace logging; implies --debug as well. #[arg(short, long)] trace: bool, /// Remove the startup delay. /// In some cases, removing the delay may cause keyboard issues on startup. #[arg(short, long, verbatim_doc_comment)] nodelay: bool, /// Milliseconds to wait before attempting to register a newly connected /// device. The default is 200. /// /// You may wish to increase this if you have a device that is failing /// to register - the device may be taking too long to become ready. #[cfg(target_os = "linux")] #[arg(short, long, verbatim_doc_comment)] wait_device_ms: Option, /// Validate configuration file and exit #[arg(long, verbatim_doc_comment)] check: bool, /// Log layer changes even if the configuration file has set the defcfg /// option to false. Useful if you are experimenting with a new /// configuration but want to default to no logging. #[arg(long, verbatim_doc_comment)] log_layer_changes: bool, } #[cfg(not(feature = "gui"))] mod cli { use super::*; /// Parse CLI arguments and initialize logging. fn cli_init() -> Result { let args = Args::parse(); #[cfg(target_os = "macos")] if args.list { karabiner_driverkit::list_keyboards(); std::process::exit(0); } let cfg_paths = args.cfg.unwrap_or_else(default_cfg); let log_lvl = match (args.debug, args.trace, args.quiet) { (_, true, false) => LevelFilter::Trace, (true, false, false) => LevelFilter::Debug, (false, false, false) => LevelFilter::Info, (_, _, true) => LevelFilter::Error, }; let mut log_cfg = ConfigBuilder::new(); if let Err(e) = log_cfg.set_time_offset_to_local() { eprintln!("WARNING: could not set log TZ to local: {e:?}"); }; log_cfg.set_time_format_custom(format_description!( version = 2, "[hour]:[minute]:[second].[subsecond digits:4]" )); CombinedLogger::init(vec![TermLogger::new( log_lvl, log_cfg.build(), TerminalMode::Mixed, ColorChoice::AlwaysAnsi, )]) .expect("logger can init"); log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] log::info!("using LLHOOK+SendInput for keyboard IO"); #[cfg(all(feature = "interception_driver", target_os = "windows"))] log::info!("using the Interception driver for keyboard IO"); if let Some(config_file) = cfg_paths.first() { if !config_file.exists() { bail!( "Could not find the config file ({})\nFor more info, pass the `-h` or `--help` flags.", cfg_paths[0].to_str().unwrap_or("?") ) } } else { bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); } if args.check { log::info!("validating config only and exiting"); let status = match cfg::new_from_file(&cfg_paths[0]) { Ok(_) => 0, Err(e) => { log::error!("{e:?}"); 1 } }; std::process::exit(status); } #[cfg(target_os = "linux")] if let Some(wait) = args.wait_device_ms { use std::sync::atomic::Ordering; log::info!("Setting device registration wait time to {wait} ms."); oskbd::WAIT_DEVICE_MS.store(wait, Ordering::SeqCst); } if args.log_layer_changes { cfg_forced::force_log_layer_changes(true); } Ok(ValidatedArgs { paths: cfg_paths, #[cfg(feature = "tcp_server")] tcp_server_address: args.tcp_server_address, #[cfg(target_os = "linux")] symlink_path: args.symlink_path, nodelay: args.nodelay, }) } pub(crate) fn main_impl() -> Result<()> { let args = cli_init()?; let kanata_arc = Kanata::new_arc(&args)?; if !args.nodelay { log::info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); std::thread::sleep(std::time::Duration::from_secs(2)); } // Start a processing loop in another thread and run the event loop in this thread. // // The reason for two different event loops is that the "event loop" only listens for // keyboard events, which it sends to the "processing loop". The processing loop handles // keyboard events while also maintaining `tick()` calls to keyberon. let (tx, rx) = std::sync::mpsc::sync_channel(100); let (server, ntx, nrx) = if let Some(address) = { #[cfg(feature = "tcp_server")] { args.tcp_server_address } #[cfg(not(feature = "tcp_server"))] { None:: } } { let mut server = TcpServer::new(address.into_inner(), tx.clone()); server.start(kanata_arc.clone()); let (ntx, nrx) = std::sync::mpsc::sync_channel(100); (Some(server), Some(ntx), Some(nrx)) } else { (None, None, None) }; Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); if let (Some(server), Some(nrx)) = (server, nrx) { #[allow(clippy::unit_arg)] Kanata::start_notification_loop(nrx, server.connections); } #[cfg(target_os = "linux")] sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; Kanata::event_loop(kanata_arc, tx) } } #[cfg(not(feature = "gui"))] pub fn main() -> Result<()> { let ret = cli::main_impl(); if let Err(ref e) = ret { log::error!("{e}\n"); } eprintln!("\nPress enter to exit"); let _ = std::io::stdin().read_line(&mut String::new()); ret } #[cfg(all(feature = "gui", target_os = "windows"))] fn main() { main_lib::win_gui::lib_main_gui(); } kanata-1.9.0/src/main_lib/mod.rs000064400000000000000000000001141046102023000145540ustar 00000000000000#[cfg(all(target_os = "windows", feature = "gui"))] pub(crate) mod win_gui; kanata-1.9.0/src/main_lib/win_gui.rs000064400000000000000000000171061046102023000154470ustar 00000000000000use crate::*; use anyhow::{anyhow, Context}; use clap::{error::ErrorKind, CommandFactory}; use kanata_state_machine::gui::*; use kanata_state_machine::*; use std::fs::File; /// Parse CLI arguments and initialize logging. fn cli_init() -> Result { let noti_lvl = LevelFilter::Error; // min lvl above which to use Win system notifications let log_file_p = "kanata_log.txt"; let args = match Args::try_parse() { Ok(args) => args, Err(e) => { if *IS_TERM { // init loggers without config so '-help' "error" or real ones can be printed let mut log_cfg = ConfigBuilder::new(); CombinedLogger::init(vec![ TermLogger::new( LevelFilter::Debug, log_cfg.build(), TerminalMode::Mixed, ColorChoice::AlwaysAnsi, ), log_win::windbg_simple_combo(LevelFilter::Debug, noti_lvl), ]) .expect("logger can init"); } else { CombinedLogger::init(vec![ log_win::windbg_simple_combo(LevelFilter::Debug, noti_lvl), WriteLogger::new( LevelFilter::Debug, Config::default(), File::create(log_file_p).unwrap(), ), ]) .expect("logger can init"); } match e.kind() { ErrorKind::DisplayHelp => { let mut cmd = Args::command(); let help = cmd.render_help(); info!("{help}"); log::set_max_level(LevelFilter::Off); if !*IS_TERM { // detached to open log still opened for writing match open::that_detached(log_file_p) { Ok(()) => {} // on the off-chance the user looks at WinDbg logs Err(ef) => error!("failed to open {log_file_p} due to {ef:?}"), } } return Err(anyhow!("")); } _ => { if !*IS_TERM { match open::that_detached(log_file_p) { Ok(()) => {} Err(ef) => error!("failed to open {log_file_p} due to {ef:?}"), } } return Err(e.into()); } } } }; let cfg_paths = args.cfg.unwrap_or_else(default_cfg); let log_lvl = match (args.debug, args.trace) { (_, true) => LevelFilter::Trace, (true, false) => LevelFilter::Debug, (false, false) => LevelFilter::Info, }; let mut log_cfg = ConfigBuilder::new(); if let Err(e) = log_cfg.set_time_offset_to_local() { eprintln!("WARNING: could not set log TZ to local: {e:?}"); }; log_cfg.set_time_format_custom(format_description!( version = 2, "[hour]:[minute]:[second].[subsecond digits:4]" )); if *IS_TERM { CombinedLogger::init(vec![ TermLogger::new( log_lvl, log_cfg.build(), TerminalMode::Mixed, ColorChoice::AlwaysAnsi, ), log_win::windbg_simple_combo(log_lvl, noti_lvl), ]) .expect("logger can init"); } else { CombinedLogger::init(vec![log_win::windbg_simple_combo(log_lvl, noti_lvl)]) .expect("logger can init"); } log::info!("kanata v{} starting", env!("CARGO_PKG_VERSION")); #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] log::info!("using LLHOOK+SendInput for keyboard IO"); #[cfg(all(feature = "interception_driver", target_os = "windows"))] log::info!("using the Interception driver for keyboard IO"); if let Some(config_file) = cfg_paths.first() { if !config_file.exists() { bail!( "Could not find the config file ({})\nFor more info, pass the `-h` or `--help` flags.", cfg_paths[0].to_str().unwrap_or("?") ) } } else { bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); } if args.check { log::info!("validating config only and exiting"); let status = match cfg::new_from_file(&cfg_paths[0]) { Ok(_) => 0, Err(e) => { log::error!("{e:?}"); 1 } }; std::process::exit(status); } Ok(ValidatedArgs { paths: cfg_paths, #[cfg(feature = "tcp_server")] tcp_server_address: args.tcp_server_address, nodelay: args.nodelay, }) } fn main_impl() -> Result<()> { let args = cli_init()?; let kanata_arc = Kanata::new_arc(&args)?; if CFG.set(kanata_arc.clone()).is_err() { warn!("Someone else set our ‘CFG’"); }; // store a clone of cfg so that we can ask it to reset itself if !args.nodelay { info!("Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep."); std::thread::sleep(std::time::Duration::from_secs(2)); } // Start a processing loop in another thread and run the event loop in this thread. // // The reason for two different event loops is that the "event loop" only listens for keyboard // events, which it sends to the "processing loop". The processing loop handles keyboard events // while also maintaining `tick()` calls to keyberon. let (tx, rx) = std::sync::mpsc::sync_channel(100); let (server, ntx, nrx) = if let Some(address) = { #[cfg(feature = "tcp_server")] { args.tcp_server_address } #[cfg(not(feature = "tcp_server"))] { None:: } } { let mut server = TcpServer::new(address.into_inner(), tx.clone()); server.start(kanata_arc.clone()); let (ntx, nrx) = std::sync::mpsc::sync_channel(100); (Some(server), Some(ntx), Some(nrx)) } else { (None, None, None) }; native_windows_gui::init().context("Failed to init Native Windows GUI")?; let ui = build_tray(&kanata_arc)?; let gui_tx = ui.layer_notice.sender(); let gui_cfg_tx = ui.cfg_notice.sender(); // allows notifying GUI on config reloads let gui_err_tx = ui.err_notice.sender(); // allows notifying GUI on erorrs (from logger) let gui_exit_tx = ui.exit_notice.sender(); // allows notifying GUI on app quit if GUI_TX.set(gui_tx).is_err() { warn!("Someone else set our ‘GUI_TX’"); }; if GUI_CFG_TX.set(gui_cfg_tx).is_err() { warn!("Someone else set our ‘GUI_CFG_TX’"); }; if GUI_ERR_TX.set(gui_err_tx).is_err() { warn!("Someone else set our ‘GUI_ERR_TX’"); }; if GUI_EXIT_TX.set(gui_exit_tx).is_err() { warn!("Someone else set our ‘GUI_EXIT_TX’"); }; Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); if let (Some(server), Some(nrx)) = (server, nrx) { #[allow(clippy::unit_arg)] Kanata::start_notification_loop(nrx, server.connections); } Kanata::event_loop(kanata_arc, tx, ui)?; Ok(()) } pub fn lib_main_gui() { let _attach_console = *IS_CONSOLE; let ret = main_impl(); if let Err(ref e) = ret { log::error!("{e}\n"); } unsafe { FreeConsole(); } } kanata-1.9.0/src/oskbd/linux.rs000064400000000000000000000725341046102023000145030ustar 00000000000000//! Contains the input/output code for keyboards on Linux. #![cfg_attr(feature = "simulated_output", allow(dead_code, unused_imports))] pub use evdev::BusType; use evdev::{uinput, Device, EventType, InputEvent, Key, PropType, RelativeAxisType}; use inotify::{Inotify, WatchMask}; use mio::{unix::SourceFd, Events, Interest, Poll, Token}; use nix::ioctl_read_buf; use rustc_hash::FxHashMap as HashMap; use signal_hook::{ consts::{SIGINT, SIGTERM, SIGTSTP}, iterator::Signals, }; use std::convert::TryFrom; use std::fs; use std::io; use std::os::unix::io::AsRawFd; use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; use std::thread; use super::*; use crate::{kanata::CalculatedMouseMove, oskbd::KeyEvent}; use kanata_parser::cfg::DeviceDetectMode; use kanata_parser::cfg::UnicodeTermination; use kanata_parser::custom_action::*; use kanata_parser::keys::*; pub struct KbdIn { devices: HashMap, /// Some(_) if devices are explicitly listed, otherwise None. missing_device_paths: Option>, poll: Poll, events: Events, token_counter: usize, /// stored to prevent dropping _inotify: Inotify, include_names: Option>, exclude_names: Option>, device_detect_mode: DeviceDetectMode, } const INOTIFY_TOKEN_VALUE: usize = 0; const INOTIFY_TOKEN: Token = Token(INOTIFY_TOKEN_VALUE); pub static WAIT_DEVICE_MS: AtomicU64 = AtomicU64::new(200); impl KbdIn { pub fn new( dev_paths: &[String], continue_if_no_devices: bool, include_names: Option>, exclude_names: Option>, device_detect_mode: DeviceDetectMode, ) -> Result { let poll = Poll::new()?; let mut missing_device_paths = None; let devices = if !dev_paths.is_empty() { missing_device_paths = Some(vec![]); devices_from_input_paths( dev_paths, missing_device_paths.as_mut().expect("initialized"), ) } else { discover_devices( include_names.as_deref(), exclude_names.as_deref(), device_detect_mode, ) }; if devices.is_empty() { if continue_if_no_devices { log::warn!("no keyboard devices found; kanata is waiting"); } else { return Err(io::Error::new( io::ErrorKind::NotFound, "No keyboard devices were found", )); } } let _inotify = watch_devinput().map_err(|e| { log::error!("failed to watch files: {e:?}"); e })?; poll.registry().register( &mut SourceFd(&_inotify.as_raw_fd()), INOTIFY_TOKEN, Interest::READABLE, )?; let mut kbdin = Self { poll, missing_device_paths, _inotify, events: Events::with_capacity(32), devices: HashMap::default(), token_counter: INOTIFY_TOKEN_VALUE + 1, include_names, exclude_names, device_detect_mode, }; for (device, dev_path) in devices.into_iter() { if let Err(e) = kbdin.register_device(device, dev_path.clone()) { log::warn!("found device {dev_path} but could not register it {e:?}"); if let Some(ref mut missing) = kbdin.missing_device_paths { missing.push(dev_path); } } } Ok(kbdin) } fn register_device(&mut self, mut dev: Device, path: String) -> Result<(), io::Error> { log::info!("registering {path}: {:?}", dev.name().unwrap_or("")); wait_for_all_keys_unpressed(&dev)?; // NOTE: This grab-ungrab-grab sequence magically fixes an issue with a Lenovo Yoga // trackpad not working. No idea why this works. dev.grab()?; dev.ungrab()?; dev.grab()?; let tok = Token(self.token_counter); self.token_counter += 1; let fd = dev.as_raw_fd(); self.poll .registry() .register(&mut SourceFd(&fd), tok, Interest::READABLE)?; self.devices.insert(tok, (dev, path)); Ok(()) } pub fn read(&mut self) -> Result, io::Error> { let mut input_events = vec![]; loop { log::trace!("polling"); if let Err(e) = self.poll.poll(&mut self.events, None) { log::error!("failed poll: {:?}", e); return Ok(vec![]); } const EVENT_LIMIT: usize = 48; let mut do_rediscover = false; for event in &self.events { if let Some((device, _)) = self.devices.get_mut(&event.token()) { if let Err(e) = device.fetch_events().map(|evs| { evs.into_iter() .take(EVENT_LIMIT) .for_each(|ev| input_events.push(ev)) }) { // Currently the kind() is uncategorized... not helpful, need to match // on os error. code 19 is ENODEV, "no such device". match e.raw_os_error() { Some(19) => { self.poll .registry() .deregister(&mut SourceFd(&device.as_raw_fd()))?; if let Some((_, path)) = self.devices.remove(&event.token()) { log::warn!("removing kbd device: {path}"); if let Some(ref mut missing) = self.missing_device_paths { missing.push(path); } } } _ => { log::error!("failed fetch events due to {e}, kind: {}", e.kind()); return Err(e); } }; } } else if event.token() == INOTIFY_TOKEN { do_rediscover = true; } else { panic!("encountered unexpected epoll event {event:?}"); } } if do_rediscover { log::info!("watch found file changes, looking for new devices"); self.rediscover_devices()?; } if !input_events.is_empty() { return Ok(input_events); } } } fn rediscover_devices(&mut self) -> Result<(), io::Error> { // This function is kinda ugly but the borrow checker doesn't like all this mutation. let mut paths_registered = vec![]; if let Some(ref mut missing) = self.missing_device_paths { if missing.is_empty() { log::info!("no devices are missing, doing nothing"); return Ok(()); } log::info!("checking for {missing:?}"); let discovered_devices = missing .iter() .filter_map(|dev_path| { for _ in 0..(WAIT_DEVICE_MS.load(Ordering::SeqCst) / 10) { // try a few times with waits in between; device might not be ready if let Ok(device) = Device::open(dev_path) { return Some((device, dev_path.clone())); } std::thread::sleep(std::time::Duration::from_millis(10)); } None }) .collect::>(); for (device, dev_path) in discovered_devices { if let Err(e) = self.register_device(device, dev_path.clone()) { log::warn!("found device {dev_path} but could not register it {e:?}"); } else { paths_registered.push(dev_path); } } } if let Some(ref mut missing) = self.missing_device_paths { missing.retain(|path| !paths_registered.contains(path)); } else { log::info!("sleeping for a moment to let devices become ready"); std::thread::sleep(std::time::Duration::from_millis( WAIT_DEVICE_MS.load(Ordering::SeqCst), )); discover_devices( self.include_names.as_deref(), self.exclude_names.as_deref(), self.device_detect_mode, ) .into_iter() .try_for_each(|(dev, path)| { if !self .devices .values() .any(|(_, registered_path)| &path == registered_path) { self.register_device(dev, path) } else { Ok(()) } })?; } Ok(()) } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum DeviceType { Keyboard, KeyboardMouse, Mouse, Other, } pub fn is_input_device(device: &Device, detect_mode: DeviceDetectMode) -> bool { if device.name() == Some("kanata") { return false; } let is_keyboard = device.supported_keys().is_some_and(has_keyboard_keys); let is_mouse = device .supported_relative_axes() .is_some_and(|axes| axes.contains(RelativeAxisType::REL_X)); let device_type = match (is_keyboard, is_mouse) { (true, true) => DeviceType::KeyboardMouse, (true, false) => DeviceType::Keyboard, (false, true) => DeviceType::Mouse, (false, false) => DeviceType::Other, }; let device_name = device.name().unwrap_or("unknown device name"); match (detect_mode, device_type) { (DeviceDetectMode::Any, _) | (DeviceDetectMode::KeyboardMice, DeviceType::Keyboard | DeviceType::KeyboardMouse) | (DeviceDetectMode::KeyboardOnly, DeviceType::Keyboard) => { let use_input = true; log::debug!( "Use for input autodetect: {use_input}. detect type {:?}; device type {:?}, device name: {}", detect_mode, device_type, device_name, ); use_input } (_, DeviceType::Other) => { log::debug!( "Use for input autodetect: false. Non-input device: {}", device_name, ); false } _ => { let use_input = false; log::debug!( "Use for input autodetect: {use_input}. detect type {:?}; device type {:?}, device name: {}", detect_mode, device_type, device_name, ); use_input } } } fn has_keyboard_keys(keys: &evdev::AttributeSetRef) -> bool { const SENSIBLE_KEYBOARD_SCANCODE_LOWER_BOUND: u16 = 1; // The next one is power button. Some keyboards have it, // but so does the power button... const SENSIBLE_KEYBOARD_SCANCODE_UPPER_BOUND: u16 = 115; let mut sensible_keyboard_keys = (SENSIBLE_KEYBOARD_SCANCODE_LOWER_BOUND ..=SENSIBLE_KEYBOARD_SCANCODE_UPPER_BOUND) .map(Key::new); sensible_keyboard_keys.any(|k| keys.contains(k)) } impl TryFrom for KeyEvent { type Error = (); fn try_from(item: InputEvent) -> Result { use OsCode::*; match item.kind() { evdev::InputEventKind::Key(k) => Ok(Self { code: OsCode::from_u16(k.0).ok_or(())?, value: KeyValue::from(item.value()), }), evdev::InputEventKind::RelAxis(axis_type) => { let dist = item.value(); let code: OsCode = match axis_type { RelativeAxisType::REL_WHEEL | RelativeAxisType::REL_WHEEL_HI_RES => { if dist > 0 { MouseWheelUp } else { MouseWheelDown } } RelativeAxisType::REL_HWHEEL | RelativeAxisType::REL_HWHEEL_HI_RES => { if dist > 0 { MouseWheelRight } else { MouseWheelLeft } } _ => return Err(()), }; Ok(KeyEvent { code, value: KeyValue::Tap, }) } _ => Err(()), } } } impl From for InputEvent { fn from(item: KeyEvent) -> Self { InputEvent::new(EventType::KEY, item.code as u16, item.value as i32) } } use std::cell::Cell; #[cfg(all(not(feature = "simulated_output"), not(feature = "passthru_ahk")))] pub struct KbdOut { device: uinput::VirtualDevice, accumulated_scroll: u16, accumulated_hscroll: u16, raw_buf: Vec, pub unicode_termination: Cell, pub unicode_u_code: Cell, } #[cfg(all(not(feature = "simulated_output"), not(feature = "passthru_ahk")))] impl KbdOut { pub fn new( symlink_path: &Option, trackpoint: bool, name: &str, bus_type: BusType, ) -> Result { // Support pretty much every feature of a Keyboard or a Mouse in a VirtualDevice so that no event from the original input devices gets lost // TODO investigate the rare possibility that a device is e.g. a Joystick and a Keyboard or a Mouse at the same time, which could lead to lost events // For some reason 0..0x300 (max value for a key) doesn't work, the closest that I've got to work is 560 let keys = evdev::AttributeSet::from_iter((0..560).map(evdev::Key)); let relative_axes = evdev::AttributeSet::from_iter([ RelativeAxisType::REL_WHEEL, RelativeAxisType::REL_HWHEEL, RelativeAxisType::REL_X, RelativeAxisType::REL_Y, RelativeAxisType::REL_Z, RelativeAxisType::REL_RX, RelativeAxisType::REL_RY, RelativeAxisType::REL_RZ, RelativeAxisType::REL_DIAL, RelativeAxisType::REL_MISC, RelativeAxisType::REL_WHEEL_HI_RES, RelativeAxisType::REL_HWHEEL_HI_RES, ]); let device = uinput::VirtualDeviceBuilder::new()? .name(&name) // libinput's "disable while typing" feature don't work when bus_type // is set to BUS_USB, but appears to work when it's set to BUS_I8042. .input_id(evdev::InputId::new(bus_type, 1, 1, 1)) .with_keys(&keys)? .with_relative_axes(&relative_axes)?; let device = if trackpoint { device.with_properties(&evdev::AttributeSet::from_iter([PropType::POINTING_STICK]))? } else { device }; let mut device = device.build()?; let devnode = device .enumerate_dev_nodes_blocking()? .next() // Expect only one. Using fold or calling next again blocks indefinitely .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "devnode is not found"))??; log::info!("Created device {:#?}", devnode); let symlink = if let Some(symlink_path) = symlink_path { let dest = PathBuf::from(symlink_path); let symlink = Symlink::new(devnode, dest)?; Some(symlink) } else { None }; handle_signals(symlink); Ok(KbdOut { device, accumulated_scroll: 0, accumulated_hscroll: 0, raw_buf: vec![], // historically was the only option, so make Enter the default unicode_termination: Cell::new(UnicodeTermination::Enter), // historically was the only option, so make KEY_U the default unicode_u_code: Cell::new(OsCode::KEY_U), }) } pub fn update_unicode_termination(&self, t: UnicodeTermination) { self.unicode_termination.replace(t); } pub fn update_unicode_u_code(&self, u: OsCode) { self.unicode_u_code.replace(u); } pub fn write_raw(&mut self, event: InputEvent) -> Result<(), io::Error> { if event.event_type() == EventType::SYNCHRONIZATION { // Possible codes are: // // SYN_REPORT: probably the only one we'll ever receive, segments atomic reads // SYN_CONFIG: unused // SYN_MT_REPORT: same as SYN_REPORT above but for touch devices, which kanata almost // certainly shouldn't be dealing with. // SYN_DROPPED: buffer full, events dropped. Not sure what this means or how to handle // this correctly. // // With this knowledge, seems fine to not bother checking. self.device.emit(&self.raw_buf)?; self.raw_buf.clear(); } else { self.raw_buf.push(event); } Ok(()) } pub fn write(&mut self, event: InputEvent) -> Result<(), io::Error> { if !self.raw_buf.is_empty() { self.device.emit(&self.raw_buf)?; self.raw_buf.clear(); } self.device.emit(&[event])?; Ok(()) } pub fn write_many(&mut self, events: &[InputEvent]) -> Result<(), io::Error> { if !self.raw_buf.is_empty() { self.device.emit(&self.raw_buf)?; self.raw_buf.clear(); } self.device.emit(events)?; Ok(()) } pub fn write_key(&mut self, key: OsCode, value: KeyValue) -> Result<(), io::Error> { let key_ev = KeyEvent::new(key, value); let input_ev = key_ev.into(); log::debug!("send to uinput: {:?}", input_ev); self.device.emit(&[input_ev])?; Ok(()) } pub fn write_code(&mut self, code: u32, value: KeyValue) -> Result<(), io::Error> { let event = InputEvent::new(EventType::KEY, code as u16, value as i32); self.device.emit(&[event])?; Ok(()) } pub fn press_key(&mut self, key: OsCode) -> Result<(), io::Error> { self.write_key(key, KeyValue::Press) } pub fn release_key(&mut self, key: OsCode) -> Result<(), io::Error> { self.write_key(key, KeyValue::Release) } /// Send using C-S-u + + spc pub fn send_unicode(&mut self, c: char) -> Result<(), io::Error> { log::debug!("sending unicode {c}"); let hex = format!("{:x}", c as u32); self.press_key(OsCode::KEY_LEFTCTRL)?; self.press_key(OsCode::KEY_LEFTSHIFT)?; self.press_key(self.unicode_u_code.get())?; self.release_key(self.unicode_u_code.get())?; self.release_key(OsCode::KEY_LEFTSHIFT)?; self.release_key(OsCode::KEY_LEFTCTRL)?; let mut s = String::new(); for c in hex.chars() { s.push(c); let osc = str_to_oscode(&s).expect("valid keycodes for unicode"); s.clear(); self.press_key(osc)?; self.release_key(osc)?; } match self.unicode_termination.get() { UnicodeTermination::Enter => { self.press_key(OsCode::KEY_ENTER)?; self.release_key(OsCode::KEY_ENTER)?; } UnicodeTermination::Space => { self.press_key(OsCode::KEY_SPACE)?; self.release_key(OsCode::KEY_SPACE)?; } UnicodeTermination::SpaceEnter => { self.press_key(OsCode::KEY_SPACE)?; self.release_key(OsCode::KEY_SPACE)?; self.press_key(OsCode::KEY_ENTER)?; self.release_key(OsCode::KEY_ENTER)?; } UnicodeTermination::EnterSpace => { self.press_key(OsCode::KEY_ENTER)?; self.release_key(OsCode::KEY_ENTER)?; self.press_key(OsCode::KEY_SPACE)?; self.release_key(OsCode::KEY_SPACE)?; } } Ok(()) } pub fn click_btn(&mut self, btn: Btn) -> Result<(), io::Error> { self.press_key(btn.into()) } pub fn release_btn(&mut self, btn: Btn) -> Result<(), io::Error> { self.release_key(btn.into()) } pub fn scroll( &mut self, direction: MWheelDirection, hi_res_distance: u16, ) -> Result<(), io::Error> { log::debug!("scroll: {direction:?} {hi_res_distance:?}"); let mut lo_res_distance = hi_res_distance / HI_RES_SCROLL_UNITS_IN_LO_RES; let leftover_hi_res_distance = hi_res_distance % HI_RES_SCROLL_UNITS_IN_LO_RES; match direction { MWheelDirection::Up | MWheelDirection::Down => { self.accumulated_scroll += leftover_hi_res_distance; lo_res_distance += self.accumulated_scroll / HI_RES_SCROLL_UNITS_IN_LO_RES; self.accumulated_scroll %= HI_RES_SCROLL_UNITS_IN_LO_RES; } MWheelDirection::Left | MWheelDirection::Right => { self.accumulated_hscroll += leftover_hi_res_distance; lo_res_distance += self.accumulated_hscroll / HI_RES_SCROLL_UNITS_IN_LO_RES; self.accumulated_hscroll %= HI_RES_SCROLL_UNITS_IN_LO_RES; } } let hi_res_scroll_event = InputEvent::new( EventType::RELATIVE, match direction { MWheelDirection::Up | MWheelDirection::Down => RelativeAxisType::REL_WHEEL_HI_RES.0, MWheelDirection::Left | MWheelDirection::Right => { RelativeAxisType::REL_HWHEEL_HI_RES.0 } }, match direction { MWheelDirection::Up | MWheelDirection::Right => i32::from(hi_res_distance), MWheelDirection::Down | MWheelDirection::Left => -i32::from(hi_res_distance), }, ); if lo_res_distance > 0 { self.write_many(&[ hi_res_scroll_event, InputEvent::new( EventType::RELATIVE, match direction { MWheelDirection::Up | MWheelDirection::Down => { RelativeAxisType::REL_WHEEL.0 } MWheelDirection::Left | MWheelDirection::Right => { RelativeAxisType::REL_HWHEEL.0 } }, match direction { MWheelDirection::Up | MWheelDirection::Right => i32::from(lo_res_distance), MWheelDirection::Down | MWheelDirection::Left => { -i32::from(lo_res_distance) } }, ), ]) } else { self.write(hi_res_scroll_event) } } pub fn move_mouse(&mut self, mv: CalculatedMouseMove) -> Result<(), io::Error> { let (axis, distance) = match mv.direction { MoveDirection::Up => (RelativeAxisType::REL_Y, -i32::from(mv.distance)), MoveDirection::Down => (RelativeAxisType::REL_Y, i32::from(mv.distance)), MoveDirection::Left => (RelativeAxisType::REL_X, -i32::from(mv.distance)), MoveDirection::Right => (RelativeAxisType::REL_X, i32::from(mv.distance)), }; self.write(InputEvent::new(EventType::RELATIVE, axis.0, distance)) } pub fn move_mouse_many(&mut self, moves: &[CalculatedMouseMove]) -> Result<(), io::Error> { let mut events = vec![]; for mv in moves { let (axis, distance) = match mv.direction { MoveDirection::Up => (RelativeAxisType::REL_Y, -i32::from(mv.distance)), MoveDirection::Down => (RelativeAxisType::REL_Y, i32::from(mv.distance)), MoveDirection::Left => (RelativeAxisType::REL_X, -i32::from(mv.distance)), MoveDirection::Right => (RelativeAxisType::REL_X, i32::from(mv.distance)), }; events.push(InputEvent::new(EventType::RELATIVE, axis.0, distance)); } self.write_many(&events) } pub fn set_mouse(&mut self, _x: u16, _y: u16) -> Result<(), io::Error> { log::warn!("setmouse does not work in Linux yet. Maybe try out warpd:\n\thttps://github.com/rvaiya/warpd"); Ok(()) } } fn devices_from_input_paths( dev_paths: &[String], missing_device_paths: &mut Vec, ) -> Vec<(Device, String)> { dev_paths .iter() .map(|dev_path| (dev_path, Device::open(dev_path))) .filter_map(|(dev_path, open_result)| match open_result { Ok(d) => Some((d, dev_path.clone())), Err(e) => { log::warn!("failed to open device '{dev_path}': {e:?}"); missing_device_paths.push(dev_path.clone()); None } }) .collect() } fn discover_devices( include_names: Option<&[String]>, exclude_names: Option<&[String]>, device_detect_mode: DeviceDetectMode, ) -> Vec<(Device, String)> { log::info!("looking for devices in /dev/input"); let devices: Vec<_> = evdev::enumerate() .map(|(path, device)| { ( device, path.to_str() .expect("non-utf8 path found for device") .to_owned(), ) }) .filter(|pd| { let is_input = is_input_device(&pd.0, device_detect_mode); (match include_names { None => is_input, Some(include_names) => { let name = pd.0.name().unwrap_or(""); if include_names.iter().any(|include| name == include) { log::info!("device [{}:{name}] is included", &pd.1); true } else { log::info!("device [{}:{name}] is ignored", &pd.1); false } } }) && (match exclude_names { None => true, Some(exclude_names) => { let name = pd.0.name().unwrap_or(""); if exclude_names.iter().any(|exclude| name == exclude) { log::info!("device [{}:{name}] is excluded", &pd.1); false } else { true } } }) }) .collect(); devices } fn watch_devinput() -> Result { let inotify = Inotify::init().expect("Failed to initialize inotify"); inotify.watches().add("/dev/input", WatchMask::CREATE)?; Ok(inotify) } #[derive(Clone)] struct Symlink { dest: PathBuf, } impl Symlink { fn new(source: PathBuf, dest: PathBuf) -> Result { if let Ok(metadata) = fs::symlink_metadata(&dest) { if metadata.file_type().is_symlink() { fs::remove_file(&dest)?; } else { return Err(io::Error::new( io::ErrorKind::AlreadyExists, format!( "Cannot create a symlink at \"{}\": path already exists.", dest.to_string_lossy() ), )); } } std::os::unix::fs::symlink(&source, &dest)?; log::info!("Created symlink {:#?} -> {:#?}", dest, source); Ok(Self { dest }) } } fn handle_signals(symlink: Option) { thread::spawn(|| { let mut signals = Signals::new([SIGINT, SIGTERM, SIGTSTP]).expect("signals register"); if let Some(signal) = (&mut signals).into_iter().next() { match signal { SIGINT | SIGTERM => { drop(symlink); signal_hook::low_level::emulate_default_handler(signal) .expect("run original sighandlers"); unreachable!(); } SIGTSTP => { drop(symlink); log::warn!("got SIGTSTP, exiting instead of pausing so keyboards don't hang"); std::process::exit(SIGTSTP); } _ => unreachable!(), } } }); } // Note for allow: the ioctl_read_buf triggers this clippy lint. // Note: CI does not yet support this lint, so also allowing unknown lints. #[allow(unknown_lints)] #[allow(clippy::manual_slice_size_calculation)] fn wait_for_all_keys_unpressed(dev: &Device) -> Result<(), io::Error> { let mut pending_release = false; const KEY_MAX: usize = OsCode::KEY_MAX as usize; let mut keystate = [0u8; KEY_MAX / 8 + 1]; loop { let mut n_pressed_keys = 0; ioctl_read_buf!(read_keystates, 'E', 0x18, u8); unsafe { read_keystates(dev.as_raw_fd(), &mut keystate) } .map_err(|_| io::Error::last_os_error())?; for i in 0..=KEY_MAX { if (keystate[i / 8] >> (i % 8)) & 0x1 > 0 { n_pressed_keys += 1; } } match n_pressed_keys { 0 => break, _ => pending_release = true, } } if pending_release { std::thread::sleep(std::time::Duration::from_micros(100)); } Ok(()) } impl Drop for Symlink { fn drop(&mut self) { let _ = fs::remove_file(&self.dest); log::info!("Deleted symlink {:#?}", self.dest); } } kanata-1.9.0/src/oskbd/macos.rs000064400000000000000000000355261046102023000144460ustar 00000000000000//! Contains the input/output code for keyboards on Macos. // Caused by unmaintained objc crate triggering warnings. #![allow(unexpected_cfgs)] #![cfg_attr( feature = "simulated_output", allow(dead_code, unused_imports, unused_variables, unused_mut) )] use super::*; use crate::kanata::CalculatedMouseMove; use crate::oskbd::KeyEvent; use anyhow::anyhow; use core_graphics::base::CGFloat; use core_graphics::display::{CGDisplay, CGPoint}; use core_graphics::event::{CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, EventField}; use core_graphics::event_source::{CGEventSource, CGEventSourceStateID}; use kanata_parser::custom_action::*; use kanata_parser::keys::*; use karabiner_driverkit::*; use libc; use objc::runtime::Class; use objc::{msg_send, sel, sel_impl}; use os_pipe::pipe; use std::convert::TryFrom; use std::fmt; use std::io; use std::io::Read; use std::io::{Error, ErrorKind}; use std::os::unix::io::AsRawFd; #[derive(Debug, Clone, Copy)] pub struct InputEvent { pub value: u64, pub page: u32, pub code: u32, } impl InputEvent { pub fn new(event: DKEvent) -> Self { InputEvent { value: event.value, page: event.page, code: event.code, } } } impl From for DKEvent { fn from(event: InputEvent) -> Self { Self { value: event.value, page: event.page, code: event.code, } } } pub struct KbdIn {} impl Drop for KbdIn { fn drop(&mut self) { release(); } } fn capture_stdout(func: F) -> String where F: FnOnce(), { // Create a pipe to capture stdout let (mut reader, writer) = pipe().unwrap(); // Save the original stdout file descriptor let stdout_fd = std::io::stdout().as_raw_fd(); let saved_stdout = unsafe { libc::dup(stdout_fd) }; // Redirect stdout to the pipe's writer unsafe { libc::dup2(writer.as_raw_fd(), stdout_fd); } // Close `writer` to prevent `read_to_string` deadlock: https://docs.rs/os_pipe/latest/os_pipe/#common-deadlocks-related-to-pipes drop(writer); // Run the provided function func(); // Restore the original stdout unsafe { libc::dup2(saved_stdout, stdout_fd); libc::close(saved_stdout); } // Read all data from the pipe let mut captured_output = String::new(); reader.read_to_string(&mut captured_output).unwrap(); captured_output } impl KbdIn { pub fn new( include_names: Option>, exclude_names: Option>, ) -> Result { if !driver_activated() { return Err(anyhow!( "Karabiner-VirtualHIDDevice driver is not activated." )); } let device_names = if let Some(names) = include_names { validate_and_register_devices(names) } else if let Some(names) = exclude_names { // TODO: filter include_names when both exclude_names and include_names are present let kb_list = capture_stdout(list_keyboards); let names_: Vec = kb_list .split("\n") .filter(|kb| !kb.is_empty() && !names.contains(&kb.to_string())) .map(|kb| kb.to_string()) .collect(); validate_and_register_devices(names_) } else { vec![] }; if !device_names.is_empty() || register_device("") { if grab() { Ok(Self {}) } else { Err(anyhow!("grab failed")) } } else { Err(anyhow!("Couldn't register any device")) } } pub fn read(&mut self) -> Result { let mut event = DKEvent { value: 0, page: 0, code: 0, }; wait_key(&mut event); Ok(InputEvent::new(event)) } } fn validate_and_register_devices(include_names: Vec) -> Vec { include_names .iter() .filter_map(|dev| match device_matches(dev) { true => Some(dev.to_string()), false => { log::warn!("Not a valid device name '{dev}'"); None } }) .filter_map(|dev| { if register_device(&dev) { Some(dev.to_string()) } else { log::warn!("Couldn't register device '{dev}'"); None } }) .collect() } impl fmt::Display for InputEvent { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use kanata_keyberon::key_code::KeyCode; let ke = KeyEvent::try_from(*self).unwrap(); let direction = match ke.value { KeyValue::Press => "↓", KeyValue::Release => "↑", KeyValue::Repeat => "⟳", KeyValue::Tap => "↕", KeyValue::WakeUp => "!", }; let key_name = KeyCode::from(ke.code); write!(f, "{}{:?}", direction, key_name) } } impl TryFrom for KeyEvent { type Error = (); fn try_from(item: InputEvent) -> Result { if let Ok(oscode) = OsCode::try_from(PageCode { page: item.page, code: item.code, }) { Ok(KeyEvent { code: oscode, value: if item.value == 1 { KeyValue::Press } else { KeyValue::Release }, }) } else { Err(()) } } } impl TryFrom for InputEvent { type Error = (); fn try_from(item: KeyEvent) -> Result { if let Ok(pagecode) = PageCode::try_from(item.code) { let val = match item.value { KeyValue::Press => 1, _ => 0, }; Ok(InputEvent { value: val, page: pagecode.page, code: pagecode.code, }) } else { Err(()) } } } #[cfg(all(not(feature = "simulated_output"), not(feature = "passthru_ahk")))] pub struct KbdOut {} #[cfg(all(not(feature = "simulated_output"), not(feature = "passthru_ahk")))] impl KbdOut { pub fn new() -> Result { Ok(KbdOut {}) } pub fn write(&mut self, event: InputEvent) -> Result<(), io::Error> { let mut devent = event.into(); log::debug!("Attempting to write {event:?} {devent:?}"); let _sent = send_key(&mut devent); Ok(()) } pub fn write_key(&mut self, key: OsCode, value: KeyValue) -> Result<(), io::Error> { if let Ok(event) = InputEvent::try_from(KeyEvent { value, code: key }) { self.write(event) } else { log::debug!("couldn't write unrecognized {key:?}"); Err(io::Error::new( io::ErrorKind::NotFound, "OsCode not recognized!", )) } } pub fn write_code(&mut self, code: u32, value: KeyValue) -> Result<(), io::Error> { if let Ok(event) = InputEvent::try_from(KeyEvent { value, code: OsCode::from_u16(code as u16).unwrap(), }) { self.write(event) } else { log::debug!("couldn't write unrecognized OsCode {code}"); Err(io::Error::new( io::ErrorKind::NotFound, "OsCode not recognized!", )) } } pub fn press_key(&mut self, key: OsCode) -> Result<(), io::Error> { self.write_key(key, KeyValue::Press) } pub fn release_key(&mut self, key: OsCode) -> Result<(), io::Error> { self.write_key(key, KeyValue::Release) } pub fn send_unicode(&mut self, c: char) -> Result<(), io::Error> { let event = Self::make_event()?; let mut arr = [0u16; 2]; // Capture the slice containing the encoded UTF-16 code units. let encoded = c.encode_utf16(&mut arr); // Pass only the part of the array that was populated. event.set_string_from_utf16_unchecked(encoded); event.set_type(CGEventType::KeyDown); event.post(CGEventTapLocation::AnnotatedSession); event.set_type(CGEventType::KeyUp); event.post(CGEventTapLocation::AnnotatedSession); Ok(()) } pub fn scroll(&mut self, _direction: MWheelDirection, _distance: u16) -> Result<(), io::Error> { let event = Self::make_event()?; event.set_type(CGEventType::ScrollWheel); match _direction { MWheelDirection::Down => event.set_integer_value_field( EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_1, (_distance as i64) * 1, ), MWheelDirection::Up => event.set_integer_value_field( EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_1, (_distance as i64) * -1, ), MWheelDirection::Left => event.set_integer_value_field( EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_2, (_distance as i64) * 1, ), MWheelDirection::Right => event.set_integer_value_field( EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_2, (_distance as i64) * -1, ), } // Mouse control only seems to work with CGEventTapLocation::HID. event.post(CGEventTapLocation::HID); Ok(()) } fn button_action(&mut self, _btn: Btn, is_click: bool) -> Result<(), io::Error> { let (event_type, button) = match _btn { Btn::Left => ( if is_click { CGEventType::LeftMouseDown } else { CGEventType::LeftMouseUp }, Some(CGMouseButton::Left), ), Btn::Right => ( if is_click { CGEventType::RightMouseDown } else { CGEventType::RightMouseUp }, Some(CGMouseButton::Right), ), Btn::Mid => ( if is_click { CGEventType::OtherMouseDown } else { CGEventType::OtherMouseUp }, Some(CGMouseButton::Center), ), // It's unclear to me which event type to use here, hence unsupported for now Btn::Forward => (CGEventType::Null, None), Btn::Backward => (CGEventType::Null, None), }; // CGEventType doesn't implement Eq, therefore the casting to u8 if event_type as u8 == CGEventType::Null as u8 { panic!("mouse buttons other than left, right, and middle aren't currently supported") } let event_source = Self::make_event_source()?; let event = Self::make_event()?; let mouse_position = event.location(); let event = CGEvent::new_mouse_event(event_source, event_type, mouse_position, button.unwrap()) .map_err(|_| { std::io::Error::new(std::io::ErrorKind::Other, "Failed to create mouse event") })?; // Mouse control only seems to work with CGEventTapLocation::HID. event.post(CGEventTapLocation::HID); Ok(()) } pub fn click_btn(&mut self, _btn: Btn) -> Result<(), io::Error> { Self::button_action(self, _btn, true) } pub fn release_btn(&mut self, _btn: Btn) -> Result<(), io::Error> { Self::button_action(self, _btn, false) } pub fn move_mouse(&mut self, _mv: CalculatedMouseMove) -> Result<(), io::Error> { let pressed = Self::pressed_buttons(); let event_type = if pressed & 1 > 0 { CGEventType::LeftMouseDragged } else if pressed & 2 > 0 { CGEventType::RightMouseDragged } else { CGEventType::MouseMoved }; let event = Self::make_event()?; let mut mouse_position = event.location(); Self::apply_calculated_move(&_mv, &mut mouse_position); if let Ok(event) = CGEvent::new_mouse_event( Self::make_event_source()?, event_type, mouse_position, CGMouseButton::Left, ) { event.post(CGEventTapLocation::HID); } Ok(()) } fn pressed_buttons() -> usize { if let Some(ns_event) = Class::get("NSEvent") { unsafe { msg_send![ns_event, pressedMouseButtons] } } else { 0 } } pub fn move_mouse_many(&mut self, _moves: &[CalculatedMouseMove]) -> Result<(), io::Error> { let event = Self::make_event()?; let mut mouse_position = event.location(); let display = CGDisplay::main(); for current_move in _moves.iter() { Self::apply_calculated_move(current_move, &mut mouse_position); } display .move_cursor_to_point(mouse_position) .map_err(|_| io::Error::new(ErrorKind::Other, "failed to move mouse"))?; Ok(()) } pub fn set_mouse(&mut self, _x: u16, _y: u16) -> Result<(), io::Error> { let display = CGDisplay::main(); let point = CGPoint::new(_x as CGFloat, _y as CGFloat); display .move_cursor_to_point(point) .map_err(|_| io::Error::new(ErrorKind::Other, "failed to move cursor to point"))?; Ok(()) } fn make_event_source() -> Result { CGEventSource::new(CGEventSourceStateID::CombinedSessionState).map_err(|_| { Error::new( ErrorKind::Other, "failed to create core graphics event source", ) }) } /// Creates a core graphics event. /// The CGEventSourceStateID is a guess at this point - all functionality works using this but /// I have not verified that this is the correct parameter. /// Note that the CFRelease function mentioned in the docs is automatically called when the /// event is dropped, therefore we don't need to care about this ourselves. fn make_event() -> Result { let event_source = Self::make_event_source()?; let event = CGEvent::new(event_source) .map_err(|_| Error::new(ErrorKind::Other, "failed to create core graphics event"))?; Ok(event) } /// Applies a calculated mouse move to a CGPoint. /// /// This does _not_ move the mouse, it just mutates the point. fn apply_calculated_move(_mv: &CalculatedMouseMove, mouse_position: &mut CGPoint) { match _mv.direction { MoveDirection::Up => mouse_position.y = mouse_position.y - _mv.distance as CGFloat, MoveDirection::Down => mouse_position.y = mouse_position.y + _mv.distance as CGFloat, MoveDirection::Left => mouse_position.x = mouse_position.x - _mv.distance as CGFloat, MoveDirection::Right => mouse_position.x = mouse_position.x + _mv.distance as CGFloat, } } } kanata-1.9.0/src/oskbd/mod.rs000064400000000000000000000063161046102023000141160ustar 00000000000000//! Platform specific code for low level keyboard read/write. #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "linux")] pub use linux::*; #[cfg(target_os = "windows")] mod windows; #[cfg(target_os = "windows")] pub use windows::*; #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "macos")] pub use macos::*; #[cfg(any( all( not(feature = "simulated_input"), feature = "simulated_output", not(feature = "passthru_ahk") ), all( feature = "simulated_input", not(feature = "simulated_output"), not(feature = "passthru_ahk") ) ))] mod simulated; // has KbdOut #[cfg(any( all( not(feature = "simulated_input"), feature = "simulated_output", not(feature = "passthru_ahk") ), all( feature = "simulated_input", not(feature = "simulated_output"), not(feature = "passthru_ahk") ) ))] pub use simulated::*; #[cfg(any( all(feature = "simulated_input", feature = "simulated_output"), all( feature = "simulated_input", feature = "simulated_output", feature = "passthru_ahk" ), ))] mod sim_passthru; // has KbdOut #[cfg(any( all(feature = "simulated_input", feature = "simulated_output"), all( feature = "simulated_input", feature = "simulated_output", feature = "passthru_ahk" ), ))] pub use sim_passthru::*; pub const HI_RES_SCROLL_UNITS_IN_LO_RES: u16 = 120; // ------------------ KeyValue -------------------- #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum KeyValue { Release = 0, Press = 1, Repeat = 2, Tap, WakeUp, } impl From for KeyValue { fn from(item: i32) -> Self { match item { 0 => Self::Release, 1 => Self::Press, 2 => Self::Repeat, _ => unreachable!(), } } } impl From for KeyValue { fn from(up: bool) -> Self { match up { true => Self::Release, false => Self::Press, } } } impl From for bool { fn from(val: KeyValue) -> Self { matches!(val, KeyValue::Release) } } use kanata_parser::keys::OsCode; #[derive(Clone, Copy)] pub struct KeyEvent { pub code: OsCode, pub value: KeyValue, } #[allow(dead_code, unused)] impl KeyEvent { pub fn new(code: OsCode, value: KeyValue) -> Self { Self { code, value } } } use core::fmt; impl fmt::Display for KeyEvent { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use kanata_keyberon::key_code::KeyCode; let direction = match self.value { KeyValue::Press => "↓", KeyValue::Release => "↑", KeyValue::Repeat => "⟳", KeyValue::Tap => "↕", KeyValue::WakeUp => "!", }; let key_name = KeyCode::from(self.code); write!(f, "{}{:?}", direction, key_name) } } impl fmt::Debug for KeyEvent { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("KeyEvent") .field( "code", &format_args!("{:?} ({})", self.code, self.code.as_u16()), ) .field("value", &self.value) .finish() } } kanata-1.9.0/src/oskbd/sim_passthru.rs000064400000000000000000000142071046102023000160560ustar 00000000000000//! Redirects output to the function provided by the entity supplying simulated input (e.g., AHK) // todo: allow sharing numpad status to differentiate between vk enter and vk numpad enter // todo: only press/release_key is implemented use super::*; use anyhow::Result; use log::*; use crate::kanata::CalculatedMouseMove; use kanata_parser::custom_action::*; use std::io; #[cfg(not(any(target_os = "windows", target_os = "macos")))] use std::fmt; use std::sync::Arc; use std::sync::OnceLock; type CbOutEvFn = dyn Fn(i64, i64, i64) -> i64 + Send + Sync + 'static; // Rust wrapper func around external callback (transmuted into this) Ahk accept only i64 arguments (vk,sc,up) pub struct FnOutEvWrapper { pub cb: Arc, } // wrapper struct to store our callback in a thread-shareable manner pub static OUTEVWRAP: OnceLock = OnceLock::new(); // ensure that our wrapper struct is created once (thread-safe) use std::sync::mpsc::{SendError, Sender as ASender}; /// Handle for writing keys to the simulated input provider. pub struct KbdOut { pub tx_kout: Option>, } use std::io::{Error as IoErr, ErrorKind::NotConnected}; impl KbdOut { #[cfg(not(target_os = "linux"))] pub fn new() -> Result { Ok(Self { tx_kout: None }) } #[cfg(target_os = "linux")] pub fn new( _s: &Option, _tp: bool, _name: &str, _bustype: evdev::BusType, ) -> Result { Ok(Self { tx_kout: None }) } #[cfg(target_os = "linux")] pub fn write_raw(&mut self, event: InputEvent) -> Result<(), io::Error> { trace!("out-raw:{event:?}"); Ok(()) } pub fn write(&mut self, event: InputEvent) -> Result<(), io::Error> { trace!("out:{event}"); if let Some(tx_kout) = &self.tx_kout { // Send key event msg → main thread so it can be polled to try receiving it after processing external input events match tx_kout.send(event) { // send won't block for an async channel Ok(res) => { debug!("✓ tx_kout → rx_kout@key_out(dll) ‘{event}’ from send_out_ev_msg@sim_passthru(oskbd)"); return Ok(res); } Err(SendError(event)) => { error!("✗ tx_kout → rx_kout@key_out(dll) ‘{event}’ from send_out_ev_msg@sim_passthru(oskbd)"); return Err(IoErr::new( NotConnected, format!("Failed sending sending {event}"), )); } } } else { debug!("✗ tx_kout doesn't exist"); } Ok(()) } pub fn write_key(&mut self, key: OsCode, value: KeyValue) -> Result<(), io::Error> { let key_ev = KeyEvent::new(key, value); let event = { #[cfg(target_os = "macos")] { key_ev.try_into().unwrap() } #[cfg(not(target_os = "macos"))] { key_ev.into() } }; self.write(event) } pub fn write_code(&mut self, code: u32, value: KeyValue) -> Result<(), io::Error> { trace!("out-code:{code};{value:?}"); Ok(()) } pub fn press_key(&mut self, key: OsCode) -> Result<(), io::Error> { self.write_key(key, KeyValue::Press) } pub fn release_key(&mut self, key: OsCode) -> Result<(), io::Error> { self.write_key(key, KeyValue::Release) } pub fn send_unicode(&mut self, c: char) -> Result<(), io::Error> { trace!("outU:{c}"); Ok(()) } pub fn click_btn(&mut self, btn: Btn) -> Result<(), io::Error> { trace!("out🖰:↓{btn:?}"); Ok(()) } pub fn release_btn(&mut self, btn: Btn) -> Result<(), io::Error> { trace!("out🖰:↑{btn:?}"); Ok(()) } pub fn scroll(&mut self, direction: MWheelDirection, distance: u16) -> Result<(), io::Error> { trace!("scroll:{direction:?},{distance:?}"); Ok(()) } pub fn move_mouse(&mut self, mv: CalculatedMouseMove) -> Result<(), io::Error> { let (direction, distance) = (mv.direction, mv.distance); trace!("out🖰:move {direction:?},{distance:?}"); Ok(()) } pub fn move_mouse_many(&mut self, moves: &[CalculatedMouseMove]) -> Result<(), io::Error> { for mv in moves { let (direction, distance) = (&mv.direction, &mv.distance); trace!("out🖰:move {direction:?},{distance:?}"); } Ok(()) } pub fn set_mouse(&mut self, x: u16, y: u16) -> Result<(), io::Error> { log::info!("out🖰:@{x},{y}"); Ok(()) } pub fn tick(&mut self) {} } #[cfg(not(any(target_os = "windows", target_os = "macos")))] #[derive(Debug, Clone, Copy)] pub struct InputEvent { pub code: u32, /// Key was released pub up: bool, } #[cfg(not(any(target_os = "windows", target_os = "macos")))] impl fmt::Display for InputEvent { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use kanata_keyberon::key_code::KeyCode; let direction = if self.up { "↑" } else { "↓" }; let key_name = KeyCode::from(OsCode::from(self.code)); write!(f, "{}{:?}", direction, key_name) } } #[cfg(not(any(target_os = "windows", target_os = "macos")))] impl InputEvent { pub fn from_oscode(code: OsCode, val: KeyValue) -> Self { Self { code: code.into(), up: val.into(), } } } #[cfg(not(any(target_os = "windows", target_os = "macos")))] impl TryFrom for KeyEvent { type Error = (); fn try_from(item: InputEvent) -> Result { Ok(Self { code: OsCode::from_u16(item.code as u16).ok_or(())?, value: match item.up { true => KeyValue::Release, false => KeyValue::Press, }, }) } } #[cfg(not(any(target_os = "windows", target_os = "macos")))] impl From for InputEvent { fn from(item: KeyEvent) -> Self { Self { code: item.code.into(), up: item.value.into(), } } } kanata-1.9.0/src/oskbd/simulated.rs000064400000000000000000000355661046102023000153370ustar 00000000000000//! Output that just prints text to stdout instead of actually doing anything OS-related. //! See <../../docs/simulated_output/sim_out.txt> for an example output. use indoc::formatdoc; use std::ffi::{OsStr, OsString}; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; pub fn concat_os_str2(a: &OsStr, b: &OsStr) -> OsString { let mut ret = OsString::with_capacity(a.len() + b.len()); // allocate once ret.push(a); ret.push(b); // doesn't allocate ret } fn append_file_name(path: impl AsRef, appendix: impl AsRef) -> PathBuf { let path = path.as_ref(); let mut result = path.to_owned(); let stem_in = path.file_stem().unwrap_or(OsStr::new("")); let stem_out = concat_os_str2(stem_in, OsStr::new(&appendix)); result.set_file_name(stem_out); if let Some(ext) = path.extension() { result.set_extension(ext); } result } #[derive(Debug, Copy, Clone, PartialEq)] pub enum LogFmtT { InKeyUp, InKeyDown, InKeyRep, InTick, KeyUp, KeyDown, Tick, MouseUp, MouseDown, MouseMove, Unicode, Code, RawUp, RawDown, } pub struct LogFmt { ticks: u64, //In // in_time: String, in_key_up: String, in_key_down: String, in_key_rep: String, in_combo: String, //Out // time: String, key_up: String, key_down: String, raw_up: String, raw_down: String, combo: String, mouse_up: String, mouse_down: String, mouse_move: String, unicode: String, code: String, } impl Default for LogFmt { fn default() -> Self { Self::new() } } impl LogFmt { pub fn new() -> Self { Self { ticks: 0, //In // in_time: String::new(), in_key_up: String::new(), in_key_down: String::new(), in_key_rep: String::new(), in_combo: String::new(), //Out // time: String::new(), key_up: String::new(), key_down: String::new(), raw_up: String::new(), raw_down: String::new(), mouse_up: String::new(), mouse_down: String::new(), mouse_move: String::new(), unicode: String::new(), code: String::new(), combo: String::new(), } } pub fn fmt(&mut self, key: LogFmtT, value: String) { let mut pad = value.len(); let mut time = "".to_string(); if self.ticks > 0 { pad = std::cmp::max(value.len(), self.ticks.to_string().len()); // add extra padding if // event tick is wider time = format!(" {: ) { let pad = self.combo.len().saturating_sub(3); let table_out = formatdoc!( "🕐Δms│{} In───┼{:─ panic!("✗ Couldn't create {}: {}", out_path_s, e), Ok(out_file) => out_file, }; match out_file.write_all(table_out.as_bytes()) { Err(e) => panic!("✗ Couldn't write to {}: {}", out_path_s, e), Ok(_) => eprintln!("Saved output → {}", out_path_s), } } } } use super::*; use crate::kanata::CalculatedMouseMove; use kanata_parser::custom_action::*; use std::io; use kanata_keyberon::key_code::KeyCode; #[cfg(not(any(target_os = "windows", target_os = "macos")))] use std::fmt; pub struct Outputs { pub events: Vec, ticks: u64, } impl Outputs { fn new() -> Self { Self { events: vec![], ticks: 0, } } fn push(&mut self, event: impl AsRef) { if self.ticks > 0 { self.events.push(format!("t:{}ms", self.ticks)); } self.events.push(event.as_ref().to_string()); self.ticks = 0; } } /// Handle for writing keys to the OS. pub struct KbdOut { pub log: LogFmt, pub outputs: Outputs, } impl KbdOut { fn new_actual() -> Result { Ok(Self { log: LogFmt::new(), outputs: Outputs::new(), }) } #[cfg(not(target_os = "linux"))] pub fn new() -> Result { Self::new_actual() } #[cfg(target_os = "linux")] pub fn new( _s: &Option, _tp: bool, _name: &str, _bustype: evdev::BusType, ) -> Result { Self::new_actual() } #[cfg(target_os = "linux")] pub fn write_raw(&mut self, event: InputEvent) -> Result<(), io::Error> { self.log.write_raw(event); self.outputs.push(format!("out-raw:{event:?}")); Ok(()) } pub fn write(&mut self, event: InputEvent) -> Result<(), io::Error> { self.outputs.push(format!("out:{event}")); Ok(()) } pub fn write_key(&mut self, key: OsCode, value: KeyValue) -> Result<(), io::Error> { let key_ev = KeyEvent::new(key, value); let event = { #[cfg(target_os = "macos")] { key_ev.try_into().unwrap() } #[cfg(not(target_os = "macos"))] { key_ev.into() } }; self.write(event) } pub fn write_code(&mut self, code: u32, value: KeyValue) -> Result<(), io::Error> { self.log.write_code(code, value); self.outputs.push(format!("out-code:{code};{value:?}")); Ok(()) } pub fn press_key(&mut self, key: OsCode) -> Result<(), io::Error> { self.log.press_key(key); self.write_key(key, KeyValue::Press) } pub fn release_key(&mut self, key: OsCode) -> Result<(), io::Error> { self.log.release_key(key); self.write_key(key, KeyValue::Release) } pub fn send_unicode(&mut self, c: char) -> Result<(), io::Error> { self.log.send_unicode(c); self.outputs.push(format!("outU:{c}")); Ok(()) } pub fn click_btn(&mut self, btn: Btn) -> Result<(), io::Error> { self.log.click_btn(btn); self.outputs.push(format!("out🖰:↓{btn:?}")); Ok(()) } pub fn release_btn(&mut self, btn: Btn) -> Result<(), io::Error> { self.log.release_btn(btn); self.outputs.push(format!("out🖰:↑{btn:?}")); Ok(()) } pub fn scroll(&mut self, direction: MWheelDirection, distance: u16) -> Result<(), io::Error> { self.log.scroll(direction, distance); self.outputs .push(format!("scroll:{direction:?},{distance:?}")); Ok(()) } pub fn move_mouse(&mut self, mv: CalculatedMouseMove) -> Result<(), io::Error> { let (direction, distance) = (mv.direction, mv.distance); self.log.move_mouse(direction, distance); self.outputs .push(format!("out🖰:move {direction:?},{distance:?}")); Ok(()) } pub fn move_mouse_many(&mut self, moves: &[CalculatedMouseMove]) -> Result<(), io::Error> { for mv in moves { let (direction, distance) = (&mv.direction, &mv.distance); self.log.move_mouse(*direction, *distance); self.outputs .push(format!("out🖰:move {direction:?},{distance:?}")); } Ok(()) } pub fn set_mouse(&mut self, x: u16, y: u16) -> Result<(), io::Error> { self.log.set_mouse(x, y); log::info!("out🖰:@{x},{y}"); Ok(()) } pub fn tick(&mut self) { self.outputs.ticks += 1; self.log.ticks += 1; } } #[cfg(not(any(target_os = "windows", target_os = "macos")))] #[derive(Debug, Clone, Copy)] pub struct InputEvent { pub code: u32, /// Key was released pub up: bool, } #[cfg(not(any(target_os = "windows", target_os = "macos")))] impl fmt::Display for InputEvent { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let direction = if self.up { "↑" } else { "↓" }; let key_name = KeyCode::from(OsCode::from(self.code)); write!(f, "{}{:?}", direction, key_name) } } #[cfg(not(any(target_os = "windows", target_os = "macos")))] impl InputEvent { pub fn from_oscode(code: OsCode, val: KeyValue) -> Self { Self { code: code.into(), up: val.into(), } } } #[cfg(not(any(target_os = "windows", target_os = "macos")))] impl TryFrom for KeyEvent { type Error = (); fn try_from(item: InputEvent) -> Result { Ok(Self { code: OsCode::from_u16(item.code as u16).ok_or(())?, value: match item.up { true => KeyValue::Release, false => KeyValue::Press, }, }) } } #[cfg(not(any(target_os = "windows", target_os = "macos")))] impl From for InputEvent { fn from(item: KeyEvent) -> Self { Self { code: item.code.into(), up: item.value.into(), } } } #[cfg(all(target_os = "windows", feature = "interception_driver"))] impl From for InputEvent { fn from(_item: KeyEvent) -> Self { unimplemented!() } } kanata-1.9.0/src/oskbd/windows/exthook_os.rs000064400000000000000000000056251046102023000172150ustar 00000000000000//! A function listener for keyboard input events replacing Windows keyboard hook API use core::fmt; use once_cell::sync::Lazy; use parking_lot::Mutex; use winapi::ctypes::*; use winapi::um::winuser::*; use crate::oskbd::{KeyEvent, KeyValue}; use kanata_keyberon::key_code::KeyCode; use kanata_parser::keys::*; pub const LLHOOK_IDLE_TIME_SECS_CLEAR_INPUTS: u64 = 60; type HookFn = dyn FnMut(InputEvent) -> bool + Send + Sync + 'static; pub static HOOK_CB: Lazy>>> = Lazy::new(|| Mutex::new(None)); // store thread-safe hook callback with a mutex (can be called from an external process) pub struct KeyboardHook {} // reusing hook type for our listener impl KeyboardHook { /// Sets input callback (panics if already registered) pub fn set_input_cb( callback: impl FnMut(InputEvent) -> bool + Send + Sync + 'static, ) -> KeyboardHook { let mut cb_opt = HOOK_CB.lock(); assert!( cb_opt.take().is_none(), "Only 1 external listener is allowed!" ); *cb_opt = Some(Box::new(callback)); KeyboardHook {} } } #[cfg(not(feature = "passthru_ahk"))] // unused KeyboardHook will be dropped, breaking our hook, disable it impl Drop for KeyboardHook { fn drop(&mut self) { let mut cb_opt = HOOK_CB.lock(); cb_opt.take(); } } #[derive(Debug, Clone, Copy)] pub struct InputEvent { // Key event received by the low level keyboard hook. pub code: u32, pub up: bool, /*Key was released*/ } impl fmt::Display for InputEvent { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let direction = if self.up { "↑" } else { "↓" }; let key_name = KeyCode::from(OsCode::from(self.code)); write!(f, "{}{:?}", direction, key_name) } } impl InputEvent { pub fn from_vk_sc(vk: c_uint, sc: c_uint, up: c_int) -> Self { let code = if vk == (VK_RETURN as u32) { // todo: do a proper check for numpad enter, maybe 0x11c isn't universal match sc { 0x11C => u32::from(VK_KPENTER_FAKE), _ => VK_RETURN as u32, } } else { vk }; Self { code, up: (up != 0), } } pub fn from_oscode(code: OsCode, val: KeyValue) -> Self { Self { code: code.into(), up: val.into(), } } } impl TryFrom for KeyEvent { type Error = (); fn try_from(item: InputEvent) -> Result { Ok(Self { code: OsCode::from_u16(item.code as u16).ok_or(())?, value: match item.up { true => KeyValue::Release, false => KeyValue::Press, }, }) } } impl From for InputEvent { fn from(item: KeyEvent) -> Self { Self { code: item.code.into(), up: item.value.into(), } } } kanata-1.9.0/src/oskbd/windows/interception.rs000064400000000000000000000206011046102023000175250ustar 00000000000000//! Windows interception-based mechanism for reading/writing input events. use std::io; use kanata_interception::{Interception, KeyState, MouseFlags, MouseState, ScanCode, Stroke}; use super::OsCodeWrapper; use crate::kanata::CalculatedMouseMove; use crate::oskbd::KeyValue; use kanata_parser::custom_action::*; use kanata_parser::keys::*; /// Key event received by the low level keyboard hook. #[derive(Debug, Clone, Copy)] pub struct InputEvent(pub Stroke); use std::fmt; impl fmt::Display for InputEvent { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) } } impl InputEvent { fn from_oscode(code: OsCode, val: KeyValue) -> Self { let mut stroke = Stroke::try_from(OsCodeWrapper(code)).unwrap_or_else(|_| { log::error!("Trying to send unmapped oscode '{code:?}', sending esc instead"); Stroke::Keyboard { code: ScanCode::Esc, state: KeyState::empty(), information: 0, } }); match &mut stroke { Stroke::Keyboard { state, .. } => { state.set( match val { KeyValue::Press | KeyValue::Repeat => KeyState::DOWN, KeyValue::Release => KeyState::UP, KeyValue::Tap => panic!("invalid value attempted to be sent"), KeyValue::WakeUp => panic!("invalid value attempted to be sent"), }, true, ); } _ => panic!("expected keyboard stroke"), } Self(stroke) } fn from_mouse_btn(btn: Btn, is_up: bool) -> Self { Self(Stroke::Mouse { state: match (btn, is_up) { (Btn::Left, true) => MouseState::LEFT_BUTTON_UP, (Btn::Left, false) => MouseState::LEFT_BUTTON_DOWN, (Btn::Right, true) => MouseState::RIGHT_BUTTON_UP, (Btn::Right, false) => MouseState::RIGHT_BUTTON_DOWN, (Btn::Mid, true) => MouseState::MIDDLE_BUTTON_UP, (Btn::Mid, false) => MouseState::MIDDLE_BUTTON_DOWN, (Btn::Backward, true) => MouseState::BUTTON_4_UP, (Btn::Backward, false) => MouseState::BUTTON_4_DOWN, (Btn::Forward, true) => MouseState::BUTTON_5_UP, (Btn::Forward, false) => MouseState::BUTTON_5_DOWN, }, flags: MouseFlags::empty(), rolling: 0, x: 0, y: 0, information: 0, }) } fn from_mouse_scroll(direction: MWheelDirection, distance: u16) -> Self { Self(Stroke::Mouse { state: match direction { MWheelDirection::Up | MWheelDirection::Down => MouseState::WHEEL, MWheelDirection::Left | MWheelDirection::Right => MouseState::HWHEEL, }, flags: MouseFlags::empty(), rolling: match direction { MWheelDirection::Up | MWheelDirection::Right => { distance.try_into().expect("checked bound of 30000 in cfg") } MWheelDirection::Down | MWheelDirection::Left => { -(i16::try_from(distance).expect("checked bound of 30000 in cfg")) } }, x: 0, y: 0, information: 0, }) } fn from_mouse_move(direction: MoveDirection, distance: u16) -> Self { Self(Stroke::Mouse { state: MouseState::MOVE, flags: MouseFlags::empty(), rolling: 0, x: match direction { MoveDirection::Left => -i32::from(distance), MoveDirection::Right => i32::from(distance), _ => 0, }, y: match direction { MoveDirection::Up => -i32::from(distance), MoveDirection::Down => i32::from(distance), _ => 0, }, information: 0, }) } fn from_mouse_move_many(moves: &[CalculatedMouseMove]) -> Self { let mut x_acc = 0; let mut y_acc = 0; for mov in moves { let acc_change = match mov.direction { MoveDirection::Up => (0, -i32::from(mov.distance)), MoveDirection::Down => (0, i32::from(mov.distance)), MoveDirection::Left => (-i32::from(mov.distance), 0), MoveDirection::Right => (i32::from(mov.distance), 0), }; x_acc += acc_change.0; y_acc += acc_change.1; } Self(Stroke::Mouse { state: MouseState::MOVE, flags: MouseFlags::empty(), rolling: 0, x: x_acc, y: y_acc, information: 0, }) } fn from_mouse_set(x: u16, y: u16) -> Self { Self(Stroke::Mouse { state: MouseState::MOVE, flags: MouseFlags::MOVE_ABSOLUTE | MouseFlags::VIRTUAL_DESKTOP, rolling: 0, x: i32::from(x), y: i32::from(y), information: 0, }) } } thread_local! { static INTRCPTN: Interception = Interception::new().expect("interception driver should init: have you completed the interception driver installation?"); } #[cfg(all(not(feature = "simulated_output"), not(feature = "passthru_ahk")))] /// Handle for writing keys to the OS. pub struct KbdOut {} fn write_interception(event: InputEvent) { let strokes = [event.0]; log::debug!("kanata sending {:?} to driver", strokes[0]); INTRCPTN.with(|ic| { match strokes[0] { // Note regarding device numbers: // Keyboard devices are 1-10 and mouse devices are 11-20. Source: // https://github.com/oblitum/Interception/blob/39eecbbc46a52e0402f783b872ef62b0254a896a/library/interception.h#L34 Stroke::Keyboard { .. } => { ic.send(1, &strokes[0..1]); } Stroke::Mouse { .. } => { ic.send(11, &strokes[0..1]); } } }) } #[cfg(all(not(feature = "simulated_output"), not(feature = "passthru_ahk")))] impl KbdOut { pub fn new() -> Result { Ok(Self {}) } pub fn write(&mut self, event: InputEvent) -> Result<(), io::Error> { write_interception(event); Ok(()) } pub fn write_code_raw(&mut self, code: u16, value: KeyValue) -> Result<(), io::Error> { super::write_code_raw(code, value) } pub fn write_code(&mut self, code: u32, value: KeyValue) -> Result<(), io::Error> { super::write_code(code as u16, value) } pub fn write_key(&mut self, key: OsCode, value: KeyValue) -> Result<(), io::Error> { self.write(InputEvent::from_oscode(key, value)) } pub fn press_key(&mut self, key: OsCode) -> Result<(), io::Error> { self.write_key(key, KeyValue::Press) } pub fn release_key(&mut self, key: OsCode) -> Result<(), io::Error> { self.write_key(key, KeyValue::Release) } pub fn click_btn(&mut self, btn: Btn) -> Result<(), io::Error> { log::debug!("click btn: {:?}", btn); write_interception(InputEvent::from_mouse_btn(btn, false)); Ok(()) } pub fn release_btn(&mut self, btn: Btn) -> Result<(), io::Error> { log::debug!("release btn: {:?}", btn); let event = InputEvent::from_mouse_btn(btn, true); write_interception(event); Ok(()) } pub fn scroll(&mut self, direction: MWheelDirection, distance: u16) -> Result<(), io::Error> { log::debug!("scroll: {direction:?} {distance:?}"); write_interception(InputEvent::from_mouse_scroll(direction, distance)); Ok(()) } /// Send using VK_PACKET pub fn send_unicode(&mut self, c: char) -> Result<(), io::Error> { super::send_uc(c, false); super::send_uc(c, true); Ok(()) } pub fn move_mouse(&mut self, mv: CalculatedMouseMove) -> Result<(), io::Error> { write_interception(InputEvent::from_mouse_move(mv.direction, mv.distance)); Ok(()) } pub fn move_mouse_many(&mut self, moves: &[CalculatedMouseMove]) -> Result<(), io::Error> { write_interception(InputEvent::from_mouse_move_many(moves)); Ok(()) } pub fn set_mouse(&mut self, x: u16, y: u16) -> Result<(), io::Error> { write_interception(InputEvent::from_mouse_set(x, y)); Ok(()) } } kanata-1.9.0/src/oskbd/windows/interception_convert.rs000064400000000000000000000423211046102023000212700ustar 00000000000000//! `Interception::Stroke` conversion functions //! //! The keyboard scancode values come from this website: //! https://handmade.network/forums/articles/t/2823-keyboard_inputs_-_scancodes%252C_raw_input%252C_text_input%252C_key_names //! //! Which states that it got these values from: //! - http://download.microsoft.com/download/1/6/1/161ba512-40e2-4cc9-843a-923143f3456c/scancode.doc (March 16, 2000). //! - http://www.computer-engineering.org/ps2keyboard/scancodes1.html //! - using MapVirtualKeyEx( VK_*, MAPVK_VK_TO_VSC_EX, 0 ) with the english us keyboard layout //! - reading win32 WM_INPUT keyboard messages. /* enum Scancode { sc_escape = 0x01, sc_1 = 0x02, sc_2 = 0x03, sc_3 = 0x04, sc_4 = 0x05, sc_5 = 0x06, sc_6 = 0x07, sc_7 = 0x08, sc_8 = 0x09, sc_9 = 0x0A, sc_0 = 0x0B, sc_minus = 0x0C, sc_equals = 0x0D, sc_backspace = 0x0E, sc_tab = 0x0F, sc_q = 0x10, sc_w = 0x11, sc_e = 0x12, sc_r = 0x13, sc_t = 0x14, sc_y = 0x15, sc_u = 0x16, sc_i = 0x17, sc_o = 0x18, sc_p = 0x19, sc_bracketLeft = 0x1A, sc_bracketRight = 0x1B, sc_enter = 0x1C, sc_controlLeft = 0x1D, sc_a = 0x1E, sc_s =0x1F, sc_d = 0x20, sc_f = 0x21, sc_g = 0x22, sc_h = 0x23, sc_j = 0x24, sc_k = 0x25, sc_l = 0x26, sc_semicolon = 0x27, sc_apostrophe = 0x28, sc_grave = 0x29, sc_shiftLeft = 0x2A, sc_backslash = 0x2B, sc_z = 0x2C, sc_x = 0x2D, sc_c = 0x2E, sc_v = 0x2F, sc_b = 0x30, sc_n = 0x31, sc_m = 0x32, sc_comma = 0x33, sc_preiod = 0x34, sc_slash = 0x35, sc_shiftRight = 0x36, sc_numpad_multiply = 0x37, sc_altLeft = 0x38, sc_space = 0x39, sc_capsLock = 0x3A, sc_f1 = 0x3B, sc_f2 = 0x3C, sc_f3 = 0x3D, sc_f4 = 0x3E, sc_f5 = 0x3F, sc_f6 = 0x40, sc_f7 = 0x41, sc_f8 = 0x42, sc_f9 = 0x43, sc_f10 = 0x44, sc_numLock = 0x45, sc_scrollLock = 0x46, sc_numpad_7 = 0x47, sc_numpad_8 = 0x48, sc_numpad_9 = 0x49, sc_numpad_minus = 0x4A, sc_numpad_4 = 0x4B, sc_numpad_5 = 0x4C, sc_numpad_6 = 0x4D, sc_numpad_plus = 0x4E, sc_numpad_1 = 0x4F, sc_numpad_2 = 0x50, sc_numpad_3 = 0x51, sc_numpad_0 = 0x52, sc_numpad_period = 0x53, sc_alt_printScreen = 0x54, /* Alt + print screen. MapVirtualKeyEx( VK_SNAPSHOT, MAPVK_VK_TO_VSC_EX, 0 ) returns scancode 0x54. */ sc_bracketAngle = 0x56, /* Key between the left shift and Z. */ sc_f11 = 0x57, sc_f12 = 0x58, sc_oem_1 = 0x5a, /* VK_OEM_WSCTRL */ sc_oem_2 = 0x5b, /* VK_OEM_FINISH */ sc_oem_3 = 0x5c, /* VK_OEM_JUMP */ sc_eraseEOF = 0x5d, sc_oem_4 = 0x5e, /* VK_OEM_BACKTAB */ sc_oem_5 = 0x5f, /* VK_OEM_AUTO */ sc_zoom = 0x62, sc_help = 0x63, sc_f13 = 0x64, sc_f14 = 0x65, sc_f15 = 0x66, sc_f16 = 0x67, sc_f17 = 0x68, sc_f18 = 0x69, sc_f19 = 0x6a, sc_f20 = 0x6b, sc_f21 = 0x6c, sc_f22 = 0x6d, sc_f23 = 0x6e, sc_oem_6 = 0x6f, /* VK_OEM_PA3 */ sc_katakana = 0x70, sc_oem_7 = 0x71, /* VK_OEM_RESET */ sc_f24 = 0x76, sc_sbcschar = 0x77, sc_convert = 0x79, sc_nonconvert = 0x7B, /* VK_OEM_PA1 */ sc_media_previous = 0xE010, sc_media_next = 0xE019, sc_numpad_enter = 0xE01C, sc_controlRight = 0xE01D, sc_volume_mute = 0xE020, sc_launch_app2 = 0xE021, sc_media_play = 0xE022, sc_media_stop = 0xE024, sc_volume_down = 0xE02E, sc_volume_up = 0xE030, sc_browser_home = 0xE032, sc_numpad_divide = 0xE035, sc_printScreen = 0xE037, /* sc_printScreen: - make: 0xE02A 0xE037 - break: 0xE0B7 0xE0AA - MapVirtualKeyEx( VK_SNAPSHOT, MAPVK_VK_TO_VSC_EX, 0 ) returns scancode 0x54; - There is no VK_KEYDOWN with VK_SNAPSHOT. */ sc_altRight = 0xE038, sc_cancel = 0xE046, /* CTRL + Pause */ sc_home = 0xE047, sc_arrowUp = 0xE048, sc_pageUp = 0xE049, sc_arrowLeft = 0xE04B, sc_arrowRight = 0xE04D, sc_end = 0xE04F, sc_arrowDown = 0xE050, sc_pageDown = 0xE051, sc_insert = 0xE052, sc_delete = 0xE053, sc_metaLeft = 0xE05B, sc_metaRight = 0xE05C, sc_application = 0xE05D, sc_power = 0xE05E, sc_sleep = 0xE05F, sc_wake = 0xE063, sc_browser_search = 0xE065, sc_browser_favorites = 0xE066, sc_browser_refresh = 0xE067, sc_browser_stop = 0xE068, sc_browser_forward = 0xE069, sc_browser_back = 0xE06A, sc_launch_app1 = 0xE06B, sc_launch_email = 0xE06C, sc_launch_media = 0xE06D, sc_pause = 0xE11D45, /* sc_pause: - make: 0xE11D 45 0xE19D C5 - make in raw input: 0xE11D 0x45 - break: none - No repeat when you hold the key down - There are no break so I don't know how the key down/up is expected to work. Raw input sends "keydown" and "keyup" messages, and it appears that the keyup message is sent directly after the keydown message (you can't hold the key down) so depending on when GetMessage or PeekMessage will return messages, you may get both a keydown and keyup message "at the same time". If you use VK messages most of the time you only get keydown messages, but some times you get keyup messages too. - when pressed at the same time as one or both control keys, generates a 0xE046 (sc_cancel) and the string for that scancode is "break". */ } */ use kanata_interception::*; use kanata_parser::keys::OsCode; // We need to wrap OsCode to impl TryFrom<..> for it, because it's in external crate. pub struct OsCodeWrapper(pub OsCode); impl TryFrom for OsCodeWrapper { type Error = (); fn try_from(item: Stroke) -> Result { Ok(match item { Stroke::Keyboard { code, state, .. } => { let code = match (state.contains(KeyState::E0), state.contains(KeyState::E1)) { (false, false) => crate::oskbd::u16_to_osc(code as u16).ok_or(())?, (true, _) => crate::oskbd::u16_to_osc((code as u16) | 0xE000).ok_or(())?, _ => return Err(()), }; OsCodeWrapper(code) } _ => return Err(()), }) } } impl TryFrom for Stroke { type Error = (); fn try_from(item: OsCodeWrapper) -> Result { let (code, state) = match item.0 { OsCode::KEY_ESC => (ScanCode::Esc, KeyState::empty()), OsCode::KEY_1 => (ScanCode::Num1, KeyState::empty()), OsCode::KEY_2 => (ScanCode::Num2, KeyState::empty()), OsCode::KEY_3 => (ScanCode::Num3, KeyState::empty()), OsCode::KEY_4 => (ScanCode::Num4, KeyState::empty()), OsCode::KEY_5 => (ScanCode::Num5, KeyState::empty()), OsCode::KEY_6 => (ScanCode::Num6, KeyState::empty()), OsCode::KEY_7 => (ScanCode::Num7, KeyState::empty()), OsCode::KEY_8 => (ScanCode::Num8, KeyState::empty()), OsCode::KEY_9 => (ScanCode::Num9, KeyState::empty()), OsCode::KEY_0 => (ScanCode::Num0, KeyState::empty()), OsCode::KEY_MINUS => (ScanCode::Minus, KeyState::empty()), OsCode::KEY_EQUAL => (ScanCode::Equals, KeyState::empty()), OsCode::KEY_BACKSPACE => (ScanCode::Backspace, KeyState::empty()), OsCode::KEY_TAB => (ScanCode::Tab, KeyState::empty()), OsCode::KEY_Q => (ScanCode::Q, KeyState::empty()), OsCode::KEY_W => (ScanCode::W, KeyState::empty()), OsCode::KEY_E => (ScanCode::E, KeyState::empty()), OsCode::KEY_R => (ScanCode::R, KeyState::empty()), OsCode::KEY_T => (ScanCode::T, KeyState::empty()), OsCode::KEY_Y => (ScanCode::Y, KeyState::empty()), OsCode::KEY_U => (ScanCode::U, KeyState::empty()), OsCode::KEY_I => (ScanCode::I, KeyState::empty()), OsCode::KEY_O => (ScanCode::O, KeyState::empty()), OsCode::KEY_P => (ScanCode::P, KeyState::empty()), OsCode::KEY_LEFTBRACE => (ScanCode::LeftBracket, KeyState::empty()), OsCode::KEY_RIGHTBRACE => (ScanCode::RightBracket, KeyState::empty()), OsCode::KEY_ENTER => (ScanCode::Enter, KeyState::empty()), OsCode::KEY_LEFTCTRL => (ScanCode::LeftControl, KeyState::empty()), OsCode::KEY_A => (ScanCode::A, KeyState::empty()), OsCode::KEY_S => (ScanCode::S, KeyState::empty()), OsCode::KEY_D => (ScanCode::D, KeyState::empty()), OsCode::KEY_F => (ScanCode::F, KeyState::empty()), OsCode::KEY_G => (ScanCode::G, KeyState::empty()), OsCode::KEY_H => (ScanCode::H, KeyState::empty()), OsCode::KEY_J => (ScanCode::J, KeyState::empty()), OsCode::KEY_K => (ScanCode::K, KeyState::empty()), OsCode::KEY_L => (ScanCode::L, KeyState::empty()), OsCode::KEY_SEMICOLON => (ScanCode::SemiColon, KeyState::empty()), OsCode::KEY_APOSTROPHE => (ScanCode::Apostrophe, KeyState::empty()), OsCode::KEY_GRAVE => (ScanCode::Grave, KeyState::empty()), OsCode::KEY_LEFTSHIFT => (ScanCode::LeftShift, KeyState::empty()), OsCode::KEY_BACKSLASH => (ScanCode::BackSlash, KeyState::empty()), OsCode::KEY_Z => (ScanCode::Z, KeyState::empty()), OsCode::KEY_X => (ScanCode::X, KeyState::empty()), OsCode::KEY_C => (ScanCode::C, KeyState::empty()), OsCode::KEY_V => (ScanCode::V, KeyState::empty()), OsCode::KEY_B => (ScanCode::B, KeyState::empty()), OsCode::KEY_N => (ScanCode::N, KeyState::empty()), OsCode::KEY_M => (ScanCode::M, KeyState::empty()), OsCode::KEY_COMMA => (ScanCode::Comma, KeyState::empty()), OsCode::KEY_DOT => (ScanCode::Period, KeyState::empty()), OsCode::KEY_SLASH => (ScanCode::Slash, KeyState::empty()), OsCode::KEY_RIGHTSHIFT => (ScanCode::RightShift, KeyState::empty()), OsCode::KEY_KPASTERISK => (ScanCode::NumpadMultiply, KeyState::empty()), OsCode::KEY_LEFTALT => (ScanCode::LeftAlt, KeyState::empty()), OsCode::KEY_SPACE => (ScanCode::Space, KeyState::empty()), OsCode::KEY_CAPSLOCK => (ScanCode::CapsLock, KeyState::empty()), OsCode::KEY_F1 => (ScanCode::F1, KeyState::empty()), OsCode::KEY_F2 => (ScanCode::F2, KeyState::empty()), OsCode::KEY_F3 => (ScanCode::F3, KeyState::empty()), OsCode::KEY_F4 => (ScanCode::F4, KeyState::empty()), OsCode::KEY_F5 => (ScanCode::F5, KeyState::empty()), OsCode::KEY_F6 => (ScanCode::F6, KeyState::empty()), OsCode::KEY_F7 => (ScanCode::F7, KeyState::empty()), OsCode::KEY_F8 => (ScanCode::F8, KeyState::empty()), OsCode::KEY_F9 => (ScanCode::F9, KeyState::empty()), OsCode::KEY_F10 => (ScanCode::F10, KeyState::empty()), OsCode::KEY_NUMLOCK => (ScanCode::NumLock, KeyState::empty()), OsCode::KEY_SCROLLLOCK => (ScanCode::ScrollLock, KeyState::empty()), OsCode::KEY_KP7 => (ScanCode::Numpad7, KeyState::empty()), OsCode::KEY_KP8 => (ScanCode::Numpad8, KeyState::empty()), OsCode::KEY_KP9 => (ScanCode::Numpad9, KeyState::empty()), OsCode::KEY_KPMINUS => (ScanCode::NumpadMinus, KeyState::empty()), OsCode::KEY_KP4 => (ScanCode::Numpad4, KeyState::empty()), OsCode::KEY_KP5 => (ScanCode::Numpad5, KeyState::empty()), OsCode::KEY_KP6 => (ScanCode::Numpad6, KeyState::empty()), OsCode::KEY_KPPLUS => (ScanCode::NumpadPlus, KeyState::empty()), OsCode::KEY_KP1 => (ScanCode::Numpad1, KeyState::empty()), OsCode::KEY_KP2 => (ScanCode::Numpad2, KeyState::empty()), OsCode::KEY_KP3 => (ScanCode::Numpad3, KeyState::empty()), OsCode::KEY_KP0 => (ScanCode::Numpad0, KeyState::empty()), OsCode::KEY_KPDOT => (ScanCode::NumpadPeriod, KeyState::empty()), OsCode::KEY_102ND => (ScanCode::Int1, KeyState::empty()), /* Key between the left shift and Z. */ OsCode::KEY_F11 => (ScanCode::F11, KeyState::empty()), OsCode::KEY_F12 => (ScanCode::F12, KeyState::empty()), OsCode::KEY_F13 => (ScanCode::F13, KeyState::empty()), OsCode::KEY_F14 => (ScanCode::F14, KeyState::empty()), OsCode::KEY_F15 => (ScanCode::F15, KeyState::empty()), OsCode::KEY_F16 => (ScanCode::F16, KeyState::empty()), OsCode::KEY_F17 => (ScanCode::F17, KeyState::empty()), OsCode::KEY_F18 => (ScanCode::F18, KeyState::empty()), OsCode::KEY_F19 => (ScanCode::F19, KeyState::empty()), OsCode::KEY_F20 => (ScanCode::F20, KeyState::empty()), OsCode::KEY_F21 => (ScanCode::F21, KeyState::empty()), OsCode::KEY_F22 => (ScanCode::F22, KeyState::empty()), OsCode::KEY_F23 => (ScanCode::F23, KeyState::empty()), OsCode::KEY_F24 => (ScanCode::F24, KeyState::empty()), OsCode::KEY_KATAKANA => (ScanCode::Katakana, KeyState::empty()), // Note: the OEM keys below don't seem to correspond to the same VK OEM // mappings as the LLHOOK codes. // ScanCode::Oem1 = 0x5A, /* VK_OEM_WSCTRL */ // ScanCode::Oem2 = 0x5B, /* VK_OEM_FINISH */ // ScanCode::Oem3 = 0x5C, /* VK_OEM_JUMP */ // ScanCode::Oem4 = 0x5E, /* VK_OEM_BACKTAB */ // ScanCode::Oem5 = 0x5F, /* VK_OEM_AUTO */ // ScanCode::Oem6 = 0x6F, /* VK_OEM_PA3 */ // ScanCode::Oem7 = 0x71, /* VK_OEM_RESET */ // ScanCode::EraseEOF = 0x5D, // ScanCode::Zoom => 0x62, // ScanCode::Help => 0x63, // ScanCode::AltPrintScreen = 0x55, /* Alt + print screen. */ // ScanCode::SBCSChar = 0x77, OsCode::KEY_HENKAN => (ScanCode::Convert, KeyState::empty()), OsCode::KEY_MUHENKAN => (ScanCode::NonConvert, KeyState::empty()), OsCode::KEY_PREVIOUSSONG => (ScanCode::Q, KeyState::E0), OsCode::KEY_NEXTSONG => (ScanCode::P, KeyState::E0), // 0x19 OsCode::KEY_KPENTER => (ScanCode::Enter, KeyState::E0), // 0x1C OsCode::KEY_RIGHTCTRL => (ScanCode::LeftControl, KeyState::E0), // 0x1D OsCode::KEY_MUTE => (ScanCode::D, KeyState::E0), // 0x20 OsCode::KEY_PLAYPAUSE => (ScanCode::G, KeyState::E0), // 0x22 // sc_media_play OsCode::KEY_VOLUMEDOWN => (ScanCode::C, KeyState::E0), // 0x2E // sc_volume_down OsCode::KEY_VOLUMEUP => (ScanCode::B, KeyState::E0), // 0x30 // sc_volume_up OsCode::KEY_KPSLASH => (ScanCode::Slash, KeyState::E0), // 0x35 // sc_numpad_divide OsCode::KEY_PRINT => (ScanCode::NumpadMultiply, KeyState::E0), // 0x37 // sc_printScreen OsCode::KEY_RIGHTALT => (ScanCode::LeftAlt, KeyState::E0), // 0x38 // sc_altRight OsCode::KEY_HOME => (ScanCode::Numpad7, KeyState::E0), // 0x47 // sc_home OsCode::KEY_UP => (ScanCode::Numpad8, KeyState::E0), // 0x48 // sc_arrowUp OsCode::KEY_PAGEUP => (ScanCode::Numpad9, KeyState::E0), // 0x49 // sc_pageUp OsCode::KEY_LEFT => (ScanCode::Numpad4, KeyState::E0), // 0x4B // sc_arrowLeft OsCode::KEY_RIGHT => (ScanCode::Numpad6, KeyState::E0), // 0x4D // sc_arrowRight OsCode::KEY_END => (ScanCode::Numpad1, KeyState::E0), // 0x4F // sc_end OsCode::KEY_DOWN => (ScanCode::Numpad2, KeyState::E0), // 0x50 // sc_arrowDown OsCode::KEY_PAGEDOWN => (ScanCode::Numpad3, KeyState::E0), // 0x51 // sc_pageDown OsCode::KEY_INSERT => (ScanCode::Numpad0, KeyState::E0), // 0x52 // sc_insert OsCode::KEY_DELETE => (ScanCode::NumpadPeriod, KeyState::E0), // 0x53 // sc_delete OsCode::KEY_LEFTMETA => (ScanCode::Oem2, KeyState::E0), // 0x5B // sc_metaLeft OsCode::KEY_RIGHTMETA => (ScanCode::Oem3, KeyState::E0), // 0x5C // sc_metaRight OsCode::KEY_FORWARD => (ScanCode::F18, KeyState::E0), // 0x69 // sc_browser_forward OsCode::KEY_BACK => (ScanCode::F19, KeyState::E0), // 0x6A // sc_browser_back OsCode::KEY_COMPOSE => (ScanCode::EraseEOF, KeyState::E0), // OsCode::KEY_TODO => 0x24 as ScanCode, // sc_media_stop // OsCode::KEY_TODO => 0x32 as ScanCode, // sc_browser_home // OsCode::KEY_TODO => 0x46 as ScanCode, // sc_cancel // OsCode::KEY_TODO => 0x5D as ScanCode, // sc_application // OsCode::KEY_TODO => 0x5E as ScanCode, // sc_power // OsCode::KEY_TODO => 0x5F as ScanCode, // sc_sleep // OsCode::KEY_TODO => 0x63 as ScanCode, // sc_wake // OsCode::KEY_TODO => 0x65 as ScanCode, // sc_browser_search // OsCode::KEY_TODO => 0x66 as ScanCode, // sc_browser_favorites // OsCode::KEY_TODO => 0x67 as ScanCode, // sc_browser_refresh // OsCode::KEY_TODO => 0x68 as ScanCode, // sc_browser_stop // 0x6B => OsCode::KEY_TODO, // sc_launch_app1 // 0x6C => OsCode::KEY_TODO, // sc_launch_email // 0x6D => OsCode::KEY_TODO, // sc_launch_media _ => return Err(()), }; Ok(Stroke::Keyboard { code, state, information: 0, }) } } kanata-1.9.0/src/oskbd/windows/llhook.rs000064400000000000000000000352511046102023000163210ustar 00000000000000//! Safe abstraction over the low-level windows keyboard hook API. // This file is taken from kbremap with minor modifications. // https://github.com/timokroeger/kbremap #![cfg_attr( feature = "simulated_output", allow(dead_code, unused_imports, unused_variables, unused_mut) )] use core::fmt; use std::cell::Cell; use std::io; use std::{mem, ptr}; use winapi::ctypes::*; use winapi::shared::minwindef::*; use winapi::shared::windef::*; use winapi::um::winuser::*; use crate::kanata::CalculatedMouseMove; use crate::oskbd::{KeyEvent, KeyValue}; use kanata_keyberon::key_code::KeyCode; use kanata_parser::custom_action::*; use kanata_parser::keys::*; pub const LLHOOK_IDLE_TIME_SECS_CLEAR_INPUTS: u64 = 60; type HookFn = dyn FnMut(InputEvent) -> bool; thread_local! { /// Stores the hook callback for the current thread. static HOOK: Cell>> = Cell::default(); } /// Wrapper for the low-level keyboard hook API. /// Automatically unregisters the hook when dropped. pub struct KeyboardHook { handle: HHOOK, } impl KeyboardHook { /// Sets the low-level keyboard hook for this thread. /// /// Panics when a hook is already registered from the same thread. #[must_use = "The hook will immediatelly be unregistered and not work."] pub fn set_input_cb(callback: impl FnMut(InputEvent) -> bool + 'static) -> KeyboardHook { HOOK.with(|state| { assert!( state.take().is_none(), "Only one keyboard hook can be registered per thread." ); state.set(Some(Box::new(callback))); KeyboardHook { handle: unsafe { SetWindowsHookExW(WH_KEYBOARD_LL, Some(hook_proc), ptr::null_mut(), 0) .as_mut() .expect("install low-level keyboard hook successfully") }, } }) } } impl Drop for KeyboardHook { fn drop(&mut self) { unsafe { UnhookWindowsHookEx(self.handle) }; HOOK.with(|state| state.take()); } } /// Key event received by the low level keyboard hook. #[derive(Debug, Clone, Copy)] pub struct InputEvent { pub code: u32, /// Key was released pub up: bool, } impl fmt::Display for InputEvent { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let direction = if self.up { "↑" } else { "↓" }; let key_name = KeyCode::from(OsCode::from(self.code)); write!(f, "{}{:?}", direction, key_name) } } impl InputEvent { #[rustfmt::skip] fn from_hook_lparam(lparam: &KBDLLHOOKSTRUCT) -> Self { let code = if lparam.vkCode == (VK_RETURN as u32) { match lparam.flags & 0x1 { 0 => VK_RETURN as u32, _ => u32::from(VK_KPENTER_FAKE), } } else { #[cfg(not(feature = "win_llhook_read_scancodes"))] { lparam.vkCode } #[cfg(feature = "win_llhook_read_scancodes")] { let extended = if lparam.flags & 0x1 == 0x1 { 0xE000 } else { 0 }; crate::oskbd::u16_to_osc((lparam.scanCode as u16) | extended) .map(Into::into) .unwrap_or(lparam.vkCode) } }; Self { code, up: lparam.flags & LLKHF_UP != 0, } } pub fn from_oscode(code: OsCode, val: KeyValue) -> Self { Self { code: code.into(), up: val.into(), } } } impl TryFrom for KeyEvent { type Error = (); fn try_from(item: InputEvent) -> Result { Ok(Self { code: OsCode::from_u16(item.code as u16).ok_or(())?, value: match item.up { true => KeyValue::Release, false => KeyValue::Press, }, }) } } impl From for InputEvent { fn from(item: KeyEvent) -> Self { Self { code: item.code.into(), up: item.value.into(), } } } /// The actual WinAPI compatible callback. /// code: determines how to process the message /// source: https://learn.microsoft.com/windows/win32/winmsg/lowlevelkeyboardproc /// <0 : must pass the message to CallNextHookEx without further processing /// and should return the value returned by CallNextHookEx /// HC_ACTION (=0) : wParam and lParam parameters contain information about the message /// /// wparam: ID keyboard message /// source: https://learn.microsoft.com/windows/win32/winmsg/lowlevelkeyboardproc /// WM_KEY(DOWN|UP) Posted to kb-focused window when a nonsystem key is pressed /// WM_SYSKEYDOWN¦UP Posted to kb-focused window when a F10 (activate menu bar) /// or ⎇X⃣ or posted to active window if no win has kb focus (check context code in lParam) /// /// lparam: pointer to a KBDLLHOOKSTRUCT struct /// source: https://learn.microsoft.com/windows/win32/api/winuser/ns-winuser-kbdllhookstruct /// vkCode :DWORD key's virtual code (1–254) /// scanCode :DWORD key's hardware scan code /// flags :DWORD flags (extended-key, event-injected, transition-state), context code /// Bits (2-3 6 reserved) Description /// 7 KF_UP >> 8 LLKHF_UP transition state: 0=key↓ 1=key↑ /// (being pressed) (being released) /// 5 KF_ALTDOWN >> 8 LLKHF_ALTDOWN context code : 1=alt↓ 0=alt↑ /// 4 0x10 LLKHF_INJECTED event was injected: 1=yes, 0=no /// 1 0x02 LLKHF_LOWER_IL_INJECTED injected by proc with lower integrity level // 1=yes 0=no (bit 4 will also set) /// 0 KF_EXTENDED >> 8 LLKHF_EXTENDED extended key (Fn, numpad): 1=yes, 0=no /// time :DWORD time stamp = GetMessageTime /// dwExtraInfo:ULONG_PTR Additional info unsafe extern "system" fn hook_proc(code: c_int, wparam: WPARAM, lparam: LPARAM) -> LRESULT { let hook_lparam = &*(lparam as *const KBDLLHOOKSTRUCT); let is_injected = hook_lparam.flags & LLKHF_INJECTED != 0; log::trace!("{code} {}{wparam} {is_injected}", { match wparam as u32 { WM_KEYDOWN => "↓", WM_KEYUP => "↑", WM_SYSKEYDOWN => "sys↓", WM_SYSKEYUP => "sys↑", _ => "?", } }); // Regarding code check: // If code is non-zero (technically <0, but 0 is the only valid value anyway), // then it must be forwarded. // Source: https://learn.microsoft.com/windows/win32/winmsg/lowlevelkeyboardproc // // Regarding in_injected check: // `SendInput()` internally calls the hook function. // Filter out injected events to prevent infinite recursion. if code != HC_ACTION || is_injected { return CallNextHookEx(ptr::null_mut(), code, wparam, lparam); } let key_event = InputEvent::from_hook_lparam(hook_lparam); let mut handled = false; HOOK.with(|state| { // The unwrap cannot fail, because we have initialized [`HOOK`] with a // valid closure before registering the hook (this function). // To access the closure we move it out of the cell and put it back // after it returned. For this to work we need to prevent recursion by // dropping injected events. Otherwise we would try to take the closure // twice and the call would fail the second time. let mut hook = state.take().expect("no recurse"); handled = hook(key_event); state.set(Some(hook)); }); if handled { 1 } else { CallNextHookEx(ptr::null_mut(), code, wparam, lparam) } } #[cfg(all(not(feature = "simulated_output"), not(feature = "passthru_ahk")))] /// Handle for writing keys to the OS. pub struct KbdOut {} #[cfg(all(not(feature = "simulated_output"), not(feature = "passthru_ahk")))] impl KbdOut { pub fn new() -> Result { Ok(Self {}) } pub fn write(&mut self, event: InputEvent) -> Result<(), io::Error> { super::send_key_sendinput(event.code as u16, event.up); Ok(()) } pub fn write_key(&mut self, key: OsCode, value: KeyValue) -> Result<(), io::Error> { let event = InputEvent::from_oscode(key, value); self.write(event) } pub fn write_code(&mut self, code: u32, value: KeyValue) -> Result<(), io::Error> { super::write_code(code as u16, value) } pub fn write_code_raw(&mut self, code: u16, value: KeyValue) -> Result<(), io::Error> { super::write_code_raw(code, value) } pub fn press_key(&mut self, key: OsCode) -> Result<(), io::Error> { self.write_key(key, KeyValue::Press) } pub fn release_key(&mut self, key: OsCode) -> Result<(), io::Error> { self.write_key(key, KeyValue::Release) } /// Send using VK_PACKET pub fn send_unicode(&mut self, c: char) -> Result<(), io::Error> { super::send_uc(c, false); super::send_uc(c, true); Ok(()) } pub fn click_btn(&mut self, btn: Btn) -> Result<(), io::Error> { log::debug!("click btn: {:?}", btn); match btn { Btn::Left => send_btn(MOUSEEVENTF_LEFTDOWN), Btn::Right => send_btn(MOUSEEVENTF_RIGHTDOWN), Btn::Mid => send_btn(MOUSEEVENTF_MIDDLEDOWN), Btn::Backward => send_xbtn(MOUSEEVENTF_XDOWN, XBUTTON1), Btn::Forward => send_xbtn(MOUSEEVENTF_XDOWN, XBUTTON2), }; Ok(()) } pub fn release_btn(&mut self, btn: Btn) -> Result<(), io::Error> { log::debug!("release btn: {:?}", btn); match btn { Btn::Left => send_btn(MOUSEEVENTF_LEFTUP), Btn::Right => send_btn(MOUSEEVENTF_RIGHTUP), Btn::Mid => send_btn(MOUSEEVENTF_MIDDLEUP), Btn::Backward => send_xbtn(MOUSEEVENTF_XUP, XBUTTON1), Btn::Forward => send_xbtn(MOUSEEVENTF_XUP, XBUTTON2), }; Ok(()) } pub fn scroll(&mut self, direction: MWheelDirection, distance: u16) -> Result<(), io::Error> { log::debug!("scroll: {direction:?} {distance:?}"); match direction { MWheelDirection::Up | MWheelDirection::Down => scroll(direction, distance), MWheelDirection::Left | MWheelDirection::Right => hscroll(direction, distance), } Ok(()) } pub fn move_mouse(&mut self, mv: CalculatedMouseMove) -> Result<(), io::Error> { move_mouse(mv.direction, mv.distance); Ok(()) } pub fn move_mouse_many(&mut self, moves: &[CalculatedMouseMove]) -> Result<(), io::Error> { move_mouse_many(moves); Ok(()) } pub fn set_mouse(&mut self, x: u16, y: u16) -> Result<(), io::Error> { log::info!("setting mouse {x} {y}"); set_mouse_xy(i32::from(x), i32::from(y)); Ok(()) } } fn send_btn(flag: u32) { unsafe { let mut inputs: [INPUT; 1] = mem::zeroed(); inputs[0].type_ = INPUT_MOUSE; // set button let mut m_input: MOUSEINPUT = mem::zeroed(); m_input.dwFlags |= flag; *inputs[0].u.mi_mut() = m_input; SendInput(1, inputs.as_mut_ptr(), mem::size_of::() as _); } } fn send_xbtn(flag: u32, xbtn: u16) { unsafe { let mut inputs: [INPUT; 1] = mem::zeroed(); inputs[0].type_ = INPUT_MOUSE; // set button let mut m_input: MOUSEINPUT = mem::zeroed(); m_input.dwFlags |= flag; m_input.mouseData = xbtn.into(); *inputs[0].u.mi_mut() = m_input; SendInput(1, inputs.as_mut_ptr(), mem::size_of::() as _); } } fn scroll(direction: MWheelDirection, distance: u16) { unsafe { let mut inputs: [INPUT; 1] = mem::zeroed(); inputs[0].type_ = INPUT_MOUSE; let mut m_input: MOUSEINPUT = mem::zeroed(); m_input.dwFlags |= MOUSEEVENTF_WHEEL; m_input.mouseData = match direction { MWheelDirection::Up => distance.into(), MWheelDirection::Down => (-i32::from(distance)) as u32, _ => unreachable!(), // unreachable based on pub fn scroll }; *inputs[0].u.mi_mut() = m_input; SendInput(1, inputs.as_mut_ptr(), mem::size_of::() as _); } } fn hscroll(direction: MWheelDirection, distance: u16) { unsafe { let mut inputs: [INPUT; 1] = mem::zeroed(); inputs[0].type_ = INPUT_MOUSE; let mut m_input: MOUSEINPUT = mem::zeroed(); m_input.dwFlags |= MOUSEEVENTF_HWHEEL; m_input.mouseData = match direction { MWheelDirection::Right => distance.into(), MWheelDirection::Left => (-i32::from(distance)) as u32, _ => unreachable!(), // unreachable based on pub fn scroll }; *inputs[0].u.mi_mut() = m_input; SendInput(1, inputs.as_mut_ptr(), mem::size_of::() as _); } } fn move_mouse(direction: MoveDirection, distance: u16) { log::debug!("move mouse: {direction:?} {distance:?}"); match direction { MoveDirection::Up => move_mouse_xy(0, -i32::from(distance)), MoveDirection::Down => move_mouse_xy(0, i32::from(distance)), MoveDirection::Left => move_mouse_xy(-i32::from(distance), 0), MoveDirection::Right => move_mouse_xy(i32::from(distance), 0), } } fn move_mouse_many(moves: &[CalculatedMouseMove]) { let mut x_acc = 0; let mut y_acc = 0; for mov in moves { let acc_change = match mov.direction { MoveDirection::Up => (0, -i32::from(mov.distance)), MoveDirection::Down => (0, i32::from(mov.distance)), MoveDirection::Left => (-i32::from(mov.distance), 0), MoveDirection::Right => (i32::from(mov.distance), 0), }; x_acc += acc_change.0; y_acc += acc_change.1; } move_mouse_xy(x_acc, y_acc); } fn move_mouse_xy(x: i32, y: i32) { mouse_event(MOUSEEVENTF_MOVE, 0, x, y); } fn set_mouse_xy(x: i32, y: i32) { mouse_event( MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK, 0, x, y, ); } // Taken from Enigo: https://github.com/enigo-rs/enigo fn mouse_event(flags: u32, data: u32, dx: i32, dy: i32) { let mut input = INPUT { type_: INPUT_MOUSE, u: unsafe { mem::transmute::( MOUSEINPUT { dx, dy, mouseData: data, dwFlags: flags, time: 0, dwExtraInfo: 0, }, ) }, }; unsafe { SendInput(1, &mut input as LPINPUT, mem::size_of::() as c_int) }; } kanata-1.9.0/src/oskbd/windows/mod.rs000064400000000000000000000242431046102023000156070ustar 00000000000000#![cfg_attr( feature = "simulated_output", allow(dead_code, unused_imports, unused_variables, unused_mut) )] #[cfg(not(feature = "simulated_input"))] use std::mem; #[cfg(not(feature = "simulated_input"))] use winapi::um::winuser::*; #[cfg(not(feature = "simulated_input"))] use encode_unicode::CharExt; #[cfg(not(feature = "simulated_input"))] use crate::oskbd::KeyValue; #[cfg(all(not(feature = "interception_driver"), not(feature = "simulated_input")))] mod llhook; // contains KbdOut any(not(feature = "simulated_output"), not(feature = "passthru_ahk")) #[cfg(all(not(feature = "interception_driver"), not(feature = "simulated_input")))] pub use llhook::*; #[cfg(all(not(feature = "interception_driver"), feature = "simulated_input"))] mod exthook_os; #[cfg(all(not(feature = "interception_driver"), feature = "simulated_input"))] pub use exthook_os::*; mod scancode_to_usvk; #[allow(unused)] pub use scancode_to_usvk::*; #[cfg(feature = "interception_driver")] mod interception; #[cfg(feature = "interception_driver")] mod interception_convert; #[cfg(feature = "interception_driver")] pub use self::interception::*; #[cfg(feature = "interception_driver")] pub use interception_convert::*; #[cfg(not(feature = "simulated_input"))] fn send_uc(c: char, up: bool) { log::debug!("sending unicode {c}"); let mut inputs: [INPUT; 2] = unsafe { mem::zeroed() }; let n_inputs = inputs .iter_mut() .zip(c.to_utf16()) .map(|(input, c)| { let mut kb_input: KEYBDINPUT = unsafe { mem::zeroed() }; kb_input.wScan = c; kb_input.dwFlags |= KEYEVENTF_UNICODE; if up { kb_input.dwFlags |= KEYEVENTF_KEYUP; } input.type_ = INPUT_KEYBOARD; unsafe { *input.u.ki_mut() = kb_input }; }) .count(); unsafe { SendInput( n_inputs as _, inputs.as_mut_ptr(), mem::size_of::() as _, ); } } #[cfg(not(feature = "simulated_output"))] fn write_code_raw(code: u16, value: KeyValue) -> Result<(), std::io::Error> { let is_key_up = match value { KeyValue::Press | KeyValue::Repeat => false, KeyValue::Release => true, KeyValue::Tap => panic!("invalid value attempted to be sent"), KeyValue::WakeUp => panic!("invalid value attempted to be sent"), }; unsafe { let mut kb_input: KEYBDINPUT = mem::zeroed(); if is_key_up { kb_input.dwFlags |= KEYEVENTF_KEYUP; } kb_input.wVk = code; let mut inputs: [INPUT; 1] = mem::zeroed(); inputs[0].type_ = INPUT_KEYBOARD; *inputs[0].u.ki_mut() = kb_input; SendInput(1, inputs.as_mut_ptr(), mem::size_of::() as _); } Ok(()) } #[cfg(not(feature = "simulated_input"))] fn write_code(code: u16, value: KeyValue) -> Result<(), std::io::Error> { send_key_sendinput( code, match value { KeyValue::Press | KeyValue::Repeat => false, KeyValue::Release => true, KeyValue::Tap => panic!("invalid value attempted to be sent"), KeyValue::WakeUp => panic!("invalid value attempted to be sent"), }, ); Ok(()) } #[cfg(not(feature = "simulated_input"))] fn send_key_sendinput(code: u16, is_key_up: bool) { unsafe { let mut kb_input: KEYBDINPUT = mem::zeroed(); if is_key_up { kb_input.dwFlags |= KEYEVENTF_KEYUP; } #[cfg(feature = "win_sendinput_send_scancodes")] { /* Credit to @VictorLemosR from GitHub for the code here 🙂: All the keys that are extended are on font 1, inside the table on column 'Scan 1 Make' and start with '0xE0'. To obtain the scancode, one could just print 'kb_input.wScan' from the function below. Font 1: https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#scan-codes To obtain a virtual key code, one could just print 'code' from the function below for a key or see font 2 Font 2: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes For example, left arrow and 4 from numpad. For the scancode, they have the same low byte, but not the same high byte, which is 0xE0. LeftArrow = 0xE04B, keypad 4 = 0x004B. For the virtual code, left arrow is 0x25 and 4 from numpad is 0x64. There is a windows function called 'MapVirtualKeyA' that can be used to convert a virtual key code to a scancode. IMPORTANT: these numbers are the VK numbers, e.g. VK_Q, VK_LSHIFT */ const EXTENDED_KEYS: [u8; 48] = if cfg!(not(feature = "win_llhook_read_scancodes")) { // BUG NOTES: // The difference between the two variants is the handling of VK_SNAPSHOT. // // It seems the winapi MapVirtualKeyA does an different mapping when passing in // VK_SNAPSHOT than the rest of the code used to expect. It gets mapped to 88, or // 0x54, and this should be non-extended, i.e. it remains 0x54 and not 0xE037. With // winiov2/gui/interception variants, MapVirtualKeyA is not used by default and // instead it's a custom mapping, which maps it to 0xE037, and this value seems to // have the correct effect. // // According to readings, MapVirtualKeyA does not do extended flag properly, so // since the VK_SNAPSHOT mapping was believed to be an extended key, the 0xE000 was // added and 0x54 became 0xE054 which doesn't do anything. Avoiding adding of the // 0xE000 fixes the issue. [ 0xb1, 0xb0, 0xa3, 0xad, 0x8c, 0xb3, 0xb2, 0xae, 0xaf, 0xac, 0x6f, 0x13, 0xa5, 0x24, 0x26, 0x21, 0x25, 0x27, 0x23, 0x28, 0x22, 0x2d, 0x2e, 0x5b, 0x5c, 0x5d, 0x5f, 0xaa, 0xa8, 0xa9, 0xa7, 0xa6, 0xac, 0xb4, 0x13, /* The 0x13 here is repeated. Why? Maybe it will generate better comparison code 😅. Probably should test+measure when making changes like this (but I didn't). The theory is that comparing on a 16-byte boundary seems good. Below taken from Rust source: const fn memchr_aligned(x: u8, text: &[u8]) -> Option { // Scan for a single byte value by reading two `usize` words at a time. // // Split `text` in three parts // - unaligned initial part, before the first word aligned address in text // - body, scan by 2 words at a time // - the last remaining part, < 2 word size */ 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, ] } else { [ 0xb1, 0xb0, 0xa3, 0xad, 0x8c, 0xb3, 0xb2, 0xae, 0xaf, 0xac, 0x6f, 0x2c, 0xa5, 0x24, 0x26, 0x21, 0x25, 0x27, 0x23, 0x28, 0x22, 0x2d, 0x2e, 0x5b, 0x5c, 0x5d, 0x5f, 0xaa, 0xa8, 0xa9, 0xa7, 0xa6, 0xac, 0xb4, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, ] }; let code_u32 = code as u32; kb_input.dwFlags |= KEYEVENTF_SCANCODE; #[cfg(not(feature = "win_llhook_read_scancodes"))] { // This MapVirtualKeyA is needed to translate back to the proper scancode // associated with with the virtual key. // E.g. take this example: // // - KEY_A is the code here // - OS layout is AZERTY // - No remapping, e.g. active layer is: // - (deflayermap (active-layer) a a) // // This means kanata received a key press at US-layout position Q. However, // translating KEY_A via osc_to_u16 will result in the scancode assocated with // US-layout position A, but we want to output the position Q scancode. It is // MapVirtualKeyA that does the correct translation for this based on the user's OS // layout. kb_input.wScan = MapVirtualKeyA(code_u32, 0) as u16; if kb_input.wScan == 0 { // The known scenario for this is VK_KPENTER_FAKE which isn't a real VK so // MapVirtualKeyA is expected to return 0. This fake VK is used to // distinguish the key within kanata since in Windows there is no output VK for // the enter key at the numpad. // // The osc_to_u16 function knows the scancode for keypad enter though, and it // isn't known to change based on language layout so this seems fine to do. kb_input.wScan = osc_to_u16(code.into()).unwrap_or(0); } } #[cfg(feature = "win_llhook_read_scancodes")] { kb_input.wScan = osc_to_u16(code.into()).unwrap_or_else(|| MapVirtualKeyA(code_u32, 0) as u16); } if kb_input.wScan == 0 { kb_input.dwFlags &= !KEYEVENTF_SCANCODE; kb_input.wVk = code; } let is_extended_key: bool = code < 0xff && EXTENDED_KEYS.contains(&(code as u8)); if is_extended_key { kb_input.wScan |= 0xE0 << 8; kb_input.dwFlags |= KEYEVENTF_EXTENDEDKEY; } } #[cfg(not(feature = "win_sendinput_send_scancodes"))] { use kanata_parser::keys::*; kb_input.wVk = match code { VK_KPENTER_FAKE => VK_RETURN as u16, _ => code, }; } let mut inputs: [INPUT; 1] = mem::zeroed(); inputs[0].type_ = INPUT_KEYBOARD; *inputs[0].u.ki_mut() = kb_input; SendInput(1, inputs.as_mut_ptr(), mem::size_of::() as _); } } kanata-1.9.0/src/oskbd/windows/scancode_to_usvk.rs000064400000000000000000000273501046102023000203630ustar 00000000000000use kanata_parser::keys::OsCode; #[rustfmt::skip] #[allow(unused)] pub fn u16_to_osc(input: u16) -> Option { Some(if input < 0xE000 { match input { 0x01 => OsCode::KEY_ESC, 0x02 => OsCode::KEY_1, 0x03 => OsCode::KEY_2, 0x04 => OsCode::KEY_3, 0x05 => OsCode::KEY_4, 0x06 => OsCode::KEY_5, 0x07 => OsCode::KEY_6, 0x08 => OsCode::KEY_7, 0x09 => OsCode::KEY_8, 0x0A => OsCode::KEY_9, 0x0B => OsCode::KEY_0, 0x0C => OsCode::KEY_MINUS, 0x0D => OsCode::KEY_EQUAL, 0x0E => OsCode::KEY_BACKSPACE, 0x0F => OsCode::KEY_TAB, 0x10 => OsCode::KEY_Q, 0x11 => OsCode::KEY_W, 0x12 => OsCode::KEY_E, 0x13 => OsCode::KEY_R, 0x14 => OsCode::KEY_T, 0x15 => OsCode::KEY_Y, 0x16 => OsCode::KEY_U, 0x17 => OsCode::KEY_I, 0x18 => OsCode::KEY_O, 0x19 => OsCode::KEY_P, 0x1A => OsCode::KEY_LEFTBRACE, 0x1B => OsCode::KEY_RIGHTBRACE, 0x1C => OsCode::KEY_ENTER, 0x1D => OsCode::KEY_LEFTCTRL, 0x1E => OsCode::KEY_A, 0x1F => OsCode::KEY_S, 0x20 => OsCode::KEY_D, 0x21 => OsCode::KEY_F, 0x22 => OsCode::KEY_G, 0x23 => OsCode::KEY_H, 0x24 => OsCode::KEY_J, 0x25 => OsCode::KEY_K, 0x26 => OsCode::KEY_L, 0x27 => OsCode::KEY_SEMICOLON, 0x28 => OsCode::KEY_APOSTROPHE, 0x29 => OsCode::KEY_GRAVE, 0x2A => OsCode::KEY_LEFTSHIFT, 0x2B => OsCode::KEY_BACKSLASH, 0x2C => OsCode::KEY_Z, 0x2D => OsCode::KEY_X, 0x2E => OsCode::KEY_C, 0x2F => OsCode::KEY_V, 0x30 => OsCode::KEY_B, 0x31 => OsCode::KEY_N, 0x32 => OsCode::KEY_M, 0x33 => OsCode::KEY_COMMA, 0x34 => OsCode::KEY_DOT, 0x35 => OsCode::KEY_SLASH, 0x36 => OsCode::KEY_RIGHTSHIFT, 0x37 => OsCode::KEY_KPASTERISK, 0x38 => OsCode::KEY_LEFTALT, 0x39 => OsCode::KEY_SPACE, 0x3A => OsCode::KEY_CAPSLOCK, 0x3B => OsCode::KEY_F1, 0x3C => OsCode::KEY_F2, 0x3D => OsCode::KEY_F3, 0x3E => OsCode::KEY_F4, 0x3F => OsCode::KEY_F5, 0x40 => OsCode::KEY_F6, 0x41 => OsCode::KEY_F7, 0x42 => OsCode::KEY_F8, 0x43 => OsCode::KEY_F9, 0x44 => OsCode::KEY_F10, 0x45 => OsCode::KEY_NUMLOCK, 0x46 => OsCode::KEY_SCROLLLOCK, 0x47 => OsCode::KEY_KP7, 0x48 => OsCode::KEY_KP8, 0x49 => OsCode::KEY_KP9, 0x4A => OsCode::KEY_KPMINUS, 0x4B => OsCode::KEY_KP4, 0x4C => OsCode::KEY_KP5, 0x4D => OsCode::KEY_KP6, 0x4E => OsCode::KEY_KPPLUS, 0x4F => OsCode::KEY_KP1, 0x50 => OsCode::KEY_KP2, 0x51 => OsCode::KEY_KP3, 0x52 => OsCode::KEY_KP0, 0x53 => OsCode::KEY_KPDOT, 0x56 => OsCode::KEY_102ND, /* Key between the left shift and Z. */ 0x57 => OsCode::KEY_F11, 0x58 => OsCode::KEY_F12, 0x64 => OsCode::KEY_F13, 0x65 => OsCode::KEY_F14, 0x66 => OsCode::KEY_F15, 0x67 => OsCode::KEY_F16, 0x68 => OsCode::KEY_F17, 0x69 => OsCode::KEY_F18, 0x6A => OsCode::KEY_F19, 0x6B => OsCode::KEY_F20, 0x6C => OsCode::KEY_F21, 0x6D => OsCode::KEY_F22, 0x6E => OsCode::KEY_F23, 0x76 => OsCode::KEY_F24, 0x70 => OsCode::KEY_KATAKANA, 0x79 => OsCode::KEY_HENKAN, // Convert 0x7B => OsCode::KEY_MUHENKAN, // Noconvert // Note: the OEM keys below don't seem to correspond to the same VK OEM // mappings as the LLHOOK codes. // ScanCode::Oem1 = 0x5A, /* VK_OEM_WSCTRL */ // ScanCode::Oem2 = 0x5B, /* VK_OEM_FINISH */ // ScanCode::Oem3 = 0x5C, /* VK_OEM_JUMP */ // ScanCode::Oem4 = 0x5E, /* VK_OEM_BACKTAB */ // ScanCode::Oem5 = 0x5F, /* VK_OEM_AUTO */ // ScanCode::Oem6 = 0x6F, /* VK_OEM_PA3 */ // ScanCode::Oem7 = 0x71, /* VK_OEM_RESET */ // ScanCode::EraseEOF = 0x5D, // ScanCode::Zoom => 0x62, // ScanCode::Help => 0x63, // ScanCode::AltPrintScreen = 0x55, /* Alt + print screen. */ // ScanCode::SBCSChar = 0x77, _ => return None, } } else { match input & 0xFF { 0x10 => OsCode::KEY_PREVIOUSSONG, 0x19 => OsCode::KEY_NEXTSONG, 0x1C => OsCode::KEY_KPENTER, 0x1D => OsCode::KEY_RIGHTCTRL, 0x20 => OsCode::KEY_MUTE, 0x22 => OsCode::KEY_PLAYPAUSE, // sc_media_play // 0x24 => OsCode::KEY_TODO, // sc_media_stop 0x2E => OsCode::KEY_VOLUMEDOWN, // sc_volume_down 0x30 => OsCode::KEY_VOLUMEUP, // sc_volume_up // 0x32 => OsCode::KEY_TODO, // sc_browser_home 0x35 => OsCode::KEY_KPSLASH, // sc_numpad_divide 0x37 => OsCode::KEY_PRINT, // sc_printScreen 0x38 => OsCode::KEY_RIGHTALT, // sc_altRight // 0x46 => OsCode::KEY_TODO, // sc_cancel 0x47 => OsCode::KEY_HOME, // sc_home 0x48 => OsCode::KEY_UP, // sc_arrowUp 0x49 => OsCode::KEY_PAGEUP, // sc_pageUp 0x4B => OsCode::KEY_LEFT, // sc_arrowLeft 0x4D => OsCode::KEY_RIGHT, // sc_arrowRight 0x4F => OsCode::KEY_END, // sc_end 0x50 => OsCode::KEY_DOWN, // sc_arrowDown 0x51 => OsCode::KEY_PAGEDOWN, // sc_pageDown 0x52 => OsCode::KEY_INSERT, // sc_insert 0x53 => OsCode::KEY_DELETE, // sc_delete 0x5B => OsCode::KEY_LEFTMETA, // sc_metaLeft 0x5C => OsCode::KEY_RIGHTMETA, // sc_metaRight 0x5D => OsCode::KEY_COMPOSE, // sc_application / compose // 0x5E => OsCode::KEY_TODO, // sc_power // 0x5F => OsCode::KEY_TODO, // sc_sleep // 0x63 => OsCode::KEY_TODO, // sc_wake // 0x65 => OsCode::KEY_TODO, // sc_browser_search // 0x66 => OsCode::KEY_TODO, // sc_browser_favorites // 0x67 => OsCode::KEY_TODO, // sc_browser_refresh // 0x68 => OsCode::KEY_TODO, // sc_browser_stop 0x69 => OsCode::KEY_FORWARD, // sc_browser_forward 0x6A => OsCode::KEY_BACK, // sc_browser_back // 0x6B => OsCode::KEY_TODO, // sc_launch_app1 // 0x6C => OsCode::KEY_TODO, // sc_launch_email // 0x6D => OsCode::KEY_TODO, // sc_launch_media _ => return None, } }) } #[allow(unused)] pub(crate) fn osc_to_u16(osc: OsCode) -> Option { Some(match osc { OsCode::KEY_ESC => 0x01, OsCode::KEY_1 => 0x02, OsCode::KEY_2 => 0x03, OsCode::KEY_3 => 0x04, OsCode::KEY_4 => 0x05, OsCode::KEY_5 => 0x06, OsCode::KEY_6 => 0x07, OsCode::KEY_7 => 0x08, OsCode::KEY_8 => 0x09, OsCode::KEY_9 => 0x0A, OsCode::KEY_0 => 0x0B, OsCode::KEY_MINUS => 0x0C, OsCode::KEY_EQUAL => 0x0D, OsCode::KEY_BACKSPACE => 0x0E, OsCode::KEY_TAB => 0x0F, OsCode::KEY_Q => 0x10, OsCode::KEY_W => 0x11, OsCode::KEY_E => 0x12, OsCode::KEY_R => 0x13, OsCode::KEY_T => 0x14, OsCode::KEY_Y => 0x15, OsCode::KEY_U => 0x16, OsCode::KEY_I => 0x17, OsCode::KEY_O => 0x18, OsCode::KEY_P => 0x19, OsCode::KEY_LEFTBRACE => 0x1A, OsCode::KEY_RIGHTBRACE => 0x1B, OsCode::KEY_ENTER => 0x1C, OsCode::KEY_LEFTCTRL => 0x1D, OsCode::KEY_A => 0x1E, OsCode::KEY_S => 0x1F, OsCode::KEY_D => 0x20, OsCode::KEY_F => 0x21, OsCode::KEY_G => 0x22, OsCode::KEY_H => 0x23, OsCode::KEY_J => 0x24, OsCode::KEY_K => 0x25, OsCode::KEY_L => 0x26, OsCode::KEY_SEMICOLON => 0x27, OsCode::KEY_APOSTROPHE => 0x28, OsCode::KEY_GRAVE => 0x29, OsCode::KEY_LEFTSHIFT => 0x2A, OsCode::KEY_BACKSLASH => 0x2B, OsCode::KEY_Z => 0x2C, OsCode::KEY_X => 0x2D, OsCode::KEY_C => 0x2E, OsCode::KEY_V => 0x2F, OsCode::KEY_B => 0x30, OsCode::KEY_N => 0x31, OsCode::KEY_M => 0x32, OsCode::KEY_COMMA => 0x33, OsCode::KEY_DOT => 0x34, OsCode::KEY_SLASH => 0x35, OsCode::KEY_RIGHTSHIFT => 0x36, OsCode::KEY_KPASTERISK => 0x37, OsCode::KEY_LEFTALT => 0x38, OsCode::KEY_SPACE => 0x39, OsCode::KEY_CAPSLOCK => 0x3A, OsCode::KEY_F1 => 0x3B, OsCode::KEY_F2 => 0x3C, OsCode::KEY_F3 => 0x3D, OsCode::KEY_F4 => 0x3E, OsCode::KEY_F5 => 0x3F, OsCode::KEY_F6 => 0x40, OsCode::KEY_F7 => 0x41, OsCode::KEY_F8 => 0x42, OsCode::KEY_F9 => 0x43, OsCode::KEY_F10 => 0x44, OsCode::KEY_NUMLOCK => 0x45, OsCode::KEY_SCROLLLOCK => 0x46, OsCode::KEY_KP7 => 0x47, OsCode::KEY_KP8 => 0x48, OsCode::KEY_KP9 => 0x49, OsCode::KEY_KPMINUS => 0x4A, OsCode::KEY_KP4 => 0x4B, OsCode::KEY_KP5 => 0x4C, OsCode::KEY_KP6 => 0x4D, OsCode::KEY_KPPLUS => 0x4E, OsCode::KEY_KP1 => 0x4F, OsCode::KEY_KP2 => 0x50, OsCode::KEY_KP3 => 0x51, OsCode::KEY_KP0 => 0x52, OsCode::KEY_KPDOT => 0x53, OsCode::KEY_102ND => 0x56, /* Key between the left shift and Z. */ OsCode::KEY_F11 => 0x57, OsCode::KEY_F12 => 0x58, OsCode::KEY_F13 => 0x64, OsCode::KEY_F14 => 0x65, OsCode::KEY_F15 => 0x66, OsCode::KEY_F16 => 0x67, OsCode::KEY_F17 => 0x68, OsCode::KEY_F18 => 0x69, OsCode::KEY_F19 => 0x6A, OsCode::KEY_F20 => 0x6B, OsCode::KEY_F21 => 0x6C, OsCode::KEY_F22 => 0x6D, OsCode::KEY_F23 => 0x6E, OsCode::KEY_F24 => 0x76, OsCode::KEY_KATAKANA => 0x70, OsCode::KEY_PREVIOUSSONG => 0xE010, OsCode::KEY_NEXTSONG => 0xE019, OsCode::KEY_KPENTER => 0xE01C, OsCode::KEY_RIGHTCTRL => 0xE01D, OsCode::KEY_MUTE => 0xE020, OsCode::KEY_PLAYPAUSE => 0xE022, // sc_media_play OsCode::KEY_VOLUMEDOWN => 0xE02E, // sc_volume_down OsCode::KEY_VOLUMEUP => 0xE030, // sc_volume_up OsCode::KEY_KPSLASH => 0xE035, // sc_numpad_divide OsCode::KEY_PRINT => 0xE037, // sc_printScreen OsCode::KEY_RIGHTALT => 0xE038, // sc_altRight OsCode::KEY_HOME => 0xE047, // sc_home OsCode::KEY_UP => 0xE048, // sc_arrowUp OsCode::KEY_PAGEUP => 0xE049, // sc_pageUp OsCode::KEY_LEFT => 0xE04B, // sc_arrowLeft OsCode::KEY_RIGHT => 0xE04D, // sc_arrowRight OsCode::KEY_END => 0xE04F, // sc_end OsCode::KEY_DOWN => 0xE050, // sc_arrowDown OsCode::KEY_PAGEDOWN => 0xE051, // sc_pageDown OsCode::KEY_INSERT => 0xE052, // sc_insert OsCode::KEY_DELETE => 0xE053, // sc_delete OsCode::KEY_LEFTMETA => 0xE05B, // sc_metaLeft OsCode::KEY_RIGHTMETA => 0xE05C, // sc_metaRight OsCode::KEY_COMPOSE => 0xE05D, // sc_application / compose OsCode::KEY_FORWARD => 0xE069, // sc_browser_forward OsCode::KEY_BACK => 0xE06A, // sc_browser_back _ => return None, }) } kanata-1.9.0/src/tcp_server.rs000075500000000000000000000332741046102023000144170ustar 00000000000000use crate::oskbd::*; use crate::Kanata; #[cfg(feature = "tcp_server")] use kanata_tcp_protocol::*; use parking_lot::Mutex; use std::net::SocketAddr; use std::sync::mpsc::SyncSender as Sender; use std::sync::Arc; #[cfg(feature = "tcp_server")] type HashMap = rustc_hash::FxHashMap; #[cfg(feature = "tcp_server")] use kanata_parser::cfg::SimpleSExpr; #[cfg(feature = "tcp_server")] use std::io::Write; #[cfg(feature = "tcp_server")] use std::net::{TcpListener, TcpStream}; #[cfg(feature = "tcp_server")] pub type Connections = Arc>>; #[cfg(not(feature = "tcp_server"))] pub type Connections = (); #[cfg(feature = "tcp_server")] use kanata_parser::custom_action::FakeKeyAction; #[cfg(feature = "tcp_server")] fn to_action(val: FakeKeyActionMessage) -> FakeKeyAction { match val { FakeKeyActionMessage::Press => FakeKeyAction::Press, FakeKeyActionMessage::Release => FakeKeyAction::Release, FakeKeyActionMessage::Tap => FakeKeyAction::Tap, FakeKeyActionMessage::Toggle => FakeKeyAction::Toggle, } } #[cfg(feature = "tcp_server")] pub struct TcpServer { pub address: SocketAddr, pub connections: Connections, pub wakeup_channel: Sender, } #[cfg(not(feature = "tcp_server"))] pub struct TcpServer { pub connections: Connections, } impl TcpServer { #[cfg(feature = "tcp_server")] pub fn new(address: SocketAddr, wakeup_channel: Sender) -> Self { Self { address, connections: Arc::new(Mutex::new(HashMap::default())), wakeup_channel, } } #[cfg(not(feature = "tcp_server"))] pub fn new(_address: SocketAddr, _wakeup_channel: Sender) -> Self { Self { connections: () } } #[cfg(feature = "tcp_server")] pub fn start(&mut self, kanata: Arc>) { use kanata_parser::cfg::FAKE_KEY_ROW; use crate::kanata::handle_fakekey_action; let listener = TcpListener::bind(self.address).expect("TCP server starts"); let connections = self.connections.clone(); let wakeup_channel = self.wakeup_channel.clone(); std::thread::spawn(move || { for stream in listener.incoming() { match stream { Ok(mut stream) => { { let k = kanata.lock(); log::info!( "new client connection, sending initial LayerChange event to inform them of current layer" ); if let Err(e) = stream.write( &ServerMessage::LayerChange { new: k.layer_info[k.layout.b().current_layer()].name.clone(), } .as_bytes(), ) { log::warn!("failed to write to stream, dropping it: {e:?}"); continue; } } let addr = stream .peer_addr() .expect("incoming conn has known address") .to_string(); connections.lock().insert( addr.clone(), stream.try_clone().expect("stream is clonable"), ); let reader = serde_json::Deserializer::from_reader( stream.try_clone().expect("stream is clonable"), ) .into_iter::(); log::info!("listening for incoming messages {addr}"); let connections = connections.clone(); let kanata = kanata.clone(); let wakeup_channel = wakeup_channel.clone(); std::thread::spawn(move || { for v in reader { match v { Ok(event) => { match event { ClientMessage::ChangeLayer { new } => { kanata.lock().change_layer(new); } ClientMessage::RequestLayerNames {} => { let msg = ServerMessage::LayerNames { names: kanata .lock() .layer_info .iter() .map(|info| info.name.clone()) .collect::>(), }; match stream.write_all(&msg.as_bytes()) { Ok(_) => {} Err(err) => log::error!( "server could not send response: {err}" ), } } ClientMessage::ActOnFakeKey { name, action } => { let mut k = kanata.lock(); let index = match k.virtual_keys.get(&name) { Some(index) => Some(*index as u16), None => { if let Err(e) = stream.write_all( &ServerMessage::Error { msg: format!( "unknown virtual/fake key: {name}" ), } .as_bytes(), ) { log::error!("stream write error: {e}"); connections.lock().remove(&addr); break; } continue; } }; if let Some(index) = index { log::info!("tcp server fake-key action: {name},{action:?}"); handle_fakekey_action( to_action(action), k.layout.bm(), FAKE_KEY_ROW, index, ); } drop(k); } ClientMessage::SetMouse { x, y } => { log::info!( "tcp server SetMouse action: x {x} y {y}" ); match kanata.lock().kbd_out.set_mouse(x, y) { Ok(_) => { log::info!("sucessfully did set mouse position to: x {x} y {y}"); // Optionally send a success message to the // client } Err(e) => { log::error!( "Failed to set mouse position: {}", e ); // Implement any error handling logic here, // such as sending an error response to // the client } } } ClientMessage::RequestCurrentLayerInfo {} => { let mut k = kanata.lock(); let cur_layer = k.layout.bm().current_layer(); let msg = ServerMessage::CurrentLayerInfo { name: k.layer_info[cur_layer].name.clone(), cfg_text: k.layer_info[cur_layer] .cfg_text .clone(), }; drop(k); match stream.write_all(&msg.as_bytes()) { Ok(_) => {} Err(err) => log::error!( "Error writing response to RequestCurrentLayerInfo: {err}" ), } } ClientMessage::RequestCurrentLayerName {} => { let mut k = kanata.lock(); let cur_layer = k.layout.bm().current_layer(); let msg = ServerMessage::CurrentLayerName { name: k.layer_info[cur_layer].name.clone(), }; drop(k); match stream.write_all(&msg.as_bytes()) { Ok(_) => {} Err(err) => log::error!( "Error writing response to RequestCurrentLayerName: {err}" ), } } } use kanata_parser::keys::*; wakeup_channel .send(KeyEvent { code: OsCode::KEY_RESERVED, value: KeyValue::WakeUp, }) .expect("write key event"); } Err(e) => { log::warn!( "client sent an invalid message, disconnecting them. Err: {e:?}" ); // Ignore write result because we're about to disconnect // the client anyway. let _ = stream.write_all( &ServerMessage::Error { msg: "disconnecting - you sent an invalid message" .into(), } .as_bytes(), ); connections.lock().remove(&addr); break; } } } }); } Err(_) => log::error!("not able to accept client connection"), } } }); } #[cfg(not(feature = "tcp_server"))] pub fn start(&mut self, _kanata: Arc>) {} } #[cfg(feature = "tcp_server")] pub fn simple_sexpr_to_json_array(exprs: &[SimpleSExpr]) -> serde_json::Value { let mut result = Vec::new(); for expr in exprs.iter() { match expr { SimpleSExpr::Atom(s) => result.push(serde_json::Value::String(s.clone())), SimpleSExpr::List(list) => result.push(simple_sexpr_to_json_array(list)), } } serde_json::Value::Array(result) } kanata-1.9.0/src/tests/sim_tests/block_keys_tests.rs000064400000000000000000000021111046102023000207450ustar 00000000000000use super::*; #[test] fn block_does_not_block_buttons() { let result = simulate( "(defcfg process-unmapped-keys yes block-unmapped-keys yes) (defsrc) (deflayer base)", "d:mlft d:mrgt d:mmid d:mbck d:mfwd t:10 d:f1 u:mlft u:mrgt u:mmid u:mbck u:mfwd t:10 u:f1", ); assert_eq!( "out🖰:↓Left\nt:1ms\nout🖰:↓Right\nt:1ms\nout🖰:↓Mid\nt:1ms\nout🖰:↓Backward\n\ t:1ms\nout🖰:↓Forward\nt:7ms\nout🖰:↑Left\nt:1ms\nout🖰:↑Right\nt:1ms\nout🖰:↑Mid\n\ t:1ms\nout🖰:↑Backward\nt:1ms\nout🖰:↑Forward", result ); } #[test] fn block_does_not_block_wheel() { let result = simulate( "(defcfg process-unmapped-keys yes block-unmapped-keys yes) (defsrc) (deflayer base)", "d:mwu d:mwd d:mwl d:mwr t:10 d:f1 u:mwu u:mwd u:mwl u:mwr t:10 u:f1", ); assert_eq!( "scroll:Up,120\nt:1ms\nscroll:Down,120\nt:1ms\nscroll:Left,120\nt:1ms\nscroll:Right,120", result ); } kanata-1.9.0/src/tests/sim_tests/capsword_sim_tests.rs000064400000000000000000000053431046102023000213240ustar 00000000000000use super::*; const CFG: &str = r##" (defcfg) (defsrc 7 8 9 0) (deflayer base (caps-word 1000) (caps-word-custom 200 (a) (b)) (caps-word-toggle 1000) (caps-word-custom-toggle 200 (a) (b)) ) "##; #[test] fn caps_word_behaves_correctly() { let result = simulate( CFG, "d:7 u:7 d:a u:a d:1 u:1 d:a u:a d:spc u:spc d:a u:a t:1000", ) .no_time(); assert_eq!( "out:↓LShift out:↓A out:↑LShift out:↑A \ out:↓Kb1 out:↑Kb1 out:↓LShift out:↓A out:↑LShift out:↑A \ out:↓Space out:↑Space out:↓A out:↑A", result ); } #[test] fn caps_word_custom_behaves_correctly() { let result = simulate( CFG, "d:8 u:8 d:a u:a d:b u:b d:a u:a d:1 u:1 d:a u:a t:1000", ) .no_time(); assert_eq!( "out:↓LShift out:↓A out:↑LShift out:↑A \ out:↓B out:↑B out:↓LShift out:↓A out:↑LShift out:↑A \ out:↓Kb1 out:↑Kb1 out:↓A out:↑A", result ); } #[test] fn caps_word_times_out() { let result = simulate(CFG, "d:7 u:7 d:a u:a t:500 d:a u:a t:1001 d:a u:a t:10").no_time(); assert_eq!( "out:↓LShift out:↓A out:↑LShift out:↑A \ out:↓LShift out:↓A out:↑LShift out:↑A \ out:↓A out:↑A", result ); } #[test] fn caps_word_custom_times_out() { let result = simulate(CFG, "d:8 u:8 d:a u:a t:100 d:a u:a t:201 d:a u:a t:10").no_time(); assert_eq!( "out:↓LShift out:↓A out:↑LShift out:↑A \ out:↓LShift out:↓A out:↑LShift out:↑A \ out:↓A out:↑A", result ); } #[test] fn caps_word_does_not_toggle() { let result = simulate(CFG, "d:7 u:7 d:a u:a t:100 d:7 u:7 t:100 d:a u:a t:10").no_time(); assert_eq!( "out:↓LShift out:↓A out:↑LShift out:↑A \ out:↓LShift out:↓A out:↑LShift out:↑A", result ); } #[test] fn caps_word_custom_does_not_toggle() { let result = simulate(CFG, "d:8 u:8 d:a u:a t:100 d:8 u:8 t:100 d:a u:a t:10").no_time(); assert_eq!( "out:↓LShift out:↓A out:↑LShift out:↑A \ out:↓LShift out:↓A out:↑LShift out:↑A", result ); } #[test] fn caps_word_toggle_does_toggle() { let result = simulate(CFG, "d:9 u:9 d:a u:a t:100 d:9 u:9 t:100 d:a u:a t:10").no_time(); assert_eq!( "out:↓LShift out:↓A out:↑LShift out:↑A \ out:↓A out:↑A", result ); } #[test] fn caps_word_custom_toggle_does_toggle() { let result = simulate(CFG, "d:0 u:0 d:a u:a t:100 d:0 u:0 t:100 d:a u:a t:10").no_time(); assert_eq!( "out:↓LShift out:↓A out:↑LShift out:↑A \ out:↓A out:↑A", result ); } kanata-1.9.0/src/tests/sim_tests/chord_sim_tests.rs000064400000000000000000000244621046102023000206040ustar 00000000000000use super::*; static SIMPLE_NONOVERLAPPING_CHORD_CFG: &str = "\ (defcfg process-unmapped-keys yes concurrent-tap-hold yes) \ (defsrc) \ (defalias c c) (defvar d d) (deflayer base) \ (defchordsv2 \ (a b) @c 200 all-released () \ (b z) $d 200 first-release () \ )"; #[test] fn sim_chord_basic_repeated_last_release() { let result = simulate( SIMPLE_NONOVERLAPPING_CHORD_CFG, "d:a t:50 d:b t:50 u:a t:50 u:b t:50 \ d:b t:50 d:a t:50 u:b t:50 u:a t:50 ", ); assert_eq!( "t:50ms\nout:↓C\nt:102ms\nout:↑C\nt:98ms\nout:↓C\nt:102ms\nout:↑C", result ); } #[test] fn sim_chord_min_idle_takes_effect() { let result = simulate( SIMPLE_NONOVERLAPPING_CHORD_CFG, "d:z t:20 d:a t:20 d:b t:20 d:d t:20", ); assert_eq!( "t:21ms out:↓Z t:1ms out:↓A t:39ms out:↓B t:1ms out:↓D", result ); } #[test] fn sim_timeout_hold_key() { let result = simulate(SIMPLE_NONOVERLAPPING_CHORD_CFG, "d:z t:201 d:b t:200"); assert_eq!( "t:201ms out:↓Z t:1ms out:↓B", result ); } #[test] fn sim_chord_basic_repeated_first_release() { let result = simulate( SIMPLE_NONOVERLAPPING_CHORD_CFG, "d:z t:50 d:b t:50 u:z t:50 u:b t:50 \ d:z t:50 d:b t:50 u:z t:50 u:b t:50 ", ); assert_eq!( "t:50ms\nout:↓D\nt:52ms\nout:↑D\nt:148ms\nout:↓D\nt:52ms\nout:↑D", result ); } static SIMPLE_OVERLAPPING_CHORD_CFG: &str = "\ (defcfg process-unmapped-keys yes concurrent-tap-hold yes chords-v2-min-idle-experimental 5) (defsrc) (deflayer base) (defchordsv2-experimental (a b) c 200 all-released () (a b z) d 250 first-release () (a b z y) e 400 first-release () )"; #[test] fn sim_chord_overlapping_timeout() { let result = simulate(SIMPLE_OVERLAPPING_CHORD_CFG, "d:a d:b t:201 d:z t:300"); assert_eq!( "t:200ms out:↓C t:252ms out:↓Z", result ); } #[test] fn sim_chord_overlapping_release() { let result = simulate( SIMPLE_OVERLAPPING_CHORD_CFG, "d:a d:b t:100 u:a d:z t:300 u:b t:300", ); assert_eq!("t:100ms\nout:↓C\nt:251ms\nout:↓Z\nt:51ms\nout:↑C", result); } #[test] fn sim_presses_for_old_chord_repress_into_new_chord() { let result = simulate( SIMPLE_OVERLAPPING_CHORD_CFG, "d:a d:b t:50 u:a t:50 d:z t:50 u:b t:50 d:a d:b t:50 u:a t:50", ) .to_ascii(); assert_eq!("t:50ms dn:C t:101ms up:C t:99ms dn:D t:11ms up:D", result); } #[test] fn sim_chord_activate_largest_overlapping() { let result = simulate( SIMPLE_OVERLAPPING_CHORD_CFG, "d:a t:50 d:b t:50 d:z t:50 d:y t:50 u:b t:50", ); assert_eq!("t:150ms\nout:↓E\nt:52ms\nout:↑E", result); } static SIMPLE_DISABLED_LAYER_CHORD_CFG: &str = "\ (defcfg process-unmapped-keys yes concurrent-tap-hold yes) (defsrc) (deflayermap (1) 2 (layer-switch 2) 3 (layer-switch 3)) (deflayermap (2) 3 (layer-while-held 3) 1 (layer-while-held 1)) (deflayermap (3) 2 (layer-while-held 2) 1 (layer-while-held 1)) (defchordsv2 (a b) x 200 all-released (1) (c d) y 200 all-released (2) (e f) z 200 all-released (3) )"; #[test] fn sim_chord_layer_1_switch_disabled() { let result = simulate( SIMPLE_DISABLED_LAYER_CHORD_CFG, "d:a t:50 d:b t:50 d:c t:50 d:d t:50 d:e t:50 d:f t:50", ); assert_eq!( "t:1ms\nout:↓A\nt:50ms\nout:↓B\nt:99ms\nout:↓Y\nt:100ms\nout:↓Z", result ); } #[test] fn sim_chord_layer_2_switch_disabled() { let result = simulate( SIMPLE_DISABLED_LAYER_CHORD_CFG, "d:2 t:50 d:a t:50 d:b t:50 d:c t:50 d:d t:50 d:e t:50 d:f t:50", ); assert_eq!( "t:100ms\nout:↓X\nt:51ms\nout:↓C\nt:50ms\nout:↓D\nt:99ms\nout:↓Z", result ); } #[test] fn sim_chord_layer_3_switch_disabled() { let result = simulate( SIMPLE_DISABLED_LAYER_CHORD_CFG, "d:3 t:50 d:a t:50 d:b t:50 d:c t:50 d:d t:50 d:e t:50 d:f t:50", ); assert_eq!( "t:100ms\nout:↓X\nt:100ms\nout:↓Y\nt:51ms\nout:↓E\nt:50ms\nout:↓F", result ); } #[test] fn sim_chord_layer_1_held_disabled() { let result = simulate( SIMPLE_DISABLED_LAYER_CHORD_CFG, "d:3 t:50 d:1 t:50 d:a t:50 d:b t:50 d:c t:50 d:d t:50 d:e t:50 d:f t:50", ); assert_eq!( "t:101ms\nout:↓A\nt:50ms\nout:↓B\nt:99ms\nout:↓Y\nt:100ms\nout:↓Z", result ); } #[test] fn sim_chord_layer_2_held_disabled() { let result = simulate( SIMPLE_DISABLED_LAYER_CHORD_CFG, "d:3 t:50 d:2 t:50 d:a t:50 d:b t:50 d:c t:50 d:d t:50 d:e t:50 d:f t:50", ); assert_eq!( "t:150ms\nout:↓X\nt:51ms\nout:↓C\nt:50ms\nout:↓D\nt:99ms\nout:↓Z", result ); } #[test] fn sim_chord_layer_3_held_disabled() { let result = simulate( SIMPLE_DISABLED_LAYER_CHORD_CFG, "d:2 t:50 d:3 t:50 d:a t:50 d:b t:50 d:c t:50 d:d t:50 d:e t:50 d:f t:50", ); assert_eq!( "t:150ms\nout:↓X\nt:100ms\nout:↓Y\nt:51ms\nout:↓E\nt:50ms\nout:↓F", result ); } #[test] fn sim_chord_layer_3_repeat() { let result = simulate( SIMPLE_DISABLED_LAYER_CHORD_CFG, "d:3 t:50 d:a t:50 d:b t:50 r:b t:50 r:b t:50\n\ d:d t:50 d:c t:50 r:c t:50 r:d t:50", ); assert_eq!( "t:100ms\nout:↓X\nt:50ms\nout:↓X\nt:50ms\nout:↓X\n\ t:100ms\nout:↓Y\nt:50ms\nout:↓Y\nt:50ms\nout:↓Y", result ); } static CHORD_INTO_TAP_HOLD_CFG: &str = "\ (defcfg process-unmapped-keys yes concurrent-tap-hold yes) (defsrc) (deflayer base) (defchordsv2 (a b) (tap-hold 200 200 x y) 200 all-released () )"; #[test] fn sim_chord_into_tap_hold() { let result = simulate( CHORD_INTO_TAP_HOLD_CFG, "d:a t:50 d:b t:149 u:a u:b t:5 \ d:a t:50 d:b t:148 u:a u:b t:1000", ); assert_eq!( "t:199ms\nout:↓Y\nt:10ms\nout:↑Y\nt:193ms\nout:↓X\nt:10ms\nout:↑X", result ); } static CHORD_WITH_PENDING_UNDERLYING_TAP_HOLD: &str = "\ (defcfg process-unmapped-keys yes concurrent-tap-hold yes) (defsrc) (deflayermap (base) a (tap-hold 200 200 a b)) (defchordsv2 (b c) d 100 all-released () )"; #[test] fn sim_chord_pending_tap_hold() { let result = simulate( CHORD_WITH_PENDING_UNDERLYING_TAP_HOLD, "d:a t:10 d:b t:10 d:c t:300", ); // unlike other actions, chordv2 activations // are intentionally not delayed by waiting actions like tap-hold. assert_eq!("t:20ms\nout:↓D\nt:179ms\nout:↓B", result); } static CHORD_WITH_TRANSPARENCY: &str = "\ (defcfg process-unmapped-keys yes concurrent-tap-hold yes) (defsrc) (deflayer base) (defchordsv2 (a b) _ 100 all-released () )"; #[test] #[should_panic] fn sim_denies_transparent() { simulate(CHORD_WITH_TRANSPARENCY, ""); } #[test] fn sim_chord_eager_tapholdpress_activation() { let result = simulate( " (defcfg concurrent-tap-hold yes) (defsrc caps j k bspc) (deflayer one (tap-hold-press 0 200 esc lctl) j k bspc) (defvirtualkeys bspc bspc) (defchordsv2 (j k) (multi (on-press press-vkey bspc) (on-release release-vkey bspc) (fork XX bspc (nop9))) 75 first-release () ) ", "d:caps t:10 d:j d:k t:100 r:bspc t:10 r:bspc t:10 u:j u:k t:100 u:caps t:1000", ) .to_ascii(); assert_eq!( "t:11ms dn:LCtrl t:7ms dn:BSpace t:92ms \ dn:BSpace t:10ms dn:BSpace t:14ms up:BSpace t:96ms up:LCtrl", result ); } #[test] fn sim_chord_eager_tapholdrelease_activation() { let result = simulate( " (defcfg concurrent-tap-hold yes) (defsrc caps j k bspc) (deflayer one (tap-hold-release 0 200 esc lctl) j k bspc) (defvirtualkeys bspc bspc) (defchordsv2 (j k) (multi (on-press press-vkey bspc) (on-release release-vkey bspc)) 75 first-release () ) ", "d:caps t:10 d:j d:k t:10 u:j u:k t:100 u:caps t:1000", ) .to_ascii(); assert_eq!( "t:20ms dn:LCtrl t:7ms dn:BSpace t:5ms up:BSpace t:88ms up:LCtrl", result ); } #[test] fn sim_chord_release_nonchord_key_has_correct_order() { let result = simulate( " (defcfg concurrent-tap-hold yes) (defsrc ralt j k) (deflayer base _ _ _) (defchordsv2 (j k) l 75 first-release () ) ", "d:ralt t:1000 d:j t:1 u:ralt t:100 u:j t:100", ) .to_ascii(); assert_eq!( "t:1ms dn:RAlt t:1075ms dn:J t:1ms up:RAlt t:24ms up:J", result ); } #[test] fn sim_chord_simultaneous_macro() { let result = simulate( " (defsrc a b o) (deflayer default (chord base a) (chord base b) (chord base o) ) (defchords base 500 (a) (macro a z) (b) (macro b) (o) o (a o) o ) ", "d:a t:10 d:b t:500", ) .to_ascii(); assert_eq!( "t:502ms dn:A dn:B t:1ms up:A up:B t:1ms dn:Z t:1ms up:Z", result ); } #[test] #[should_panic] fn sim_chord_error_on_duplicate_keyset() { simulate( " (defcfg concurrent-tap-hold yes) (defsrc) (deflayer base) (defchordsv2 (1 2) (one-shot 2000 lsft) 20 all-released () (2 1) (one-shot 2000 lctl) 20 all-released () ) ", "", ); } #[test] fn sim_chord_oneshot() { let result = simulate( " (defcfg concurrent-tap-hold yes) (defsrc)(deflayer base) (defchordsv2 (a b) (one-shot 2500 rsft) 35 first-release () ) ", "d:a t:10 d:b t:10 u:a t:10 u:b t:3000 \ d:a t:10 d:b t:10 u:a t:10 u:b t:500 d:c u:c t:3000", ) .to_ascii(); assert_eq!( "t:10ms dn:RShift t:2500ms up:RShift t:530ms \ dn:RShift t:521ms dn:C t:5ms up:RShift up:C", result ); } #[test] fn sim_chord_timeout_events() { let result = simulate( " (defcfg concurrent-tap-hold yes process-unmapped-keys yes ) (defvirtualkeys v-macro-word-end (macro spc) ) (defsrc a b c) (defchordsv2-experimental (a b c) (macro x y z (on-press tap-vkey v-macro-word-end)) 200 all-released () (a b) (macro x y (on-press tap-vkey v-macro-word-end)) 200 all-released () ) (deflayer base a b c) ", "d:a t:10 d:b t:3000 u:a u:b t:100", ) .to_ascii(); assert_eq!( "t:201ms dn:X t:1ms up:X t:1ms dn:Y t:1ms up:Y t:4ms dn:Space t:1ms up:Space", result ); } kanata-1.9.0/src/tests/sim_tests/delay_tests.rs000064400000000000000000000024651046102023000177320ustar 00000000000000use super::*; #[test] #[ignore] // timing-based: fails intermittently fn on_press_delay() { let start = std::time::Instant::now(); let result = simulate( "(defsrc) (deflayermap (base) a (on-press-delay 10))", "d:a t:50 u:a t:50", ); assert_eq!("", result); let end = std::time::Instant::now(); let duration = end - start; assert!(duration > std::time::Duration::from_millis(9)); assert!(duration < std::time::Duration::from_millis(19)); } #[test] #[ignore] // timing-based: fails intermittently fn on_release_delay() { let start = std::time::Instant::now(); let result = simulate( "(defsrc) (deflayermap (base) a (on-release-delay 10))", "d:a t:50 u:a t:50", ); assert_eq!("", result); let end = std::time::Instant::now(); let duration = end - start; assert!(duration > std::time::Duration::from_millis(9)); assert!(duration < std::time::Duration::from_millis(19)); } #[test] #[ignore] // timing-based: fails intermittently fn no_delay() { let start = std::time::Instant::now(); let result = simulate("(defsrc) (deflayermap (base) a XX)", "d:a t:50 u:a t:50"); assert_eq!("", result); let end = std::time::Instant::now(); let duration = end - start; assert!(duration < std::time::Duration::from_millis(10)); } kanata-1.9.0/src/tests/sim_tests/layer_sim_tests.rs000064400000000000000000000057261046102023000206230ustar 00000000000000use super::*; #[test] fn transparent_base() { let result = simulate( "(defcfg process-unmapped-keys yes concurrent-tap-hold yes) \ (defsrc a) \ (deflayer base _)", "d:a t:50 u:a t:50", ); assert_eq!("out:↓A\nt:50ms\nout:↑A", result); } #[test] fn delegate_base() { let result = simulate( "(defcfg process-unmapped-keys yes \ delegate-to-first-layer yes) (defsrc a b) \ (deflayer base c (layer-switch 2)) \ (deflayer 2 _ _)", "d:b t:50 u:b t:50 d:a t:50 u:a t:50", ); assert_eq!("t:100ms\nout:↓C\nt:50ms\nout:↑C", result); } #[test] fn delegate_base_but_base_is_transparent() { let result = simulate( "(defcfg process-unmapped-keys yes \ delegate-to-first-layer yes) (defsrc a b) \ (deflayer base _ (layer-switch 2)) \ (deflayer 2 _ _)", "d:b t:50 u:b t:50 d:a t:50 u:a t:50", ); assert_eq!("t:100ms\nout:↓A\nt:50ms\nout:↑A", result); } #[test] fn layer_switching() { let result = simulate( "(defcfg process-unmapped-keys yes delegate-to-first-layer yes) (defsrc a b c d) (deflayer base x y z (layer-switch 2)) (deflayer 2 e f _ (layer-switch 3)) (deflayer 3 g _ _ (layer-switch 4)) (deflayer 4 _ _ _ XX) ", "d:c t:20 u:c t:20 d:d t:20 u:d t:20 d:b t:20 u:b t:20 d:c t:20 u:c t:20 d:d t:20 u:d t:20 d:a t:20 u:a t:20 d:b t:20 u:b t:20 d:d t:20 u:d t:20 d:a t:20 u:a t:20", ); assert_eq!( "out:↓Z\nt:20ms\nout:↑Z\nt:60ms\nout:↓F\nt:20ms\nout:↑F\nt:20ms\nout:↓Z\nt:20ms\nout:↑Z\nt:60ms\nout:↓G\nt:20ms\nout:↑G\nt:20ms\nout:↓Y\nt:20ms\nout:↑Y\nt:60ms\nout:↓X\nt:20ms\nout:↑X", result ); } #[test] fn layer_holding() { let result = simulate( "(defcfg process-unmapped-keys yes delegate-to-first-layer no) (defsrc a b c d e f) (deflayer base x y z (layer-while-held 2) XX XX) (deflayer 2 e f _ XX (layer-while-held 3) XX) (deflayer 3 g _ _ XX XX (layer-while-held 4)) (deflayer 4 _ _ _ XX XX XX) ", "d:c t:20 u:c t:20 d:d t:20 d:a t:20 u:a t:20 d:b t:20 u:b t:20 d:c t:20 u:c t:20 d:e t:20 d:a t:20 u:a t:20 d:b t:20 u:b t:20 d:c t:20 u:c t:20 d:f t:20 d:a t:20 u:a t:20 d:b t:20 u:b t:20 d:c t:20 u:c t:20", ); assert_eq!( "out:↓Z\nt:20ms\nout:↑Z\nt:40ms\nout:↓E\nt:20ms\nout:↑E\nt:20ms\nout:↓F\nt:20ms\nout:↑F\nt:20ms\nout:↓Z\nt:20ms\nout:↑Z\nt:40ms\nout:↓G\nt:20ms\nout:↑G\nt:20ms\nout:↓F\nt:20ms\nout:↑F\nt:20ms\nout:↓Z\nt:20ms\nout:↑Z\nt:40ms\nout:↓G\nt:20ms\nout:↑G\nt:20ms\nout:↓F\nt:20ms\nout:↑F\nt:20ms\nout:↓Z\nt:20ms\nout:↑Z", result ); } kanata-1.9.0/src/tests/sim_tests/macro_sim_tests.rs000064400000000000000000000117631046102023000206060ustar 00000000000000use super::*; #[test] fn macro_cancel_on_press() { let cfg = "\ (defsrc a b c) (deflayer base (macro-cancel-on-press z 100 y) (macro x 100 w) c)"; test_on_press(cfg); let cfg = "\ (defsrc a b c) (deflayer base (macro-repeat-cancel-on-press z 100 y 100) (macro x 100 w) c)"; test_on_press(cfg); } fn test_on_press(cfg: &str) { // Cancellation should happen. let result = simulate(cfg, "d:a t:50 d:c t:100").to_ascii(); assert_eq!("t:1ms dn:Z t:1ms up:Z t:48ms dn:C", result); // Macro should complete if allowed to. let result = simulate(cfg, "d:a u:a t:150 d:c t:100").to_ascii(); assert_eq!( "t:1ms dn:Z t:1ms up:Z t:101ms dn:Y t:1ms up:Y t:46ms dn:C", result ); // The window for macro cancellation should not persist to a new macro that is not cancellable. let result = simulate(cfg, "d:a t:120 d:b t:20 d:c t:100").to_ascii(); assert_eq!( "t:1ms dn:Z t:1ms up:Z t:101ms dn:Y t:1ms up:Y \ t:17ms dn:X t:1ms up:X t:18ms dn:C t:83ms dn:W t:1ms up:W", result ); let result = simulate(cfg, "d:a t:10 d:c u:c t:10 d:b t:20 d:c t:100").to_ascii(); assert_eq!( "t:1ms dn:Z t:1ms up:Z t:8ms dn:C t:1ms up:C t:10ms \ dn:X t:1ms up:X t:18ms dn:C t:83ms dn:W t:1ms up:W", result ); } #[test] fn macro_release_cancel_and_cancel_on_press() { let cfg = "\ (defsrc a b c) (deflayer base (macro-release-cancel-and-cancel-on-press z 100 y 100) (macro x 100 w) c)"; test_release_and_on_press(cfg); let cfg = "\ (defsrc a b c) (deflayer base (macro-repeat-release-cancel-and-cancel-on-press z 100 y 100) (macro x 100 w) c)"; test_release_and_on_press(cfg); } fn test_release_and_on_press(cfg: &str) { // Cancellation should happen for press. let result = simulate(cfg, "d:a t:50 d:c t:100").to_ascii(); assert_eq!("t:1ms dn:Z t:1ms up:Z t:48ms dn:C", result); // Cancellation should happen for release let result = simulate(cfg, "d:a u:a t:150 d:c t:100").to_ascii(); assert_eq!("t:1ms dn:Z t:1ms up:Z t:148ms dn:C", result); // Macro should complete if allowed to. let result = simulate(cfg, "d:a t:150 d:c t:100").to_ascii(); assert_eq!( "t:1ms dn:Z t:1ms up:Z t:101ms dn:Y t:1ms up:Y t:46ms dn:C", result ); // The window for macro cancellation should not persist to a new macro that is not cancellable. let result = simulate(cfg, "d:a t:120 d:b t:20 d:c t:100").to_ascii(); assert_eq!( "t:1ms dn:Z t:1ms up:Z t:101ms dn:Y t:1ms up:Y \ t:17ms dn:X t:1ms up:X t:18ms dn:C t:83ms dn:W t:1ms up:W", result ); let result = simulate(cfg, "d:a t:10 d:c u:c t:10 d:b t:20 d:c t:100").to_ascii(); assert_eq!( "t:1ms dn:Z t:1ms up:Z t:8ms dn:C t:1ms up:C t:10ms \ dn:X t:1ms up:X t:18ms dn:C t:83ms dn:W t:1ms up:W", result ); let result = simulate(cfg, "d:a u:a t:10 t:10 d:b u:b t:20 d:c t:100").to_ascii(); assert_eq!( "t:1ms dn:Z t:1ms up:Z t:19ms \ dn:X t:1ms up:X t:18ms dn:C t:83ms dn:W t:1ms up:W", result ); } #[test] fn macro_repeat() { let cfg = "\ (defsrc a b c d) (deflayer base (macro-repeat Digit1 50) (macro-repeat-release-cancel Digit1 50) (macro-repeat-cancel-on-press Digit1 50) (macro-repeat-release-cancel-and-cancel-on-press Digit1 50))"; let result = simulate(cfg, "d:a t:125 u:a").to_ascii(); assert_eq!( "t:1ms dn:Kb1 t:1ms up:Kb1 t:52ms dn:Kb1 t:1ms up:Kb1 t:52ms dn:Kb1 t:1ms up:Kb1", result ); let result = simulate(cfg, "d:b t:125 u:b").to_ascii(); assert_eq!( "t:1ms dn:Kb1 t:1ms up:Kb1 t:52ms dn:Kb1 t:1ms up:Kb1 t:52ms dn:Kb1 t:1ms up:Kb1", result ); let result = simulate(cfg, "d:c t:125 u:c").to_ascii(); assert_eq!( "t:1ms dn:Kb1 t:1ms up:Kb1 t:52ms dn:Kb1 t:1ms up:Kb1 t:52ms dn:Kb1 t:1ms up:Kb1", result ); let result = simulate(cfg, "d:d t:125 u:d").to_ascii(); assert_eq!( "t:1ms dn:Kb1 t:1ms up:Kb1 t:52ms dn:Kb1 t:1ms up:Kb1 t:52ms dn:Kb1 t:1ms up:Kb1", result ); } #[test] fn macro_release_cancel() { let cfg = "\ (defsrc a b c) (deflayer base (macro-release-cancel z 100 y 100) (macro x 100 w) c)"; test_release(cfg); let cfg = "\ (defsrc a b c) (deflayer base (macro-repeat-release-cancel z 100 y 100) (macro x 100 w) c)"; test_release(cfg); } fn test_release(cfg: &str) { // Cancellation should not happen for press. let result = simulate(cfg, "d:a t:50 d:c t:100").to_ascii(); assert_eq!( "t:1ms dn:Z t:1ms up:Z t:48ms dn:C t:53ms dn:Y t:1ms up:Y", result ); // Cancellation should happen for release let result = simulate(cfg, "d:a u:a t:150 d:c t:100").to_ascii(); assert_eq!("t:1ms dn:Z t:1ms up:Z t:148ms dn:C", result); // Macro should complete if allowed to. let result = simulate(cfg, "d:a t:150 d:c t:20").to_ascii(); assert_eq!( "t:1ms dn:Z t:1ms up:Z t:101ms dn:Y t:1ms up:Y t:46ms dn:C", result ); } kanata-1.9.0/src/tests/sim_tests/mod.rs000064400000000000000000000077051046102023000161730ustar 00000000000000//! Contains tests that use simulated inputs. //! //! One way to write tests is to write the configuration, write the simulated input, and then let //! the test fail by comparing the output to an empty string. Run the test then inspect the failure //! and see if the real output looks sensible according to what is expected. use crate::tests::*; use crate::{ oskbd::{KeyEvent, KeyValue}, str_to_oscode, Kanata, }; use rustc_hash::FxHashMap; mod block_keys_tests; mod capsword_sim_tests; mod chord_sim_tests; mod delay_tests; mod layer_sim_tests; mod macro_sim_tests; mod oneshot_tests; mod override_tests; mod release_sim_tests; mod repeat_sim_tests; mod seq_sim_tests; mod switch_sim_tests; mod template_sim_tests; mod timing_tests; mod unicode_sim_tests; mod unmod_sim_tests; mod use_defsrc_sim_tests; mod vkey_sim_tests; mod zippychord_sim_tests; fn simulate>(cfg: S, sim: S) -> String { simulate_with_file_content(cfg, sim, Default::default()) } fn simulate_with_file_content>( cfg: S, sim: S, file_content: FxHashMap, ) -> String { init_log(); let _lk = match CFG_PARSE_LOCK.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; let mut k = Kanata::new_from_str(cfg.as_ref(), file_content).expect("failed to parse cfg"); for pair in sim.as_ref().split_whitespace() { match pair.split_once(':') { Some((kind, val)) => match kind { "t" => { let tick = str::parse::(val).expect("valid num for tick"); k.tick_ms(tick, &None).unwrap(); } "d" => { let key_code = str_to_oscode(val).expect("valid keycode"); k.handle_input_event(&KeyEvent { code: key_code, value: KeyValue::Press, }) .expect("input handles fine"); } "u" => { let key_code = str_to_oscode(val).expect("valid keycode"); k.handle_input_event(&KeyEvent { code: key_code, value: KeyValue::Release, }) .expect("input handles fine"); } "r" => { let key_code = str_to_oscode(val).expect("valid keycode"); k.handle_input_event(&KeyEvent { code: key_code, value: KeyValue::Repeat, }) .expect("input handles fine"); } _ => panic!("invalid item {pair}"), }, None => panic!("invalid item {pair}"), } } drop(_lk); k.kbd_out.outputs.events.join("\n") } #[allow(unused)] trait SimTransform { /// Changes newlines to spaces. fn to_spaces(self) -> Self; /// Removes out:↑_ items from the string. Also transforms newlines to spaces. fn no_releases(self) -> Self; /// Removes t:_ms items from the string. Also transforms newlines to spaces. fn no_time(self) -> Self; /// Replaces out:↓_ with dn:_ and out:↑_ with up:_. Also transforms newlines to spaces. fn to_ascii(self) -> Self; } impl SimTransform for String { fn to_spaces(self) -> Self { self.replace('\n', " ") } fn no_time(self) -> Self { self.split_ascii_whitespace() .filter(|s| !s.starts_with("t:")) .collect::>() .join(" ") } fn no_releases(self) -> Self { self.split_ascii_whitespace() .filter(|s| !s.starts_with("out:↑") && !s.starts_with("up:")) .collect::>() .join(" ") } fn to_ascii(self) -> Self { self.split_ascii_whitespace() .map(|s| s.replace("out:↑", "up:").replace("out:↓", "dn:")) .collect::>() .join(" ") } } kanata-1.9.0/src/tests/sim_tests/oneshot_tests.rs000064400000000000000000000021031046102023000203000ustar 00000000000000use super::*; #[test] fn oneshot_pause() { let result = simulate( " (defsrc a lmet rmet) (deflayer base 1 @lme @rme) (deflayer numbers 2 @lme @rme) (deflayer navigation (one-shot 2000 lalt) @lme @rme) (deflayer symbols 4 @lme @rme) (defvirtualkeys callum (switch ((and nop1 nop2)) (layer-while-held numbers) break (nop1) (layer-while-held navigation) break (nop2) (layer-while-held symbols) break) activate-callum (multi (one-shot-pause-processing 5) (switch ((or nop1 nop2)) (multi (on-press release-vkey callum) (on-press press-vkey callum)) break () (on-press release-vkey callum) break))) (defalias lme (multi nop1 (on-press tap-vkey activate-callum) (on-release tap-vkey activate-callum)) rme (multi nop2 (on-press tap-vkey activate-callum) (on-release tap-vkey activate-callum))) ", "d:lmet t:10 d:a u:a t:10 u:lmet t:10 d:a u:a t:10", ) .to_ascii(); assert_eq!("t:10ms dn:LAlt t:20ms dn:Kb1 t:5ms up:LAlt up:Kb1", result); } kanata-1.9.0/src/tests/sim_tests/override_tests.rs000064400000000000000000000032001046102023000204370ustar 00000000000000use super::*; #[test] fn override_with_unmod() { let result = simulate( " (defoverrides (a) (b) (b) (a) ) (defalias b (unshift b) a (unshift a) ) (defsrc a b) (deflayer base @a @b) ", "d:lsft t:50 d:a t:50 u:a t:50 d:b t:50 u:b t:50", ) .to_ascii() .no_time(); assert_eq!( "dn:LShift up:LShift dn:B up:B dn:LShift up:LShift dn:A up:A dn:LShift", result ); } #[test] fn override_release_mod_change_key() { let cfg = " (defsrc) (deflayer base) (defoverrides (lsft a) (lsft 9) (lsft 1) (lctl 2)) "; let result = simulate(cfg, "d:lsft t:10 d:a t:10 u:lsft t:10 u:a t:10").to_ascii(); assert_eq!("dn:LShift t:10ms dn:Kb9 t:10ms up:LShift up:Kb9", result); let result = simulate(cfg, "d:lsft t:10 d:a t:10 u:a t:10 u:lsft t:10").to_ascii(); assert_eq!( "dn:LShift t:10ms dn:Kb9 t:10ms up:Kb9 t:10ms up:LShift", result ); let result = simulate(cfg, "d:lsft t:10 d:a t:10 d:c t:10").to_ascii(); assert_eq!("dn:LShift t:10ms dn:Kb9 t:10ms up:Kb9 dn:C", result); let result = simulate(cfg, "d:lsft t:10 d:1 t:10 d:c t:10").to_ascii(); assert_eq!( "dn:LShift t:10ms up:LShift dn:LCtrl dn:Kb2 t:10ms up:LCtrl up:Kb2 dn:LShift dn:C", result ); } #[test] fn override_eagerly_releases() { let result = simulate( " (defcfg override-release-on-activation yes) (defsrc) (deflayer base) (defoverrides (lsft a) (lsft 9)) ", "d:lsft t:10 d:a t:10 u:lsft t:10 u:a t:10", ) .to_ascii(); assert_eq!( "dn:LShift t:10ms dn:Kb9 t:1ms up:Kb9 t:9ms up:LShift", result ); } kanata-1.9.0/src/tests/sim_tests/release_sim_tests.rs000064400000000000000000000011101046102023000211060ustar 00000000000000use super::*; #[test] fn release_standard() { let result = simulate( " (defsrc a) (deflayer base (multi lalt a)) ", " d:a t:10 u:a t:10 ", ) .to_ascii(); assert_eq!("dn:LAlt dn:A t:10ms up:LAlt up:A", result); } #[test] fn release_reversed() { let result = simulate( " (defsrc a) (deflayer base (multi lalt a reverse-release-order)) ", " d:a t:10 u:a t:10 ", ) .to_ascii(); assert_eq!("dn:LAlt dn:A t:10ms up:A up:LAlt", result); } kanata-1.9.0/src/tests/sim_tests/repeat_sim_tests.rs000064400000000000000000000061521046102023000207610ustar 00000000000000use super::*; #[test] fn repeat_standard() { let result = simulate( " (defsrc a) (deflayer base b) ", " d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a ", ); assert_eq!( "out:↓B\nt:10ms\nout:↓B\nt:10ms\nout:↓B\nt:10ms\nout:↑B", result ); } #[test] fn repeat_layer_while_held() { let result = simulate( " (defsrc a b) (deflayer base a (layer-while-held held)) (deflayer held c b) ", " d:b t:10 r:b t:10 d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a ", ); assert_eq!( "t:20ms\nout:↓C\nt:10ms\nout:↓C\nt:10ms\nout:↓C\nt:10ms\nout:↑C", result ); } #[test] fn repeat_layer_switch() { let result = simulate( " (defsrc a b) (deflayer base a (layer-switch swtc)) (deflayer swtc d b) ", " d:b t:10 r:b t:10 d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a ", ); assert_eq!( "t:20ms\nout:↓D\nt:10ms\nout:↓D\nt:10ms\nout:↓D\nt:10ms\nout:↑D", result ); } #[test] fn repeat_layer_held_trans() { let result = simulate( " (defsrc a b) (deflayer base e (layer-while-held held)) (deflayer held _ b) ", " d:b t:10 r:b t:10 d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a ", ); assert_eq!( "t:20ms\nout:↓E\nt:10ms\nout:↓E\nt:10ms\nout:↓E\nt:10ms\nout:↑E", result ); } #[test] fn repeat_many_layer_held_trans() { let result = simulate( " (defsrc a b c d e) (deflayer base e (layer-while-held held1) _ _ _) (deflayer held1 f b (layer-while-held held2) _ _) (deflayer held2 _ _ _ (layer-while-held held3) _) (deflayer held3 _ _ _ _ (layer-while-held held4)) (deflayer held4 _ _ _ _ _) ", " d:b t:10 r:b t:10 d:c t:10 r:c t:10 d:d t:10 r:d t:10 d:e t:10 r:e t:10 d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a ", ); assert_eq!( "t:80ms\nout:↓F\nt:10ms\nout:↓F\nt:10ms\nout:↓F\nt:10ms\nout:↑F", result ); } #[test] fn repeat_base_layer_trans() { let result = simulate( " (defsrc a) (deflayer base _) ", " d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a ", ); assert_eq!( "out:↓A\nt:10ms\nout:↓A\nt:10ms\nout:↓A\nt:10ms\nout:↑A", result ); } #[test] fn repeat_delegate_to_base_layer_trans() { let result = simulate( " (defcfg delegate-to-first-layer yes) (defsrc a c b) (deflayer base e _ (layer-switch swtc)) (deflayer swtc _ _ _) ", " d:b t:10 r:b t:10 d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a d:c t:10 r:c t:10 r:c t:10 u:c t:10 r:c ", ); assert_eq!( "t:20ms\nout:↓E\nt:10ms\nout:↓E\nt:10ms\nout:↓E\nt:10ms\nout:↑E\n\ t:10ms\nout:↓C\nt:10ms\nout:↓C\nt:10ms\nout:↓C\nt:10ms\nout:↑C", result ); } kanata-1.9.0/src/tests/sim_tests/seq_sim_tests.rs000064400000000000000000000201421046102023000202640ustar 00000000000000use super::*; #[test] fn special_nop_keys() { let result = simulate( "(defcfg sequence-input-mode visible-backspaced) (defsrc a b c d e) (deflayer base sldr nop0 c nop9 0) (defvirtualkeys s1 (macro h i)) (defseq s1 (nop0 c nop9)) ", "d:b d:d t:50 u:b u:d t:50 d:a d:b d:c t:50 u:a u:b u:c t:50 d:d t:50", ); assert_eq!( "t:102ms\nout:↓C\nt:50ms\nout:↑C\nt:48ms\n\ out:↓BSpace\nout:↑BSpace\n\ t:2ms\nout:↓H\nt:1ms\nout:↑H\nt:1ms\nout:↓I\nt:1ms\nout:↑I", result ); } #[test] fn chorded_keys_visible_backspaced() { let result = simulate( "(defcfg sequence-input-mode visible-backspaced) (defsrc 0) (deflayer base sldr) (defvirtualkeys s1 z) (defseq s1 (S-(a b))) ", "d:0 u:0 d:lsft t:50 d:a d:b t:50 u:lsft u:a u:b t:500 d:0 u:0 d:rsft t:50 d:a d:b t:50 u:rsft u:a u:b t:500 d:0 u:0 d:rsft t:50 d:a u:rsft t:50 d:b u:a u:b t:500", ) .no_time() .to_ascii(); assert_eq!( // 2nd row is buggy/unexpected! RShift isn't released before outputting Z // Workarounds: // - remap your rsft key to lsft // - use release-key in the macro via virtual keys // - accept and use the quirky behaviour; maybe it's what you wanted? "dn:LShift dn:A dn:B dn:BSpace up:BSpace dn:BSpace up:BSpace up:LShift up:A up:B dn:Z up:Z \ dn:RShift dn:A dn:B dn:BSpace up:BSpace dn:BSpace up:BSpace up:A up:B dn:Z up:Z up:RShift \ dn:RShift dn:A up:RShift dn:B up:A up:B", result ); } const OVERLAP_CFG: &str = " (defcfg sequence-input-mode visible-backspaced) (defsrc 0) (deflayer base sldr) (defvirtualkeys s1 y) (defvirtualkeys s2 z) (defvirtualkeys s3 l) (defvirtualkeys s4 m) (defvirtualkeys s5 n) (defvirtualkeys s6 o) (defvirtualkeys s7 p) (defvirtualkeys s8 q) (defseq s1 (O-(a b))) (defseq s2 (a b)) (defseq s3 (O-(c d) e)) (defseq s4 (c d e)) (defseq s5 (O-(c d) O-(f g))) (defseq s6 (O-(c d) f g)) (defseq s7 (c d O-(f g))) ;; (defseq s8 (c d f g)) KNOWN BUGGY CASE! breaks s6 detection "; #[test] fn overlapping_activate_overlap() { let result = simulate(OVERLAP_CFG, "d:0 d:a d:b t:100 u:a u:b u:0"); assert_eq!( "t:1ms\nout:↓A\nt:1ms\nout:↓B\n\ out:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\nt:1ms\nout:↑A\nout:↑B\n\ out:↓Y\nt:1ms\nout:↑Y", result ); } #[test] fn overlapping_activate_nonoverlap() { let result = simulate(OVERLAP_CFG, "d:0 d:a t:10 u:a t:10 d:b t:10 u:b t:10 u:0"); assert_eq!( "t:1ms\nout:↓A\nt:9ms\nout:↑A\nt:10ms\nout:↓B\n\ out:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\n\ t:1ms\nout:↑B\nout:↓Z\nt:1ms\nout:↑Z", result ); } #[test] fn overlapping_then_nonoverlap_activate_overlap() { let result = simulate(OVERLAP_CFG, "d:0 d:c d:d d:e t:100 u:c u:d u:e u:0"); assert_eq!( "t:1ms\nout:↓C\nt:1ms\nout:↓D\nt:1ms\nout:↓E\n\ out:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\n\ t:1ms\nout:↑C\nout:↑D\nout:↑E\nout:↓L\nt:1ms\nout:↑L", result ); } #[test] fn overlapping_then_nonoverlap_activate_non_overlap() { let result = simulate(OVERLAP_CFG, "d:0 d:c u:c d:d d:e t:100 u:d u:e u:0"); assert_eq!( "t:1ms\nout:↓C\nt:1ms\nout:↑C\nt:1ms\nout:↓D\nt:1ms\nout:↓E\n\ out:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\n\ t:1ms\nout:↑D\nout:↑E\nout:↓M\nt:1ms\nout:↑M", result ); } #[test] fn overlapping_then_overlap_activate_overlap1() { let result = simulate(OVERLAP_CFG, "d:0 d:c d:d d:f d:g t:100"); assert_eq!( "t:1ms\nout:↓C\nt:1ms\nout:↓D\nt:1ms\nout:↓F\nt:1ms\nout:↓G\n\ out:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\n\ t:1ms\nout:↑C\nout:↑D\nout:↑F\nout:↑G\nout:↓N\nt:1ms\nout:↑N", result ); } #[test] fn overlapping_then_overlap_activate_overlap2() { let result = simulate(OVERLAP_CFG, "d:0 d:c d:d u:c u:d d:f d:g t:100"); assert_eq!( "t:1ms\nout:↓C\nt:1ms\nout:↓D\nt:1ms\nout:↑C\nt:1ms\nout:↑D\nt:1ms\nout:↓F\nt:1ms\nout:↓G\n\ out:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\n\ t:1ms\nout:↑F\nout:↑G\nout:↓N\nt:1ms\nout:↑N", result ); } #[test] fn overlapping_then_overlap_activate_overlap3() { let result = simulate(OVERLAP_CFG, "d:0 d:c d:d u:c u:d t:10 d:f d:g t:100"); assert_eq!( "t:1ms\nout:↓C\nt:1ms\nout:↓D\nt:1ms\nout:↑C\nt:1ms\nout:↑D\nt:6ms\nout:↓F\nt:1ms\nout:↓G\n\ out:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\n\ t:1ms\nout:↑F\nout:↑G\nout:↓N\nt:1ms\nout:↑N", result ); } #[test] fn overlapping_then_overlap_activate_nonoverlap() { let result = simulate(OVERLAP_CFG, "d:0 d:c d:d u:c u:d t:10 d:f u:f d:g t:100"); assert_eq!( "t:1ms\nout:↓C\nt:1ms\nout:↓D\nt:1ms\nout:↑C\nt:1ms\nout:↑D\nt:6ms\nout:↓F\nt:1ms\nout:↑F\nt:1ms\nout:↓G\n\ out:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\n\ t:1ms\nout:↑G\nout:↓O\nt:1ms\nout:↑O", result ); } #[test] fn non_overlapping_then_overlap_activate_overlap() { let result = simulate(OVERLAP_CFG, "d:0 d:c u:c d:d u:d d:f d:g t:100"); assert_eq!( "t:1ms\nout:↓C\nt:1ms\nout:↑C\nt:1ms\nout:↓D\nt:1ms\nout:↑D\nt:1ms\nout:↓F\nt:1ms\nout:↓G\n\ out:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\nout:↓BSpace\nout:↑BSpace\n\ t:1ms\nout:↑F\nout:↑G\nout:↓P\nt:1ms\nout:↑P", result ); } #[test] fn non_overlapping_then_overlap_activate_nothing() { let result = simulate(OVERLAP_CFG, "d:0 d:c u:c d:d u:d d:f u:f d:g t:100"); assert_eq!( "t:1ms\nout:↓C\nt:1ms\nout:↑C\nt:1ms\nout:↓D\nt:1ms\nout:↑D\nt:1ms\nout:↓F\nt:1ms\nout:↑F\nt:1ms\nout:↓G", result ); } /* BUG: chorded_hidden_delay_type * * Enable this test when fixing. * * Backtracking currently destroys information about held modifiers before finally outputting the * invalid sequence characters. There is also no logic to keep modifier keys held for the * appropriate duration according to the modifier bits information, even if the information was * preserved. Seems like a complicated low-value edge-case bug to fix so for now will just document * it... Nobody has reported it yet anyway. And visible-backspaced seems preferable in most cases * anyway. #[test] fn chorded_keys_hidden_delaytype() { let result = simulate( "(defcfg sequence-input-mode hidden-delay-type) (defsrc 0) (deflayer base sldr) (defvirtualkeys s1 z) (defseq s1 (S-(a b))) ", "d:0 u:0 d:lsft t:50 d:a d:b t:50 u:lsft u:a u:b t:500 d:0 u:0 d:rsft t:50 d:a d:b t:50 u:rsft u:a u:b t:500 d:0 u:0 d:rsft t:50 d:a u:rsft t:50 d:b u:a u:b t:500", ); assert_eq!( "", result ); } */ #[test] fn noerase() { let result = simulate( "(defcfg sequence-input-mode visible-backspaced) (defsrc) (deflayermap (base) 0 sldr u (t! maybe-noerase u) ) (deftemplate maybe-noerase (char) (multi (switch ((key-history ' 1)) (sequence-noerase 1) fallthrough () $char break )) ) (defvirtualkeys s1 z) (defseq s1 (' u)) ", "d:0 u:0 d:' t:50 d:u t:500", ) .no_time() .to_ascii(); assert_eq!( "dn:Quote dn:U dn:BSpace up:BSpace up:Quote up:U dn:Z up:Z", result, ); } kanata-1.9.0/src/tests/sim_tests/switch_sim_tests.rs000064400000000000000000000017261046102023000210040ustar 00000000000000use super::*; #[test] fn sim_switch_layer() { let result = simulate( " (defcfg) (defsrc a b) (defalias b (switch ((layer base)) x break ((layer other)) y break)) (deflayer base (layer-while-held other) @b) (deflayer other XX @b) ", "d:b u:b t:10 d:a d:b u:b u:a t:10", ) .no_time(); assert_eq!("out:↓X out:↑X out:↓Y out:↑Y", result); } #[test] fn sim_switch_base_layer() { let result = simulate( " (defcfg) (defsrc a b c) (defalias b (switch ((base-layer base)) x break ((base-layer other)) y break)) (deflayer base (layer-switch other) @b c) (deflayer other XX @b (layer-while-held base)) ", "d:b u:b t:10 d:a d:b u:b u:a t:10 d:c t:10 d:b t:10 u:c u:b t:10", ) .no_time(); assert_eq!("out:↓X out:↑X out:↓Y out:↑Y out:↓Y out:↑Y", result); } kanata-1.9.0/src/tests/sim_tests/template_sim_tests.rs000064400000000000000000000007261046102023000213150ustar 00000000000000use super::*; #[test] fn nested_template() { let result = simulate( " (deftemplate one (v1) a b c $v1 ) (deftemplate two (v2) (t! one $v2) e f g ) (defsrc (t! two d)) (deflayer base (t! two x)) ", "d:a t:10 u:a t:10 d:d t:10 u:d t:10 d:g t:10 u:g t:10", ) .no_time(); assert_eq!("out:↓A out:↑A out:↓X out:↑X out:↓G out:↑G", result); } kanata-1.9.0/src/tests/sim_tests/timing_tests.rs000064400000000000000000000015221046102023000201140ustar 00000000000000use std::thread::sleep; use std::time::Duration; use crate::Kanata; use instant::Instant; #[test] fn one_second_is_roughly_1000_counted_ticks() { let mut k = Kanata::new_from_str("(defsrc)(deflayer base)", Default::default()) .expect("failed to parse cfg"); let mut accumulated_ticks = 0; let start = Instant::now(); while start.elapsed() < Duration::from_secs(1) { sleep(Duration::from_millis(1)); accumulated_ticks += k.get_ms_elapsed(); } let actually_elapsed_ms = start.elapsed().as_millis(); // Allow fudge of 1% // In practice this is within 1ms purely due to the remainder. eprintln!("ticks:{accumulated_ticks}, actual elapsed:{actually_elapsed_ms}"); assert!(accumulated_ticks < (actually_elapsed_ms + 10)); assert!(accumulated_ticks > (actually_elapsed_ms - 10)); } kanata-1.9.0/src/tests/sim_tests/unicode_sim_tests.rs000064400000000000000000000015411046102023000211240ustar 00000000000000use super::*; #[test] fn unicode() { let result = simulate( r##" (defcfg) (defsrc 6 7 8 9 0 f1) (deflayer base (unicode r#"("#) (unicode r#")"#) (unicode r#"""#) (unicode "(") (unicode ")") (tap-dance 200 (f1(unicode 😀)f2(unicode 🙂))) ) "##, "d:6 d:7 d:8 d:9 d:0 t:100", ) .no_time(); assert_eq!(r#"outU:( outU:) outU:" outU:( outU:)"#, result); } #[test] #[cfg(target_os = "macos")] fn macos_unicode_handling() { let result = simulate( r##" (defcfg) (defsrc a) (deflayer base (unicode "🎉") ;; Test with an emoji that uses multi-unit UTF-16 ) "##, "d:a t:100", ) .no_time(); assert_eq!("outU:🎉", result); } kanata-1.9.0/src/tests/sim_tests/unmod_sim_tests.rs000064400000000000000000000047231046102023000206250ustar 00000000000000use super::*; #[test] fn unmod_keys_functionality_works() { let result = simulate( " (defcfg) (defsrc f1 1 2 3 4 5 6 7 8 9 0) (deflayer base (multi lctl rctl lsft rsft lmet rmet lalt ralt) (unmod a) (unmod (lctl) b) (unmod (rctl) c) (unmod (lsft) d) (unmod (rsft) e) (unmod (lmet) f) (unmod (rmet) g) (unmod (lalt) h) (unmod (ralt) i) (unmod (lctl lsft lmet lalt) j) ) ", "d:f1 t:5 d:1 u:1 t:5 d:2 u:2 t:5 d:3 u:3 t:5 d:4 u:4 t:5 d:5 u:5 t:5 d:6 u:6 t:5 d:7 u:7 t:5 d:8 u:8 t:5 d:9 u:9 t:5 d:0 u:0 t:5", ) .no_time() .to_ascii(); assert_eq!( "dn:LCtrl dn:RCtrl dn:LShift dn:RShift dn:LGui dn:RGui dn:LAlt dn:RAlt \ up:LCtrl up:RCtrl up:LShift up:RShift up:LGui up:RGui up:LAlt up:RAlt dn:A up:A \ dn:LCtrl dn:RCtrl dn:LShift dn:RShift dn:LGui dn:RGui dn:LAlt dn:RAlt \ up:LCtrl dn:B up:B dn:LCtrl \ up:RCtrl dn:C up:C dn:RCtrl \ up:LShift dn:D up:D dn:LShift \ up:RShift dn:E up:E dn:RShift \ up:LGui dn:F up:F dn:LGui \ up:RGui dn:G up:G dn:RGui \ up:LAlt dn:H up:H dn:LAlt \ up:RAlt dn:I up:I dn:RAlt \ up:LCtrl up:LShift up:LGui up:LAlt dn:J up:J dn:LCtrl dn:LShift dn:LGui dn:LAlt", result ); } #[test] #[should_panic] fn unmod_keys_mod_list_cannot_be_empty() { simulate( " (defcfg) (defsrc a) (deflayer base (unmod () a)) ", "", ); } #[test] #[should_panic] fn unmod_keys_mod_list_cannot_have_nonmod_key() { simulate( " (defcfg) (defsrc a) (deflayer base (unmod (lmet c) a)) ", "", ); } #[test] #[should_panic] fn unmod_keys_mod_list_cannot_have_empty_keys_after_mod_list() { simulate( " (defcfg) (defsrc a) (deflayer base (unmod (lmet))) ", "", ); } #[test] #[should_panic] fn unmod_keys_mod_list_cannot_have_empty_keys() { simulate( " (defcfg) (defsrc a) (deflayer base (unmod)) ", "", ); } #[test] #[should_panic] fn unmod_keys_mod_list_cannot_have_invalid_keys() { simulate( " (defcfg) (defsrc a) (deflayer base (unmod invalid-key)) ", "", ); } kanata-1.9.0/src/tests/sim_tests/use_defsrc_sim_tests.rs000064400000000000000000000027131046102023000216220ustar 00000000000000use super::*; #[test] fn use_defsrc_deflayer() { let result = simulate( r##" (defcfg) (defsrc a b c d) (deflayer base 1 2 3 (layer-while-held other) ) (deflayer other 4 5 (layer-while-held src) XX ) (deflayer src use-defsrc use-defsrc XX XX ) "##, "d:d d:c d:b d:a t:100", ) .to_ascii(); assert_eq!("t:2ms dn:B t:1ms dn:A", result); } #[test] fn use_defsrc_deflayermap() { const CFG: &str = " (defcfg process-unmapped-keys yes) (defsrc a b c d) (deflayer base 1 (layer-while-held othermap1) (layer-while-held othermap2) (layer-while-held othermap3) ) (deflayermap (othermap1) a 5 ___ use-defsrc ) (deflayermap (othermap2) a 6 __ use-defsrc _ x ) (deflayermap (othermap3) a 7 _ use-defsrc __ x ) "; let result = simulate(CFG, "d:b d:a d:c d:e t:10").to_ascii(); assert_eq!("t:1ms dn:Kb5 t:1ms dn:C t:1ms dn:E", result); let result = simulate(CFG, "d:c d:a d:c d:e t:10").to_ascii(); assert_eq!("t:1ms dn:Kb6 t:1ms dn:X t:1ms dn:E", result); let result = simulate(CFG, "d:d d:a d:c d:e t:10").to_ascii(); assert_eq!("t:1ms dn:Kb7 t:1ms dn:C t:1ms dn:X", result); } kanata-1.9.0/src/tests/sim_tests/vkey_sim_tests.rs000064400000000000000000000013271046102023000204560ustar 00000000000000use super::*; const CFG: &str = r" (defsrc a b c) (defvirtualkeys lmet lmet) (defalias hm (hold-for-duration 50 lmet)) (deflayer base (multi @hm (macro-repeat 40 @hm)) (multi 1 @hm) (release-key lmet) ) "; #[test] fn hold_for_duration() { let result = simulate(CFG, "d:a t:200 u:a t:60").to_ascii(); assert_eq!("t:1ms dn:LGui t:258ms up:LGui", result); let result = simulate(CFG, "d:a u:a t:25 d:c u:c t:25").to_ascii(); assert_eq!("t:2ms dn:LGui t:23ms up:LGui", result); let result = simulate(CFG, "d:a u:a t:25 d:b u:b t:25 d:b u:b t:60").to_ascii(); assert_eq!( "t:2ms dn:LGui t:23ms dn:Kb1 t:1ms up:Kb1 t:24ms dn:Kb1 t:1ms up:Kb1 t:49ms up:LGui", result ); } kanata-1.9.0/src/tests/sim_tests/zippychord_sim_tests.rs000064400000000000000000000536501046102023000217010ustar 00000000000000use super::*; static ZIPPY_CFG: &str = "(defsrc lalt)(deflayer base (caps-word 2000))(defzippy file)"; static ZIPPY_FILE_CONTENT: &str = " dy day dy 1 Monday abc Alphabet pr pre ⌫ pra partner pr q pull request r df recipient w a Washington xy WxYz rq request rqa request␣assistance .g git .g f p git fetch -p 12 hi 1234 bye "; fn simulate_with_zippy_file_content(cfg: &str, input: &str, content: &str) -> String { let mut fcontent = FxHashMap::default(); fcontent.insert("file".into(), content.into()); simulate_with_file_content(cfg, input, fcontent) } #[test] fn sim_zippychord_capitalize() { let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:a t:10 d:b t:10 d:spc t:10 d:c u:a u:b u:c u:spc t:300 \ d:a t:10 d:b t:10 d:spc t:10 d:c t:300", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:A t:10ms dn:B t:10ms dn:Space t:10ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:LShift up:A dn:A up:LShift \ dn:L up:L dn:P up:P dn:H up:H up:A dn:A up:B dn:B dn:E up:E dn:T up:T \ t:1ms up:A t:1ms up:B t:1ms up:C t:1ms up:Space t:296ms \ dn:A t:10ms dn:B t:10ms dn:Space t:10ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:LShift up:A dn:A up:LShift \ dn:L up:L dn:P up:P dn:H up:H up:A dn:A up:B dn:B dn:E up:E dn:T up:T", result ); } #[test] fn sim_zippychord_followup_with_prev() { let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:d t:10 d:y t:10 u:d u:y t:10 d:1 t:300", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:D t:10ms dn:BSpace up:BSpace \ up:D dn:D dn:A up:A up:Y dn:Y \ t:10ms up:D t:1ms up:Y t:9ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:LShift dn:M up:M up:LShift dn:O up:O dn:N up:N dn:D up:D dn:A up:A dn:Y up:Y", result ); } #[test] fn sim_zippychord_followup_no_prev() { let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:r t:10 u:r t:10 d:d d:f t:10 t:300", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:R t:10ms up:R t:10ms dn:D t:1ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:R up:R dn:E up:E dn:C up:C dn:I up:I dn:P up:P dn:I up:I dn:E up:E dn:N up:N dn:T up:T", result ); } #[test] fn sim_zippychord_washington() { let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:w d:spc t:10 u:w u:spc t:10 d:a d:spc t:10 u:a u:spc t:300", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:W t:1ms dn:Space t:9ms up:W t:1ms up:Space t:9ms \ dn:A t:1ms dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:LShift dn:W up:W up:LShift \ up:A dn:A dn:S up:S dn:H up:H dn:I up:I dn:N up:N dn:G up:G dn:T up:T dn:O up:O dn:N up:N \ t:9ms up:A t:1ms up:Space", result ); } #[test] fn sim_zippychord_overlap() { let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:r t:10 d:q t:10 d:a t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:R t:10ms dn:BSpace up:BSpace \ up:R dn:R dn:E up:E up:Q dn:Q dn:U up:U dn:E up:E dn:S up:S dn:T up:T t:10ms \ dn:Space up:Space \ up:A dn:A dn:S up:S dn:S up:S dn:I up:I dn:S up:S dn:T up:T up:A dn:A dn:N up:N dn:C up:C dn:E up:E", result ); let result = simulate_with_zippy_file_content(ZIPPY_CFG, "d:1 d:2 d:3 d:4 t:20", ZIPPY_FILE_CONTENT) .to_ascii(); assert_eq!( "dn:Kb1 t:1ms dn:BSpace up:BSpace dn:H up:H dn:I up:I t:1ms dn:Kb3 t:1ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:B up:B dn:Y up:Y dn:E up:E", result ); } #[test] fn sim_zippychord_lsft() { // test lsft behaviour while pressed let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:lsft t:10 d:d t:10 d:y t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:LShift t:10ms dn:D t:10ms dn:BSpace up:BSpace up:D dn:D up:LShift dn:A up:A up:Y dn:Y dn:LShift", result ); let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:lsft t:10 d:x t:10 d:y t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:LShift t:10ms dn:X t:10ms dn:BSpace up:BSpace \ dn:W up:W up:LShift up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z dn:LShift", result ); // ensure lsft-held behaviour goes away when released let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:lsft t:10 d:d u:lsft t:10 d:y t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:LShift t:10ms dn:D t:1ms up:LShift t:9ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y", result ); let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:lsft t:10 d:x u:lsft t:10 d:y t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:LShift t:10ms dn:X t:1ms up:LShift t:9ms dn:BSpace up:BSpace \ dn:LShift dn:W up:W up:LShift up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z", result ); } #[test] fn sim_zippychord_rsft() { // test rsft behaviour while pressed let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:rsft t:10 d:d t:10 d:y t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:RShift t:10ms dn:D t:10ms dn:BSpace up:BSpace up:D dn:D up:RShift dn:A up:A up:Y dn:Y dn:RShift", result ); let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:rsft t:10 d:x t:10 d:y t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:RShift t:10ms dn:X t:10ms dn:BSpace up:BSpace \ dn:W up:W up:RShift up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z dn:RShift", result ); // ensure rsft-held behaviour goes away when released let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:rsft t:10 d:d u:rsft t:10 d:y t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:RShift t:10ms dn:D t:1ms up:RShift t:9ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y", result ); let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:rsft t:10 d:x u:rsft t:10 d:y t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:RShift t:10ms dn:X t:1ms up:RShift t:9ms dn:BSpace up:BSpace \ dn:LShift dn:W up:W up:LShift up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z", result ); } #[test] fn sim_zippychord_ralt() { // test ralt behaviour while pressed let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:ralt t:10 d:d t:10 d:y t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:RAlt t:10ms dn:D t:10ms dn:BSpace up:BSpace up:RAlt up:D dn:D dn:A up:A up:Y dn:Y dn:RAlt", result ); let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:ralt t:10 d:x t:10 d:y t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:RAlt t:10ms dn:X t:10ms dn:BSpace up:BSpace \ up:RAlt dn:LShift dn:W up:W up:LShift up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z dn:RAlt", result ); // ensure rsft-held behaviour goes away when released let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:ralt t:10 d:d u:ralt t:10 d:y t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:RAlt t:10ms dn:D t:1ms up:RAlt t:9ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y", result ); let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:ralt t:10 d:x u:ralt t:10 d:y t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:RAlt t:10ms dn:X t:1ms up:RAlt t:9ms dn:BSpace up:BSpace \ dn:LShift dn:W up:W up:LShift up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z", result ); } #[test] fn sim_zippychord_caps_word() { let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:lalt u:lalt t:10 d:d t:10 d:y t:10 u:d u:y t:10 d:spc u:spc t:2000 d:d d:y t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "t:10ms dn:LShift dn:D t:10ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y \ t:10ms up:D t:1ms up:LShift up:Y t:9ms dn:Space t:1ms up:Space \ t:1999ms dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y", result ); let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:lalt t:10 d:y t:10 d:x t:10 u:x u:y t:10 d:spc u:spc t:1000 d:y d:x t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "t:10ms dn:LShift dn:Y t:10ms dn:BSpace up:BSpace \ dn:W up:W up:X dn:X up:Y dn:Y dn:Z up:Z \ t:10ms up:X t:1ms up:LShift up:Y t:9ms dn:Space t:1ms up:Space \ t:999ms dn:Y t:1ms dn:BSpace up:BSpace dn:LShift dn:W up:W up:LShift \ up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z", result ); } #[test] fn sim_zippychord_triple_combo() { let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:. d:g t:10 u:. u:g d:f t:10 u:f d:p t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:Dot t:1ms dn:BSpace up:BSpace up:G dn:G dn:I up:I dn:T up:T t:9ms up:Dot t:1ms up:G \ t:1ms dn:F t:8ms up:F t:1ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:G up:G dn:I up:I dn:T up:T dn:Space up:Space \ dn:F up:F dn:E up:E dn:T up:T dn:C up:C dn:H up:H dn:Space up:Space \ dn:Minus up:Minus up:P dn:P", result ); } #[test] fn sim_zippychord_disabled_by_typing() { let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:v u:v t:10 d:d d:y t:100", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!("dn:V t:1ms up:V t:9ms dn:D t:1ms dn:Y", result); } #[test] fn sim_zippychord_prefix() { let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:p d:r u:p u:r t:10 d:q u:q t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E dn:Space up:Space \ dn:BSpace up:BSpace t:1ms up:P t:1ms up:R t:7ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:U up:U dn:L up:L dn:L up:L dn:Space up:Space \ dn:R up:R dn:E up:E up:Q dn:Q dn:U up:U dn:E up:E dn:S up:S dn:T up:T t:1ms up:Q", result ); let result = simulate_with_zippy_file_content( ZIPPY_CFG, "d:p d:r d:a t:10 u:d u:r u:a", ZIPPY_FILE_CONTENT, ) .to_ascii() .no_time() .no_releases(); assert_eq!( "dn:P dn:BSpace \ dn:P dn:R dn:E dn:Space dn:BSpace \ dn:BSpace dn:BSpace dn:A dn:R dn:T dn:N dn:E dn:R", result ); } #[test] fn sim_zippychord_smartspace_full() { let result = simulate_with_zippy_file_content( "(defsrc)(deflayer base)(defzippy file smart-space full)", "d:d d:y t:10 u:d u:y t:100 d:. t:10 u:. t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y dn:Space up:Space \ t:9ms up:D t:1ms up:Y t:99ms dn:BSpace up:BSpace dn:Dot t:10ms up:Dot", result ); // Test that prefix works as intended. let result = simulate_with_zippy_file_content( "(defsrc)(deflayer base)(defzippy file smart-space add-space-only)", "d:p d:r t:10 u:p u:r t:100 d:. t:10 u:. t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E \ dn:Space up:Space dn:BSpace up:BSpace \ t:9ms up:P t:1ms up:R t:99ms dn:Dot t:10ms up:Dot", result ); } #[test] fn sim_zippychord_smartspace_spaceonly() { let result = simulate_with_zippy_file_content( "(defsrc)(deflayer base)(defzippy file smart-space add-space-only)", "d:d d:y t:10 u:d u:y t:100 d:. t:10 u:. t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y dn:Space up:Space \ t:9ms up:D t:1ms up:Y t:99ms dn:Dot t:10ms up:Dot", result ); // Test that prefix works as intended. let result = simulate_with_zippy_file_content( "(defsrc)(deflayer base)(defzippy file smart-space add-space-only)", "d:p d:r t:10 u:p u:r t:100 d:. t:10 u:. t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E \ dn:Space up:Space dn:BSpace up:BSpace \ t:9ms up:P t:1ms up:R t:99ms dn:Dot t:10ms up:Dot", result ); } #[test] fn sim_zippychord_smartspace_none() { let result = simulate_with_zippy_file_content( "(defsrc)(deflayer base)(defzippy file smart-space none)", "d:d d:y t:10 u:d u:y t:100 d:. t:10 u:. t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y \ t:9ms up:D t:1ms up:Y t:99ms dn:Dot t:10ms up:Dot", result ); // Test that prefix works as intended. let result = simulate_with_zippy_file_content( "(defsrc)(deflayer base)(defzippy file smart-space add-space-only)", "d:p d:r t:10 u:p u:r t:100 d:. t:10 u:. t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E \ dn:Space up:Space dn:BSpace up:BSpace \ t:9ms up:P t:1ms up:R t:99ms dn:Dot t:10ms up:Dot", result ); } #[test] fn sim_zippychord_smartspace_overlap() { let result = simulate_with_zippy_file_content( "(defsrc)(deflayer base)(defzippy file smart-space full)", "d:r t:10 d:q t:10 d:a t:10", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:R t:10ms dn:BSpace up:BSpace \ up:R dn:R dn:E up:E up:Q dn:Q dn:U up:U dn:E up:E dn:S up:S dn:T up:T dn:Space up:Space t:10ms \ dn:BSpace up:BSpace dn:Space up:Space \ up:A dn:A dn:S up:S dn:S up:S dn:I up:I dn:S up:S dn:T up:T up:A dn:A dn:N up:N dn:C up:C dn:E up:E \ dn:Space up:Space", result ); let result = simulate_with_zippy_file_content( "(defsrc)(deflayer base)(defzippy file smart-space full)", "d:1 d:2 d:3 d:4 t:20", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:Kb1 t:1ms dn:BSpace up:BSpace dn:H up:H dn:I up:I dn:Space up:Space \ t:1ms dn:Kb3 t:1ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:B up:B dn:Y up:Y dn:E up:E dn:Space up:Space", result ); } #[test] fn sim_zippychord_smartspace_followup() { let result = simulate_with_zippy_file_content( "(defsrc)(deflayer base)(defzippy file smart-space full)", "d:d t:10 d:y t:10 u:d u:y t:10 d:1 t:300", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:D t:10ms dn:BSpace up:BSpace \ up:D dn:D dn:A up:A up:Y dn:Y dn:Space up:Space \ t:10ms up:D t:1ms up:Y t:9ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:LShift dn:M up:M up:LShift dn:O up:O dn:N up:N dn:D up:D dn:A up:A dn:Y up:Y dn:Space up:Space", result ); } const CUSTOM_PUNC_CFG: &str = "\ (defsrc) (deflayer base) (defzippy file smart-space full smart-space-punctuation (z ! ® *) output-character-mappings ( ® AG-r * S-AG-v ! S-1))"; #[test] fn sim_zippychord_smartspace_custom_punc() { // 1 without lsft: no smart-space-erase let result = simulate_with_zippy_file_content( CUSTOM_PUNC_CFG, "d:d t:10 d:y t:10 u:d u:y t:10 d:1 t:300", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:D t:10ms dn:BSpace up:BSpace \ up:D dn:D dn:A up:A up:Y dn:Y dn:Space up:Space \ t:10ms up:D t:1ms up:Y t:9ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:LShift dn:M up:M up:LShift dn:O up:O dn:N up:N dn:D up:D dn:A up:A dn:Y up:Y dn:Space up:Space", result ); // S-1 = !: smart-space-erase let result = simulate_with_zippy_file_content( CUSTOM_PUNC_CFG, "d:1 d:2 t:10 u:1 u:2 t:10 d:lsft d:1 u:1 u:lsft t:300", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:Kb1 t:1ms dn:BSpace up:BSpace \ dn:H up:H dn:I up:I dn:Space up:Space t:9ms \ up:Kb1 t:1ms up:Kb2 t:9ms \ dn:LShift t:1ms dn:BSpace up:BSpace dn:Kb1 t:1ms up:Kb1 t:1ms up:LShift", result ); // z: smart-space-erase let result = simulate_with_zippy_file_content( CUSTOM_PUNC_CFG, "d:1 d:2 t:10 u:1 u:2 t:10 d:z u:z t:300", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:Kb1 t:1ms dn:BSpace up:BSpace \ dn:H up:H dn:I up:I dn:Space up:Space t:9ms \ up:Kb1 t:1ms up:Kb2 t:9ms \ dn:BSpace up:BSpace dn:Z t:1ms up:Z", result ); // r no altgr: no smart-space-erase let result = simulate_with_zippy_file_content( CUSTOM_PUNC_CFG, "d:1 d:2 t:10 u:1 u:2 t:10 d:r u:r t:300", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:Kb1 t:1ms dn:BSpace up:BSpace \ dn:H up:H dn:I up:I dn:Space up:Space t:9ms \ up:Kb1 t:1ms up:Kb2 t:9ms \ dn:R t:1ms up:R", result ); // r with altgr: smart-space-erase let result = simulate_with_zippy_file_content( CUSTOM_PUNC_CFG, "d:1 d:2 t:10 u:1 u:2 t:10 d:ralt d:r u:r u:ralt t:300", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:Kb1 t:1ms dn:BSpace up:BSpace \ dn:H up:H dn:I up:I dn:Space up:Space t:9ms \ up:Kb1 t:1ms up:Kb2 t:9ms \ dn:RAlt t:1ms dn:BSpace up:BSpace dn:R t:1ms up:R t:1ms up:RAlt", result ); // v with altgr+lsft: smart-space-erase let result = simulate_with_zippy_file_content( CUSTOM_PUNC_CFG, "d:1 d:2 t:10 u:1 u:2 t:10 d:ralt d:lsft d:v u:v u:ralt u:lsft t:300", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:Kb1 t:1ms dn:BSpace up:BSpace \ dn:H up:H dn:I up:I dn:Space up:Space t:9ms \ up:Kb1 t:1ms up:Kb2 t:9ms \ dn:RAlt t:1ms dn:LShift t:1ms dn:BSpace up:BSpace dn:V t:1ms up:V t:1ms up:RAlt t:1ms up:LShift", result ); } #[test] fn sim_zippychord_non_followup_subsequent_with_potential_followups_available() { let result = simulate_with_zippy_file_content( "(defsrc)(deflayer base)(defzippy file smart-space full)", "d:g d:. t:10 u:g u:. t:1000 d:g d:. t:10 u:g u:. t:1000", ZIPPY_FILE_CONTENT, ) .to_ascii(); assert_eq!( "dn:G t:1ms dn:BSpace up:BSpace up:G dn:G dn:I up:I dn:T up:T dn:Space up:Space t:9ms \ up:G t:1ms up:Dot t:999ms \ dn:G t:1ms dn:BSpace up:BSpace up:G dn:G dn:I up:I dn:T up:T dn:Space up:Space t:9ms \ up:G t:1ms up:Dot", result ); } const DEAD_KEYS_CFG: &str = "\ (defsrc) (deflayer base) (defzippy file smart-space full output-character-mappings ( ’ (no-erase ') ‘ (no-erase `) é (single-output ' e) è (single-output ` e) ))"; static DEAD_KEYS_FILE_CONTENT: &str = " by h’elo bye by‘e by d ft‘a’ng by d a aye cy hélo cye byè cy d ftéèng cy d a aye "; #[test] fn sim_zippychord_noerase() { let result = simulate_with_zippy_file_content( DEAD_KEYS_CFG, "d:b d:y t:100 d:e u:b u:y u:e t:1000", DEAD_KEYS_FILE_CONTENT, ) .no_releases() .no_time() .to_ascii(); assert_eq!( "dn:B dn:BSpace dn:H dn:Quote dn:E dn:L dn:O dn:Space \ dn:BSpace dn:BSpace dn:BSpace dn:BSpace dn:BSpace \ dn:B dn:Y dn:Grave dn:E dn:Space", result, ); let result = simulate_with_zippy_file_content( DEAD_KEYS_CFG, "d:b d:y t:100 u:b u:y d:d t:10 u:d d:a t:10 u:a t:1000", DEAD_KEYS_FILE_CONTENT, ) .no_releases() .no_time() .to_ascii(); assert_eq!( "dn:B dn:BSpace dn:H dn:Quote dn:E dn:L dn:O dn:Space \ dn:BSpace dn:BSpace dn:BSpace dn:BSpace dn:BSpace \ dn:F dn:T dn:Grave dn:A dn:Quote dn:N dn:G dn:Space \ dn:BSpace dn:BSpace dn:BSpace dn:BSpace dn:BSpace dn:BSpace \ dn:A dn:Y dn:E dn:Space", result, ); } #[test] fn sim_zippychord_single_output() { let result = simulate_with_zippy_file_content( DEAD_KEYS_CFG, "d:c d:y t:100 d:e u:c u:y u:e t:1000", DEAD_KEYS_FILE_CONTENT, ) .no_releases() .no_time() .to_ascii(); assert_eq!( "dn:C dn:BSpace dn:H dn:Quote dn:E dn:L dn:O dn:Space \ dn:BSpace dn:BSpace dn:BSpace dn:BSpace dn:BSpace \ dn:B dn:Y dn:Grave dn:E dn:Space", result, ); let result = simulate_with_zippy_file_content( DEAD_KEYS_CFG, "d:c d:y t:100 u:c u:y d:d t:10 u:d d:a t:10 u:a t:1000", DEAD_KEYS_FILE_CONTENT, ) .no_releases() .no_time() .to_ascii(); assert_eq!( "dn:C dn:BSpace dn:H dn:Quote dn:E dn:L dn:O dn:Space \ dn:BSpace dn:BSpace dn:BSpace dn:BSpace dn:BSpace \ dn:F dn:T dn:Quote dn:E dn:Grave dn:E dn:N dn:G dn:Space \ dn:BSpace dn:BSpace dn:BSpace dn:BSpace dn:BSpace dn:BSpace dn:BSpace \ dn:A dn:Y dn:E dn:Space", result, ); } kanata-1.9.0/src/tests.rs000064400000000000000000000104021046102023000133660ustar 00000000000000use kanata_parser::cfg::*; use std::sync::Mutex; #[cfg(all( feature = "simulated_output", not(feature = "simulated_input"), not(target_os = "macos"), not(feature = "interception_driver") ))] mod sim_tests; static CFG_PARSE_LOCK: Mutex<()> = Mutex::new(()); fn init_log() { use simplelog::*; use std::sync::OnceLock; static LOG_INIT: OnceLock<()> = OnceLock::new(); LOG_INIT.get_or_init(|| { let mut log_cfg = ConfigBuilder::new(); if let Err(e) = log_cfg.set_time_offset_to_local() { eprintln!("WARNING: could not set log TZ to local: {e:?}"); }; log_cfg.set_time_format_rfc3339(); CombinedLogger::init(vec![TermLogger::new( // Note: set to a different level to see logs in tests. // Also, not all tests call init_log so you might have to add the call there too. LevelFilter::Off, log_cfg.build(), TerminalMode::Stderr, ColorChoice::AlwaysAnsi, )]) .expect("logger can init"); }); } #[test] fn parse_simple() { init_log(); let _lk = match CFG_PARSE_LOCK.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; new_from_file(&std::path::PathBuf::from("./cfg_samples/simple.kbd")).unwrap(); } #[test] fn parse_minimal() { init_log(); let _lk = match CFG_PARSE_LOCK.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; new_from_file(&std::path::PathBuf::from("./cfg_samples/minimal.kbd")).unwrap(); } #[test] fn parse_deflayermap() { init_log(); let _lk = match CFG_PARSE_LOCK.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; new_from_file(&std::path::PathBuf::from("./cfg_samples/deflayermap.kbd")).unwrap(); } #[test] fn parse_default() { init_log(); let _lk = match CFG_PARSE_LOCK.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; new_from_file(&std::path::PathBuf::from("./cfg_samples/kanata.kbd")).unwrap(); } #[test] fn parse_jtroo() { init_log(); let _lk = match CFG_PARSE_LOCK.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; let cfg = new_from_file(&std::path::PathBuf::from("./cfg_samples/jtroo.kbd")).unwrap(); assert_eq!(cfg.layer_info.len(), 8); } #[test] fn parse_f13_f24() { init_log(); let _lk = match CFG_PARSE_LOCK.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; new_from_file(&std::path::PathBuf::from("./cfg_samples/f13_f24.kbd")).unwrap(); } #[test] fn parse_home_row_mods() { init_log(); let _lk = match CFG_PARSE_LOCK.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; new_from_file(&std::path::PathBuf::from( "./cfg_samples/home-row-mod-basic.kbd", )) .unwrap(); new_from_file(&std::path::PathBuf::from( "./cfg_samples/home-row-mod-advanced.kbd", )) .unwrap(); } #[test] fn parse_press_release_toggle_vkeys() { init_log(); let _lk = match CFG_PARSE_LOCK.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; new_from_file(&std::path::PathBuf::from( "./cfg_samples/key-toggle_press-only_release-only.kbd", )) .unwrap(); } #[test] fn parse_automousekeys_only() { init_log(); let _lk = match CFG_PARSE_LOCK.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; new_from_file(&std::path::PathBuf::from( "./cfg_samples/automousekeys-only.kbd", )) .unwrap(); } #[test] fn parse_automousekeys_full_map() { init_log(); let _lk = match CFG_PARSE_LOCK.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; new_from_file(&std::path::PathBuf::from( "./cfg_samples/automousekeys-full-map.kbd", )) .unwrap(); } #[test] #[cfg(target_pointer_width = "64")] fn sizeof_state() { init_log(); assert_eq!( std::mem::size_of::< kanata_keyberon::layout::State< &'static &'static [&'static kanata_parser::custom_action::CustomAction], >, >(), 2 * std::mem::size_of::() ); }