diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..6313b56c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 19905a3b8..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: 'Bug: title_goes_here' -labels: bug -assignees: jtroo - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**Version** -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. - -**Relevant kanata configs** -E.g. defcfg, defsrc, deflayer, defalias items. If in doubt, feel free to include your entire config. - -**To reproduce** -Steps to reproduce the behaviour. - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Debug logs** -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. - -**Operating system** -Linux or Windows? - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..35a58655d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,79 @@ +name: "Bug report" +description: Create a report to help the project 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. + Ensure any config snippets are either in the next section or are code formatted. + validations: + required: true + - type: textarea + id: config + attributes: + label: Relevant kanata config + render: text + description: E.g. defcfg, defsrc, deflayer, defalias items. If in doubt, feel free to include your entire config. + validations: + required: false + - type: textarea + id: reproduce + attributes: + label: To Reproduce + description: | + Walk through the steps needed to reproduce the bug. + Use the simulator if it is not device/OS related: https://jtroo.github.io/. + 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. + render: text + validations: + required: false + - type: input + id: os + attributes: + label: Operating system and I/O mechanism + description: E.g. Linux, macOS, Windows 10, Windows 11 with Interception driver + placeholder: e.g. Linux + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: Add any other context about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..8a1826f17 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Discussions + url: https://github.com/jtroo/kanata/discussions + about: Ask for help or interact with the community. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index a988ccbe9..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: 'Feature request: feature_summary_goes_here' -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..f6f08c91c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,35 @@ +name: "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 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..df26b4524 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +## 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 diff --git a/.github/workflows/build-everything.yml b/.github/workflows/build-everything.yml new file mode 100644 index 000000000..3d839bb33 --- /dev/null +++ b/.github/workflows/build-everything.yml @@ -0,0 +1,17 @@ +name: build-everything + +on: + workflow_dispatch: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-Dwarnings" + +jobs: + build-linux: + uses: ./.github/workflows/linux-build.yml + build-windows: + uses: ./.github/workflows/windows-build.yml + build-macos: + uses: ./.github/workflows/macos-build.yml diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml new file mode 100644 index 000000000..81d8ec4a0 --- /dev/null +++ b/.github/workflows/linux-build.yml @@ -0,0 +1,32 @@ +name: linux-build + +on: + workflow_dispatch: + branches: [ "main" ] + workflow_call: + +env: + CARGO_TERM_COLOR: always + +jobs: + build-linux-x64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "persist-cross-job-linux-x64" + - name: Do the stuff on x64 ubuntu linux + shell: bash + run: | + mkdir -p artifacts-x64 + cargo build --release + mv target/release/kanata artifacts-x64/kanata_linux_x64 + cargo build --release --features cmd + mv target/release/kanata artifacts-x64/kanata_linux_cmd_allowed_x64 + - uses: actions/upload-artifact@v7 + with: + name: linux-binaries-x64 + path: | + artifacts-x64/kanata_linux_x64 + artifacts-x64/kanata_linux_cmd_allowed_x64 diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml new file mode 100644 index 000000000..d984626d3 --- /dev/null +++ b/.github/workflows/macos-build.yml @@ -0,0 +1,60 @@ +name: macos-build + +on: + workflow_dispatch: + branches: [ "main" ] + workflow_call: + +env: + CARGO_TERM_COLOR: always + +jobs: + build-macos-arm64: + runs-on: macos-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + target: aarch64-apple-darwin + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "persist-cross-job-macos-aarch64" + - 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@v7 + with: + name: macos-binaries-arm64 + path: | + artifacts-arm64/kanata_macos_arm64 + artifacts-arm64/kanata_macos_cmd_allowed_arm64 + + build-macos-x64: + runs-on: macos-15-intel + + steps: + - uses: actions/checkout@v6 + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "persist-cross-job-macos-x64" + - name: Do the stuff on x64 + shell: bash + run: | + mkdir -p artifacts + cargo build --release + mv target/release/kanata artifacts/kanata_macos_x64 + cargo build --release --features cmd + mv target/release/kanata artifacts/kanata_macos_cmd_allowed_x64 + - uses: actions/upload-artifact@v7 + with: + name: macos-binaries-x64 + path: | + artifacts/kanata_macos_x64 + artifacts/kanata_macos_cmd_allowed_x64 + diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b1567062e..ca0dc5f8b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -6,18 +6,23 @@ on: paths: - Cargo.* - src/**/* + - keyberon/**/* - cfg_samples/**/* - - test_cfgs/**/* - - .github/workflows/**/* + - parser/**/* + - tcp_protocol/**/* + - wasm/**/* + - .github/workflows/rust.yml pull_request: branches: [ "main" ] paths: - Cargo.* - src/**/* - keyberon/**/* + - parser/**/* - cfg_samples/**/* - - test_cfgs/**/* - - .github/workflows/**/* + - tcp_protocol/**/* + - wasm/**/* + - .github/workflows/rust.yml env: CARGO_TERM_COLOR: always @@ -28,46 +33,147 @@ jobs: fmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Check fmt run: cargo fmt --all --check + build-android: + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - build: linux + os: ubuntu-latest + target: aarch64-linux-android + + steps: + - uses: actions/checkout@v6 + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "persist-cross-job" + workspaces: ./ + - run: rustup target add aarch64-linux-android + + - name: Build for Android + run: cargo check --target aarch64-linux-android + build-test-clippy-linux: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - build: linux + os: ubuntu-latest + target: x86_64-unknown-linux-musl + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - uses: Swatinem/rust-cache@v2 with: shared-key: "persist-cross-job" workspaces: ./ - run: rustup component add clippy - - name: Run tests - run: cargo test --verbose - - name: Run tests all features - run: cargo test --all-features --verbose + + - name: Run tests no features + run: cargo test --all --no-default-features - name: Run clippy no features - run: cargo clippy -- -D warnings - - name: Run clippy all features - run: cargo clippy --all-features -- -D warnings + 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 tests simulated output on_idle + run: cargo test --features=simulated_output -- must_be_single_threaded --ignored --test-threads=1 + - 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: windows-latest + runs-on: ${{ matrix.os }} strategy: matrix: - target: - - x86_64-pc-windows-msvc + include: + - build: windows + os: windows-latest + target: x86_64-pc-windows-msvc + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - uses: Swatinem/rust-cache@v2 with: shared-key: "persist-cross-job" workspaces: ./ - run: rustup component add clippy - - name: Run tests - run: cargo test --verbose - - name: Run tests all features - run: cargo test --all-features --verbose + + - name: Run tests no features + run: cargo test --all --no-default-features - name: Run clippy no features - run: cargo clippy + 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 -p kanata -p kanata-parser -p kanata-keyberon -p kanata-tcp-protocol --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 tests simulated output on_idle + run: cargo test --features=simulated_output -- sim_tests::vkey_sim_tests::on_idle --ignored + - 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@v6 + - 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 + run: cargo clippy --all --features=cmd -- -D warnings diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml new file mode 100644 index 000000000..3046ce9ad --- /dev/null +++ b/.github/workflows/windows-build.yml @@ -0,0 +1,83 @@ +name: windows-build + +on: + workflow_dispatch: + branches: [ "main" ] + workflow_call: + +env: + CARGO_TERM_COLOR: always + +jobs: + build-windows-x64: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v6 + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "persist-cross-job-win-x64" + - name: Build x64 + shell: powershell + run: | + md artifacts + cargo build --release --features win_manifest,win_sendinput_send_scancodes,win_llhook_read_scancodes --target x86_64-pc-windows-msvc + mv target/x86_64-pc-windows-msvc/release/kanata.exe artifacts/kanata_windows_tty_winIOv2_x64.exe + cargo build --release --features win_manifest,win_sendinput_send_scancodes,win_llhook_read_scancodes,cmd --target x86_64-pc-windows-msvc + mv target/x86_64-pc-windows-msvc/release/kanata.exe artifacts/kanata_windows_tty_winIOv2_cmd_allowed_x64.exe + cargo build --release --features win_manifest,interception_driver --target x86_64-pc-windows-msvc + mv target/x86_64-pc-windows-msvc/release/kanata.exe artifacts/kanata_windows_tty_wintercept_x64.exe + cargo build --release --features win_manifest,cmd,interception_driver --target x86_64-pc-windows-msvc + mv target/x86_64-pc-windows-msvc/release/kanata.exe artifacts/kanata_windows_tty_wintercept_cmd_allowed_x64.exe + cargo build --release --features gui,win_manifest,win_sendinput_send_scancodes,win_llhook_read_scancodes --target x86_64-pc-windows-msvc + mv target/x86_64-pc-windows-msvc/release/kanata.exe artifacts/kanata_windows_gui_winIOv2_x64.exe + cargo build --release --features gui,win_manifest,win_sendinput_send_scancodes,win_llhook_read_scancodes,cmd --target x86_64-pc-windows-msvc + mv target/x86_64-pc-windows-msvc/release/kanata.exe artifacts/kanata_windows_gui_winIOv2_cmd_allowed_x64.exe + cargo build --release --features gui,win_manifest,interception_driver --target x86_64-pc-windows-msvc + mv target/x86_64-pc-windows-msvc/release/kanata.exe artifacts/kanata_windows_gui_wintercept_x64.exe + cargo build --release --features gui,win_manifest,cmd,interception_driver --target x86_64-pc-windows-msvc + mv target/x86_64-pc-windows-msvc/release/kanata.exe artifacts/kanata_windows_gui_wintercept_cmd_allowed_x64.exe + cargo build --release --features passthru_ahk --package=simulated_passthru --target x86_64-pc-windows-msvc + mv target/x86_64-pc-windows-msvc/release/kanata_passthru.dll artifacts/kanata_passthru_x64.dll + - uses: actions/upload-artifact@v7 + with: + name: windows-binaries-x64 + path: | + artifacts/kanata_windows_tty_winIOv2_x64.exe + artifacts/kanata_windows_tty_winIOv2_cmd_allowed_x64.exe + artifacts/kanata_windows_tty_wintercept_x64.exe + artifacts/kanata_windows_tty_wintercept_cmd_allowed_x64.exe + artifacts/kanata_windows_gui_winIOv2_x64.exe + artifacts/kanata_windows_gui_winIOv2_cmd_allowed_x64.exe + artifacts/kanata_windows_gui_wintercept_x64.exe + artifacts/kanata_windows_gui_wintercept_cmd_allowed_x64.exe + artifacts/kanata_passthru_x64.dll + + build-windows-arm64: + runs-on: windows-11-arm + + steps: + - uses: actions/checkout@v6 + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "persist-cross-job-win-arm64" + - name: Build arm64 + shell: powershell + run: | + md artifacts + cargo build --release --features win_manifest,win_sendinput_send_scancodes,win_llhook_read_scancodes --target aarch64-pc-windows-msvc + mv target/aarch64-pc-windows-msvc/release/kanata.exe artifacts/kanata_windows_tty_winIOv2_arm64.exe + cargo build --release --features win_manifest,win_sendinput_send_scancodes,win_llhook_read_scancodes,cmd --target aarch64-pc-windows-msvc + mv target/aarch64-pc-windows-msvc/release/kanata.exe artifacts/kanata_windows_tty_winIOv2_cmd_allowed_arm64.exe + cargo build --release --features gui,win_manifest,win_sendinput_send_scancodes,win_llhook_read_scancodes --target aarch64-pc-windows-msvc + mv target/aarch64-pc-windows-msvc/release/kanata.exe artifacts/kanata_windows_gui_winIOv2_arm64.exe + cargo build --release --features gui,win_manifest,win_sendinput_send_scancodes,win_llhook_read_scancodes,cmd --target aarch64-pc-windows-msvc + mv target/aarch64-pc-windows-msvc/release/kanata.exe artifacts/kanata_windows_gui_winIOv2_cmd_allowed_arm64.exe + - uses: actions/upload-artifact@v7 + with: + name: windows-binaries-arm64 + path: | + artifacts/kanata_windows_tty_winIOv2_arm64.exe + artifacts/kanata_windows_tty_winIOv2_cmd_allowed_arm64.exe + artifacts/kanata_windows_gui_winIOv2_arm64.exe + artifacts/kanata_windows_gui_winIOv2_cmd_allowed_arm64.exe diff --git a/.gitignore b/.gitignore index dc4969608..c2d6d6a9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ **/target .vscode/ +CLAUDE.md +.DS_Store +PLAN.md + +# Manual testing files +test_*.kbd +manual_test/ +*.test.kbd diff --git a/Cargo.lock b/Cargo.lock index ec29f5ce8..18bca0ac5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,88 +1,138 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" -version = "0.19.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] -name = "ahash" -version = "0.7.6" +name = "aho-corasick" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ - "getrandom", - "once_cell", - "version_check", + "memchr", ] [[package]] -name = "aho-corasick" -version = "1.0.1" +name = "anstream" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" dependencies = [ - "memchr", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +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.4.5" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ffd3d69bd89910509a5d31d1f1353f38ccffdd116dd0099bbd6627f7bd8ad8" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "atomic-polyfill" -version = "0.1.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" dependencies = [ "critical-section", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.67" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -100,6 +150,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + [[package]] name = "bitvec" version = "1.0.1" @@ -112,17 +168,49 @@ dependencies = [ "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.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytemuck" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" + [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +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.0.79" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +dependencies = [ + "jobserver", + "libc", + "shlex", +] [[package]] name = "cfg-if" @@ -130,34 +218,39 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" -version = "4.3.0" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.0" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ + "anstream", "anstyle", - "bitflags", "clap_lex", "strsim", ] [[package]] name = "clap_derive" -version = "4.3.0" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", @@ -167,15 +260,132 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.0" +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 = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +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.9.1", + "core-foundation 0.10.1", + "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 = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +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.9.1", + "core-foundation 0.10.1", + "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.1.1" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "deranged" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6548a0ad5d2549e111e1f6a11a6c2e2d00ce6a3dafe22948d67c2b443f775e52" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] [[package]] name = "dirs" @@ -198,6 +408,20 @@ dependencies = [ "windows-sys 0.48.0", ] +[[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" @@ -210,51 +434,107 @@ 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.1" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ - "errno-dragonfly", "libc", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "error-code" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "evdev" -version = "0.12.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78122445d7c3ce840f1a4f31eb7cee198b9004d03bfc161762d9b4a959d757c" +checksum = "a3c10865aeab1a7399b3c2d6046e8dcc7f5227b656f235ed63ef5ee45a47b8f8" dependencies = [ "bitvec", "cfg-if", "libc", - "nix 0.23.2", - "thiserror", + "nix 0.29.0", ] +[[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.9" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -263,9 +543,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.2" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "hash32" @@ -278,18 +558,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "heapless" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ "atomic-polyfill", "hash32", @@ -300,33 +577,52 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +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 = "1.9.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ - "autocfg", + "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.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf888f9575c290197b2c948dc9e9ff10bd1a39ad1ea8585f734585fa6b9d3f9" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" dependencies = [ - "bitflags", + "bitflags 1.3.2", "inotify-sys", "libc", ] @@ -347,77 +643,131 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abe875cf6439eb5d98f0fdbcc6128fdef4870c47e0f898735105ff77685dfcd1" [[package]] -name = "io-lifetimes" -version = "1.0.10" +name = "is-docker" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", + "once_cell", ] [[package]] name = "is-terminal" -version = "0.4.7" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ "hermit-abi", - "io-lifetimes", - "rustix", - "windows-sys 0.48.0", + "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.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.6" +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.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] [[package]] name = "kanata" -version = "1.4.0-prerelease-2" +version = "1.12.0-prerelease-2" dependencies = [ "anyhow", + "arboard", "clap", + "core-foundation 0.10.1", + "core-graphics 0.24.0", "dirs", + "embed-resource", "encode_unicode", "evdev", + "indoc", "inotify", "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.2", + "nix 0.26.4", + "objc", "once_cell", + "open", + "os_pipe", "parking_lot", "radix_trie", + "regex", "rustc-hash", "sd-notify", - "serde", "serde_json", "signal-hook", "simplelog", + "strip-ansi-escapes", + "time", + "web-time", "winapi", + "windows-sys 0.52.0", ] [[package]] name = "kanata-interception" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43d8fef551ec7944871b21ad005f3728999fc1ebad09f9625db5c692d42f44e" +checksum = "e2e57b9b964d19ac86906ea56e8a88001eb03c2b9cb96e4127225438750bb606" dependencies = [ - "bitflags", + "bitflags 1.3.2", "interception-sys", "num_enum", "serde", @@ -425,11 +775,12 @@ dependencies = [ [[package]] name = "kanata-keyberon" -version = "0.18.0" +version = "0.1120.1" dependencies = [ "arraydeque", "heapless", "kanata-keyberon-macros", + "rustc-hash", ] [[package]] @@ -444,43 +795,112 @@ dependencies = [ [[package]] name = "kanata-parser" -version = "0.1.0" +version = "0.1120.1" dependencies = [ "anyhow", - "evdev", + "bitflags 2.9.1", + "bytemuck", "kanata-keyberon", "log", "miette", "once_cell", + "ordered-float", "parking_lot", - "radix_trie", + "patricia_tree", "rustc-hash", + "simplelog", "thiserror", ] +[[package]] +name = "kanata-sim" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "dirs", + "kanata", + "log", + "simplelog", + "time", +] + +[[package]] +name = "kanata-tcp-protocol" +version = "0.1120.1" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "kanata-wasm" +version = "0.1.0" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "kanata", + "log", + "rustc-hash", + "wasm-bindgen", +] + +[[package]] +name = "kanata_example_tcp_client" +version = "1.1.0" +dependencies = [ + "anyhow", + "clap", + "kanata-tcp-protocol", + "log", + "serde_json", + "simplelog", +] + +[[package]] +name = "karabiner-driverkit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97e4fc764f78a4a5cbf705c9502a51a0ba4931dbee25f4fbc59654059f6ca9e9" +dependencies = [ + "cc", + "os_info", +] + [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.1", + "libc", +] [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -488,27 +908,24 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] -name = "memchr" -version = "2.5.0" +name = "malloc_buf" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] [[package]] -name = "memoffset" -version = "0.6.5" +name = "memchr" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" @@ -521,9 +938,9 @@ dependencies = [ [[package]] name = "miette" -version = "5.9.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a236ff270093b0b67451bc50a509bd1bad302cb1d3c7d37d5efe931238581fa9" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" dependencies = [ "backtrace", "backtrace-ext", @@ -542,9 +959,9 @@ dependencies = [ [[package]] name = "miette-derive" -version = "5.9.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4901771e1d44ddb37964565c654a3223ba41a594d02b8da471cc4464912b5cfa" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", @@ -553,33 +970,47 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.45.0", + "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", + "bitflags 1.3.2", "lazy_static", + "muldiv 0.2.1", "winapi", "winapi-build", ] @@ -595,29 +1026,42 @@ dependencies = [ [[package]] name = "nix" -version = "0.23.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ - "bitflags", - "cc", + "bitflags 1.3.2", "cfg-if", "libc", - "memoffset 0.6.5", + "memoffset", + "pin-utils", ] [[package]] name = "nix" -version = "0.26.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.9.1", "cfg-if", + "cfg_aliases", "libc", - "memoffset 0.7.1", - "pin-utils", - "static_assertions", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", ] [[package]] @@ -643,27 +1087,146 @@ dependencies = [ [[package]] name = "num_threads" -version = "0.1.6" +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 = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ + "bitflags 2.9.1", + "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.9.1", + "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.9.1", + "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.9.1", + "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.9.1", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", ] [[package]] name = "object" -version = "0.30.3" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.17.1" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +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" @@ -671,6 +1234,36 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + +[[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" @@ -679,25 +1272,40 @@ checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] -name = "parking_lot_core" -version = "0.9.7" +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.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "edb45b6331bbdbb54c9a29413703e892ab94f83a31e4a546c778495a91e7fbca" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-sys 0.45.0", + "bitflags 2.9.1", ] [[package]] @@ -706,6 +1314,25 @@ 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" @@ -713,23 +1340,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", ] [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.27" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -752,29 +1379,41 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.9.1", ] [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", - "redox_syscall", + "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.8.2" +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 = "d1a59b5d8e97dee33696bf13c5ba8ab85341c002922fba050069326b9c498974" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -783,15 +1422,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -801,65 +1440,80 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.37.19" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.9.1", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sd-notify" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "621e3680f3e07db4c9c2c3fb07c6223ab2fab2e54bd3c04c3ae037990f428c32" +checksum = "1be20c5f7f393ee700f8b2f28ea35812e4e212f40774b550cd2a93ea91684451" [[package]] name = "semver" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -868,20 +1522,36 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +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.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", @@ -889,35 +1559,59 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +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.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acee08041c5de3d5048c8b3f6f13fafb3026b24ba43c6a695a0c76179b844369" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" dependencies = [ "log", "termcolor", "time", ] +[[package]] +name = "simulated_passthru" +version = "0.0.1" +dependencies = [ + "anyhow", + "encode_unicode", + "kanata", + "kanata-interception", + "lazy_static", + "log", + "native-windows-gui", + "parking_lot", + "regex", + "widestring", + "win_dbg_logger", + "winapi", +] + [[package]] name = "smallvec" -version = "1.10.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smawk" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "spin" @@ -935,22 +1629,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] -name = "static_assertions" -version = "1.1.0" +name = "strip-ansi-escapes" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte", +] [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "supports-color" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4950e7174bffabe99455511c39707310e7e9b440364a2fcb1cc21521be57b354" +checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" dependencies = [ "is-terminal", "is_ci", @@ -967,18 +1664,18 @@ dependencies = [ [[package]] name = "supports-unicode" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6c2cb240ab5dd21ed4906895ee23fe5a48acdbd15a3ce388e7b62a9b66baf7" +checksum = "f850c19edd184a205e883199a261ed44471c81e39bd95b1357f5febbef00e77a" dependencies = [ "is-terminal", ] [[package]] name = "syn" -version = "2.0.16" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -993,9 +1690,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "termcolor" -version = "1.1.3" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] @@ -1023,97 +1720,176 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +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.21" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ + "deranged", "itoa", "libc", + "num-conv", "num_threads", - "serde", + "powerfmt", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" 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.2" +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 = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] [[package]] name = "toml_edit" -version = "0.19.9" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.20", ] [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-linebreak" -version = "0.1.4" +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 = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" dependencies = [ - "hashbrown", - "regex", + "libc", + "vswhom-sys", ] [[package]] -name = "unicode-width" -version = "0.1.10" +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 = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] [[package]] -name = "version_check" -version = "0.9.4" +name = "vte_generate_state_changes" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" +dependencies = [ + "proc-macro2", + "quote", +] [[package]] name = "wasi" @@ -1121,6 +1897,95 @@ 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.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +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 = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "win_dbg_logger" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d1b4c22244dc27534d81e2f6fc3efd6b20e50c010f177efc20b719ec759a779" +dependencies = [ + "log", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1145,11 +2010,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -1160,145 +2025,194 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.45.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.48.5", ] [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +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.48.0", + "windows-targets 0.52.6", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "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.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "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.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +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 = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_key_tester" +version = "0.3.0" +dependencies = [ + "anyhow", + "clap", + "kanata", + "kanata-interception", + "log", + "native-windows-gui", + "simplelog", + "winapi", +] [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.4.6" +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 = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +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" @@ -1307,3 +2221,20 @@ 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" diff --git a/Cargo.toml b/Cargo.toml index e7cada242..cc2e1e632 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,44 +1,83 @@ +[workspace] +members = [ + "./", + "parser", + "keyberon", + "example_tcp_client", + "tcp_protocol", + "windows_key_tester", + "simulated_input", + "simulated_passthru", + "wasm", +] +exclude = [ + "interception", + "key-sort-add", +] +resolver = "2" + [package] name = "kanata" -version = "1.4.0-prerelease-2" +version = "1.12.0-prerelease-2" authors = ["jtroo "] description = "Multi-layer keyboard customization" -keywords = ["cli", "linux", "windows", "keyboard", "layout"] +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" -edition = "2021" +license = "LGPL-3.0-only" +edition = "2024" +default-run = "kanata" + +[lib] +name = "kanata_state_machine" +path = "src/lib.rs" +crate-type = ["rlib", "staticlib"] + +[[bin]] +name = "kanata" +path = "src/main.rs" [dependencies] -clap = { version = "4.1.6", features = [ "std", "derive", "help", "suggestions" ], default_features = false } -log = { version = "0.4.8", default_features = false } -simplelog = "0.12.0" anyhow = "1" -parking_lot = "0.12" +clap = { version = "4", features = [ "std", "derive", "help", "suggestions" ], default-features = false } +dirs = "5.0.1" +indoc = { version = "2.0.4", optional = true } +log = { version = "0.4.8", default-features = false } +miette = { version = "5.7.0", features = ["fancy"] } once_cell = "1" -serde = { version = "1", features = ["alloc", "derive"], default_features = false } -serde_json = { version = "1", features = ["alloc"], default_features = false } +parking_lot = "0.12" radix_trie = "0.2" rustc-hash = "1.1.0" -miette = { version = "5.7.0", features = ["fancy"] } -dirs = "5.0.1" +simplelog = "0.12.0" +serde_json = { version = "1", features = ["std"], default-features = false, optional = true } +time = "0.3.47" +web-time = "1.1.0" -# kanata-keyberon = "0.17.0" -# Uncomment below and comment out above for testing local keyberon changes. -# Otherwise any changes to the local files will not reflect in the compiled -# binary. -kanata-keyberon = { path = "keyberon" } +kanata-keyberon = { path = "keyberon", version = "0.1120.1" } +kanata-parser = { path = "parser", version = "0.1120.1" } +kanata-tcp-protocol = { path = "tcp_protocol", version = "0.1120.1" } -kanata-parser = { path = "parser" } +[target.'cfg(not(any(target_arch = "wasm32", target_os = "android")))'.dependencies] +arboard = "3.4" -[target.'cfg(target_os = "linux")'.dependencies] -evdev = "=0.12.0" -signal-hook = "0.3.14" -inotify = { version = "0.10.0", default_features = false } -mio = { version = "0.8.4", features = ["os-poll", "os-ext"] } +[target.'cfg(target_os = "macos")'.dependencies] +karabiner-driverkit = "0.3.0" +objc = "0.2.7" +core-graphics = "0.24.0" +open = { version = "5", optional = true } +libc = "0.2" +os_pipe = "1.2.1" +core-foundation = "0.10.1" + +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +evdev = "0.13.0" +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] @@ -47,14 +86,64 @@ winapi = { version = "0.3.9", features = [ "wincon", "timeapi", "mmsystem", + "winuser", + "windef", + "minwindef", ] } -native-windows-gui = { version = "1.0.12", default_features = false } -kanata-interception = { version = "0.2.0", optional = true } +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 + +[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] -cmd = [] +default = ["tcp_server","win_sendinput_send_scancodes", "zippychord"] perf_logging = [] -interception_driver = ["kanata-interception"] +tcp_server = ["dep:serde_json", "kanata-keyberon/tap_hold_tracker"] +win_sendinput_send_scancodes = ["kanata-parser/win_sendinput_send_scancodes"] +win_llhook_read_scancodes = ["kanata-parser/win_llhook_read_scancodes"] +winiov2 = ["win_llhook_read_scancodes","win_sendinput_send_scancodes"] +win_manifest = ["dep:embed-resource", "dep:indoc", "dep:regex"] +# delete cargo-clippy when the objc crate is replaced +cargo-clippy = [] +cmd = ["kanata-parser/cmd"] +interception_driver = ["dep:kanata-interception", "kanata-parser/interception_driver"] +simulated_output = ["dep:indoc"] +simulated_input = ["dep:indoc"] +passthru_ahk = ["simulated_input","simulated_output"] +gui = ["win_manifest","kanata-parser/gui", + "win_sendinput_send_scancodes","win_llhook_read_scancodes", + "dep:muldiv","dep:strip-ansi-escapes","dep: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" diff --git a/EnableUIAccess/EnableUIAccess_launch.ahk b/EnableUIAccess/EnableUIAccess_launch.ahk new file mode 100644 index 000000000..13e61ace4 --- /dev/null +++ b/EnableUIAccess/EnableUIAccess_launch.ahk @@ -0,0 +1,134 @@ +#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, "*") +} diff --git a/EnableUIAccess/Lib/EnableUIAccess.ahk b/EnableUIAccess/Lib/EnableUIAccess.ahk new file mode 100644 index 000000000..13a180ac6 --- /dev/null +++ b/EnableUIAccess/Lib/EnableUIAccess.ahk @@ -0,0 +1,271 @@ +#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') +} diff --git a/EnableUIAccess/README.md b/EnableUIAccess/README.md new file mode 100644 index 000000000..2e9494ae4 --- /dev/null +++ b/EnableUIAccess/README.md @@ -0,0 +1,3 @@ +# EnableUIAccess + +See [the guide documentation for context](https://github.com/jtroo/kanata/blob/main/docs/config.adoc#windows-only-work-elevated). diff --git a/README.md b/README.md index 8a45922dd..1090f9369 100644 --- a/README.md +++ b/README.md @@ -15,24 +15,30 @@ ## What does this do? -This is a software keyboard remapper for Linux and Windows. A short summary of -the features: +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) -- cross-platform human readable configuration file To see all of the features, see the [configuration guide](./docs/config.adoc). -The most similar project is [kmonad](https://github.com/david-janssen/kmonad), -which served as the inspiration for kanata. [Here's a comparison document](./docs/kmonad_comparison.md). +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 video +### 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 @@ -54,7 +60,8 @@ You will need to keep the window that starts kanata running to keep kanata activ Some tips for running kanata in the background: - Windows: https://github.com/jtroo/kanata/discussions/193 -- Linux: https://github.com/jtroo/kanata/discussions/130 +- 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 @@ -76,7 +83,7 @@ Using `cargo install`: cargo install kanata - # On Linux, this may not work without `sudo`, see below + # On Linux and macOS, this may not work without `sudo`, see below kanata --cfg Build and run yourself in Linux: @@ -96,6 +103,20 @@ Build and run yourself in Windows. 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 @@ -103,7 +124,7 @@ Sample configuration files are found in [cfg_samples](./cfg_samples). The 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), +release. All key names can be found in the [keys module](./parser/src/keys/mod.rs), and you can also define your own key names. @@ -145,15 +166,21 @@ For example: cargo build --release --features cmd,interception_driver cargo install --features cmd,interception_driver ``` + ## Other installation methods +
+Repositories for kanata + [![Packaging status](https://repology.org/badge/vertical-allrepos/kanata.svg)](https://repology.org/project/kanata/versions) +
+ ## Notable features -- Human readable configuration file. +- Human-readable configuration file. - [Minimal example](./cfg_samples/minimal.kbd) - [Full guide](./docs/config.adoc) - [Simple example with explanations](./cfg_samples/simple.kbd) @@ -164,7 +191,7 @@ cargo install --features cmd,interception_driver - 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](http://www.oblita.com/interception) support (use `kanata_wintercept.exe`) +- [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 @@ -175,20 +202,15 @@ Contributions are welcome! Unless explicitly stated otherwise, your contributions to kanata will be made under the LGPL-3.0-only[*] license. -The exception to this is the code under the [keyberon](./keyberon) directory, -which is licensed under the MIT license, and likewise, contributions to code -in this directory will be made under the MIT license unless explicitly stated -otherwise. +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 -If you want to test changes in the keyberon library code, -you should change the top-level `Cargo.toml` file. -Look at the comments around the `kanata-keyberon` dependency -to understand what changes to make. - ## How you can help - Try it out and let me know what you think. Feel free to file an issue or @@ -200,6 +222,22 @@ to understand what changes to make. - 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) + - [kanata-switcher (Linux, all DEs)](https://github.com/7mind/kanata-switcher) + - [kwanata (Linux-KDE)](https://github.com/jfsicilia/kwanata): A KDE Plasma companion for Kanata - Automatically activates Kanata's layers/virtualkeys and launches or raises apps. + ## What does the name mean? I wanted a "k" word since this relates to keyboards. According to Wikipedia, @@ -250,7 +288,7 @@ 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/david-janssen/kmonad). This is an excellent project +[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 @@ -262,24 +300,29 @@ 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 -- [kmonad](https://github.com/david-janssen/kmonad): The inspiration for kanata (Linux, Windows, Mac) +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 me (jtroo), it may not be the best tool for +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. diff --git a/assets/kanata.ico b/assets/kanata.ico new file mode 100644 index 000000000..80838d3d6 Binary files /dev/null and b/assets/kanata.ico differ diff --git a/assets/reload_32px.png b/assets/reload_32px.png new file mode 100644 index 000000000..63601649d Binary files /dev/null and b/assets/reload_32px.png differ diff --git a/build.rs b/build.rs new file mode 100644 index 000000000..3f73d35e4 --- /dev/null +++ b/build.rs @@ -0,0 +1,72 @@ +fn main() -> std::io::Result<()> { + #[cfg(feature = "win_manifest")] + { + windows::build()?; + } + Ok(()) +} + +#[cfg(feature = "win_manifest")] +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!("{version}.0"); + } 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(()) + } +} diff --git a/cfg_samples/automousekeys-full-map.kbd b/cfg_samples/automousekeys-full-map.kbd new file mode 100644 index 000000000..84b387f3d --- /dev/null +++ b/cfg_samples/automousekeys-full-map.kbd @@ -0,0 +1,76 @@ +(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 +) diff --git a/cfg_samples/automousekeys-only.kbd b/cfg_samples/automousekeys-only.kbd new file mode 100644 index 000000000..5fbf90576 --- /dev/null +++ b/cfg_samples/automousekeys-only.kbd @@ -0,0 +1,66 @@ +(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 + ___ @_ +) diff --git a/cfg_samples/chords.tsv b/cfg_samples/chords.tsv new file mode 100644 index 000000000..8805591bd --- /dev/null +++ b/cfg_samples/chords.tsv @@ -0,0 +1,192 @@ +rus 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 diff --git a/cfg_samples/colemak.kbd b/cfg_samples/colemak.kbd new file mode 100644 index 000000000..b2d5ee2cf --- /dev/null +++ b/cfg_samples/colemak.kbd @@ -0,0 +1,63 @@ +;; +;; 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 () +) + diff --git a/cfg_samples/deflayermap.kbd b/cfg_samples/deflayermap.kbd new file mode 100644 index 000000000..62c082623 --- /dev/null +++ b/cfg_samples/deflayermap.kbd @@ -0,0 +1,26 @@ +;; 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 +) diff --git a/cfg_samples/fancy_symbols.kbd b/cfg_samples/fancy_symbols.kbd new file mode 100644 index 000000000..098a24224 --- /dev/null +++ b/cfg_samples/fancy_symbols.kbd @@ -0,0 +1,43 @@ +;; 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 • • • + • • • 🔣🔈⓪⓿₀ • 🔣🔈−➖₋⊖ 🔣🔈+➕₊⊕ • • 🔣⌨💡➕₊⊕ 🔣⌨💡➖₋⊖ + • + 🔣˜ • • • • • • • • • • - = 🔣⌫ • 🔣⤒↖ 🔣🔢 • • • • + 🔣↹ • • • • • • • • • • 🔣「〔⎡ 🔣」〕⎣ 🔣\ 🔣⌦ 🔣⤓↘ • • • • • + • • • • • • • • • • • • 🔣↩⌤␤ • • • + • • • • • • • • • • / • • • • • 🔣🔢↩⌤␤ + 🔣⌃ 🔣❖⌘ 🔣⌥ 🔣␣ 🔣▤𝌆 • • • • • • • ) diff --git a/cfg_samples/home-row-mod-advanced.kbd b/cfg_samples/home-row-mod-advanced.kbd new file mode 100644 index 000000000..9b793d0aa --- /dev/null +++ b/cfg_samples/home-row-mod-advanced.kbd @@ -0,0 +1,67 @@ +;; 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-keys 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 +;; +;; Suggested further reading: +;; +;; GitHub discussion on more advanced tap-hold improvements: +;; +;; https://github.com/jtroo/kanata/discussions/1455 +;; +;; Configuration guide documentation on all tap-hold options: +;; +;; https://github.com/jtroo/kanata/blob/main/docs/config.adoc#tap-hold + +(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) +) diff --git a/cfg_samples/home-row-mod-basic.kbd b/cfg_samples/home-row-mod-basic.kbd new file mode 100644 index 000000000..388e1bdab --- /dev/null +++ b/cfg_samples/home-row-mod-basic.kbd @@ -0,0 +1,30 @@ +;; 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 @; +) \ No newline at end of file diff --git a/cfg_samples/home-row-mod-prior-idle.kbd b/cfg_samples/home-row-mod-prior-idle.kbd new file mode 100644 index 000000000..86e391c34 --- /dev/null +++ b/cfg_samples/home-row-mod-prior-idle.kbd @@ -0,0 +1,52 @@ +;; Home row mods with tap-hold-require-prior-idle. +;; +;; This replaces the layer-switching workaround in home-row-mod-advanced.kbd +;; with two native features: +;; +;; tap-hold-require-prior-idle — during fast typing, tap-hold keys resolve as tap +;; immediately (no waiting state, no accidental holds) +;; +;; tap-hold-opposite-hand — hold activates only when the next key is on +;; the opposite hand; same-hand rolls always tap +;; +;; The result: no nomods layer, no fakekeys, no multi wrappers. +;; +;; Configuration guide: +;; https://github.com/jtroo/kanata/blob/main/docs/config.adoc#tap-hold-require-prior-idle +;; https://github.com/jtroo/kanata/blob/main/docs/config.adoc#tap-hold + +(defcfg + process-unmapped-keys yes + ;; If any key was pressed within 150ms, resolve tap-hold as tap immediately. + ;; Prevents accidental modifier activation during fast typing. + tap-hold-require-prior-idle 150 +) + +(defsrc + a s d f j k l ; +) + +;; Declare which keys belong to each hand. +;; tap-hold-opposite-hand uses this to decide hold vs tap. +(defhands + (left q w e r t a s d f g z x c v b) + (right y u i o p h j k l ; n m , . /) +) + +(deflayer base + @a @s @d @f @j @k @l @; +) + +(defalias + ;; Note: consider using different time values for your different fingers. + ;; For example, your pinkies might be slower to release keys and index + ;; fingers faster. + a (tap-hold-opposite-hand 150 a lmet) + s (tap-hold-opposite-hand 150 s lalt) + d (tap-hold-opposite-hand 150 d lctl) + f (tap-hold-opposite-hand 150 f lsft) + j (tap-hold-opposite-hand 150 j rsft) + k (tap-hold-opposite-hand 150 k rctl) + l (tap-hold-opposite-hand 150 l ralt) + ; (tap-hold-opposite-hand 150 ; rmet) +) diff --git a/cfg_samples/included-file.kbd b/cfg_samples/included-file.kbd new file mode 100644 index 000000000..1c755703d --- /dev/null +++ b/cfg_samples/included-file.kbd @@ -0,0 +1,3 @@ +(defalias + included-alias (macro i spc a m spc i n c l u d e d) +) diff --git a/cfg_samples/japanese_mac_eisu_kana.kbd b/cfg_samples/japanese_mac_eisu_kana.kbd new file mode 100644 index 000000000..eed623ef6 --- /dev/null +++ b/cfg_samples/japanese_mac_eisu_kana.kbd @@ -0,0 +1,27 @@ +#| + 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) +) + diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index b96ce8ee6..9ea50615d 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -1,12 +1,24 @@ #| 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. -This sample file has documentation but it is lower effort than the configuration -guide. The configuration guide can be found at: +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. -https://github.com/jtroo/kanata/blob/main/docs/config.adoc |# ;; Single-line comments are prefixed by double-semicolon. A single semicolon @@ -27,12 +39,12 @@ 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. +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 configuration items are optional. +;; 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, @@ -48,6 +60,15 @@ If you need help, you are welcome to ask. ;; 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. @@ -65,12 +86,31 @@ If you need help, you are welcome to ask. ;; ;; 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" @@ -99,6 +139,24 @@ If you need help, you are welcome to ask. ;; ;; 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. @@ -134,6 +192,17 @@ If you need help, you are welcome to ask. ;; ;; 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 @@ -147,21 +216,132 @@ If you need help, you are welcome to ask. ;; ;; 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 three variants of deflocalkeys-*: +;; 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 @@ -192,10 +372,18 @@ If you need help, you are welcome to ask. ì 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 @@ -219,8 +407,6 @@ If you need help, you are welcome to ask. ;; 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. -;; -;; There are currently a maximum of 25 layers allowed. (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 [ ] \ @@ -239,17 +425,57 @@ If you need help, you are welcome to ask. 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-timeout 100 - hold-timeout 200 - tt $tap-timeout + 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) + + ;; tap-hold-opposite-hand-release is a release-time variant of + ;; tap-hold-opposite-hand. It waits for the interrupting key to be pressed + ;; AND released before deciding, which avoids misfires on fast same-hand + ;; rolls. Requires defhands. + ;; actl (tap-hold-opposite-hand-release 200 a lctl + ;; (same-hand tap) (unknown-hand hold) (timeout hold)) ) ;; defalias is used to declare a shortcut for a more complicated action to keep @@ -277,9 +503,11 @@ If you need help, you are welcome to ask. 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 timeout + ;; 1. tap repress timeout ;; 2. hold timeout ;; 3. tap action ;; 4. hold action @@ -287,11 +515,11 @@ If you need help, you are welcome to ask. ;; The hold timeout is the number of milliseconds after which the hold action ;; will activate. ;; - ;; The tap timeout is best explained in a roundabout way. When you press and + ;; 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 timeout window (number is milliseconds). Simply + ;; 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. ;; @@ -325,13 +553,45 @@ If you need help, you are welcome to ask. 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) + ;; If you add reset-timeout-on-press to tap-hold-release-timeout, + ;; the timeout will reset on a press to give you more time to release + ;; a key to activate the hold. + ect2 (tap-hold-release-timeout 200 200 e @chr esc reset-timeout-on-press) ;; 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. + ;; that is a list of keys that will trigger an early tap when pressed. ;; 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)) + ;; A variant of tap-hold-release-keys accepts another parameter, + ;; which is a list of keys that activates the tap + ;; on a press->release of a listed key. + umk2 (tap-hold-release-tap-keys-release 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: u hold: misc layer early tap if any of: (a o e) are pressed + ;; Unlike tap-hold-release-keys, other keys do NOT trigger early hold. + ;; This is useful for home row mods where fast typing should not trigger modifiers. + utk (tap-hold-tap-keys 200 200 u @msc (a o e)) + + ;; tap-hold-keys is a flexible tap-hold with named key list options. + ;; tap: u hold: misc layer + ;; (tap-on-press a o e) — tap immediately if a, o, or e are pressed + ;; (tap-on-press-release ' , .) — tap if ', ,, or . are pressed then released + ;; (hold-on-press 1 2 3) — hold immediately if 1, 2, or 3 are pressed + ;; All list options are optional. Unlisted keys use PermissiveHold behavior. + uthk (tap-hold-keys 200 200 u @msc + (tap-on-press a o e) + (tap-on-press-release ' , .) + (hold-on-press 1 2 3)) + + ;; tap-hold-order resolves by release order instead of timeout. + ;; tap: a hold: lctl buffer: 50ms (fast typing grace period) + aor (tap-hold-order 200 50 a lctl) + ;; tap for capslk, hold for lctl cap (tap-hold 200 200 caps lctl) @@ -345,6 +605,20 @@ If you need help, you are welcome to ask. 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 @@ -384,9 +658,17 @@ If you need help, you are welcome to ask. ;; 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 @@ -409,12 +691,16 @@ If you need help, you are welcome to ask. 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 1 500 bspc S-1 500 bspc S-2) + 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. @@ -427,16 +713,121 @@ If you need help, you are welcome to ask. 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 + ;; If you have cmd enabled, + ;; you may choose to add `init-cmd` which will execute the program + ;; and you can later use `cmd-exit` to choose what to output based on exit code. + ;; (init-cmd powershell.exe -c "if (-not (Test-Path 'myfile')) { exit 1 }") + + ;; 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 + + ;; device-history evaluates to `true` if the Nth most recent device + ;; matches the given device ID defined in definputdevices. Currently macOS only. + ;; ((device-history 1 1)) x break + ;; ((device-history 2 1)) y break + + ;; If you have cmd enabled, you can check on the + ;; exit code of the most recent `init-cmd` occurrence. + ;; ((cmd-exit 0)) a 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 @@ -451,29 +842,69 @@ If you need help, you are welcome to ask. ;; ;; 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 - _ _ _ _ _ _ nlk kp7 kp8 kp9 _ _ _ _ + @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". This will re-parse everything -;; except for linux-dev. So in Linux, you cannot live reload and switch keyboard -;; devices at the time of writing. The variants `lrpv` and `lrnx` will cycle -;; between multiple configuration files, if they are specified in the startup. -;; arguments. +;; 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 _ _ _ _ _ _ _ _ + _ @qwr @dvk lrld lrpv lrnx (lrld-num 1) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @@ -513,6 +944,13 @@ If you need help, you are welcome to ask. 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. @@ -565,10 +1003,21 @@ If you need help, you are welcome to ask. ;; 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) - ) + 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 @@ -631,7 +1080,7 @@ If you need help, you are welcome to ask. _ @mwu @mwd @mwl @mwr _ _ _ _ _ @ma↑ _ _ _ _ pgup bck _ fwd _ _ _ _ @ma← @ma↓ @ma→ _ _ _ pgdn mlft _ mrgt mmid _ mbck mfwd _ @ms↑ _ _ - _ _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ + @fms _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ _ _ _ _ _ _ _ ) @@ -652,6 +1101,20 @@ If you need help, you are welcome to ask. mwl (mwheel-left 50 120) mwr (mwheel-right 50 120) + ;; There are similar wheel actions with `-accel` that have + ;; accelerating/inertial scrolling. + ;; The parameters are: + ;; 1. initial velocity + ;; 2. maximum velocity + ;; 3. acceleration multiplier + ;; 4. deceleration multiplier + ;; The units are arbitrary. + ;; The author finds the values in the example below + ;; to be a decent-feeling starting paint. + ;; Experiment to find your preference. + mau (mwheel-accel-up 3 1200 1.15 0.93) + mad (mwheel-accel-down 3 1200 1.15 0.93) + ;; 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. @@ -680,6 +1143,12 @@ If you need help, you are welcome to ask. ;; 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 @@ -705,21 +1174,60 @@ If you need help, you are welcome to ask. _ _ _ _ _ _ _ ) +;; 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) + pisfvk (on-physical-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 and on-release-fakekey. The -;; purpose of fake keys is for a use case such as holding modifier keys for -;; any number of keypresses and then releasing the modifiers when desired. +;; 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) or -;; on release (on-release-fakekey). The use cases for the on-release variant -;; are left up to your own creativity. +;; 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 _ _ _ _ _ _ _ _ _ @@ -756,6 +1264,7 @@ If you need help, you are welcome to ask. 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 @@ -764,6 +1273,8 @@ If you need help, you are welcome to ask. ) 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) @@ -793,6 +1304,11 @@ If you need help, you are welcome to ask. ;; 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`. ;; @@ -817,6 +1333,58 @@ If you need help, you are welcome to ask. (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)) +) + +;; sequence-noerase +;; +;; When you have a keyboard locale that uses dead keys, +;; you may be pressing two keys that only actually output one symbol. +;; By default, when visible-backspaced does the backtracking backspace, +;; it backspaces according to input count. +;; With dead keys, this may result in too many backspaces. +;; +;; The sequence-noerase action is a no-output action +;; that tells the sequences action to have one fewer backspace +;; when backtracking with visible-backspaced. + +(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 seq-output-1 (macro a b c d e f g)) +(defseq seq-output-1 (' u)) + ;; Input chording. ;; ;; Not to be confused with output chords (like C-S-a or the chords layer @@ -871,9 +1439,205 @@ If you need help, you are welcome to ask. ( 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) + ) +) + +#| +There is a more recent version of defoverrides that offers more customizability. +Instead of 2 list items per override entry, +`defoverridesv2` mandates 4, though the extra 2 can be empty. + +You cannot have both a v1 and v2 of `defoverrides` at the same time. + +The 3rd item is an "exclude modifiers list" which is composed of modifier key names +(such as `lctl`, `lalt`) that, if held, will disable the override from activating. + +The 4th item is an "exclude layers" list which is composed of layer names +that while active as the most recent `layer-switch` or `layer-while-held`, +will disable the override from activating. + +(defoverridesv2 + ;; lctl+a will become lalt+9 + ;; except when lsft is held or other-layer is active. + (lctl a) (lalt 9) (lsft) (other-layer)) + + ;; lctl+b will always become lalt+0 + (lctl b) (lalt 0) () () +) +|# + +#| + +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'") + |# +) diff --git a/cfg_samples/kanata.plist b/cfg_samples/kanata.plist new file mode 100644 index 000000000..f8eb9ea7d --- /dev/null +++ b/cfg_samples/kanata.plist @@ -0,0 +1,61 @@ + + + + + + Label + dev.kanata.kanata + + ProgramArguments + + /usr/local/bin/kanata + -c + /etc/kanata/kanata.kbd + + + UserName + root + + RunAtLoad + + + KeepAlive + + SuccessfulExit + + + + StandardOutPath + /var/log/kanata.log + + StandardErrorPath + /var/log/kanata.log + + diff --git a/cfg_samples/karabiner-vhid-daemon.plist b/cfg_samples/karabiner-vhid-daemon.plist new file mode 100644 index 000000000..c9be20a9b --- /dev/null +++ b/cfg_samples/karabiner-vhid-daemon.plist @@ -0,0 +1,46 @@ + + + + + + Label + org.pqrs.Karabiner-VirtualHIDDevice-Daemon + + ProgramArguments + + /Library/Application Support/org.pqrs/Karabiner-DriverKit-VirtualHIDDevice/Applications/Karabiner-VirtualHIDDevice-Daemon.app/Contents/MacOS/Karabiner-VirtualHIDDevice-Daemon + + + UserName + root + + RunAtLoad + + + KeepAlive + + + StandardOutPath + /var/log/karabiner-vhid-daemon.log + + StandardErrorPath + /var/log/karabiner-vhid-daemon.log + + diff --git a/cfg_samples/key-toggle_press-only_release-only.kbd b/cfg_samples/key-toggle_press-only_release-only.kbd new file mode 100644 index 000000000..6e63e894c --- /dev/null +++ b/cfg_samples/key-toggle_press-only_release-only.kbd @@ -0,0 +1,34 @@ +#| + +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 +) diff --git a/cfg_samples/opposite-hand-hrm.kbd b/cfg_samples/opposite-hand-hrm.kbd new file mode 100644 index 000000000..353aa2d95 --- /dev/null +++ b/cfg_samples/opposite-hand-hrm.kbd @@ -0,0 +1,47 @@ +;; Home row mods using tap-hold-opposite-hand +;; +;; Hold activates only when the next key is on the opposite hand, +;; which substantially reduces misfires during fast same-hand rolls. +;; Same-hand keys resolve as tap by default. +;; +;; Compare with home-row-mod-basic.kbd which uses plain tap-hold. + +(defcfg + process-unmapped-keys yes +) + +;; Assign physical keys to hands. Keys not listed have no hand +;; assignment and are governed by (unknown-hand ) (default: ignore). +(defhands + (left q w e r t a s d f g z x c v b) + (right y u i o p h j k l ; n m , . /)) + +(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 , . / + spc +) + +(defvar + tap-time 200 + hold-time 180 +) + +(defalias + a (tap-hold-opposite-hand $hold-time a lmet) + s (tap-hold-opposite-hand $hold-time s lalt) + d (tap-hold-opposite-hand $hold-time d lctl) + f (tap-hold-opposite-hand $hold-time f lsft) + j (tap-hold-opposite-hand $hold-time j rsft) + k (tap-hold-opposite-hand $hold-time k rctl) + l (tap-hold-opposite-hand $hold-time l ralt) + ; (tap-hold-opposite-hand $hold-time ; rmet) +) + +(deflayer base + 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 , . / + spc +) diff --git a/cfg_samples/push-msg.kbd b/cfg_samples/push-msg.kbd new file mode 100644 index 000000000..c4e25cd32 --- /dev/null +++ b/cfg_samples/push-msg.kbd @@ -0,0 +1,110 @@ +;; push-msg Sample Configuration +;; +;; This configuration demonstrates the push-msg action for sending +;; messages to external tools via Kanata's TCP server. +;; +;; To use this config: +;; kanata -p 7070 -c push-msg.kbd +;; +;; Then connect a TCP client to localhost:7070 to receive messages. + +(defcfg + process-unmapped-keys yes +) + +(defsrc + caps a s d f g h j k l ; ' + z x c v b n m , . / +) + +;; ========================================================================== +;; Layer Definitions +;; ========================================================================== + +(deflayer base + @caps a s d f g h j k l ; ' + z x c v b n m , . / +) + +(deflayer nav + @caps left down up right g h j k l ; ' + z x c v b n m , . / +) + +;; ========================================================================== +;; Aliases with push-msg +;; ========================================================================== + +(defalias + ;; Caps Lock: tap for Esc (with notification), hold for nav layer (with notification) + caps (tap-hold 200 200 + (multi esc (push-msg "layer:base")) + (multi (layer-toggle nav) (push-msg "layer:nav:hold")) + ) +) + +;; ========================================================================== +;; Virtual Keys - Triggerable via TCP ActOnFakeKey +;; ========================================================================== +;; +;; External tools can trigger these using TCP commands: +;; {"ActOnFakeKey":{"name":"email-sig","action":"Tap"}} +;; {"ActOnFakeKey":{"name":"switch-nav","action":"Tap"}} + +(defvirtualkeys + ;; Text expansion macro (S-x for shift+x to get capitals) + email-sig (macro + S-b e s t spc r e g a r d s , ret ret + S-j o h n spc S-d o e + ) + + ;; Layer switches that also push messages + switch-nav (multi + (layer-switch nav) + (push-msg "layer:nav:activated") + ) + + switch-base (multi + (layer-switch base) + (push-msg "layer:base:activated") + ) + + ;; Notify external tools (no keyboard action) + notify-ready (push-msg "status:ready") + notify-busy (push-msg "status:busy") +) + +;; ========================================================================== +;; Usage Examples for External Tools +;; ========================================================================== +;; +;; Python TCP client example: +;; +;; import socket +;; import json +;; +;; sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +;; sock.connect(('127.0.0.1', 7070)) +;; buffer = "" +;; +;; while True: +;; data = sock.recv(4096).decode() +;; buffer += data +;; while '\n' in buffer: +;; line, buffer = buffer.split('\n', 1) +;; if line: +;; msg = json.loads(line) +;; if 'MessagePush' in msg: +;; print(f"Received: {msg['MessagePush']['message']}") +;; elif 'LayerChange' in msg: +;; print(f"Layer: {msg['LayerChange']['new']}") +;; +;; Triggering virtual keys from external tools: +;; +;; # Send this JSON to trigger the email-sig virtual key: +;; echo '{"ActOnFakeKey":{"name":"email-sig","action":"Tap"}}' | nc localhost 7070 +;; +;; # Or trigger layer switch: +;; echo '{"ActOnFakeKey":{"name":"switch-nav","action":"Tap"}}' | nc localhost 7070 +;; +;; ========================================================================== diff --git a/cfg_samples/simple.kbd b/cfg_samples/simple.kbd index 1208cdcd8..a6f67e0f1 100644 --- a/cfg_samples/simple.kbd +++ b/cfg_samples/simple.kbd @@ -26,8 +26,6 @@ ;; 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. -;; -;; There are currently a maximum of 25 layers allowed. (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 [ ] \ diff --git a/cfg_samples/transparent_default.kbd b/cfg_samples/transparent_default.kbd deleted file mode 100644 index edd4fcb70..000000000 --- a/cfg_samples/transparent_default.kbd +++ /dev/null @@ -1,16 +0,0 @@ -(defcfg - linux-dev /dev/input/by-path/platform-i8042-serio-0-event-kbd -) - -(defsrc - f13 f14 f15 -) - -(deflayer default - _ (layer-switch test) (layer-toggle test) -) - -(deflayer test - (layer-switch default) (layer-toggle default) _ -) - diff --git a/cfg_samples/tray-icon/3trans.parent.png b/cfg_samples/tray-icon/3trans.parent.png new file mode 100644 index 000000000..a199266de Binary files /dev/null and b/cfg_samples/tray-icon/3trans.parent.png differ diff --git a/cfg_samples/tray-icon/6name-match.png b/cfg_samples/tray-icon/6name-match.png new file mode 100644 index 000000000..cd0a00efa Binary files /dev/null and b/cfg_samples/tray-icon/6name-match.png differ diff --git a/cfg_samples/tray-icon/_custom-icons/s.png b/cfg_samples/tray-icon/_custom-icons/s.png new file mode 100644 index 000000000..4692a76f3 Binary files /dev/null and b/cfg_samples/tray-icon/_custom-icons/s.png differ diff --git a/cfg_samples/tray-icon/icons/1symbols.png b/cfg_samples/tray-icon/icons/1symbols.png new file mode 100644 index 000000000..f55da3329 Binary files /dev/null and b/cfg_samples/tray-icon/icons/1symbols.png differ diff --git a/cfg_samples/tray-icon/img/2Nav Num.png b/cfg_samples/tray-icon/img/2Nav Num.png new file mode 100644 index 000000000..b70b4415e Binary files /dev/null and b/cfg_samples/tray-icon/img/2Nav Num.png differ diff --git a/cfg_samples/tray-icon/license_icons.txt b/cfg_samples/tray-icon/license_icons.txt new file mode 100644 index 000000000..e45a2e224 --- /dev/null +++ b/cfg_samples/tray-icon/license_icons.txt @@ -0,0 +1,24 @@ +BSD 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. diff --git a/cfg_samples/tray-icon/tray-icon.kbd b/cfg_samples/tray-icon/tray-icon.kbd new file mode 100644 index 000000000..c52570411 --- /dev/null +++ b/cfg_samples/tray-icon/tray-icon.kbd @@ -0,0 +1,29 @@ +(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' diff --git a/cfg_samples/tray-icon/tray-icon.png b/cfg_samples/tray-icon/tray-icon.png new file mode 100644 index 000000000..7508e5c82 Binary files /dev/null and b/cfg_samples/tray-icon/tray-icon.png differ diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..fad53bcb0 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,6 @@ + +### Converting ".adoc" to html + +To generate html from the these documentation files, use ["asciidoctor"](https://asciidoctor.org) +(they are not fully compatible with the separate "asciidoc" project) + diff --git a/docs/avoid-sudo-linux.md b/docs/avoid-sudo-linux.md deleted file mode 100644 index 63dd41205..000000000 --- a/docs/avoid-sudo-linux.md +++ /dev/null @@ -1,38 +0,0 @@ -# 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. - -Add a udev rule (in either `/etc/udev/rules.d` or `/lib/udev/rules.d`) with the following content: - -``` -KERNEL=="uinput", MODE="0660", GROUP="uinput", OPTIONS+="static_node=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 -``` - -# Credits - -The original text was taken and adapted from: https://github.com/kmonad/kmonad/blob/master/doc/faq.md#linux diff --git a/docs/config-stylesheet.css b/docs/config-stylesheet.css new file mode 100644 index 000000000..fd8b46e0d --- /dev/null +++ b/docs/config-stylesheet.css @@ -0,0 +1,543 @@ +html{font-family:sans-serif;-webkit-text-size-adjust:100%} +a{background:none} +a:focus{outline:thin dotted} +a:active,a:hover{outline:0} +h1{font-size:2em;margin:.67em 0} +b,strong{font-weight:bold} +abbr{font-size:.9em} +abbr[title]{cursor:help;border-bottom:1px dotted #dddddf;text-decoration:none} +dfn{font-style:italic} +hr{height:0} +mark{background:#ff0;color:#000} +code,kbd,pre,samp{font-family:monospace;font-size:1em} +pre{white-space:pre-wrap} +q{quotes:"\201C" "\201D" "\2018" "\2019"} +small{font-size:80%} +sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} +sup{top:-.5em} +sub{bottom:-.25em} +img{border:0} +svg:not(:root){overflow:hidden} +figure{margin:0} +audio,video{display:inline-block} +audio:not([controls]){display:none;height:0} +fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em} +legend{border:0;padding:0} +button,input,select,textarea{font-family:inherit;font-size:100%;margin:0} +button,input{line-height:normal} +button,select{text-transform:none} +button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer} +button[disabled],html input[disabled]{cursor:default} +input[type=checkbox],input[type=radio]{padding:0} +button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0} +textarea{overflow:auto;vertical-align:top} +table{border-collapse:collapse;border-spacing:0} +*,::before,::after{box-sizing:border-box} +html,body{font-size:100%} +body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;line-height:1;position:relative;cursor:auto;-moz-tab-size:4;-o-tab-size:4;tab-size:4;word-wrap:anywhere;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased} +a:hover{cursor:pointer} +img,object,embed{max-width:100%;height:auto} +object,embed{height:100%} +img{-ms-interpolation-mode:bicubic} +.left{float:left!important} +.right{float:right!important} +.text-left{text-align:left!important} +.text-right{text-align:right!important} +.text-center{text-align:center!important} +.text-justify{text-align:justify!important} +.hide{display:none} +img,object,svg{display:inline-block;vertical-align:middle} +textarea{height:auto;min-height:50px} +select{width:100%} +.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{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} +} diff --git a/docs/config.adoc b/docs/config.adoc index 05e58894c..c4dc4eb37 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1,29 +1,91 @@ -= Configuration -:toc: -:toc-placement!: -:toc-title!: - -This document describes how to create a kanata configuration file. The kanata -configuration file will determine your keyboard behaviour upon running kanata. += 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.11.0/docs/config.adoc[v1.11.0] +* https://github.com/jtroo/kanata/blob/v1.10.1/docs/config.adoc[v1.10.1] +* https://github.com/jtroo/kanata/blob/v1.10.0/docs/config.adoc[v1.10.0] +* https://github.com/jtroo/kanata/blob/v1.9.0/docs/config.adoc[v1.9.0] +* https://github.com/jtroo/kanata/blob/v1.8.1/docs/config.adoc[v1.8.1] +* https://github.com/jtroo/kanata/blob/v1.8.0/docs/config.adoc[v1.8.0] +endif::[] +ifndef::env-github[] +* link:/config-1.11.0.html[v1.11.0] +* link:/config-1.10.1.html[v1.10.1] +* link:/config-1.10.0.html[v1.10.0] +* link:/config-1.9.0.html[v1.9.0] +* link:/config-1.8.1.html[v1.8.1] +* link:/config-1.8.0.html[v1.8.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. -If you have any questions or confusions, feel free to file an issue or start a -discussion. 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. +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: -[[table-of-contents]] -== Table of contents -toc::[] +- 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: @@ -50,27 +112,58 @@ a multi-line comment block [[defsrc]] === defsrc -<> -Your configuration file must have exactly one `defsrc` entry. This defines the -order of keys that the `+deflayer+` entries will operate on. +**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** -A `defsrc` entry is composed of `defsrc` followed by key names that are -separated by whitespace. +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 <>. -It should be noted that 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. +Keys not processed by Kanata has implications on various actions. +For example: -The the primary source of all key names is the -https://github.com/jtroo/kanata/blob/main/src/keys/mod.rs[str_to_oscode] -function in the source code. Please feel free to file an issue if you're unable -to find the key you're looking for. +- 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 @@ -82,15 +175,38 @@ approximately 60% keyboard layout: ) ---- -For non-US keyboards, see <>. +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 @@ -99,11 +215,12 @@ 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. There is currently a maximum of 25 layers allowed. +using actions. An example `defsrc` and `deflayer` that remaps QWERTY to the Dvorak layout would be: +.Example: [source] ---- (defsrc @@ -123,15 +240,114 @@ would be: ) ---- +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. +|=== + +If a key is not mapped explicitly or through these wildcards, it will be implicitly mapped to a <>. + +**Key processing** + +The `$input` keys in all `deflayermap` sections will be processed by kanata, even if: + +* the layer is inactive +* the key is omitted from `defsrc` +* the `process-unmapped-keys` configuration is omitted, set to `no`, or the key is within `(all-except ...)` + +This is particularly relevant for the issue described in <>. + [[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] @@ -144,31 +360,115 @@ An example minimal configuration is: 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. You can use `deflocalkeys` to define additional key names that can be -used in `defsrc`, `deflayer` and anywhere else in the configuration. +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. + +NOTE: On macOS, the ISO "#" key to the left of Enter reports +`event.code` as `Backslash` in the browser, but the operating system +delivers it to kanata as a distinct HID usage that the regular +`\`/`Backslash` name does not match. Use the name `non_us_pound` +(aliases: `NonUSPound`, `nuhs`) for that physical key on macOS. If +you want the same config to work on Linux as well, pair this with +`deflocalkeys-linux` to map evdev code 43 to the same name. + +[[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) +---- -There are three variants of deflocalkeys: +[cols="1,5"] +|=== +| `$variant` +| One of: `win winiov2 wintercept linux macos` -- `deflocalkeys-win` -- `deflocalkeys-wintercept` -- `deflocalkeys-linux` +| `$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 binary. - -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. +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] ---- @@ -176,6 +476,10 @@ Please contribute to the document if you are able! ì 187 ) +(deflocalkeys-winiov2 + ì 187 +) + (deflocalkeys-wintercept ì 187 ) @@ -184,6 +488,10 @@ Please contribute to the document if you are able! ì 13 ) +(deflocalkeys-macos + ì 13 +) + (defsrc grv 1 2 3 4 5 6 7 8 9 0 - ì bspc ) @@ -192,34 +500,44 @@ Please contribute to the document if you are able! 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. -(https://github.com/jtroo/kanata/releases/tag/win-keycode-tester-v0.2.0[prebuilt binary]) +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. Between the hook and interception versions, some +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 🙂. - -[[optional-defcfg-entries]] -== Optional defcfg entries +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 🙂. -[[defcfg]] -=== defcfg -<> +[[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. -It can be empty but there are options that can change kanata's behaviour that -will be described in this section. - -.Example: +.Example of an empty defcfg: [source] ---- (defcfg) @@ -227,707 +545,625 @@ will be described in this section. [[process-unmapped-keys]] === process-unmapped-keys -<> -Enabling this configuration makes kanata process 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. +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. -Without this, some actions like `+rpt+`, `+tap-hold-release+`, `+one-shot+`, -will not work correctly for subsequent key presses that are not in defsrc. +By default, keys excluded from `defsrc` will not work in various scenarios. +Some examples: -This is disabled by default. The reason this is not enabled by default is -because some keys may not work correctly if they are intercepted. For example, -see the <> option below. +- 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 yes) +(defcfg process-unmapped-keys (all-except lctl ralt)) ---- -[[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. +== Aliases and variables[[aliases-and-vars]] -This configuration is disabled by default and can be enabled by giving it the -value `yes`. +Before learning about actions, +it will be useful to first learn about aliases and variables. -.Example: -[source] ----- -(defcfg - danger-enable-cmd yes -) ----- +[[aliases]] +=== Aliases -[[sequence-timeout]] -=== sequence-timeout -<> +**Reference** -This option customizes the key sequence timeout (unit: ms). Its default value -is 1000. The purpose of this item is explained in <>. +Using the `defalias` configuration entry, you can introduce a shortcut label +for an action. -.Example: +.Syntax: [source] ---- -(defcfg - sequence-timeout 2000 -) +(defalias + $alias-name1 $action1 + $alias-name2 $action2 + ... + $alias-nameN $actionN) ---- -[[sequence-input-mode]] -=== sequence-input-mode -<> - -This option customizes the key sequence input mode. Its default value when not -configured is `hidden-suppressed`. +[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. -The options are: +| `$action` +| The ouput action used wherever the alias name is referenced. +|=== -- `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. +**Description** -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. +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. -See <> for more about sequences. +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] ---- -(defcfg - sequence-input-mode visible-backspaced +(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. -[[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. +[source] +---- +(deflayer example + @cap a s d f +) +---- -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 +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] ---- -(defseq - fk1 (lsft a b) - fk2 (S-(c d)) +(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) ) ---- -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. +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] ---- -(defcfg - sequence-backtrack-modcancel no +(deflayer example + ;; this is equivalent to the previous deflayer example + (tap-hold 200 200 caps lctl) a s d f ) ---- -[[log-layer-changes]] -=== log-layer-changes -<> +[[variables]] +=== Variables -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. +**Reference** -.Example: +Using the `defvar` configuration entry, +you can introduce a shortcut label for an arbitrary string or list. + +.Syntax: [source] ---- -(defcfg - log-layer-changes no -) +(defvar + $var-name1 $var-value1 + $var-name2 $var-value2 + ... + $var-nameN $var-valueN) ---- -[[delegate-to-first-layer]] -== delegate-to-first-layer -<> +[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. -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. +**Description** -For more context, see https://github.com/jtroo/kanata/issues/435. +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] ---- -(defcfg - delegate-to-first-layer yes +(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) ) ---- -[[linux-only-linux-dev]] -=== Linux only: linux-dev -<> +[[concat-in-defvar]] +==== concat in defvar -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. +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] ---- -(defcfg - linux-dev /dev/input/by-path/platform-i8042-serio-0-event-kbd +(defvar + rootpath "/home/myuser/mysubdir" + ;; $otherpath will be the string: /home/myuser/mysubdir/helloworld + otherpath (concat $rootpath "/helloworld") ) ---- -If you want to specify multiple keyboards, you can separate the paths with a -colon `+:+`. +[[actions]] +== Actions -.Example: -[source] ----- -(defcfg - linux-dev /dev/input/dev1:/dev/input/dev2 -) ----- +The actions kanata provides are what make it truly customizable. +This section explains the available actions. -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: +[[live-reload]] +=== Live reload -[source] ----- -(defcfg - linux-dev /dev/input/path-to\:device -) ----- +**Reference** -[[linux-only-linux-dev-names-include]] -=== Linux only: linux-dev-names-include -<> +Live reload variants: -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. -This option is parsed identically to `linux-dev`. +[cols="1,5"] +|=== +| `lrld` +| String action that live-reloads the currently-used configuration file. -Kanata will print device names on startup with log lines that look like below: +| `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. ----- -registering /dev/input/eventX: "Name goes here" ----- +| `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] ---- -(defcfg - linux-dev-names-include "Device 1 name:Device \:2\: Name" +(deflayer has-live-reload + lrld a s d f ) ---- -[[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`. +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. -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. +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] ---- -(defcfg - linux-dev-names-exclude "Device 1 name:Device \:2\: Name" +(deflayer has-live-reloads + lrld lrpv lrnx (lrld-num 3) ) ---- -[[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 specifying multiple config files in the command line: -.Example: [source] ---- -(defcfg - linux-continue-if-no-devs-found yes -) +kanata -c startup.cfg -c 2nd.cfg -c 3rd.cfg ---- -[[linux-only-linux-unicode-u-code]] -=== Linux only: linux-unicode-u-code -<> +Given the above startup command, +activating `(lrld-num 2)` would reload the `2nd.cfg` file. -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. +[[layer-switch]] +=== layer-switch -.Example: +**Reference** + +A list action that changes the active base layer. + +.Syntax: [source] ---- -(defcfg - linux-unicode-u-code v -) +(layer-switch $layer-name) ---- -[[linux-only-linux-unicode-termination]] -=== Linux only: linux-unicode-termination -<> +[cols="1,5"] +|=== +| `$layer-name` +| Layer name to switch to. +|=== -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` +**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] ---- -(defcfg - linux-unicode-termination space -) +(defalias dvk (layer-switch dvorak)) ---- -[[linux-only-x11-repeat-rate]] -=== Linux only: linux-x11-repeat-delay-rate -<> +[[layer-while-held]] +=== layer-while-held -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. +**Reference** -This configuration item does not affect Wayland or no-desktop environments. +A list action that changes the active layer while the key is held. -.Example: +.Syntax: [source] ---- -(defcfg - linux-x11-repeat-delay-rate 400,50 -) +(layer-while-held $layer-name) ---- -[[windows-only-windows-altgr]] -=== Windows only: windows-altgr -<> +[cols="1,5"] +|=== +| `$layer-name` +| Layer name to activate while key is held. +|=== -There is an option for Windows to help mitigate the strange behaviour of AltGr -(ralt) if you're using that 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: +**Description** -* `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 +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] ---- -(defcfg - windows-altgr add-lctl-release -) +(defalias nav (layer-while-held navigation)) ---- -For more context, see: https://github.com/jtroo/kanata/issues/55. +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. -NOTE: Even with these workarounds, putting `+lctl+`+`+ralt+` in your defsrc may not -work properly with other applications that also use keyboard interception. -Known application with issues: GWSL/VcXsrv +[[transparent-key]] +=== Transparent key -[[windows-only--windows-interception-mouse-hwid]] -=== Windows only: windows-interception-mouse-hwid -<> +**Reference** -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 binary). +[cols="1,5"] +|=== +| `+_+` +| String action that activates the action of the layer "underneath" the active one. +|=== -The intended use case for this is for laptops such as a Thinkpad, which have -mouse buttons that may be desirable to activate kanata actions with. +**Description** -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. +Kanata maintains a layer stack consisting in order of: -https://github.com/jtroo/kanata/issues/108[Relevant issue]. +* a stack of temporary layers, where each `layer-while-held` adds one layer on top +* the base layer, manipulated by `layer-switch` +* if `delegate-to-first-layer` is enabled: the first layer defined by `deflayer` or `deflayermap` +* `defsrc` + +A single underscore `+_+` acts as a "transparent" key in the current layer. +It will invoke the action assigned to the same key position on the next layer in the stack. + +NOTE: If `delegate-to-first-layer` is enabled and the base layer is currently set to the first defined layer, a transparent key on the base layer will fall through directly to `defsrc`. + +[[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] ---- -(defcfg - windows-interception-mouse-hwid "70, 0, 60, 0" -) +(defsrc a b c d) +(defalias src use-defsrc) +(deflayer remap-only-c-to-d + _ _ d @src) ---- -[[using-multiple-defcfg-entries]] -=== Using multiple defcfg entries -<> +[[no-op]] +=== No-op + +**Reference** -The `defcfg` entry is treated as a list with pairs of strings. For example: +[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] ---- -(defcfg a 1 b 2) +(deflayer contains-no-ops + XX ✗ ∅ •) ---- -This will be treated as configuration `a` having value `1` and configuration -`b` having value `2`. +[[unicode]] +=== Unicode -An example defcfg containing all of the options is shown below. It should be -noted options that are Linux-only or Windows-only will be ignored when used on -a non-applicable operating system. +**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] ---- -;; 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 - 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-dev-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" -) +(unicode $unicode-codepoint) ---- -[[aliases-and-vars]] -== Aliases and variables -<> +[cols="1,4"] +|=== +| `$unicode-codepoint` +| One unicode codepoint. +Be warned that many emojis/glyphs/graphemes +are composed of multiple codepoints. +|=== -Before learning about actions, -it will be useful to first learn about aliases and variables. +**Description** -[[aliases]] -=== Aliases -<> +NOTE: The <> may output unicode characters more consistently. -Using the `defalias` configuration entry, you can introduce a shortcut label -for an action. +The `+unicode+` (or `+🔣+`) action accepts a single unicode character (but not +a composed character, so 🤲, but not 🤲🏿), +or a single unicode number prefixed with `U+`. +For example, both of these actions are the same: +- `(unicode 🚆)` +- `(unicode U+1F686)` -Similar to how `defcfg` works, `defalias` 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. However, unlike `+defcfg+`, the second item -in `defalias` may be a "list" as opposed to a single string like it was in -`defcfg`. +If you want to output a glyph that is composed of multiple codepoints, +you can use <> with multiple `unicode` actions. -A list is a sequence of strings 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. +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. +Furthermore, on Wayland the unicode mechanism may be broken entirely. +On Wayland you can install then use the `wtype` program, +using <> to execute it. +For example: `(cmd wtype á)` .Example: [source] ---- (defalias - ;; tap for caps lock, hold for left control - cap (tap-hold 200 200 caps lctl) + sml (unicode 😀) + 😀 (🔣 😀) + 🙁 (unicode 🙁) +) +(deflayer has-happy-sad + @sml @🙁 @😀 🔣😀 d f ) ---- -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. +If you want output parentheses `+( )+` via unicode you can quote them. +.Example with parentheses [source] ---- -(deflayer example - @cap a s d f +(defalias + lp (unicode "(") + rp (unicode ")") ) ---- -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. +If you want to output double quotes via unicode +you need a special quoting syntax. -.Example: +.Example use of double-quote within a string [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) + dq (unicode r#"""#) ) ---- -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. +[[output-chordscombos]] +=== Output chords/combos -.Example: -[source] ----- -(deflayer example - ;; this is equivalent to the previous deflayer example - (tap-hold 200 200 caps lctl) a s d f -) ----- +**Reference** -[[variables]] -=== Variables -<> +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. -Using the `defvar` configuration entry, -you can introduce a shortcut label for an arbitrary string or list. -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. +[cols="1,6"] +|=== +| `+C-+` +| Left Control -The most common use case is to define common number strings -for actions such as `tap-hold`, `tap-dance`, and `one-shot`. +| `+RC-+` +| Right Control -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. +| `+A-+` +| Left Alt -Variables can be used to substitute most values. -Some notable exceptions are: +| `+RA-+` +| Right Alt, also known as AltGr -- variables cannot be used in `defcfg`, `defsrc`, or `deflocalkeys` -- variables cannot be used to substitute a layer name -- variables cannot be used to substitute an action name +| `+AG-+` +| Also means Right Alt/AltGr -Variables are referred to by prefixing their name with `$`. +| `+S-+` +| Left Shift -.Example: -[source] ----- -(defvar - tap-timeout 100 - hold-timeout 200 - tt $tap-timeout - ht $hold-timeout -) +| `+RS-+` +| Right Shift -(defalias - th1 (tap-hold $tt $ht caps lctl) - th2 (tap-hold $tt $ht spc lsft) -) ----- - -[[actions]] -== Actions - -The actions kanata provides are what make it truly customizable. This section -explains the available actions. - -[[live-reload]] -=== Live reload -<> - -You can put the `+lrld+` action onto a key to live-reload your configuration -file. If kanata can't parse the file, it will continue using the previous -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. - -.Example: -[source] ----- -(deflayer has-live-reloads - lrld lrpv lrnx -) ----- - -Example specifying multiple config files in the command line: - -[source] ----- -kanata -c startup.cfg -c 2nd.cfg -c 3rd.cfg ----- - -[[layer-switch]] -=== layer-switch -<> - -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 -<> - -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. +| `+M-+` +| Left Meta -.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 -<> - -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. - -.Example: -[source] ----- -(defsrc - a b c -) - -(deflayer remap-only-c-to-d - _ _ d -) ----- - -[[no-op]] -=== No-op -<> - -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. - -.Example: -[source] ----- -(deflayer contains-no-op - XX a s d f -) ----- - -[[unicode]] -=== Unicode -<> +| `+RM-+` +| Right Meta +|=== -The `+unicode+` action accepts a single unicode character. The character will -not be repeatedly typed if you hold the key down. +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. -You may use a unicode character as an alias if desired. +**Description** -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. +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. -[source] ----- -(defalias - sml (unicode 😀) - 🙁 (unicode 🙁) -) -(deflayer has-happy-sad - @sml @🙁 a s d f -) ----- +Output chords are typically used do one-off actions such as: -[[output-chords-combos]] -=== Output chords/combos -<> +- 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 -You may want to remap a key to automatically be pressed in combination with -modifiers such as Control or Shift. You can achieve this by prefixing the -normal key name with one or more of: +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)+`. -* `+C-+`: Left Control -* `+A-+`: Left Alt -* `+S-+`: Left Shift -* `+M-+`: Left Meta, a.k.a. Windows, GUI, Command, Super -* `+RA-+` or `+AG+`: Right Alt, a.k.a. AltGr +The output chord prefix strings are: -These modifiers may be combined together if desired. +* `+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 @@ -940,7 +1176,19 @@ These modifiers may be combined together if desired. [[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 @@ -972,12 +1220,32 @@ and would output `ctrl+c` in the example case. [[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`: release a key, accepts `defsrc` compatible names -* `release-layer`: release a while-held layer +* `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. @@ -987,7 +1255,24 @@ There is currently no known practical use case for [[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. @@ -1038,16 +1323,99 @@ 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, +the hold actions can be used within `defsrc` and `deflayermap` +to remap mouse buttons like keyboard keys. + +NOTE: +On Windows, the Kanata process must be restarted +for it to begin or to stop handling mouse events; +changing defsrc then live-reloading will not +begin handling mouse events +if defsrc previously did not have any mouse events in defsrc. + +**Description** The mouse button actions are: @@ -1058,6 +1426,10 @@ The mouse button actions are: * `mbck`: backward mouse button The mouse button will be held while the key mapped to it is held. +On Linux, Windows, and macOS, +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. @@ -1088,18 +1460,65 @@ The actions are as follows: [[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`. + +The remapping of mouse button and wheel events is effective on Linux, Windows, and macOS. + +NOTE: +On Windows, the Kanata process must be restarted +for it to begin or to stop handling mouse events; +changing defsrc then live-reloading will not +begin handling mouse events +if defsrc previously did not have any mouse events in defsrc. +On macOS, live-reload can install the mouse event tap on the fly +when a new config introduces mouse keys, but stopping the tap +once installed still requires a restart. +Mouse input on macOS requires Accessibility or Input Monitoring +permission in System Settings > Privacy & Security. + +**Description** The mouse wheel actions are: -* `mwheel-up`: vertical scroll up -* `mwheel-down`: vertical scroll down -* `mwheel-left`: horizontal scroll left -* `mwheel-right`: horizontal scroll right +* `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 +(unit: arbitrary). In both Linux and Windows, 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]. @@ -1107,16 +1526,138 @@ NOTE: In Linux, not all desktop environments support the `REL_WHEEL_HI_RES` even 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 Windows, 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-wheel-inertial]] +===== Mouse wheel: inertial variants + +**Reference** + +The `mwheel-accel-*` actions allow you to emulate a mouse wheel, +with "inertial" properties, +meaning scroll speed accelerates while active and decelerates when not. +You can adjust factors of acceleration and deceleration. + +.Syntax: +[source] +---- +(mwheel-accel-$variant $initial-velocity $maximum-velocity $acceleration-multiplier $deceleration-multiplier) +---- + +[cols="2,3"] +|=== +| `$variant` +| One of `up down left right` representing the scroll direction to use. + +| `$initial-velocity` +| Starting scroll speed, in arbitrary units. +Suggested starting point: `3`. + +| `$maximum-velocity` +| Starting scroll speed, in arbitrary units. +Suggested starting point: `1200`. + +| `$acceleration-multiplier` +| Determines acceleration rate of scroll speed. +Suggested starting point: `1.15`. + +| `deceleration-multiplier` +| Determines deceleration rate of scroll speed. +Suggested starting point: `0.93`. +Setting to 0 will make scroll stop abruptly. +|=== + +**Description** + +An alternative scrolling action can be used via `mwheel-accel-*`. +This action has "inertial" behaviour which you may prefer. + +The recommended starting point for the parameters are as below: + +.Example configuration: +[source] +---- +(defvar + mw-initial-v 3 + mw-maximum-v 1200 + mw-accel 1.15 + mw-decel 0.93) +(defalias + mwu (mwheel-accel-up $mw-initial-v $mw-maximum-v $mw-accel $mw-decel) + mwd (mwheel-accel-down $mw-initial-v $mw-maximum-v $mw-accel $mw-decel)) +---- + [[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. + +| `$acceleration-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` -* `movemouse-down` -* `movemouse-left` -* `movemouse-right` +* `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 @@ -1125,10 +1666,10 @@ 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` -* `movemouse-accel-down` -* `movemouse-accel-left` -* `movemouse-accel-right` +* `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 @@ -1136,30 +1677,54 @@ 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` sets the absolute mouse position. +The action `setmouse` or `set🖱` sets the absolute mouse position. -WARNING: This is only supported in Windows right now. +WARNING: This is not supported on Linux. 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. + +The coordinate system is platform-specific: + +* **macOS**: Pixel coordinates. `0,0` is the top-left of the main display, + maximum values are your screen resolution (e.g., `1920,1080`). +* **Windows**: Normalized coordinates from `0,0` (top-left) + to `65535,65535` (bottom-right). Multiple monitors are treated as one virtual desktop. + +Experimentation will be needed to find the correct values for your setup. + +[[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] ---- @@ -1180,38 +1745,70 @@ Experimentation will be needed. 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↑ _ _ - _ _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ + @fst _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ _ _ _ _ _ _ _ ) ---- [[tap-dance]] === tap-dance -<> - -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 +**Reference** -You may put normal keys or other actions in `+tap-dance+`. +The `tap-dance` action allows performing different actions +based on number of consecutive taps of the same key. -.Example: +.Syntax: [source] ---- -(defalias - ;; 1 tap : "A" key - ;; 2 taps: Control+C +(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)) @@ -1241,7 +1838,55 @@ In the example below, repeated taps will, in order: [[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 this key, or 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 this key, or 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 @@ -1254,7 +1899,7 @@ Some of the intended use cases are: * 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 `+@os1+` in the example below will keep Left Shift held for +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 @@ -1263,14 +1908,14 @@ recently pressed `+one-shot+` key. There are four variants of the `+one-shot+` action: -- `+one-shot-press+`: +- `+one-shot-press+` or `+one-shot↓+`: end on the first press of another key -- `+one-shot-release+`: +- `+one-shot-release+` or `+one-shot↑+`: end on the first release of another key -- `+one-shot-press-pcancel+`: +- `+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+`: +- `+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 @@ -1282,6 +1927,9 @@ 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] ---- @@ -1294,35 +1942,270 @@ The default name `+one-shot+` corresponds to `+one-shot-press+`. ) ---- +[[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 if 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 [?reset-timeout-on-press]) +(tap-hold-release-keys $tap-repress-timeout $hold-timeout $tap-action $hold-action $tap-keys) +(tap-hold-release-tap-keys-release $tap-repress-timeout $hold-timeout $tap-action $hold-action $tap-trigger-keys-on-press $tap-trigger-keys-on-press-then-release) +(tap-hold-except-keys $tap-repress-timeout $hold-timeout $tap-action $hold-action $tap-keys) +(tap-hold-tap-keys $tap-repress-timeout $hold-timeout $tap-action $hold-action $tap-keys) +(tap-hold-keys $tap-repress-timeout $hold-timeout $tap-action $hold-action [options...]) +(tap-hold-opposite-hand $timeout $tap-action $hold-action [options...]) +(tap-hold-opposite-hand-release $timeout $tap-action $hold-action [options...]) +(tap-hold-order $tap-repress-timeout $buffer-ms $tap-action $hold-action [options...]) +---- + +[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. +Optionally include `reset-timeout-on-press` to reset timeout duration on a new press, +giving more time for a subsequent release to activate hold instead of timeout. + +| `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-release-tap-keys-release` +| Activate `$hold-action` early if held and another input key is pressed and released. +The `$tap-keys-...` parameters are lists of key names. +Activate `$tap-action` early +if a key within `$tap-trigger-keys-on-press` +is pressed before hold activates +Activate `$tap-action` early +if a key within `$tap-trigger-keys-on-press-then-release` +is pressed then released 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. + +| `tap-hold-tap-keys` +| 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. +Unlike `tap-hold-release-keys`, does NOT activate `$hold-action` early +when other keys are pressed and released. +Waits for full `$hold-timeout` before activating `$hold-action`. +This is useful for home row mods where fast typing should not trigger modifiers. + +| `tap-hold-keys` +| A flexible tap-hold variant with named key list options. +Options are: `(tap-on-press )` activates `$tap-action` early if a listed key is pressed; +`(tap-on-press-release )` activates `$tap-action` early if a listed key is pressed then released; +`(hold-on-press )` activates `$hold-action` early if a listed key is pressed. +For any other key: if that key is pressed and released while this key is waiting, +`$hold-action` is triggered early (PermissiveHold behavior). +All list options are optional. + +| `tap-hold-opposite-hand` +| Resolves to `$hold-action` when a key from the opposite hand (per `defhands`) is pressed. +Requires a `defhands` directive. Supports list-form options for fine-grained control. +See <> below. + +| `tap-hold-opposite-hand-release` +| Like `tap-hold-opposite-hand` but waits for the interrupting key to be pressed AND released +before committing. This avoids misfires on fast same-hand rolls where keystrokes briefly overlap. +Requires a `defhands` directive. Supports the same options as `tap-hold-opposite-hand`. + +| `tap-hold-order` +| Resolves purely by key release order, with no timeout. +If the tap-hold key is released before the other key, activates `$tap-action`; +if the other key is released first (while the tap-hold key is still held), activates `$hold-action`. +`$buffer-ms` is a grace period after the tap-hold key is pressed during which +release-order logic is ignored so that fast typing resolves as tap. +Optionally, `(require-prior-idle )` can short-circuit to tap when a different key +was pressed within that many milliseconds before the tap-hold key; +`require-prior-idle` checks prior typing activity before release-order logic begins, +whereas `buffer-ms` creates an unconditional tap-only window after the key is pressed. +This option is available on all tap-hold variants; see <>. +|=== + +All `tap-hold` variants support an optional trailing `(require-prior-idle )` option +to override the global <> setting for that specific action. +Use `(require-prior-idle 0)` to disable idle detection for a specific key. + +**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. +NOTE: for more discussion on tap-hold, +you may want to have a look at this GitHub discussion: +https://github.com/jtroo/kanata/discussions/1455[link to discussion]. + The action takes 4 parameters in the listed order: -. tap timeout (unit: ms) +. tap repress timeout (unit: ms) . hold timeout (unit: ms) . tap action . hold action -The tap timeout is the number of milliseconds within which a rapid +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+` +* `+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+` +* `+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 @@ -1341,36 +2224,218 @@ but you should be wary of activating the hold action unintentionally. There are further additional variants of `tap-hold-press` and `tap-hold-release`: -. `tap-hold-press-timeout` -. `tap-hold-release-timeout` +- `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` +The `release` variant also accepts an optional 6th argument, +`reset-timeout-on-press` which if included changes the timeout behaviour. +This flag will make the timeout duration reset if a new key is pressed. +This may result in longer timeouts, +but can help with more consistent hold activations; +because it may be challenging to release a key in time +to activate the hold action instead of the timeout action. + +- `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`. -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. +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-release-tap-keys-release` + +This variant behaves nearly the same as `tap-hold-release-keys`, +but has another condition with respect to the eager tap. +It accepts a second list that activates the eager tap +if the any key listed within is pressed then released; +as opposed to only on a press. + +The keys in the 5th and 6th parameters 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: u hold: misc layer early tap if any of: + ;; (z x c v) are pressed, OR + ;; (a s d f) are pressed THEN released + umk2 (tap-hold-release-tap-keys-release 200 200 u @msc (z x c v) (a s d f)) +) +---- + +- `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: 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)) +) +---- + +- `tap-hold-tap-keys` or `tap⬓tapkeys` + +This variant takes a 5th parameter which is a list of keys +that trigger an early tap when they are pressed. +Unlike `tap-hold-release-keys`, pressing and releasing other keys +does NOT activate hold early - the full hold timeout is always waited. +This is useful for home row mods where fast typing should not trigger modifiers. + +The keys in the 5th parameter correspond to the physical input keys, +or in other words the key that corresponds to `defsrc`. + +.Example: +[source] +---- +(defalias + ;; tap: a hold: lsft early tap if any of: (s d f) are pressed + ;; other keys do NOT trigger early hold + ath (tap-hold-tap-keys 200 200 a lsft (s d f)) +) +---- + +==== defhands and tap-hold-opposite-hand + +`defhands` assigns physical keys to `left` / `right` hand groups. +`tap-hold-opposite-hand` uses this to resolve hold when the next key +is on the opposite hand — reducing misfires compared to manual key lists. + +[source] +---- +(defhands + (left q w e r t a s d f g z x c v b) + (right y u i o p h j k l ; n m , . /)) + +(defalias + fctl (tap-hold-opposite-hand 180 f lctl)) +---- + +`defhands` rules: at most one block; each group (`(left ...)`/`(right ...)`) at most once; +a key cannot appear in both groups; partial definitions (only one hand) are allowed; +unlisted keys have no hand assignment. + +`tap-hold-opposite-hand` takes `$timeout $tap-action $hold-action` followed by +optional option lists. Most use `(name value)`, while `neutral-keys` uses +followup key atoms: `(neutral-keys spc tab ...)`. +It requires `defhands` to be defined. +Internally it uses the same `HoldTapConfig::Custom` machinery as `tap-hold-release-keys`. + +[cols="1,1,1,2"] +|=== +| Option | Values | Default | Description + +| `(timeout )` | `tap`, `hold` | `tap` | Action when hold timeout elapses (optimized for home-row-mod behavior). +| `(same-hand )` | `tap`, `hold`, `ignore` | `tap` | Action when a same-hand key is pressed. +| `(neutral-keys key ...)` | one or more key names | (empty) | Keys treated as neutral, overriding their `defhands` assignment. +| `(neutral )` | `tap`, `hold`, `ignore` | `ignore` | Action when a `neutral-keys` key is pressed. +| `(unknown-hand )` | `tap`, `hold`, `ignore` | `ignore` | Action when a key not in `defhands` is pressed. +| `(require-prior-idle )` | `0`–`65535` | (disabled) | If a key was pressed within this many ms before the tap-hold key, resolve as tap immediately. Useful for avoiding accidental holds during fast typing. +| `(tap-repress-timeout )` | `0`–`65535` | `0` | If the same key is re-pressed within this many ms after a tap, immediately tap again instead of entering the hold decision. `0` disables (every press enters the hold decision). See <> for details. +|=== + +When a value is `ignore`, that key press is skipped and the action +waits for the next key or timeout. + +Example with options: +[source] +---- +(defalias + fctl (tap-hold-opposite-hand 180 f lctl + (same-hand ignore) + (timeout hold) + (neutral-keys spc tab) + (neutral tap))) +---- + +See `cfg_samples/opposite-hand-hrm.kbd` for a full working example. + [[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, @@ -1380,9 +2445,13 @@ 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. The -number keys will be parsed as delays, so they must be aliased to be used in a macro. +(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. @@ -1393,10 +2462,15 @@ 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] @@ -1419,11 +2493,17 @@ The actions supported in `+macro+` are: ) ---- -There is a variant of the `+macro+` action that will cancel all active macros -upon releasing the key: `+macro-release-cancel+`. It 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. +[[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 @@ -1441,7 +2521,43 @@ and the rest of the macro does not run. ) ---- -There are further variants of the two `macro` actions which repeat while held. +[[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, @@ -1452,12 +2568,47 @@ only the most recently pressed macro will be repeated. (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. @@ -1503,28 +2654,48 @@ while recording with `(dynamic-macro-record 0)` will be ignored. ) ---- -[[fork]] -=== fork -<> +[[caps-word]] +=== caps-word -The fork action 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. +**Reference** -.Example: +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] ---- -(defalias - frk (fork k @special (lalt ralt)) -) +(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) ---- -[[caps-word]] -=== caps-word -<> +[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` action triggers a state where the `lsft` key +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 `_` @@ -1553,145 +2724,3075 @@ and the extra keys in this list: - `bspc del` - `up down left rght` -You can use `caps-word-custom` instead of `caps-word` +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) - ) -) ----- + ;; 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 +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) +(device-history $device-id $device-recency) +(cmd-exit $exit-code) +---- + +[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`. + +| `device-history` +| Evaluates to true if the device in the `$device-recency` slot +matches `$device-id`. A recency of 1 is the most recent device +that sent an event. The max recency is 8. +Device IDs are defined via <>. +Currently supported on macOS only. + +| `cmd-exit` +| Requires a binary compiled with `cmd` enabled. +| Evaluates to true if the process exit code of the most recent `init-cmd` matches `$exit-code`. + The `$exit-code` parameter must be a 32 bit signed integer. +|=== + +.Syntax of optional `switch` argument `init-cmd`: +[source] +---- +(switch + (init-cmd $binary $arg1 $arg2 ... $argN) + $logic-check1 $action1 $post-activate1 ...) +---- + +**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 + ) +) +---- + +==== `init-cmd` and `cmd-exit` + +With a <> Kanata variant +you have access to the `init-cmd` optional switch parameter +which goes before the normal switch syntax. +This will execute a program synchronously, +blocking the rest of Kanata's execution until it completes. +What this enables is later use of `cmd-exit` logic item +which can output different actions based on which exit code is returned by the command. + +.Example: +[source] +---- +(defalias + my-switch-with-cmd (switch + (init-cmd powershell.exe -c "if (-not (Test-Path 'myfile')) { exit 1 }") + ((cmd-exit 0)) a break + ((cmd-exit 1)) b break + () c 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-;') ')'") +) +---- + +[[push-msg]] +=== push-msg + +NOTE: This action requires the TCP server to be enabled via the +<> command line argument. +If used without the TCP server enabled, a warning will be logged +and the action will have no effect. + +**Reference** + +The `push-msg` action sends an arbitrary message to all connected TCP clients. +This enables communication between your keyboard configuration and external tools +without the security risks or performance overhead of executing shell commands. + +.Syntax: +[source] +---- +(push-msg $message) +---- + +[cols="1,3"] +|=== +| `$message` +| A string or S-expression to send to TCP clients. + Strings are sent as-is; S-expressions are converted to JSON arrays. +|=== + +**Description** + +The `push-msg` action broadcasts a message to all TCP clients connected to Kanata's +TCP server. This is useful for: + +- Triggering external tool actions from keyboard shortcuts +- Layer change notifications to status bars or tray applications +- Integration with automation tools like Raycast, Alfred, or Hammerspoon +- Controlling other applications without spawning shell processes + +Messages are sent as newline-terminated JSON in the format: + +[source,json] +---- +{"MessagePush":{"message":"your-message-here"}} +---- + +Compared to the <> action, `push-msg` offers: + +- **Better security**: No shell execution, no privilege escalation risks +- **Better performance**: Sub-millisecond latency vs ~100ms for command execution +- **No special flags**: Does not require `danger-enable-cmd` in defcfg + +.Example - Basic messages: +[source] +---- +(defalias + ;; Send a simple string message + notify (push-msg "key-pressed") + + ;; Send layer change notification + nav-on (push-msg "layer:nav:activated") + nav-off (push-msg "layer:nav:deactivated") +) +---- + +.Example - Integration with external tools: +[source] +---- +(defalias + ;; Trigger a Raycast extension + raycast-clip (push-msg "raycast:clipboard-history") + + ;; Control media playback via external script + play-pause (push-msg "media:toggle") + next-track (push-msg "media:next") + + ;; Switch keyboard layouts via external tool + layout-next (push-msg "xkb:next-layout") +) +---- + +.Example - Using with defvirtualkeys for external triggering: +[source] +---- +;; Define virtual keys that can be triggered via TCP's ActOnFakeKey command +;; External tools can trigger these using: {"ActOnFakeKey":{"name":"email-sig","action":"Tap"}} +(defvirtualkeys + email-sig (macro S-b e s t spc r e g a r d s , ret ret S-j o h n) + nav-mode (layer-switch nav) +) + +;; Combine push-msg with other actions +(defalias + launch-term (multi + (push-msg "app:launch:terminal") + (layer-switch base) + ) +) +---- + +To receive these messages, connect a TCP client to Kanata's server port. +See the https://github.com/jtroo/kanata/blob/main/example_tcp_client/src/main.rs[example TCP client] +for implementation guidance. + +[[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 +) +---- + +[[defoverridesv2]] +=== defoverridesv2 + +There is a more recent version of defoverrides that offers more customizability. +Instead of 2 list items per override entry, +`defoverridesv2` mandates 4, though the extra 2 can be empty. + +You cannot have both a v1 and v2 of `defoverrides` at the same time. + +The 3rd item is an "exclude modifiers list" which is composed of modifier key names +(such as `lctl`, `lalt`) that, if held, will disable the override from activating. + +NOTE: if all excluded list modifiers are released while the +override input is still active, the override will activate. +You can avoid this by ensuring to always release the non-modifier key +before releasing any modifiers. + +The 4th item is an "exclude layers" list which is composed of layer names +that while active as the most recent `layer-switch` or `layer-while-held`, +will disable the override from activating. + +NOTE: if the layer changes while the override input is still active, +the override will activate. + +.Example: +[source] +---- +(defoverridesv2 + ;; lctl+a will become lalt+9 + ;; except when lsft is held or other-layer is active. + (lctl a) (lalt 9) (lsft) (other-layer)) + + ;; lctl+b will always become lalt+0 + (lctl b) (lalt 0) () () +) +---- + +[[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 conditionally-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. + +== 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. + +[[chordsv2-processing-order]] +=== Action processing order + +The chordsv2 system handles keys before any layer action handling. +This has a consequence of not entering the delay and interruption flow +of actions such as `tap-hold-release`. + +If you want a chordsv2 action activation to be processed +by the aforementioned category of actions +you can move the action into a virtual key +(see <>) and have the chordsv2 action +press and release the virtual key. + +See a sample below that showcases a reusable template. + +.Example: +[source] +---- +(defcfg concurrent-tap-hold yes) +(defsrc) +(deflayermap (base) + caps (tap-hold-release 0 200 esc lctl)) + +;; defines a vkey named v-$key, example v-bspc +;; and an alias @v-bspc that press and and releases the v-key +;; within on-press and on-release respectively. +(deftemplate v- (key) + (defvirtualkeys (concat v- $key) $key) + (defalias (concat v- $key) + (multi (on-press press-vkey (concat v- $key)) (on-release release-vkey (concat v- $key)))) +) + +(t! v- bspc) +(t! v- del) +(defchordsv2 + (j k) @v-bspc 75 first-release () + (s d) @v-del 75 first-release () +) +---- + +[[definputdevices]] +== definputdevices + +**Reference** + +The optional `definputdevices` block maps user-chosen numeric device IDs +to physical input devices, enabling per-device key mappings via the +<> action's `(device-history $id $recency)` condition. + +.Syntax: +[source] +---- +(definputdevices + $id1 ($matcher1a $matcher1b ...) + $id2 ($matcher2a ...) + ... +) +---- + +[cols="1,4"] +|=== +| `$id` +| An integer from 1-255 used to reference this device in `(device-history $id $recency)` conditions. The value is arbitrary; use `defvar` to give IDs meaningful names. + +| `($matcher ...)` +| A list of matcher expressions. All matchers within an entry must match for a device to be assigned that ID (AND semantics). +|=== + +.Available matchers: +[cols="1,4"] +|=== +| `(name "...")` +| Matches devices whose product name contains the given string. + +| `(hash "...")` +| Matches devices whose hex hash equals the given string. +Use `kanata --list` to see device hashes. + +| `(vendor_id N)` +| Matches devices with the given USB vendor ID. Valid values: decimal 0-65535, or hex `0x0-0xFFFF`. + +| `(product_id N)` +| Matches devices with the given USB product ID. Valid values: decimal 0-65535, or hex `0x0-0xFFFF`. +|=== + +**Description** + +`definputdevices` is useful when you have multiple keyboards with different +physical layouts and want to unify their behavior — for example, to remap +swapped modifier key positions between Ctrl, Meta, and Alt across devices. + +The kanata-defined IDs are arbitrary numbers whose only purpose is referencing +within the `switch` action's `(device-history $id $recency)` condition. +Using `defvar` to name them is recommended for readability. + +Device history only records press events (not release or repeat). +If a key is pressed on a device not listed in `definputdevices`, the history +slot will be recorded as "unknown" and will not match any device ID. + +.Example: +[source] +---- +(defvar + id-AppleInternal 1 + id-Go60 2 +) + +(definputdevices + $id-AppleInternal ((name "Apple Internal Keyboard")) + $id-Go60 ((name "Go60") (vendor_id 0x1D50) (product_id 0x615E)) +) + +(defsrc a) +(deflayer base + (switch + ((device-history $id-AppleInternal 1)) x break + ((device-history $id-Go60 1)) y break + () a break)) +---- + +In this example, pressing `a` on the internal keyboard outputs `x`, +pressing `a` on the Go60 outputs `y`, and pressing `a` on any other +device outputs `a`. + +NOTE: Device IDs are matched at startup. Devices plugged in after kanata +starts will not be recognized. Live reload does not re-read device mappings. + +NOTE: Currently supported on macOS only. Linux support is planned. + +[[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 +) +---- + +[[tap-hold-require-prior-idle]] +=== tap-hold-require-prior-idle + +This configuration applies to all `tap-hold` variants. +When a different physical key was pressed within the configured number of +milliseconds before a `tap-hold` key is pressed, the `tap-hold` key will +immediately resolve as its tap action without entering the waiting state. +This prevents accidental modifier activations during fast typing. +The concept is equivalent to ZMK's `require-prior-idle-ms`. + +NOTE: For more info, see <>. + +The default value is `0` (disabled) and the unit is milliseconds. + +.Example: +[source] +---- +(defcfg + tap-hold-require-prior-idle 150 +) +---- + +- If you type a normal key and then press a `tap-hold` key within the threshold, + the `tap-hold` key immediately outputs its tap action. +- If the keyboard is idle for longer than the threshold before pressing a `tap-hold` key, + normal `tap-hold` behavior applies (waiting state, hold on timeout, etc.). +- When `tap-hold-require-prior-idle` triggers, the tap action is chosen before any other + `tap-hold` configuration (e.g., `tap-hold-opposite-hand`, `concurrent-tap-hold`) + is consulted. + +==== Per-action override + +The global `tap-hold-require-prior-idle` value can be overridden on individual +`tap-hold` actions using the `(require-prior-idle )` option. +This is useful when most keys benefit from idle detection (e.g., home-row mods) +but specific keys (e.g., a layer-tap key) need immediate activation even during +fast typing. + +The option can be appended to any `tap-hold` variant as a trailing +`(keyword value)` list: + +.Example: disable for a specific key +[source] +---- +(defcfg tap-hold-require-prior-idle 150) +(defalias + ;; HRMs use the global 150ms threshold + a (tap-hold 200 200 a lmet) + s (tap-hold 200 200 s lalt) + ;; Layer key disables idle detection for immediate activation + nav (tap-hold 200 200 tab (layer-toggle nav) (require-prior-idle 0)) +) +---- + +.Example: enable for a specific key without a global setting +[source] +---- +(defalias + a (tap-hold 200 200 a lmet (require-prior-idle 150)) +) +---- + +- `(require-prior-idle 0)` disables the feature for that action, + even when a global threshold is set. +- A non-zero value enables or changes the threshold for that action only. +- When no per-action override is specified, the global `defcfg` value is used. +- Works with all `tap-hold` variants: `tap-hold`, `tap-hold-press`, + `tap-hold-release`, `tap-hold-press-timeout`, `tap-hold-release-timeout`, + `tap-hold-release-keys`, `tap-hold-except-keys`, `tap-hold-tap-keys`, + and `tap-hold-opposite-hand`. + +[[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, macOS, 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 and macOS. 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. + +On macOS, this feature uses the same CGEventTap as the existing mouse +button input support, and so requires the same Accessibility or Input +Monitoring permission in System Settings > Privacy & Security. + +.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`, `I8042`, and `virtual`, 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" + ) +) +---- + +[[macos-only-macos-continue-if-no-devs-found]] +=== macOS only: macos-continue-if-no-devs-found + +By default, kanata will exit with an error if no input devices matching +the configured include list are found at startup. Setting +`macos-continue-if-no-devs-found` changes this: kanata keeps running and +waits for matching devices to connect, then grabs them automatically. + +This is useful for Bluetooth or USB keyboards that may not be connected +at boot time. When the device later appears (e.g. Bluetooth pairs, USB +plugged in), kanata captures it and begins remapping. + +Works with `macos-dev-names-include` and `definputdevices`. +This has no effect with macOS laptops when no device filter is configured +because the default enumeration always finds the built-in keyboard. + +.Example: +[source] +---- +(defcfg + macos-dev-names-include ("My BT Keyboard") + macos-continue-if-no-devs-found yes +) +---- + +[[windows-only-windows-altgr]] +=== Windows only: windows-altgr + +There is an option for Windows to mitigate the strange behaviour of AltGr +(key names: `ralt | altgr`) if you're using `process-unmapped-keys yes` +or have the AltGr key or the Left Control key +in your `defsrc` or in any `deflayermap` `$input` position. +This is applicable for many non-US layouts +that use AltGr for adding modifier accents to characters. +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, +within <> you should use +`process-unmapped-keys (all-except lctl ralt))` or `process-unmapped-keys no` +**and** also omit both `ralt` and `lctl` from `defsrc` +and any <> `$input` position. + +.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. +In this case it is recommended to ensure the keys are omitted from processing. + +NOTE: Even if a workaround is applied in Kanata, +if you use other LLHOOK-using programs such as +PowerToys Keyboard Manager or AutoHotKey (AHK), +you can still experience issues. +PowerToys has no workaround while with AHK +you may be able to script your own workaround. + +=== 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" +) +---- + +== Command line arguments[[cli-args]] + +When executing Kanata, you can pass a variety of arguments to the program +to change Kanata's behaviour. +This section describes the purpose and usage of these arguments. + +[[args-help]] +=== Show help text: `-h`, `--help` + +Only show help text that describes these arguments, +then exit the program. + +[[args-config-file]] +=== Configuration file(s): `-c`, `--cfg` + +Configuration file(s) to use with Kanata. +When not specified, default file locations are checked. +The default files checked depend on your operating system +and are described by `--help`. + +This argument can be used more than once; +see <> to understand the behaviour of this. +The startup configuration will be the first file listed. + +[[args-tcp]] +=== TCP server address: `-p`, `--port` + +Enable the TCP server capability and listen on either: +- a specified port, on all IP addresses +- a specific `IP:PORT` + +This enables use cases such as +https://github.com/jtroo/kanata?tab=readme-ov-file#community-projects-related-to-kanata[application aware switching]. + +The protocol is plaintext newline-terminated JSON. +The source of truth is the +https://github.com/jtroo/kanata/blob/main/tcp_protocol/src/lib.rs[TCP protocol code]. + +You may also be interested in the +https://github.com/jtroo/kanata/blob/main/example_tcp_client/src/main.rs[example client]. + +==== TCP Protocol Overview + +The TCP server uses a simple request/response model with JSON messages. +Each message is a single line of JSON terminated by a newline (`\n`). + +- **Client → Server**: Commands to control Kanata (reload config, switch layers, etc.) +- **Server → Client**: Responses to commands and event notifications (layer changes, config reloads, etc.) + +==== Client Commands + +These JSON messages can be sent from a TCP client to control Kanata: + +===== Layer Control + +[cols="1,2"] +|=== +| Command | Description + +| `{"ChangeLayer":{"new":"layer-name"}}` +| Switch to the specified layer. Equivalent to the `layer-switch` keyboard action. + +| `{"RequestLayerNames":{}}` +| Request a list of all defined layer names. Server responds with `LayerNames`. + +| `{"RequestCurrentLayerName":{}}` +| Request the name of the currently active layer. Server responds with `CurrentLayerName`. + +| `{"RequestCurrentLayerInfo":{}}` +| Request the current layer's name and full configuration text. Server responds with `CurrentLayerInfo`. +|=== + +.Example - Query and switch layers: +[source,bash] +---- +# Get all layer names +echo '{"RequestLayerNames":{}}' | nc localhost 7070 + +# Get current layer +echo '{"RequestCurrentLayerName":{}}' | nc localhost 7070 + +# Switch to nav layer +echo '{"ChangeLayer":{"new":"nav"}}' | nc localhost 7070 +---- + +===== Virtual Key Actions + +[cols="1,2"] +|=== +| Command | Description + +| `{"RequestFakeKeyNames":{}}` +| Request a list of all defined virtual key names. Server responds with `FakeKeyNames`. + +| `{"ActOnFakeKey":{"name":"key-name","action":"Tap"}}` +| Trigger a virtual key defined in `defvirtualkeys`. Actions: `Press`, `Release`, `Tap`, `Toggle`. +|=== + +Virtual keys must be defined in your configuration using `defvirtualkeys`. +See the <> section for details. + +.Example - Trigger a text expansion macro: +[source] +---- +;; In your config: +(defvirtualkeys + email-sig (macro S-b e s t spc r e g a r d s) +) + +;; From TCP client: +echo '{"ActOnFakeKey":{"name":"email-sig","action":"Tap"}}' | nc localhost 7070 +---- + +===== Mouse Control + +[cols="1,2"] +|=== +| Command | Description + +| `{"SetMouse":{"x":100,"y":200}}` +| Set the mouse cursor position to absolute screen coordinates. +|=== + +This is the TCP equivalent of the <> keyboard action. + +===== Configuration Reload + +[cols="1,2"] +|=== +| Command | Description + +| `{"Reload":{}}` +| Reload the current configuration file. Equivalent to `lrld` keyboard action. + +| `{"ReloadNext":{}}` +| Load the next configuration file in the list. Equivalent to `lrnx` keyboard action. + +| `{"ReloadPrev":{}}` +| Load the previous configuration file in the list. Equivalent to `lrpv` keyboard action. + +| `{"ReloadNum":{"index":0}}` +| Load configuration file at the specified index (0-based). + +| `{"ReloadFile":{"path":"/path/to/config.kbd"}}` +| Load a specific configuration file by path. +|=== + +All reload commands support optional `wait` and `timeout_ms` fields for synchronous confirmation: + +[source,json] +---- +{"Reload":{"wait":true,"timeout_ms":5000}} +---- + +When `wait` is `true`, the server blocks until the reload completes or times out, then sends a `ReloadResult` message. The `timeout_ms` field specifies the maximum wait time in milliseconds (default: 5000). + +===== Server Information + +[cols="1,2"] +|=== +| Command | Description + +| `{"Hello":{}}` +| Request server version and capabilities. Server responds with `HelloOk`. +|=== + +==== Server Messages + +These JSON messages are sent from Kanata to connected TCP clients: + +===== Event Notifications + +These are sent automatically when events occur: + +[cols="1,2"] +|=== +| Message | Description + +| `{"LayerChange":{"new":"layer-name"}}` +| Sent when the active layer changes. + +| `{"ConfigFileReload":{"new":"/path/to/config.kbd"}}` +| Sent when a configuration file is reloaded. + +| `{"MessagePush":{"message":"your-message"}}` +| Sent when a `push-msg` action is triggered from the keyboard configuration. + +| `{"Error":{"msg":"error description"}}` +| Sent when an error occurs processing a command. + +| `{"HoldActivated":{"key":"caps"}}` +| Sent when a tap-hold key transitions to hold state. The `key` field is the physical key name. + +| `{"TapActivated":{"key":"a"}}` +| Sent when a tap-hold key triggers its tap action. The `key` field is the physical key name. +|=== + +===== Query Responses + +These are sent in response to client queries: + +[cols="1,2"] +|=== +| Message | Description + +| `{"LayerNames":{"names":["base","nav","num"]}}` +| Response to `RequestLayerNames`. Contains all defined layer names. + +| `{"FakeKeyNames":{"names":["email-sig","nav-mode"]}}` +| Response to `RequestFakeKeyNames`. Contains all defined virtual key names. + +| `{"CurrentLayerName":{"name":"base"}}` +| Response to `RequestCurrentLayerName`. Contains the active layer name. + +| `{"CurrentLayerInfo":{"name":"base","cfg_text":"..."}}` +| Response to `RequestCurrentLayerInfo`. Contains the layer name and its full configuration text. + +| `{"HelloOk":{"version":"1.11.0","protocol":1,"capabilities":[...]}}` +| Response to `Hello`. Contains server version, protocol version, and supported capabilities. Includes `hold-activated` and `tap-activated`. + +| `{"ReloadResult":{"ok":true}}` +| Response to reload commands when `wait` was `true`. Indicates whether the config reload succeeded. If timed out, includes `timeout_ms`. +|=== + +For a complete implementation example, see the +https://github.com/jtroo/kanata/blob/main/example_tcp_client/src/main.rs[example TCP client]. + +[[args-quiet]] +=== Disable logs other than errors: `-q`, `--quiet` + +Silence info and warning logs so that only errors are logged. +This might be desirable when running in the background as a system service. + +[[args-debug]] +=== Enable debug logs: `-d`, `--debug` + +Add extra debug logging. +This is helpful when diagnosing an issue +or discovering key names. + +[[args-trace]] +=== Enable trace logs: `-t`, `--trace` + +Add extra trace logging. +Also includes debug logging. +This is sometimes helpful when diagnosing an issue, +but is very verbose. -[[cmd]] -=== cmd -<> +[[args-nodelay]] +=== Remove startup delay: `-n`, `--nodelay` -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. +By default, Kanata adds a delay before reading keyboard inputs. +This helps protect against stale states +when started from a terminal. -NOTE: The command is executed directly and not via a shell, so you cannot make -use of environment variables, e.g. `+~+` or `+$HOME+` in Linux will not be -substituted with your home directory. +Without this delay, Kanata may interpret any keys you're still holding down when it starts +as being stuck. -.Example: -[source] ----- -(defalias - cm1 (cmd rm -fr /tmp/testing) +However, this may be undesirable +when running as a background service. - ;; 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") -) ----- +[[args-check]] +=== Only check configuration: `--check` -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 keys, chords, and -chorded lists are supported. Delays and other actions are not supported. +Check the configuration file validity and then exit. -[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\)/'") +[[args-log-layer-changes]] +=== Force log changes: `--log-layer-changes` - ;; 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-;') ')'") -) ----- +Your final desired `defcfg` could be to have logging of layer changes as false. +However, for testing purposes you may want to temporarily log the layers. +This configuration forces logging to happen for this use case. -[[arbitrary-code]] -=== arbitrary-code -<> +[[args-no-wait]] +=== Skip exit prompt: `--no-wait` -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! +By default, kanata displays "Press enter to exit" and waits for user input +before exiting. This flag skips that prompt and exits immediately. -WARNING: This is not cross platform! +This is useful when running kanata as a background service (e.g., systemd) +where you want automatic restart on failure. Without this flag, +the service would hang waiting for stdin input that never comes. -WARNING: When using the Interception driver, this action is still sent over -SendInput. +[[args-linux-dev-symlink]] +=== Linux only - Device symlink path: `-s`, `--symlink-path` -[source] ----- -(defalias - ab1 (arbitrary-code 700) -) ----- +If you want another program to consume the Kanata device output, +you can use this flag to have a consistent device filesystem path. -[[global-overrides]] -== Global overrides -<> +[[args-wait-device]] +=== Linux only - Wait before device grab attempt: `-w`, `--wait-device-ms` -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: +Some devices take a while to become ready and can fail to be grabbed. +This configuration adds a delay before trying to grab devices +in case this is an issue impacting you. -1. the input key list that gets replaced -2. the output key list to replace the input keys with +[[args-macos-list-devices]] +=== macOS only - Only list keyboards: `-l`, `--list` -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). +List keyboard names that can be used +within defcfg and then exit. -Only zero or one `defoverrides` is allowed in a configuration file. +[[args-macos-release-grab-on-lock]] +=== macOS only - Release grab on lock / user switch: `--release-grab-on-lock` + +By default, kanata's keyboard grab on macOS stays active even when +the screen is locked or another user takes the console via fast user +switching. This means anyone else at the keyboard (including you on +the lock-screen prompt) types through your remap. + +Pass `--release-grab-on-lock` to make kanata poll its CGSession state +and release the grab whenever the screen is locked +(`CGSSessionScreenIsLocked`) or kanata's session loses the console +(`kCGSSessionOnConsoleKey == false`). The grab is re-acquired once +kanata's session is back on the console with the screen unlocked. + +This is useful on shared Macs where another user's lock-screen +password or session should not be remapped. Off by default to +preserve the historical always-grab behavior for single-user setups. + +== 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. +* `(on-physical-idle )`: +Activate a virtual key action when the physical keyboard has +had all keys released +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. +The variant `on-physical-idle` may be more desirable if you want the timer +to only start based on having all keyboard keys be released. .Example: [source] ---- -;; Swap numbers and their symbols with respect to shift -(defoverrides - (1) (lsft 1) - (2) (lsft 2) - ;; repeat for all remaining numbers +(defvirtualkeys + ;; Define some virtual keys that perform modifier actions + ctl lctl + sft lsft + met lmet + alt lalt - (lsft 1) (1) - (lsft 2) (2) - ;; repeat for all remaining numbers + ;; 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 ) ---- -[[advanced-weird-features]] -== Advanced/weird features +.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]] -=== Fake keys -<> +Fake keys can be defined inside of `deffakekeys`. -You can define up to 767 fake keys. These keys are not directly mapped to any -physical key presses and can only be activated via these actions: +The actions are: -* `+(on-press-fakekey )+`: Activate a fake key +* `+(on-press-fakekey )+` or `on↓fakekey`: Activate a fake key action when pressing the key mapped to this action. -* `+(on-release-fakekey )+`: Activate a fake key +* `+(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. -A fake key can be defined in a `+deffakekeys+` configuration entry. Configuring -this entry is similar to `+defalias+`, but you cannot make use of aliases -inside of `+deffakekeys+` to shorten an action. You can however refer to -previously defined fake keys. - -The aforementioned `++` can be one of three values: +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] @@ -1704,7 +5805,7 @@ The aforementioned `++` can be one of three values: ;; Press all modifiers pal (multi - (on-press-fakekey ctl press) + (on-press fakekey ctl press) (on-press-fakekey sft press) (on-press-fakekey met press) (on-press-fakekey alt press) @@ -1725,52 +5826,27 @@ The aforementioned `++` can be one of three values: pal (on-press-fakekey pal tap) ral (on-press-fakekey ral tap) -) -(deflayer use-fake-keys - @psf @rsf @pal @ral a s d f + isf (on-idle-fakekey sft tap 1000) ) ----- -If you find that an application isn't registering keypresses correctly with -`+multi+` because the sequence activates too quickly, you can try using fake -key actions alongside the delay actions below. - -* `+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. - -NOTE: You will likely want to use `+macro+` instead of fake keys with delays now -that `+macro+` supports more actions. - -[source] ----- -(defalias - 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) - ) +(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 fake keys]. +https://github.com/jtroo/kanata/issues/80[issue that sparked the creation of virtual keys]. -Something notable about fake keys is that they don't always interrupt the state -of an active `+tap-dance-eager+`. If a `macro` action is assigned to a fake +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 @@ -1781,31 +5857,229 @@ 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 `+deffakekeys+`. The first parameter of a -pair must be a defined fake key name. The second parameter is a list of keys -that will activate a fake key tap when typed in the defined order. More +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-fakekey tap)+` +`+(on-press tap-vkey )+` .Example: [source] ---- (defseq git-status (g s t)) -(deffakekeys git-status (macro g i t spc s t a t u s)) +(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-; / /) +) ---- -For more context, you can read the +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 below. + +Additionally useful to note in this example is the use of a template +to define a sequence and its virtual key in one template expansion, +with template parameters: + +- virtual key name +- activating key sequence +- action triggered by sequence activation + +This template may be useful to you within your own configuration. + +.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 .)) +---- + +==== sequence-noerase + +When you have a keyboard locale that uses dead keys, +you may be pressing two keys that only actually output one symbol. +By default, when the `visible-backspaced` input mode does the backtracking backspaces, +it backspaces according to input count. +With dead keys, this may result in too many backspaces. + +The `sequence-noerase` action is a no-output action +that tells the sequences action to have one fewer backspace +when backtracking with visible-backspaced. + +.Example: +[source] +---- +(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 seq-output-1 (macro a b c d e f g)) +(defseq seq-output-1 (' u)) + +---- + +==== 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 is sequences] +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 @@ -1869,6 +6143,8 @@ 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 @@ -1897,7 +6173,9 @@ 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, @@ -1948,7 +6226,6 @@ VAR_NAME=var_value [[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 @@ -1961,3 +6238,380 @@ 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` + + - `🎭`, `fakekey`, `vk`, or `virtualkey` to activate virtual keys + + - `🔀`, `ls`, or `layer-switch` to switch layers + + +Virtual key format is `vk:name[:action]`, `fakekey:name[:action]`, or `virtualkey:name[:action]`. + +When using the emoji prefix, the format is `🎭name[:action]`. Action is one of: + + - `press` or `p` (default if omitted) + + - `release` + + - `tap` or `t` + + - `toggle` or `g` + +Example: `vk:vk_bear:tap` or `🎭vk_bear:tap`. + + +Layer switch format is `ls:name` or `layer-switch:name`. + +When using the emoji prefix, the format is `🔀name`. + +This switches to the specified layer as the new default layer. + +Example: `ls:nav` or `🔀nav`. + + +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. diff --git a/docs/dropping-root.md b/docs/dropping-root.md deleted file mode 100644 index 4517d629c..000000000 --- a/docs/dropping-root.md +++ /dev/null @@ -1,27 +0,0 @@ -Create a user for kanata and add it to the input groups - -``` -sudo useradd -r -s /bin/false kanata -sudo groupadd uinput -sudo usermod -aG input kanata -sudo usermod -aG uinput kanata -``` - -Add a new udev rule - -``` -sudo touch /etc/udev/rules.d/99-uinput.rules -``` - -Add the following line to it - -``` -KERNEL=="uinput", MODE="0660", GROUP="uinput", OPTIONS+="static_node=uinput" -``` - -If you are using the default `/opt/kanata` directory - - -``` -sudo chown -R kanata:$USER /opt/kanata -sudo chmod -R 0770 /opt/kanata -``` diff --git a/docs/fancy_symbols.md b/docs/fancy_symbols.md new file mode 100644 index 000000000..87fe4d49f --- /dev/null +++ b/docs/fancy_symbols.md @@ -0,0 +1,46 @@ +### 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 diff --git a/docs/kmonad_comparison.md b/docs/kmonad_comparison.md index 39c414952..1017354a9 100644 --- a/docs/kmonad_comparison.md +++ b/docs/kmonad_comparison.md @@ -4,7 +4,7 @@ The kmonad project is the closest alternative for this project. ## Benefits of kmonad over kanata -- MacOS support +- ~MacOS support~ (this is implemented now) - Different features ## Why I built and use kanata diff --git a/docs/locales.adoc b/docs/locales.adoc index 86cd80906..0a94f6367 100644 --- a/docs/locales.adoc +++ b/docs/locales.adoc @@ -1,14 +1,47 @@ +//// +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-placement!: -:toc-title!: +:toc-title: pass:[TABLE OF CONTENTS] +:toclevels: 3 +//// -== Table of contents -toc::[] +== 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) +== ISO German QWERTZ (Windows, non-interception)[[german]] -=== Using `deflocalkeys-win`: +=== Using `deflocalkeys-win`:[[german-defwin]] [%collapsible] ==== @@ -35,7 +68,7 @@ toc::[] ---- ==== -=== Without using `deflocalkeys`: +=== Without using `deflocalkeys`:[[german-nodeflocalkeys]] [%collapsible] ==== @@ -50,7 +83,7 @@ toc::[] ---- ==== -=== Example aliases +=== Example aliases[[german-aliases]] [%collapsible] ==== @@ -100,3 +133,220 @@ toc::[] ) ---- ==== + +== 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 +) +---- +==== diff --git a/docs/platform-known-issues.adoc b/docs/platform-known-issues.adoc index a6f7724e2..71f482623 100644 --- a/docs/platform-known-issues.adoc +++ b/docs/platform-known-issues.adoc @@ -1,11 +1,26 @@ += 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 officially supported platforms today -are Windows 10/11 and Linux. +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", @@ -14,39 +29,73 @@ which map to the binaries == Windows 11 LLHOOK -* The emoji picker does not work properly -** https://github.com/jtroo/kanata/issues/240 - -== Windows 10+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 can misbehave +* 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 Interception +== 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 varying success due to many applications + not supporting the `ibus` input mechanism. + Using xkb to map keys to unicode + or using clipboard actions are more consistent solutions +** 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 + +* Keyboard and mouse input processing requires the Input Monitoring permission in + System Settings > Privacy & Security. On first startup kanata asks macOS to + register itself under that pane so there is something to toggle on; if the + permission has already been denied, kanata exits early with a message pointing + you to the same setting instead of failing deeper in the driver stack. +* In order to send mouse events/movements, it may be necessary to also add the kanata binary in + `System Settings > Privacy & Security > Accessibility` (discussed in https://github.com/jtroo/kanata/issues/1574[issue #1574]). +* Once the mouse event tap is installed (on startup or via live-reload), it + continues running for the lifetime of the process. Removing all mouse keys + from `defsrc` then live-reloading does not stop the tap thread, though it + becomes a no-op pass-through. diff --git a/docs/release-template.md b/docs/release-template.md index f9866390a..aff6ee314 100644 --- a/docs/release-template.md +++ b/docs/release-template.md @@ -1,10 +1,14 @@ +## 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 - +* TODO: fill this out
## Sample configuration file @@ -16,11 +20,59 @@ The attached `kanata.kbd` file is tested to work with the current version. The o
Instructions -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. +Download the appropriate `kanata-windows-variant.zip` file for your machine CPU. Extract and move the desired binary variant to its intended location. Optionally, download `kanata.kbd`. With the two files in the same directory, you can double-click the extracted `.exe` file 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 via `cmd` or `powershell` to use a different configuration file: + +`kanata_windows_binaryvariant.exe --cfg ` -You need to run `kanata.exe` via `cmd` or `powershell` to use a different configuration file: +### Binary variants -`kanata.exe --cfg ` +Explanation of items in the binary variant: + +- x64 vs. arm64: + - Select x64 if your machine's CPU is Intel or AMD. If ARM, use arm64. +- tty vs gui: + - tty runs in a terminal, gui runs as a system tray application +- cmd\_allowed vs. not + - cmd\_allowed allows the `cmd` actions; otherwise, they are compiled out of the application +- winIOv2 vs. wintercept + - winIOv2 uses the LLHOOK and SendInput Windows mechanisms to intercept and send events. + - wintercept uses the [Interception driver](https://github.com/oblitum/Interception). Beware of its known issue that disables keyboards and mice until system reboot: [Link to issue](https://github.com/oblitum/Interception/issues/25). + - 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, and `kanata` will work in more applications. + +### wintercept installation + +#### 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 suspect 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: + +**Example:** + +``` +C:\Users\my_user\Documents\ + kanata_windows_wintercept_x64.exe + kanata.kbd + interception.dll +``` + +### kanata\_passthru_x64.dll + +The Windows `kanata_passthru_x64.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, take `kanata_passthru_x64.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.
@@ -29,64 +81,81 @@ You need to run `kanata.exe` via `cmd` or `powershell` to use a different config
Instructions -Download `kanata`. +Download the `kanata-linux-x64.zip` file. + + Extract and move the desired binary variant to its intended location. Run the binary 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. + +**Example:** -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 ` +sudo ./kanata_linux_x64 --cfg ` ``` To avoid requiring `sudo`, [follow the instructions here](https://github.com/jtroo/kanata/wiki/Avoid-using-sudo-on-Linux). +### Binary variants + +Explanation of items in the binary variant: + +- cmd\_allowed vs. not + - cmd\_allowed allows the `cmd` actions; otherwise, they are compiled out of the application +
-## cmd_allowed variants +## macOS
-Explanation +Instructions -The binaries with the name `cmd_allowed` are conditionally compiled with the `cmd` action enabled. +The supported Karabiner driver version in this release is `v6.2.0`. -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. +**NOTE**: macOS mouse button input requires Accessibility or Input Monitoring permission in System Settings > Privacy & Security. -
+### Binary variants -## wintercept variants +Explanation of items in the binary variant: -
-Explanation and instructions +- x64 vs. arm64: + - Select x64 if your machine's CPU is Intel. If ARM, use arm64. +- cmd\_allowed vs. not + - cmd\_allowed allows the `cmd` actions; otherwise, they are compiled out of the application -### Warning: known issue +### Instructions for macOS 11 and newer -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. +You must use the Karabiner driver version `v6.2.0`. -### Description +Please read through this issue comment: -These variants use the [Interception driver](http://www.oblita.com/interception) instead of Windows hooks. You will need to install the driver using the assets from the linked website 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, including administrator-privileged apps. +https://github.com/jtroo/kanata/issues/1264#issuecomment-2763085239 -### Steps to install the driver +Also have a read through this discussion: -- extract the `.zip` -- run a shell with administrator privilege -- run the script `"command line installer/install-interception.exe"` -- reboot +https://github.com/jtroo/kanata/discussions/1537 -### Additional installation steps +At some point it may be beneficial to provide concise and accurate instructions within this documentation. The maintainer (jtroo) does not own macOS devices to validate; please contribute the instructions to the file `docs/release-template.md` if you are able. -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`. +### Install Karabiner driver for macOS 10 and older: -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`. +- Install the [Karabiner kernel extension](https://github.com/pqrs-org/Karabiner-VirtualHIDDevice). -E.g. if you start kanata from your `Documents` folder, put the file there: +### After installing the appropriate driver for your OS (both macOS <=10 and >=11) + +Download the appropriate `kanata-macos-variant.zip` for your machine CPU. + +Extract and move the desired binary variant to its intended location. Run the binary 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:** ``` -C:\Users\my_user\Documents\ - kanata_wintercept.exe - kanata.kbd - interception.dll +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). +
## sha256 checksums diff --git a/docs/sequence-adding-chords-ideas.md b/docs/sequence-adding-chords-ideas.md index b7fe3a345..5bd5621c3 100644 --- a/docs/sequence-adding-chords-ideas.md +++ b/docs/sequence-adding-chords-ideas.md @@ -61,7 +61,7 @@ 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 the sequence `[..., , 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 diff --git a/docs/setup-linux.md b/docs/setup-linux.md new file mode 100644 index 000000000..38715c6d6 --- /dev/null +++ b/docs/setup-linux.md @@ -0,0 +1,159 @@ +# 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. Create the uinput group (if it doesn’t exist) + +```bash +sudo groupdel uinput 2>/dev/null +sudo groupadd --system uinput +``` + +### 2. Add your user to the `input` and `uinput` group + +```sh +sudo usermod -aG input $USER +sudo usermod -aG uinput $USER +``` + +Verify: + +```sh +groups +``` + +You may need to log out and back in for it to take effect. + +### 3. Load the uinput kernel module + +```sh +sudo modprobe uinput +``` + +This ensures `/dev/uinput` exists. + +### 4. Make sure the uinput device file has the right permissions. + +Create the udev rule: + +```bash +sudo tee /etc/udev/rules.d/99-input.rules > /dev/null < /dev/uinput +``` + +## 5. (Optional) Run Kanata immediately if the group change isn’t active + +If `uinput` is not listed in `groups` even after adding your user: + +```bash +newgrp uinput -c kanata +``` + +This temporarily gives the current shell the `uinput` group so kanata can access `/dev/uinput` until the next login. + +### 6a. (Optional) Create and enable a systemd user service + +First, create the directory for user services: + +```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 --no-wait' +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=default.target +``` + +Note: The `--no-wait` flag is required for `Restart=on-failure` to work. +Without it, kanata waits for user input on exit, which blocks automatic restart. + +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 +``` + +### 6b. 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="--cfg=/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 diff --git a/docs/setup-macos.md b/docs/setup-macos.md new file mode 100644 index 000000000..f96e1c6c6 --- /dev/null +++ b/docs/setup-macos.md @@ -0,0 +1,225 @@ +# Instructions + +On macOS, kanata grabs the keyboard via the +[Karabiner-DriverKit-VirtualHIDDevice](https://github.com/pqrs-org/Karabiner-DriverKit-VirtualHIDDevice) +driver. The kanata process must run as root because the Karabiner virtual HID +daemon exposes its IPC under `/Library/Application Support/org.pqrs/tmp/rootonly/`, +which only root can access. This page walks through installing the driver, +granting permissions, and (optionally) registering kanata as a LaunchDaemon so +it starts at boot. + +### 1. Prerequisites + +- macOS 11 (Big Sur) or newer. +- Xcode Command Line Tools, only if you intend to build kanata from source: + +```sh +xcode-select --install +``` + +### 2. Install Karabiner-DriverKit-VirtualHIDDevice + +The supported driver version is `v6.2.0`. Kanata's bundled +`karabiner-driverkit` crate is built against that release's IPC, and pqrs +ships protocol changes between minor versions, so newer driver releases +are not guaranteed to work. Download the `.pkg` from the +[v6.2.0 release page](https://github.com/pqrs-org/Karabiner-DriverKit-VirtualHIDDevice/releases/tag/v6.2.0) +and run the installer. + +Then activate the driver and approve its system extension: + +```sh +sudo /Applications/.Karabiner-VirtualHIDDevice-Manager.app/Contents/MacOS/Karabiner-VirtualHIDDevice-Manager forceActivate +``` + +Open `System Settings > General > Login Items & Extensions > Driver Extensions` +and toggle on the entry for `org.pqrs.Karabiner-DriverKit-VirtualHIDDevice`. +A reboot may be required if you previously ran `deactivate`. + +Verify the system extension is activated: + +```sh +sudo launchctl list | grep org.pqrs +``` + +If you have **Karabiner-Elements** installed, you should see +`org.pqrs.service.daemon.Karabiner-VirtualHIDDevice-Daemon` listed — KE +manages the daemon automatically and you can skip ahead to Step 3. + +If you installed **only** the standalone DriverKit package (no +Karabiner-Elements), the daemon will not start on its own. You need to +launch it manually or install a LaunchDaemon so it starts at boot. + +**Quick test (won't survive reboot):** + +```sh +sudo "/Library/Application Support/org.pqrs/Karabiner-DriverKit-VirtualHIDDevice/Applications/Karabiner-VirtualHIDDevice-Daemon.app/Contents/MacOS/Karabiner-VirtualHIDDevice-Daemon" & +``` + +**Persistent (LaunchDaemon):** use the included plist to run the daemon at boot: + +```sh +sudo cp cfg_samples/karabiner-vhid-daemon.plist \ + /Library/LaunchDaemons/org.pqrs.Karabiner-VirtualHIDDevice-Daemon.plist +sudo chown root:wheel \ + /Library/LaunchDaemons/org.pqrs.Karabiner-VirtualHIDDevice-Daemon.plist +sudo launchctl bootstrap system \ + /Library/LaunchDaemons/org.pqrs.Karabiner-VirtualHIDDevice-Daemon.plist +``` + +Verify it started: + +```sh +sudo launchctl list | grep org.pqrs +``` + +You should now see the daemon listed. + +### 3. Install the kanata binary + +Either download a pre-built binary from the +[releases page](https://github.com/jtroo/kanata/releases) and place it on +your `PATH`: + +```sh +chmod +x kanata-macos-arm64 +sudo mv kanata-macos-arm64 /usr/local/bin/kanata +``` + +Or build from source: + +```sh +git clone https://github.com/jtroo/kanata && cd kanata +cargo build --release +sudo cp target/release/kanata /usr/local/bin/kanata +``` + +### 4. Grant macOS privacy permissions + +kanata needs Input Monitoring permission in +`System Settings > Privacy & Security > Input Monitoring` to read keyboard +devices. The first time you run kanata as root, macOS will prompt you to add +the binary; you can also pre-add `/usr/local/bin/kanata` (or wherever you +installed it) by clicking the `+` button and selecting the binary. + +kanata also needs Accessibility permission in +`System Settings > Privacy & Security > Accessibility`. Installers can ask +macOS to register/show the Accessibility prompt without starting the remap +loop: + +```sh +/usr/local/bin/kanata --macos-request-permissions || true +open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" +``` + +When launched from Terminal, macOS can report the Terminal/responsible process +as trusted. If kanata does not appear as its own item in Accessibility, add the +installed kanata binary manually with the `+` button. + +### 5. Smoke test from terminal + +Pick a sample config (or your own) and run: + +```sh +sudo kanata -c cfg_samples/simple.kbd +``` + +You should see log lines similar to: + +``` +[INFO] kanata v1.x.x starting +[INFO] entering the processing loop +[INFO] init: catching only releases and sending immediately +[INFO] Sleeping for 2s. Please release any keys now. +[INFO] Starting kanata proper +``` + +Press a remapped key to confirm the mapping fires. `ctrl+space+esc` (held +together) cleanly exits kanata. + +If kanata aborts immediately with a `libc++abi` / `filesystem_error` message, +the on-process diagnostic will print three likely causes: not running as root, +the Karabiner driver not installed/approved, or another process is grabbing +the keyboard exclusively. See the troubleshooting section below. + +### 6. (Optional) Install as a LaunchDaemon + +For login-time / boot-time startup, use the sample LaunchDaemon plist in +[`cfg_samples/kanata.plist`](../cfg_samples/kanata.plist). + +Edit the two paths in `ProgramArguments` to point at your kanata binary and +your config file (the defaults are `/usr/local/bin/kanata` and +`/etc/kanata/kanata.kbd`), then install: + +```sh +sudo cp cfg_samples/kanata.plist /Library/LaunchDaemons/dev.kanata.kanata.plist +sudo chown root:wheel /Library/LaunchDaemons/dev.kanata.kanata.plist +sudo launchctl bootstrap system /Library/LaunchDaemons/dev.kanata.kanata.plist +``` + +Verify it is running: + +```sh +sudo launchctl print system/dev.kanata.kanata +``` + +Logs are written to `/var/log/kanata.log`. + +After editing your kanata config (or the plist itself), reload with: + +```sh +sudo launchctl kickstart -k system/dev.kanata.kanata +``` + +### 7. Uninstall + +Remove the LaunchDaemon (if you installed it): + +```sh +sudo launchctl bootout system/dev.kanata.kanata +sudo rm /Library/LaunchDaemons/dev.kanata.kanata.plist +``` + +If you installed the VHID daemon plist (standalone DriverKit only): + +```sh +sudo launchctl bootout system/org.pqrs.Karabiner-VirtualHIDDevice-Daemon +sudo rm /Library/LaunchDaemons/org.pqrs.Karabiner-VirtualHIDDevice-Daemon.plist +``` + +Remove the kanata binary: + +```sh +sudo rm /usr/local/bin/kanata +``` + +If you are also fully removing the Karabiner driver: + +```sh +sudo /Applications/.Karabiner-VirtualHIDDevice-Manager.app/Contents/MacOS/Karabiner-VirtualHIDDevice-Manager deactivate +``` + +You may then delete the `Karabiner-VirtualHIDDevice-Manager.app` from +`/Applications/`. + +### 8. Troubleshooting + +- **`libc++abi: terminating due to uncaught exception ... filesystem_error`** + on startup: the three likely causes, in order, are (1) kanata is not running + as root, (2) the Karabiner driver is not installed or its system extension + is not approved, (3) another process is already grabbing the keyboard + exclusively. The kanata process prints this same hint to stderr right before + it aborts. +- **`connect_failed asio.system:2` in a loop**: the Karabiner VirtualHIDDevice + daemon is not running. This happens when using the standalone DriverKit + package without Karabiner-Elements. See Step 2 above for how to start the + daemon manually or install it as a LaunchDaemon. +- **A remapped key fires but nothing types**: confirm the kanata binary has + Input Monitoring permission in `System Settings > Privacy & Security`. If + you reinstalled the binary in place, macOS sometimes invalidates the + permission, so toggle it off and back on. +- **Stuck modifier or layer after the lock screen / fast user switch**: + kanata pauses its grab while the screen is locked or another user has the + console; the first keystroke after unlock can be dropped. This is by design. +- See [`docs/platform-known-issues.adoc`](./platform-known-issues.adoc) for + the full list of known macOS issues. diff --git a/docs/simulated_output/sim.kbd b/docs/simulated_output/sim.kbd new file mode 100644 index 000000000..650acf4b4 --- /dev/null +++ b/docs/simulated_output/sim.kbd @@ -0,0 +1,30 @@ +(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) + ‗ 🔣🤲🏿 ‗ + @⌂‹◆ @⌂‹⎇ @⌂‹⎈ @⌂‹⇧ @⌂⇧› @⌂⎈› @⌂⎇› @⌂◆›) diff --git a/docs/simulated_output/sim.txt b/docs/simulated_output/sim.txt new file mode 100644 index 000000000..bce4547a2 --- /dev/null +++ b/docs/simulated_output/sim.txt @@ -0,0 +1 @@ +↓j 🕐1600 ↓l 🕐5000 ↓1 🕐50 ↑1 🕐50 ↓1 🕐50 ↑1 🕐50 ↑j 🕐50 ↑l 🕐50 diff --git a/docs/simulated_output/sim_out.txt b/docs/simulated_output/sim_out.txt new file mode 100644 index 000000000..3bf6652a4 --- /dev/null +++ b/docs/simulated_output/sim_out.txt @@ -0,0 +1,17 @@ +🕐Δ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 │ ↓⇧› ↓⎇› 🤲 🤲 ↑⇧› ↑⎇› diff --git a/docs/simulated_passthru_ahk/[COPY HERE] kanata_passthru.dll _ b/docs/simulated_passthru_ahk/[COPY HERE] kanata_passthru.dll _ new file mode 100644 index 000000000..e69de29bb diff --git a/docs/simulated_passthru_ahk/kanata_dll.kbd b/docs/simulated_passthru_ahk/kanata_dll.kbd new file mode 100644 index 000000000..04b44c62c --- /dev/null +++ b/docs/simulated_passthru_ahk/kanata_dll.kbd @@ -0,0 +1,17 @@ +;;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⌂⇧›) diff --git a/docs/simulated_passthru_ahk/kanata_passthru.ahk b/docs/simulated_passthru_ahk/kanata_passthru.ahk new file mode 100644 index 000000000..37cba17b2 --- /dev/null +++ b/docs/simulated_passthru_ahk/kanata_passthru.ahk @@ -0,0 +1,225 @@ +#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 + } +} diff --git a/docs/switch-design b/docs/switch-design new file mode 100644 index 000000000..1d0d94e12 --- /dev/null +++ b/docs/switch-design @@ -0,0 +1,198 @@ +# 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 +---- diff --git a/docs/win-tray/win-tray-layer-change.gif b/docs/win-tray/win-tray-layer-change.gif new file mode 100644 index 000000000..62b1f4a5d Binary files /dev/null and b/docs/win-tray/win-tray-layer-change.gif differ diff --git a/docs/win-tray/win-tray-screen.png b/docs/win-tray/win-tray-screen.png new file mode 100644 index 000000000..fe7c5aa67 Binary files /dev/null and b/docs/win-tray/win-tray-screen.png differ diff --git a/example_tcp_client/Cargo.lock b/example_tcp_client/Cargo.lock deleted file mode 100644 index e327c9cbf..000000000 --- a/example_tcp_client/Cargo.lock +++ /dev/null @@ -1,415 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "anyhow" -version = "1.0.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91f1f46651137be86f3a2b9a8359f9ab421d04d941c62b5982e1ca21113adf9" - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bumpalo" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clap" -version = "3.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9" -dependencies = [ - "atty", - "bitflags", - "clap_derive", - "clap_lex", - "indexmap", - "once_cell", - "strsim", - "termcolor", - "textwrap", -] - -[[package]] -name = "clap_derive" -version = "3.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "heck" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "indexmap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "itoa" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" - -[[package]] -name = "js-sys" -version = "0.3.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "kanata_example_tcp_client" -version = "1.0.0" -dependencies = [ - "anyhow", - "clap", - "log", - "serde", - "serde_json", - "simplelog", -] - -[[package]] -name = "libc" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b" - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - -[[package]] -name = "once_cell" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" - -[[package]] -name = "os_str_bytes" -version = "6.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" - -[[package]] -name = "serde" -version = "1.0.142" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590c437916fb6b221e1d00df6e3294f3fccd70ca7e92541c475d6ed6ef5fee2" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.142" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b5b8d809babe02f538c2cfec6f2c1ed10804c0e5a6a041a049a4f5588ccc2e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "simplelog" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dfff04aade74dd495b007c831cd6f4e0cee19c344dd9dc0884c0289b70a786" -dependencies = [ - "log", - "termcolor", - "time", -] - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - -[[package]] -name = "time" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b7cc93fc23ba97fde84f7eea56c55d1ba183f495c6715defdfc7b9cb8c870f" -dependencies = [ - "itoa", - "js-sys", - "libc", - "num_threads", - "time-macros", -] - -[[package]] -name = "time-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" - -[[package]] -name = "unicode-ident" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasm-bindgen" -version = "0.2.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" - -[[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-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.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/example_tcp_client/Cargo.toml b/example_tcp_client/Cargo.toml index 47b75186d..e397a9b18 100644 --- a/example_tcp_client/Cargo.toml +++ b/example_tcp_client/Cargo.toml @@ -1,15 +1,15 @@ [package] name = "kanata_example_tcp_client" -Description = "Example kanata TCP client" -version = "1.0.0" +description = "Example kanata TCP client" +version = "1.1.0" edition = "2021" license = "LGPL-3.0" authors = ["jtroo "] [dependencies] anyhow = "1" -clap = { version = "3", features = [ "derive" ] } +clap = { version = "4", features = [ "derive" ] } +kanata-tcp-protocol = { path = "../tcp_protocol" } log = "0.4.8" simplelog = "0.12" -serde = { version = "1", features = ["derive"] } -serde_json = "1" +serde_json = "1" \ No newline at end of file diff --git a/example_tcp_client/src/main.rs b/example_tcp_client/src/main.rs index 0090d24a8..26bbf7b01 100644 --- a/example_tcp_client/src/main.rs +++ b/example_tcp_client/src/main.rs @@ -1,10 +1,9 @@ use clap::Parser; -use serde::{Deserialize, Serialize}; +use kanata_tcp_protocol::*; use simplelog::*; - -use std::io::{stdin, Read, Write}; +use std::io::{BufRead, BufReader, Write, stdin}; use std::net::{SocketAddr, TcpStream}; -use std::str::FromStr; +use std::process::exit; use std::time::Duration; #[derive(Parser, Debug)] @@ -12,7 +11,7 @@ use std::time::Duration; struct Args { /// Port that kanata's TCP server is listening on #[clap(short, long)] - port: u16, + port: Option, /// Enable debug logging #[clap(short, long)] @@ -26,21 +25,94 @@ struct Args { fn main() { let args = Args::parse(); init_logger(&args); + print_usage(); + + let port = match args.port { + Some(p) => p, + None => { + log::error!("no port provided via the -p|--port flag; exiting"); + exit(1); + } + }; log::info!("attempting to connect to kanata"); let kanata_conn = TcpStream::connect_timeout( - &SocketAddr::from(([127, 0, 0, 1], args.port)), + &SocketAddr::from(([127, 0, 0, 1], port)), Duration::from_secs(5), ) .expect("connect to kanata"); log::info!("successfully connected"); - let writer_stream = kanata_conn - .try_clone() - .expect("clone writer"); + let writer_stream = kanata_conn.try_clone().expect("clone writer"); let reader_stream = kanata_conn; std::thread::spawn(move || write_to_kanata(writer_stream)); read_from_kanata(reader_stream); } +fn print_usage() { + log::info!( + "\n\ + You can also use any other software to connect to kanata over TCP.\n\ + The protocol is plaintext JSON with newline terminated messages. +\n\ + Layer change notifications from kanata look like:\n\ + {} +\n\ + Requests to change kanata's layer look like:\n\ + {} +\n\ + Configuration reload commands:\n\ + - reload: {}\n\ + - reload next: {}\n\ + - reload previous: {}\n\ + - reload specific index: {}\n\ + - reload specific file: {} +\n\ + Server responses for commands look like:\n\ + - Success: {}\n\ + - Error: {} + ", + serde_json::to_string(&ServerMessage::LayerChange { + new: "newly-changed-to-layer".into() + }) + .expect("deserializable"), + serde_json::to_string(&ClientMessage::ChangeLayer { + new: "requested-layer".into() + }) + .expect("deserializable"), + serde_json::to_string(&ClientMessage::Reload { + wait: None, + timeout_ms: None + }) + .expect("deserializable"), + serde_json::to_string(&ClientMessage::ReloadNext { + wait: None, + timeout_ms: None + }) + .expect("deserializable"), + serde_json::to_string(&ClientMessage::ReloadPrev { + wait: None, + timeout_ms: None + }) + .expect("deserializable"), + serde_json::to_string(&ClientMessage::ReloadNum { + index: 1, + wait: None, + timeout_ms: None + }) + .expect("deserializable"), + serde_json::to_string(&ClientMessage::ReloadFile { + path: "/path/to/config.kbd".to_string(), + wait: None, + timeout_ms: None, + }) + .expect("deserializable"), + serde_json::to_string(&ServerResponse::Ok).expect("deserializable"), + serde_json::to_string(&ServerResponse::Error { + msg: "Invalid config index: 5. Only 2 configs are available (0-1).".to_string() + }) + .expect("deserializable"), + ) +} + fn init_logger(args: &Args) { let log_lvl = match (args.debug, args.trace) { (_, true) => LevelFilter::Trace, @@ -49,7 +121,7 @@ fn init_logger(args: &Args) { }; 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); + eprintln!("WARNING: could not set log TZ to local: {e:?}"); }; CombinedLogger::init(vec![TermLogger::new( log_lvl, @@ -64,54 +136,124 @@ fn init_logger(args: &Args) { ); } -#[derive(Debug, Serialize, Deserialize)] -pub enum ServerMessage { - LayerChange { new: String }, -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum ClientMessage { - ChangeLayer { new: String }, -} - -impl FromStr for ServerMessage { - type Err = serde_json::Error; - - fn from_str(s: &str) -> std::result::Result { - serde_json::from_str(s) - } -} - fn write_to_kanata(mut s: TcpStream) { log::info!("writer starting"); - log::info!("writer: type layer name then press enter to send a change layer request to kanata"); - let mut layer = String::new(); + log::info!("writer: enter commands to send to kanata:"); + log::info!(" - layer name: change to that layer"); + log::info!(" - fk:KEYNAME: tap fake key"); + log::info!(" - reload: reload current config"); + log::info!(" - reload-next: reload next config"); + log::info!(" - reload-prev: reload previous config"); + log::info!(" - reload-num:N: reload config at index N"); + log::info!(" - reload-file:PATH: reload config file at PATH"); + let mut input = String::new(); loop { - stdin().read_line(&mut layer).expect("stdin is readable"); - let new = layer.trim_end().to_owned(); - log::info!("writer: telling kanata to change layer to \"{new}\""); - let msg = - serde_json::to_string(&ClientMessage::ChangeLayer { new }).expect("deserializable"); - let expected_wsz = msg.len(); - let wsz = s.write(msg.as_bytes()).expect("stream writable"); - if wsz != expected_wsz { - panic!("failed to write entire message {wsz} {expected_wsz}"); - } - layer.clear(); + stdin().read_line(&mut input).expect("stdin is readable"); + let command = input.trim_end().to_owned(); + + let msg = if command.starts_with("fk:") { + let fkname = command.trim_start_matches("fk:").into(); + log::info!("writer: telling kanata to tap fake key \"{fkname}\""); + serde_json::to_string(&ClientMessage::ActOnFakeKey { + name: fkname, + action: FakeKeyActionMessage::Tap, + }) + .expect("deserializable") + } else if command == "reload" { + log::info!("writer: telling kanata to reload current config"); + serde_json::to_string(&ClientMessage::Reload { + wait: None, + timeout_ms: None, + }) + .expect("deserializable") + } else if command == "reload-next" { + log::info!("writer: telling kanata to reload next config"); + serde_json::to_string(&ClientMessage::ReloadNext { + wait: None, + timeout_ms: None, + }) + .expect("deserializable") + } else if command == "reload-prev" { + log::info!("writer: telling kanata to reload previous config"); + serde_json::to_string(&ClientMessage::ReloadPrev { + wait: None, + timeout_ms: None, + }) + .expect("deserializable") + } else if command.starts_with("reload-num:") { + let index_str = command.trim_start_matches("reload-num:"); + match index_str.parse::() { + Ok(index) => { + log::info!("writer: telling kanata to reload config at index {index}"); + serde_json::to_string(&ClientMessage::ReloadNum { + index, + wait: None, + timeout_ms: None, + }) + .expect("deserializable") + } + Err(_) => { + log::error!("Invalid number format for reload-num: {index_str}"); + input.clear(); + continue; + } + } + } else if command.starts_with("reload-file:") { + let path = command.trim_start_matches("reload-file:").to_string(); + log::info!("writer: telling kanata to reload config file \"{path}\""); + serde_json::to_string(&ClientMessage::ReloadFile { + path, + wait: None, + timeout_ms: None, + }) + .expect("deserializable") + } else { + log::info!("writer: telling kanata to change layer to \"{command}\""); + serde_json::to_string(&ClientMessage::ChangeLayer { new: command }) + .expect("deserializable") + }; + + s.write_all(msg.as_bytes()).expect("stream writable"); + input.clear(); } } -fn read_from_kanata(mut s: TcpStream) { +fn read_from_kanata(s: TcpStream) { log::info!("reader starting"); - let mut buf = vec![0; 256]; + let mut reader = BufReader::new(s); + let mut msg = String::new(); loop { - let sz = s.read(&mut buf).expect("stream readable"); - let msg = String::from_utf8_lossy(&buf[..sz]); - let parsed_msg = ServerMessage::from_str(&msg).expect("kanata sends valid message"); + msg.clear(); + reader.read_line(&mut msg).expect("stream readable"); + + // Try to parse as ServerResponse first (for command responses) + if let Ok(response) = serde_json::from_str::(&msg) { + match response { + ServerResponse::Ok => { + log::info!("✓ Command executed successfully"); + } + ServerResponse::Error { msg } => { + log::error!("✗ Command failed: {}", msg); + } + } + continue; + } + + // Fall back to parsing as ServerMessage (for notifications) + let parsed_msg: ServerMessage = match serde_json::from_str(&msg) { + Ok(msg) => msg, + Err(e) => { + log::warn!("could not parse server message {msg}: {e:?}"); + std::process::exit(1); + } + }; match parsed_msg { ServerMessage::LayerChange { new } => { log::info!("reader: kanata changed layers to \"{new}\""); } + msg => { + log::info!("got msg: {msg:?}"); + } } } } diff --git a/interception/Cargo.toml b/interception/Cargo.toml index 7f5f5acfe..fafd9508b 100644 --- a/interception/Cargo.toml +++ b/interception/Cargo.toml @@ -1,7 +1,10 @@ +[workspace] +members = ["."] + [package] name = "kanata-interception" description = "Safe wrapper for Interception. Forked for use with kanata." -version = "0.2.0" +version = "0.3.0" authors = ["Joe Kaushal "] edition = "2018" repository = "https://github.com/jtroo/kanata" diff --git a/interception/src/scancode.rs b/interception/src/scancode.rs index bba61e3f3..5f39e37d7 100644 --- a/interception/src/scancode.rs +++ b/interception/src/scancode.rs @@ -34,13 +34,10 @@ pub enum ScanCode { I = 0x17, O = 0x18, P = 0x19, - LeftBracket = 0x1A, RightBracket = 0x1B, Enter = 0x1C, - LeftControl = 0x1D, - A = 0x1E, S = 0x1F, D = 0x20, @@ -50,13 +47,11 @@ pub enum ScanCode { J = 0x24, K = 0x25, L = 0x26, - SemiColon = 0x27, Apostrophe = 0x28, Grave = 0x29, LeftShift = 0x2A, BackSlash = 0x2B, - Z = 0x2C, X = 0x2D, C = 0x2E, @@ -64,7 +59,6 @@ pub enum ScanCode { B = 0x30, N = 0x31, M = 0x32, - Comma = 0x33, Period = 0x34, Slash = 0x35, @@ -73,7 +67,6 @@ pub enum ScanCode { LeftAlt = 0x38, Space = 0x39, CapsLock = 0x3A, - F1 = 0x3B, F2 = 0x3C, F3 = 0x3D, @@ -84,46 +77,37 @@ pub enum ScanCode { F8 = 0x42, F9 = 0x43, F10 = 0x44, - NumLock = 0x45, ScrollLock = 0x46, - Numpad7 = 0x47, Numpad8 = 0x48, Numpad9 = 0x49, - NumpadMinus = 0x4A, - Numpad4 = 0x4B, Numpad5 = 0x4C, Numpad6 = 0x4D, - NumpadPlus = 0x4E, - Numpad1 = 0x4F, Numpad2 = 0x50, Numpad3 = 0x51, Numpad0 = 0x52, - NumpadPeriod = 0x53, - AltPrintScreen = 0x54, /* Alt + print screen. */ - Int1 = 0x56, /* Key between the left shift and Z. */ - + AltPrintScreen = 0x54, + SC_55 = 0x55, + Int1 = 0x56, F11 = 0x57, F12 = 0x58, - - Oem1 = 0x5A, /* VK_OEM_WSCTRL */ - Oem2 = 0x5B, /* VK_OEM_FINISH */ - Oem3 = 0x5C, /* VK_OEM_JUMP */ - + SC_59 = 0x59, + Oem1 = 0x5A, + Oem2 = 0x5B, + Oem3 = 0x5C, EraseEOF = 0x5D, - - Oem4 = 0x5E, /* VK_OEM_BACKTAB */ - Oem5 = 0x5F, /* VK_OEM_AUTO */ - + Oem4 = 0x5E, + Oem5 = 0x5F, + SC_60 = 0x60, + SC_61 = 0x61, Zoom = 0x62, Help = 0x63, - F13 = 0x64, F14 = 0x65, F15 = 0x66, @@ -135,13 +119,149 @@ pub enum ScanCode { F21 = 0x6C, F22 = 0x6D, F23 = 0x6E, - - Oem6 = 0x6F, /* VK_OEM_PA3 */ + Oem6 = 0x6F, Katakana = 0x70, - Oem7 = 0x71, /* VK_OEM_RESET */ + Oem7 = 0x71, + SC_72 = 0x72, + SC_73 = 0x73, + SC_74 = 0x74, + SC_75 = 0x75, F24 = 0x76, - SBCSChar = 0x77, + SC_78 = 0x78, Convert = 0x79, - NonConvert = 0x7B, /* VK_OEM_PA1 */ + SC_7A = 0x7A, + NonConvert = 0x7B, + SC_7C = 0x7C, + SC_7D = 0x7D, + SC_7E = 0x7E, + SC_7F = 0x7F, + SC_80 = 0x80, + SC_81 = 0x81, + SC_82 = 0x82, + SC_83 = 0x83, + SC_84 = 0x84, + SC_85 = 0x85, + SC_86 = 0x86, + SC_87 = 0x87, + SC_88 = 0x88, + SC_89 = 0x89, + SC_8A = 0x8A, + SC_8B = 0x8B, + SC_8C = 0x8C, + SC_8D = 0x8D, + SC_8E = 0x8E, + SC_8F = 0x8F, + SC_90 = 0x90, + SC_91 = 0x91, + SC_92 = 0x92, + SC_93 = 0x93, + SC_94 = 0x94, + SC_95 = 0x95, + SC_96 = 0x96, + SC_97 = 0x97, + SC_98 = 0x98, + SC_99 = 0x99, + SC_9A = 0x9A, + SC_9B = 0x9B, + SC_9C = 0x9C, + SC_9D = 0x9D, + SC_9E = 0x9E, + SC_9F = 0x9F, + SC_A0 = 0xA0, + SC_A1 = 0xA1, + SC_A2 = 0xA2, + SC_A3 = 0xA3, + SC_A4 = 0xA4, + SC_A5 = 0xA5, + SC_A6 = 0xA6, + SC_A7 = 0xA7, + SC_A8 = 0xA8, + SC_A9 = 0xA9, + SC_AA = 0xAA, + SC_AB = 0xAB, + SC_AC = 0xAC, + SC_AD = 0xAD, + SC_AE = 0xAE, + SC_AF = 0xAF, + SC_B0 = 0xB0, + SC_B1 = 0xB1, + SC_B2 = 0xB2, + SC_B3 = 0xB3, + SC_B4 = 0xB4, + SC_B5 = 0xB5, + SC_B6 = 0xB6, + SC_B7 = 0xB7, + SC_B8 = 0xB8, + SC_B9 = 0xB9, + SC_BA = 0xBA, + SC_BB = 0xBB, + SC_BC = 0xBC, + SC_BD = 0xBD, + SC_BE = 0xBE, + SC_BF = 0xBF, + SC_C0 = 0xC0, + SC_C1 = 0xC1, + SC_C2 = 0xC2, + SC_C3 = 0xC3, + SC_C4 = 0xC4, + SC_C5 = 0xC5, + SC_C6 = 0xC6, + SC_C7 = 0xC7, + SC_C8 = 0xC8, + SC_C9 = 0xC9, + SC_CA = 0xCA, + SC_CB = 0xCB, + SC_CC = 0xCC, + SC_CD = 0xCD, + SC_CE = 0xCE, + SC_CF = 0xCF, + SC_D0 = 0xD0, + SC_D1 = 0xD1, + SC_D2 = 0xD2, + SC_D3 = 0xD3, + SC_D4 = 0xD4, + SC_D5 = 0xD5, + SC_D6 = 0xD6, + SC_D7 = 0xD7, + SC_D8 = 0xD8, + SC_D9 = 0xD9, + SC_DA = 0xDA, + SC_DB = 0xDB, + SC_DC = 0xDC, + SC_DD = 0xDD, + SC_DE = 0xDE, + SC_DF = 0xDF, + SC_E0 = 0xE0, + SC_E1 = 0xE1, + SC_E2 = 0xE2, + SC_E3 = 0xE3, + SC_E4 = 0xE4, + SC_E5 = 0xE5, + SC_E6 = 0xE6, + SC_E7 = 0xE7, + SC_E8 = 0xE8, + SC_E9 = 0xE9, + SC_EA = 0xEA, + SC_EB = 0xEB, + SC_EC = 0xEC, + SC_ED = 0xED, + SC_EE = 0xEE, + SC_EF = 0xEF, + SC_F0 = 0xF0, + SC_F1 = 0xF1, + SC_F2 = 0xF2, + SC_F3 = 0xF3, + SC_F4 = 0xF4, + SC_F5 = 0xF5, + SC_F6 = 0xF6, + SC_F7 = 0xF7, + SC_F8 = 0xF8, + SC_F9 = 0xF9, + SC_FA = 0xFA, + SC_FB = 0xFB, + SC_FC = 0xFC, + SC_FD = 0xFD, + SC_FE = 0xFE, + SC_NonExtendMax = 0xFF, } diff --git a/justfile b/justfile index 4f79eb5b0..39d71cefb 100644 --- a/justfile +++ b/justfile @@ -1,18 +1,99 @@ +set 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" - cargo build --release --features cmd && cp target/release/kanata "{{output_dir}}/kanata_cmd_allowed" + 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. Run as follows: `just --shell powershell.exe --shell-arg -c build_release_windows `. +# Build the release binaries for Windows and put the binaries+cfg in the output directory. build_release_windows output_dir: - cargo build --release; cp target/release/kanata.exe "{{output_dir}}\kanata.exe" - cargo build --release --features interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_wintercept.exe" - cargo build --release --features cmd; cp target/release/kanata.exe "{{output_dir}}\kanata_cmd_allowed.exe" - cargo build --release --features cmd,interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_wintercept_cmd_allowed.exe" + 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 -p kanata-wasm -p kanata-tcp-protocol -- --nocapture + cargo test --features=simulated_output sim_tests + cargo test --features=simulated_output -- must_be_single_threaded --ignored --test-threads=1 + cargo clippy --all + +fmt: + cargo fmt --all + +[doc('Run fmt, check, and clippy')] +check: + cargo fmt --all + cargo check + cargo clippy --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 + +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 + 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 + +[doc('Deprecated. The wasm-pack project is no longer maintained; prefer wasm-build instead. +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}}" + +[doc('Include the trailing `\` or `/` in the output_dir parameter. The parameter should be an absolute path.')] +wasm-build output_dir: + cd wasm; echo "*" > pkg/.gitignore + cd wasm; cargo build --lib --release --target wasm32-unknown-unknown + cd wasm; wasm-bindgen ../target/wasm32-unknown-unknown/release/kanata_wasm.wasm --out-dir pkg --typescript --target web + wasm-opt wasm/pkg/kanata_wasm_bg.wasm -o wasm/pkg/kanata_wasm.wasm-opt.wasm -Oz + rm wasm/pkg/kanata_wasm_bg.wasm + mv wasm/pkg/kanata_wasm.wasm-opt.wasm wasm/pkg/kanata_wasm_bg.wasm + cp wasm/pkg/kanata_wasm_bg.wasm "{{output_dir}}" + cp wasm/pkg/kanata_wasm.js "{{output_dir}}" diff --git a/key-sort-add/Cargo.toml b/key-sort-add/Cargo.toml index 3d00b02cb..bb5f000ec 100644 --- a/key-sort-add/Cargo.toml +++ b/key-sort-add/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = ["."] + [package] name = "key-sort-add" version = "0.1.0" diff --git a/key-sort-add/mapping.txt b/key-sort-add/mapping.txt new file mode 100644 index 000000000..b563cf294 --- /dev/null +++ b/key-sort-add/mapping.txt @@ -0,0 +1,2173 @@ +=== kc to osc +KeyCode::Escape => OsCode::KEY_ESC, +KeyCode::Kb1 => OsCode::KEY_1, +KeyCode::Kb2 => OsCode::KEY_2, +KeyCode::Kb3 => OsCode::KEY_3, +KeyCode::Kb4 => OsCode::KEY_4, +KeyCode::Kb5 => OsCode::KEY_5, +KeyCode::Kb6 => OsCode::KEY_6, +KeyCode::Kb7 => OsCode::KEY_7, +KeyCode::Kb8 => OsCode::KEY_8, +KeyCode::Kb9 => OsCode::KEY_9, +KeyCode::Kb0 => OsCode::KEY_0, +KeyCode::Minus => OsCode::KEY_MINUS, +KeyCode::Equal => OsCode::KEY_EQUAL, +KeyCode::BSpace => OsCode::KEY_BACKSPACE, +KeyCode::Tab => OsCode::KEY_TAB, +KeyCode::Q => OsCode::KEY_Q, +KeyCode::W => OsCode::KEY_W, +KeyCode::E => OsCode::KEY_E, +KeyCode::R => OsCode::KEY_R, +KeyCode::T => OsCode::KEY_T, +KeyCode::Y => OsCode::KEY_Y, +KeyCode::U => OsCode::KEY_U, +KeyCode::I => OsCode::KEY_I, +KeyCode::O => OsCode::KEY_O, +KeyCode::P => OsCode::KEY_P, +KeyCode::LBracket => OsCode::KEY_LEFTBRACE, +KeyCode::RBracket => OsCode::KEY_RIGHTBRACE, +KeyCode::Enter => OsCode::KEY_ENTER, +KeyCode::LCtrl => OsCode::KEY_LEFTCTRL, +KeyCode::A => OsCode::KEY_A, +KeyCode::S => OsCode::KEY_S, +KeyCode::D => OsCode::KEY_D, +KeyCode::F => OsCode::KEY_F, +KeyCode::G => OsCode::KEY_G, +KeyCode::H => OsCode::KEY_H, +KeyCode::J => OsCode::KEY_J, +KeyCode::K => OsCode::KEY_K, +KeyCode::L => OsCode::KEY_L, +KeyCode::SColon => OsCode::KEY_SEMICOLON, +KeyCode::Quote => OsCode::KEY_APOSTROPHE, +KeyCode::Grave => OsCode::KEY_GRAVE, +KeyCode::LShift => OsCode::KEY_LEFTSHIFT, +KeyCode::Bslash => OsCode::KEY_BACKSLASH, +KeyCode::Z => OsCode::KEY_Z, +KeyCode::X => OsCode::KEY_X, +KeyCode::C => OsCode::KEY_C, +KeyCode::V => OsCode::KEY_V, +KeyCode::B => OsCode::KEY_B, +KeyCode::N => OsCode::KEY_N, +KeyCode::M => OsCode::KEY_M, +KeyCode::Comma => OsCode::KEY_COMMA, +KeyCode::Dot => OsCode::KEY_DOT, +KeyCode::Slash => OsCode::KEY_SLASH, +KeyCode::RShift => OsCode::KEY_RIGHTSHIFT, +KeyCode::KpAsterisk => OsCode::KEY_KPASTERISK, +KeyCode::LAlt => OsCode::KEY_LEFTALT, +KeyCode::Space => OsCode::KEY_SPACE, +KeyCode::CapsLock => OsCode::KEY_CAPSLOCK, +KeyCode::F1 => OsCode::KEY_F1, +KeyCode::F2 => OsCode::KEY_F2, +KeyCode::F3 => OsCode::KEY_F3, +KeyCode::F4 => OsCode::KEY_F4, +KeyCode::F5 => OsCode::KEY_F5, +KeyCode::F6 => OsCode::KEY_F6, +KeyCode::F7 => OsCode::KEY_F7, +KeyCode::F8 => OsCode::KEY_F8, +KeyCode::F9 => OsCode::KEY_F9, +KeyCode::F10 => OsCode::KEY_F10, +KeyCode::NumLock => OsCode::KEY_NUMLOCK, +KeyCode::Clear => OsCode::KEY_CLEAR, +KeyCode::ScrollLock => OsCode::KEY_SCROLLLOCK, +KeyCode::Kp7 => OsCode::KEY_KP7, +KeyCode::Kp8 => OsCode::KEY_KP8, +KeyCode::Kp9 => OsCode::KEY_KP9, +KeyCode::KpMinus => OsCode::KEY_KPMINUS, +KeyCode::Kp4 => OsCode::KEY_KP4, +KeyCode::Kp5 => OsCode::KEY_KP5, +KeyCode::Kp6 => OsCode::KEY_KP6, +KeyCode::KpPlus => OsCode::KEY_KPPLUS, +KeyCode::Kp1 => OsCode::KEY_KP1, +KeyCode::Kp2 => OsCode::KEY_KP2, +KeyCode::Kp3 => OsCode::KEY_KP3, +KeyCode::Kp0 => OsCode::KEY_KP0, +KeyCode::KpDot => OsCode::KEY_KPDOT, +KeyCode::F11 => OsCode::KEY_F11, +KeyCode::F12 => OsCode::KEY_F12, +KeyCode::KpEnter => OsCode::KEY_KPENTER, +KeyCode::RCtrl => OsCode::KEY_RIGHTCTRL, +KeyCode::KpSlash => OsCode::KEY_KPSLASH, +KeyCode::SysReq => OsCode::KEY_SYSRQ, +KeyCode::RAlt => OsCode::KEY_RIGHTALT, +KeyCode::Home => OsCode::KEY_HOME, +KeyCode::Up => OsCode::KEY_UP, +KeyCode::PgUp => OsCode::KEY_PAGEUP, +KeyCode::Left => OsCode::KEY_LEFT, +KeyCode::Right => OsCode::KEY_RIGHT, +KeyCode::End => OsCode::KEY_END, +KeyCode::Down => OsCode::KEY_DOWN, +KeyCode::PgDown => OsCode::KEY_PAGEDOWN, +KeyCode::Insert => OsCode::KEY_INSERT, +KeyCode::Delete => OsCode::KEY_DELETE, +KeyCode::Mute => OsCode::KEY_MUTE, +KeyCode::VolDown => OsCode::KEY_VOLUMEDOWN, +KeyCode::VolUp => OsCode::KEY_VOLUMEUP, +KeyCode::Power => OsCode::KEY_POWER, +KeyCode::KpEqual => OsCode::KEY_KPEQUAL, +KeyCode::Pause => OsCode::KEY_PAUSE, +KeyCode::KpComma => OsCode::KEY_KPCOMMA, +KeyCode::LGui => OsCode::KEY_LEFTMETA, +KeyCode::RGui => OsCode::KEY_RIGHTMETA, +KeyCode::Stop => OsCode::KEY_STOP, +KeyCode::Again => OsCode::KEY_AGAIN, +KeyCode::Undo => OsCode::KEY_UNDO, +KeyCode::Copy => OsCode::KEY_COPY, +KeyCode::Paste => OsCode::KEY_PASTE, +KeyCode::Find => OsCode::KEY_FIND, +KeyCode::Cut => OsCode::KEY_CUT, +KeyCode::Help => OsCode::KEY_HELP, +KeyCode::Menu => OsCode::KEY_MENU, +KeyCode::MediaCalc => OsCode::KEY_CALC, +KeyCode::MediaSleep => OsCode::KEY_SLEEP, +KeyCode::MediaWWW => OsCode::KEY_WWW, +KeyCode::MediaCoffee => OsCode::KEY_COFFEE, +KeyCode::MediaBack => OsCode::KEY_BACK, +KeyCode::MediaForward => OsCode::KEY_FORWARD, +KeyCode::MediaEjectCD => OsCode::KEY_EJECTCD, +KeyCode::MediaNextSong => OsCode::KEY_NEXTSONG, +KeyCode::MediaPlayPause => OsCode::KEY_PLAYPAUSE, +KeyCode::MediaPreviousSong => OsCode::KEY_PREVIOUSSONG, +KeyCode::MediaStopCD => OsCode::KEY_STOPCD, +KeyCode::MediaRefresh => OsCode::KEY_REFRESH, +KeyCode::MediaEdit => OsCode::KEY_EDIT, +KeyCode::MediaScrollUp => OsCode::KEY_SCROLLUP, +KeyCode::MediaScrollDown => OsCode::KEY_SCROLLDOWN, +KeyCode::F13 => OsCode::KEY_F13, +KeyCode::F14 => OsCode::KEY_F14, +KeyCode::F15 => OsCode::KEY_F15, +KeyCode::F16 => OsCode::KEY_F16, +KeyCode::F17 => OsCode::KEY_F17, +KeyCode::F18 => OsCode::KEY_F18, +KeyCode::F19 => OsCode::KEY_F19, +KeyCode::F20 => OsCode::KEY_F20, +KeyCode::F21 => OsCode::KEY_F21, +KeyCode::F22 => OsCode::KEY_F22, +KeyCode::F23 => OsCode::KEY_F23, +KeyCode::F24 => OsCode::KEY_F24, +KeyCode::Wakeup => OsCode::KEY_WAKEUP, +KeyCode::BrightnessUp => OsCode::KEY_BRIGHTNESSUP, +KeyCode::BrightnessDown => OsCode::KEY_BRIGHTNESSDOWN, +KeyCode::KbdIllumUp => OsCode::KEY_KBDILLUMUP, +KeyCode::KbdIllumDown => OsCode::KEY_KBDILLUMDOWN, +KeyCode::Lang1 => OsCode::KEY_HANGEUL, +KeyCode::Lang2 => OsCode::KEY_HANJA, +KeyCode::NonUsBslash => OsCode::KEY_102ND, +KeyCode::PScreen => OsCode::KEY_PRINT, +KeyCode::Application => OsCode::KEY_COMPOSE, +KeyCode::AltErase => OsCode::KEY_ALTERASE, +KeyCode::Cancel => OsCode::KEY_CANCEL, +KeyCode::MediaMute => OsCode::KEY_MICMUTE, +KeyCode::Intl1 => OsCode::KEY_RO, +KeyCode::Intl3 => OsCode::KEY_YEN, +KeyCode::K0xAA => OsCode::KEY_MEDIA, +KeyCode::K0xAB => OsCode::KEY_EMAIL, +KeyCode::K0xAC => OsCode::KEY_PLAYER, +KeyCode::K0xAD => OsCode::KEY_HOMEPAGE, +KeyCode::K0xAE => OsCode::KEY_MAIL, +KeyCode::K0xAF => OsCode::KEY_MUHENKAN, +KeyCode::K0xB0 => OsCode::KEY_HENKAN, +KeyCode::K0xB1 => OsCode::KEY_KATAKANA, +KeyCode::K0xB2 => OsCode::KEY_KATAKANAHIRAGANA, +KeyCode::K0xB3 => OsCode::KEY_HIRAGANA, +KeyCode::K252 => OsCode::KEY_252, +KeyCode::K253 => OsCode::KEY_253, +KeyCode::K254 => OsCode::KEY_254, +KeyCode::K255 => OsCode::KEY_255, +KeyCode::K256 => OsCode::BTN_0, +KeyCode::K257 => OsCode::BTN_1, +KeyCode::K258 => OsCode::BTN_2, +KeyCode::K259 => OsCode::BTN_3, +KeyCode::K260 => OsCode::BTN_4, +KeyCode::K261 => OsCode::BTN_5, +KeyCode::K262 => OsCode::BTN_6, +KeyCode::K263 => OsCode::BTN_7, +KeyCode::K264 => OsCode::BTN_8, +KeyCode::K265 => OsCode::BTN_9, +KeyCode::K266 => OsCode::KEY_266, +KeyCode::K267 => OsCode::KEY_267, +KeyCode::K268 => OsCode::KEY_268, +KeyCode::K269 => OsCode::KEY_269, +KeyCode::K270 => OsCode::KEY_270, +KeyCode::K271 => OsCode::KEY_271, +KeyCode::K272 => OsCode::BTN_LEFT, +KeyCode::K273 => OsCode::BTN_RIGHT, +KeyCode::K274 => OsCode::BTN_MIDDLE, +KeyCode::K275 => OsCode::BTN_SIDE, +KeyCode::K276 => OsCode::BTN_EXTRA, +KeyCode::K277 => OsCode::BTN_FORWARD, +KeyCode::K278 => OsCode::BTN_BACK, +KeyCode::K279 => OsCode::BTN_TASK, +KeyCode::K280 => OsCode::KEY_280, +KeyCode::K281 => OsCode::KEY_281, +KeyCode::K282 => OsCode::KEY_282, +KeyCode::K283 => OsCode::KEY_283, +KeyCode::K284 => OsCode::KEY_284, +KeyCode::K285 => OsCode::KEY_285, +KeyCode::K286 => OsCode::KEY_286, +KeyCode::K287 => OsCode::KEY_287, +KeyCode::K288 => OsCode::BTN_TRIGGER, +KeyCode::K289 => OsCode::BTN_THUMB, +KeyCode::K290 => OsCode::BTN_THUMB2, +KeyCode::K291 => OsCode::BTN_TOP, +KeyCode::K292 => OsCode::BTN_TOP2, +KeyCode::K293 => OsCode::BTN_PINKIE, +KeyCode::K294 => OsCode::BTN_BASE, +KeyCode::K295 => OsCode::BTN_BASE2, +KeyCode::K296 => OsCode::BTN_BASE3, +KeyCode::K297 => OsCode::BTN_BASE4, +KeyCode::K298 => OsCode::BTN_BASE5, +KeyCode::K299 => OsCode::BTN_BASE6, +KeyCode::K300 => OsCode::KEY_300, +KeyCode::K301 => OsCode::KEY_301, +KeyCode::K302 => OsCode::KEY_302, +KeyCode::K303 => OsCode::BTN_DEAD, +KeyCode::K304 => OsCode::BTN_SOUTH, +KeyCode::K305 => OsCode::BTN_EAST, +KeyCode::K306 => OsCode::BTN_C, +KeyCode::K307 => OsCode::BTN_NORTH, +KeyCode::K308 => OsCode::BTN_WEST, +KeyCode::K309 => OsCode::BTN_Z, +KeyCode::K310 => OsCode::BTN_TL, +KeyCode::K311 => OsCode::BTN_TR, +KeyCode::K312 => OsCode::BTN_TL2, +KeyCode::K313 => OsCode::BTN_TR2, +KeyCode::K314 => OsCode::BTN_SELECT, +KeyCode::K315 => OsCode::BTN_START, +KeyCode::K316 => OsCode::BTN_MODE, +KeyCode::K317 => OsCode::BTN_THUMBL, +KeyCode::K318 => OsCode::BTN_THUMBR, +KeyCode::K319 => OsCode::KEY_319, +KeyCode::K320 => OsCode::BTN_TOOL_PEN, +KeyCode::K321 => OsCode::BTN_TOOL_RUBBER, +KeyCode::K322 => OsCode::BTN_TOOL_BRUSH, +KeyCode::K323 => OsCode::BTN_TOOL_PENCIL, +KeyCode::K324 => OsCode::BTN_TOOL_AIRBRUSH, +KeyCode::K325 => OsCode::BTN_TOOL_FINGER, +KeyCode::K326 => OsCode::BTN_TOOL_MOUSE, +KeyCode::K327 => OsCode::BTN_TOOL_LENS, +KeyCode::K328 => OsCode::BTN_TOOL_QUINTTAP, +KeyCode::K329 => OsCode::BTN_STYLUS3, +KeyCode::K330 => OsCode::BTN_TOUCH, +KeyCode::K331 => OsCode::BTN_STYLUS, +KeyCode::K332 => OsCode::BTN_STYLUS2, +KeyCode::K333 => OsCode::BTN_TOOL_DOUBLETAP, +KeyCode::K334 => OsCode::BTN_TOOL_TRIPLETAP, +KeyCode::K335 => OsCode::BTN_TOOL_QUADTAP, +KeyCode::K336 => OsCode::BTN_GEAR_DOWN, +KeyCode::K337 => OsCode::BTN_GEAR_UP, +KeyCode::K338 => OsCode::KEY_338, +KeyCode::K339 => OsCode::KEY_339, +KeyCode::K340 => OsCode::KEY_340, +KeyCode::K341 => OsCode::KEY_341, +KeyCode::K342 => OsCode::KEY_342, +KeyCode::K343 => OsCode::KEY_343, +KeyCode::K344 => OsCode::KEY_344, +KeyCode::K345 => OsCode::KEY_345, +KeyCode::K346 => OsCode::KEY_346, +KeyCode::K347 => OsCode::KEY_347, +KeyCode::K348 => OsCode::KEY_348, +KeyCode::K349 => OsCode::KEY_349, +KeyCode::K350 => OsCode::KEY_350, +KeyCode::K351 => OsCode::KEY_351, +KeyCode::K352 => OsCode::KEY_OK, +KeyCode::K353 => OsCode::KEY_SELECT, +KeyCode::K354 => OsCode::KEY_GOTO, +KeyCode::K355 => OsCode::KEY_CLEAR, +KeyCode::K356 => OsCode::KEY_POWER2, +KeyCode::K357 => OsCode::KEY_OPTION, +KeyCode::K358 => OsCode::KEY_INFO, +KeyCode::K359 => OsCode::KEY_TIME, +KeyCode::K360 => OsCode::KEY_VENDOR, +KeyCode::K361 => OsCode::KEY_ARCHIVE, +KeyCode::K362 => OsCode::KEY_PROGRAM, +KeyCode::K363 => OsCode::KEY_CHANNEL, +KeyCode::K364 => OsCode::KEY_FAVORITES, +KeyCode::K365 => OsCode::KEY_EPG, +KeyCode::K366 => OsCode::KEY_PVR, +KeyCode::K367 => OsCode::KEY_MHP, +KeyCode::K368 => OsCode::KEY_LANGUAGE, +KeyCode::K369 => OsCode::KEY_TITLE, +KeyCode::K370 => OsCode::KEY_SUBTITLE, +KeyCode::K371 => OsCode::KEY_ANGLE, +KeyCode::K372 => OsCode::KEY_FULL_SCREEN, +KeyCode::K373 => OsCode::KEY_MODE, +KeyCode::K374 => OsCode::KEY_KEYBOARD, +KeyCode::K375 => OsCode::KEY_ASPECT_RATIO, +KeyCode::K376 => OsCode::KEY_PC, +KeyCode::K377 => OsCode::KEY_TV, +KeyCode::K378 => OsCode::KEY_TV2, +KeyCode::K379 => OsCode::KEY_VCR, +KeyCode::K380 => OsCode::KEY_VCR2, +KeyCode::K381 => OsCode::KEY_SAT, +KeyCode::K382 => OsCode::KEY_SAT2, +KeyCode::K383 => OsCode::KEY_CD, +KeyCode::K384 => OsCode::KEY_TAPE, +KeyCode::K385 => OsCode::KEY_RADIO, +KeyCode::K386 => OsCode::KEY_TUNER, +KeyCode::K387 => OsCode::KEY_PLAYER, +KeyCode::K388 => OsCode::KEY_TEXT, +KeyCode::K389 => OsCode::KEY_DVD, +KeyCode::K390 => OsCode::KEY_AUX, +KeyCode::K391 => OsCode::KEY_MP3, +KeyCode::K392 => OsCode::KEY_AUDIO, +KeyCode::K393 => OsCode::KEY_VIDEO, +KeyCode::K394 => OsCode::KEY_DIRECTORY, +KeyCode::K395 => OsCode::KEY_LIST, +KeyCode::K396 => OsCode::KEY_MEMO, +KeyCode::K397 => OsCode::KEY_CALENDAR, +KeyCode::K398 => OsCode::KEY_RED, +KeyCode::K399 => OsCode::KEY_GREEN, +KeyCode::K400 => OsCode::KEY_YELLOW, +KeyCode::K401 => OsCode::KEY_BLUE, +KeyCode::K402 => OsCode::KEY_CHANNELUP, +KeyCode::K403 => OsCode::KEY_CHANNELDOWN, +KeyCode::K404 => OsCode::KEY_FIRST, +KeyCode::K405 => OsCode::KEY_LAST, +KeyCode::K406 => OsCode::KEY_AB, +KeyCode::K407 => OsCode::KEY_NEXT, +KeyCode::K408 => OsCode::KEY_RESTART, +KeyCode::K409 => OsCode::KEY_SLOW, +KeyCode::K410 => OsCode::KEY_SHUFFLE, +KeyCode::K411 => OsCode::KEY_BREAK, +KeyCode::K412 => OsCode::KEY_PREVIOUS, +KeyCode::K413 => OsCode::KEY_DIGITS, +KeyCode::K414 => OsCode::KEY_TEEN, +KeyCode::K415 => OsCode::KEY_TWEN, +KeyCode::K416 => OsCode::KEY_VIDEOPHONE, +KeyCode::K417 => OsCode::KEY_GAMES, +KeyCode::K418 => OsCode::KEY_ZOOMIN, +KeyCode::K419 => OsCode::KEY_ZOOMOUT, +KeyCode::K420 => OsCode::KEY_ZOOMRESET, +KeyCode::K421 => OsCode::KEY_WORDPROCESSOR, +KeyCode::K422 => OsCode::KEY_EDITOR, +KeyCode::K423 => OsCode::KEY_SPREADSHEET, +KeyCode::K424 => OsCode::KEY_GRAPHICSEDITOR, +KeyCode::K425 => OsCode::KEY_PRESENTATION, +KeyCode::K426 => OsCode::KEY_DATABASE, +KeyCode::K427 => OsCode::KEY_NEWS, +KeyCode::K428 => OsCode::KEY_VOICEMAIL, +KeyCode::K429 => OsCode::KEY_ADDRESSBOOK, +KeyCode::K430 => OsCode::KEY_MESSENGER, +KeyCode::K431 => OsCode::KEY_DISPLAYTOGGLE, +KeyCode::K432 => OsCode::KEY_SPELLCHECK, +KeyCode::K433 => OsCode::KEY_LOGOFF, +KeyCode::K434 => OsCode::KEY_DOLLAR, +KeyCode::K435 => OsCode::KEY_EURO, +KeyCode::K436 => OsCode::KEY_FRAMEBACK, +KeyCode::K437 => OsCode::KEY_FRAMEFORWARD, +KeyCode::K438 => OsCode::KEY_CONTEXT_MENU, +KeyCode::K439 => OsCode::KEY_MEDIA_REPEAT, +KeyCode::K440 => OsCode::KEY_10CHANNELSUP, +KeyCode::K441 => OsCode::KEY_10CHANNELSDOWN, +KeyCode::K442 => OsCode::KEY_IMAGES, +KeyCode::K443 => OsCode::KEY_443, +KeyCode::K444 => OsCode::KEY_444, +KeyCode::K445 => OsCode::KEY_445, +KeyCode::K446 => OsCode::KEY_446, +KeyCode::K447 => OsCode::KEY_447, +KeyCode::K448 => OsCode::KEY_DEL_EOL, +KeyCode::K449 => OsCode::KEY_DEL_EOS, +KeyCode::K450 => OsCode::KEY_INS_LINE, +KeyCode::K451 => OsCode::KEY_DEL_LINE, +KeyCode::K452 => OsCode::KEY_452, +KeyCode::K453 => OsCode::KEY_453, +KeyCode::K454 => OsCode::KEY_454, +KeyCode::K455 => OsCode::KEY_455, +KeyCode::K456 => OsCode::KEY_456, +KeyCode::K457 => OsCode::KEY_457, +KeyCode::K458 => OsCode::KEY_458, +KeyCode::K459 => OsCode::KEY_459, +KeyCode::K460 => OsCode::KEY_460, +KeyCode::K461 => OsCode::KEY_461, +KeyCode::K462 => OsCode::KEY_462, +KeyCode::K463 => OsCode::KEY_463, +KeyCode::K464 => OsCode::KEY_FN, +KeyCode::K465 => OsCode::KEY_FN_ESC, +KeyCode::K466 => OsCode::KEY_FN_F1, +KeyCode::K467 => OsCode::KEY_FN_F2, +KeyCode::K468 => OsCode::KEY_FN_F3, +KeyCode::K469 => OsCode::KEY_FN_F4, +KeyCode::K470 => OsCode::KEY_FN_F5, +KeyCode::K471 => OsCode::KEY_FN_F6, +KeyCode::K472 => OsCode::KEY_FN_F7, +KeyCode::K473 => OsCode::KEY_FN_F8, +KeyCode::K474 => OsCode::KEY_FN_F9, +KeyCode::K475 => OsCode::KEY_FN_F10, +KeyCode::K476 => OsCode::KEY_FN_F11, +KeyCode::K477 => OsCode::KEY_FN_F12, +KeyCode::K478 => OsCode::KEY_FN_1, +KeyCode::K479 => OsCode::KEY_FN_2, +KeyCode::K480 => OsCode::KEY_FN_D, +KeyCode::K481 => OsCode::KEY_FN_E, +KeyCode::K482 => OsCode::KEY_FN_F, +KeyCode::K483 => OsCode::KEY_FN_S, +KeyCode::K484 => OsCode::KEY_FN_B, +KeyCode::K485 => OsCode::KEY_485, +KeyCode::K486 => OsCode::KEY_486, +KeyCode::K487 => OsCode::KEY_487, +KeyCode::K488 => OsCode::KEY_488, +KeyCode::K489 => OsCode::KEY_489, +KeyCode::K490 => OsCode::KEY_490, +KeyCode::K491 => OsCode::KEY_491, +KeyCode::K492 => OsCode::KEY_492, +KeyCode::K493 => OsCode::KEY_493, +KeyCode::K494 => OsCode::KEY_494, +KeyCode::K495 => OsCode::KEY_495, +KeyCode::K496 => OsCode::KEY_496, +KeyCode::K497 => OsCode::KEY_BRL_DOT1, +KeyCode::K498 => OsCode::KEY_BRL_DOT2, +KeyCode::K499 => OsCode::KEY_BRL_DOT3, +KeyCode::K500 => OsCode::KEY_BRL_DOT4, +KeyCode::K501 => OsCode::KEY_BRL_DOT5, +KeyCode::K502 => OsCode::KEY_BRL_DOT6, +KeyCode::K503 => OsCode::KEY_BRL_DOT7, +KeyCode::K504 => OsCode::KEY_BRL_DOT8, +KeyCode::K505 => OsCode::KEY_BRL_DOT9, +KeyCode::K506 => OsCode::KEY_BRL_DOT10, +KeyCode::K507 => OsCode::KEY_507, +KeyCode::K508 => OsCode::KEY_508, +KeyCode::K509 => OsCode::KEY_509, +KeyCode::K510 => OsCode::KEY_510, +KeyCode::K511 => OsCode::KEY_511, +KeyCode::K512 => OsCode::KEY_NUMERIC_0, +KeyCode::K513 => OsCode::KEY_NUMERIC_1, +KeyCode::K514 => OsCode::KEY_NUMERIC_2, +KeyCode::K515 => OsCode::KEY_NUMERIC_3, +KeyCode::K516 => OsCode::KEY_NUMERIC_4, +KeyCode::K517 => OsCode::KEY_NUMERIC_5, +KeyCode::K518 => OsCode::KEY_NUMERIC_6, +KeyCode::K519 => OsCode::KEY_NUMERIC_7, +KeyCode::K520 => OsCode::KEY_NUMERIC_8, +KeyCode::K521 => OsCode::KEY_NUMERIC_9, +KeyCode::K522 => OsCode::KEY_NUMERIC_STAR, +KeyCode::K523 => OsCode::KEY_NUMERIC_POUND, +KeyCode::K524 => OsCode::KEY_NUMERIC_A, +KeyCode::K525 => OsCode::KEY_NUMERIC_B, +KeyCode::K526 => OsCode::KEY_NUMERIC_C, +KeyCode::K527 => OsCode::KEY_NUMERIC_D, +KeyCode::K528 => OsCode::KEY_CAMERA_FOCUS, +KeyCode::K529 => OsCode::KEY_WPS_BUTTON, +KeyCode::K530 => OsCode::KEY_TOUCHPAD_TOGGLE, +KeyCode::K531 => OsCode::KEY_TOUCHPAD_ON, +KeyCode::K532 => OsCode::KEY_TOUCHPAD_OFF, +KeyCode::K533 => OsCode::KEY_CAMERA_ZOOMIN, +KeyCode::K534 => OsCode::KEY_CAMERA_ZOOMOUT, +KeyCode::K535 => OsCode::KEY_CAMERA_UP, +KeyCode::K536 => OsCode::KEY_CAMERA_DOWN, +KeyCode::K537 => OsCode::KEY_CAMERA_LEFT, +KeyCode::K538 => OsCode::KEY_CAMERA_RIGHT, +KeyCode::K539 => OsCode::KEY_ATTENDANT_ON, +KeyCode::K540 => OsCode::KEY_ATTENDANT_OFF, +KeyCode::K541 => OsCode::KEY_ATTENDANT_TOGGLE, +KeyCode::K542 => OsCode::KEY_LIGHTS_TOGGLE, +KeyCode::K543 => OsCode::KEY_543, +KeyCode::K544 => OsCode::BTN_DPAD_UP, +KeyCode::K545 => OsCode::BTN_DPAD_DOWN, +KeyCode::K546 => OsCode::BTN_DPAD_LEFT, +KeyCode::K547 => OsCode::BTN_DPAD_RIGHT, +KeyCode::K548 => OsCode::KEY_548, +KeyCode::K549 => OsCode::KEY_549, +KeyCode::K550 => OsCode::KEY_550, +KeyCode::K551 => OsCode::KEY_551, +KeyCode::K552 => OsCode::KEY_552, +KeyCode::K553 => OsCode::KEY_553, +KeyCode::K554 => OsCode::KEY_554, +KeyCode::K555 => OsCode::KEY_555, +KeyCode::K556 => OsCode::KEY_556, +KeyCode::K557 => OsCode::KEY_557, +KeyCode::K558 => OsCode::KEY_558, +KeyCode::K559 => OsCode::KEY_559, +KeyCode::K560 => OsCode::KEY_ALS_TOGGLE, +KeyCode::K561 => OsCode::KEY_ROTATE_LOCK_TOGGLE, +KeyCode::K562 => OsCode::KEY_562, +KeyCode::K563 => OsCode::KEY_563, +KeyCode::K564 => OsCode::KEY_564, +KeyCode::K565 => OsCode::KEY_565, +KeyCode::K566 => OsCode::KEY_566, +KeyCode::K567 => OsCode::KEY_567, +KeyCode::K568 => OsCode::KEY_568, +KeyCode::K569 => OsCode::KEY_569, +KeyCode::K570 => OsCode::KEY_570, +KeyCode::K571 => OsCode::KEY_571, +KeyCode::K572 => OsCode::KEY_572, +KeyCode::K573 => OsCode::KEY_573, +KeyCode::K574 => OsCode::KEY_574, +KeyCode::K575 => OsCode::KEY_575, +KeyCode::K576 => OsCode::KEY_BUTTONCONFIG, +KeyCode::K577 => OsCode::KEY_TASKMANAGER, +KeyCode::K578 => OsCode::KEY_JOURNAL, +KeyCode::K579 => OsCode::KEY_CONTROLPANEL, +KeyCode::K580 => OsCode::KEY_APPSELECT, +KeyCode::K581 => OsCode::KEY_SCREENSAVER, +KeyCode::K582 => OsCode::KEY_VOICECOMMAND, +KeyCode::K583 => OsCode::KEY_ASSISTANT, +KeyCode::K584 => OsCode::KEY_KBD_LAYOUT_NEXT, +KeyCode::K585 => OsCode::KEY_585, +KeyCode::K586 => OsCode::KEY_586, +KeyCode::K587 => OsCode::KEY_587, +KeyCode::K588 => OsCode::KEY_588, +KeyCode::K589 => OsCode::KEY_589, +KeyCode::K590 => OsCode::KEY_590, +KeyCode::K591 => OsCode::KEY_591, +KeyCode::K592 => OsCode::KEY_BRIGHTNESS_MIN, +KeyCode::K593 => OsCode::KEY_BRIGHTNESS_MAX, +KeyCode::K594 => OsCode::KEY_594, +KeyCode::K595 => OsCode::KEY_595, +KeyCode::K596 => OsCode::KEY_596, +KeyCode::K597 => OsCode::KEY_597, +KeyCode::K598 => OsCode::KEY_598, +KeyCode::K599 => OsCode::KEY_599, +KeyCode::K600 => OsCode::KEY_600, +KeyCode::K601 => OsCode::KEY_601, +KeyCode::K602 => OsCode::KEY_602, +KeyCode::K603 => OsCode::KEY_603, +KeyCode::K604 => OsCode::KEY_604, +KeyCode::K605 => OsCode::KEY_605, +KeyCode::K606 => OsCode::KEY_606, +KeyCode::K607 => OsCode::KEY_607, +KeyCode::K608 => OsCode::KEY_KBDINPUTASSIST_PREV, +KeyCode::K609 => OsCode::KEY_KBDINPUTASSIST_NEXT, +KeyCode::K610 => OsCode::KEY_KBDINPUTASSIST_PREVGROUP, +KeyCode::K611 => OsCode::KEY_KBDINPUTASSIST_NEXTGROUP, +KeyCode::K612 => OsCode::KEY_KBDINPUTASSIST_ACCEPT, +KeyCode::K613 => OsCode::KEY_KBDINPUTASSIST_CANCEL, +KeyCode::K614 => OsCode::KEY_RIGHT_UP, +KeyCode::K615 => OsCode::KEY_RIGHT_DOWN, +KeyCode::K616 => OsCode::KEY_LEFT_UP, +KeyCode::K617 => OsCode::KEY_LEFT_DOWN, +KeyCode::K618 => OsCode::KEY_ROOT_MENU, +KeyCode::K619 => OsCode::KEY_MEDIA_TOP_MENU, +KeyCode::K620 => OsCode::KEY_NUMERIC_11, +KeyCode::K621 => OsCode::KEY_NUMERIC_12, +KeyCode::K622 => OsCode::KEY_AUDIO_DESC, +KeyCode::K623 => OsCode::KEY_3D_MODE, +KeyCode::K624 => OsCode::KEY_NEXT_FAVORITE, +KeyCode::K625 => OsCode::KEY_STOP_RECORD, +KeyCode::K626 => OsCode::KEY_PAUSE_RECORD, +KeyCode::K627 => OsCode::KEY_VOD, +KeyCode::K628 => OsCode::KEY_UNMUTE, +KeyCode::K629 => OsCode::KEY_FASTREVERSE, +KeyCode::K630 => OsCode::KEY_SLOWREVERSE, +KeyCode::K631 => OsCode::KEY_DATA, +KeyCode::K632 => OsCode::KEY_ONSCREEN_KEYBOARD, +KeyCode::K633 => OsCode::KEY_633, +KeyCode::K634 => OsCode::KEY_634, +KeyCode::K635 => OsCode::KEY_635, +KeyCode::K636 => OsCode::KEY_636, +KeyCode::K637 => OsCode::KEY_637, +KeyCode::K638 => OsCode::KEY_638, +KeyCode::K639 => OsCode::KEY_639, +KeyCode::K640 => OsCode::KEY_640, +KeyCode::K641 => OsCode::KEY_641, +KeyCode::K642 => OsCode::KEY_642, +KeyCode::K643 => OsCode::KEY_643, +KeyCode::K644 => OsCode::KEY_644, +KeyCode::K645 => OsCode::KEY_645, +KeyCode::K646 => OsCode::KEY_646, +KeyCode::K647 => OsCode::KEY_647, +KeyCode::K648 => OsCode::KEY_648, +KeyCode::K649 => OsCode::KEY_649, +KeyCode::K650 => OsCode::KEY_650, +KeyCode::K651 => OsCode::KEY_651, +KeyCode::K652 => OsCode::KEY_652, +KeyCode::K653 => OsCode::KEY_653, +KeyCode::K654 => OsCode::KEY_654, +KeyCode::K655 => OsCode::KEY_655, +KeyCode::K656 => OsCode::KEY_656, +KeyCode::K657 => OsCode::KEY_657, +KeyCode::K658 => OsCode::KEY_658, +KeyCode::K659 => OsCode::KEY_659, +KeyCode::K660 => OsCode::KEY_660, +KeyCode::K661 => OsCode::KEY_661, +KeyCode::K662 => OsCode::KEY_662, +KeyCode::K663 => OsCode::KEY_663, +KeyCode::K664 => OsCode::KEY_664, +KeyCode::K665 => OsCode::KEY_665, +KeyCode::K666 => OsCode::KEY_666, +KeyCode::K667 => OsCode::KEY_667, +KeyCode::K668 => OsCode::KEY_668, +KeyCode::K669 => OsCode::KEY_669, +KeyCode::K670 => OsCode::KEY_670, +KeyCode::K671 => OsCode::KEY_671, +KeyCode::K672 => OsCode::KEY_672, +KeyCode::K673 => OsCode::KEY_673, +KeyCode::K674 => OsCode::KEY_674, +KeyCode::K675 => OsCode::KEY_675, +KeyCode::K676 => OsCode::KEY_676, +KeyCode::K677 => OsCode::KEY_677, +KeyCode::K678 => OsCode::KEY_678, +KeyCode::K679 => OsCode::KEY_679, +KeyCode::K680 => OsCode::KEY_680, +KeyCode::K681 => OsCode::KEY_681, +KeyCode::K682 => OsCode::KEY_682, +KeyCode::K683 => OsCode::KEY_683, +KeyCode::K684 => OsCode::KEY_684, +KeyCode::K685 => OsCode::KEY_685, +KeyCode::K686 => OsCode::KEY_686, +KeyCode::K687 => OsCode::KEY_687, +KeyCode::K688 => OsCode::KEY_688, +KeyCode::K689 => OsCode::KEY_689, +KeyCode::K690 => OsCode::KEY_690, +KeyCode::K691 => OsCode::KEY_691, +KeyCode::K692 => OsCode::KEY_692, +KeyCode::K693 => OsCode::KEY_693, +KeyCode::K694 => OsCode::KEY_694, +KeyCode::K695 => OsCode::KEY_695, +KeyCode::K696 => OsCode::KEY_696, +KeyCode::K697 => OsCode::KEY_697, +KeyCode::K698 => OsCode::KEY_698, +KeyCode::K699 => OsCode::KEY_699, +KeyCode::K700 => OsCode::KEY_700, +KeyCode::K701 => OsCode::KEY_701, +KeyCode::K702 => OsCode::KEY_702, +KeyCode::K703 => OsCode::KEY_703, +KeyCode::K704 => OsCode::BTN_TRIGGER_HAPPY1, +KeyCode::K705 => OsCode::BTN_TRIGGER_HAPPY2, +KeyCode::K706 => OsCode::BTN_TRIGGER_HAPPY3, +KeyCode::K707 => OsCode::BTN_TRIGGER_HAPPY4, +KeyCode::K708 => OsCode::BTN_TRIGGER_HAPPY5, +KeyCode::K709 => OsCode::BTN_TRIGGER_HAPPY6, +KeyCode::K710 => OsCode::BTN_TRIGGER_HAPPY7, +KeyCode::K711 => OsCode::BTN_TRIGGER_HAPPY8, +KeyCode::K712 => OsCode::BTN_TRIGGER_HAPPY9, +KeyCode::K713 => OsCode::BTN_TRIGGER_HAPPY10, +KeyCode::K714 => OsCode::BTN_TRIGGER_HAPPY11, +KeyCode::K715 => OsCode::BTN_TRIGGER_HAPPY12, +KeyCode::K716 => OsCode::BTN_TRIGGER_HAPPY13, +KeyCode::K717 => OsCode::BTN_TRIGGER_HAPPY14, +KeyCode::K718 => OsCode::BTN_TRIGGER_HAPPY15, +KeyCode::K719 => OsCode::BTN_TRIGGER_HAPPY16, +KeyCode::K720 => OsCode::BTN_TRIGGER_HAPPY17, +KeyCode::K721 => OsCode::BTN_TRIGGER_HAPPY18, +KeyCode::K722 => OsCode::BTN_TRIGGER_HAPPY19, +KeyCode::K723 => OsCode::BTN_TRIGGER_HAPPY20, +KeyCode::K724 => OsCode::BTN_TRIGGER_HAPPY21, +KeyCode::K725 => OsCode::BTN_TRIGGER_HAPPY22, +KeyCode::K726 => OsCode::BTN_TRIGGER_HAPPY23, +KeyCode::K727 => OsCode::BTN_TRIGGER_HAPPY24, +KeyCode::K728 => OsCode::BTN_TRIGGER_HAPPY25, +KeyCode::K729 => OsCode::BTN_TRIGGER_HAPPY26, +KeyCode::K730 => OsCode::BTN_TRIGGER_HAPPY27, +KeyCode::K731 => OsCode::BTN_TRIGGER_HAPPY28, +KeyCode::K732 => OsCode::BTN_TRIGGER_HAPPY29, +KeyCode::K733 => OsCode::BTN_TRIGGER_HAPPY30, +KeyCode::K734 => OsCode::BTN_TRIGGER_HAPPY31, +KeyCode::K735 => OsCode::BTN_TRIGGER_HAPPY32, +KeyCode::K736 => OsCode::BTN_TRIGGER_HAPPY33, +KeyCode::K737 => OsCode::BTN_TRIGGER_HAPPY34, +KeyCode::K738 => OsCode::BTN_TRIGGER_HAPPY35, +KeyCode::K739 => OsCode::BTN_TRIGGER_HAPPY36, +KeyCode::K740 => OsCode::BTN_TRIGGER_HAPPY37, +KeyCode::K741 => OsCode::BTN_TRIGGER_HAPPY38, +KeyCode::K742 => OsCode::BTN_TRIGGER_HAPPY39, +KeyCode::K743 => OsCode::BTN_TRIGGER_HAPPY40, +KeyCode::K744 => OsCode::BTN_MAX, +KeyCode::MWU => OsCode::MouseWheelUp, +KeyCode::MWD => OsCode::MouseWheelDown, +KeyCode::MWL => OsCode::MouseWheelLeft, +KeyCode::MWR => OsCode::MouseWheelRight, + +=== osc to u16 +KEY_RESERVED = 0, +KEY_ESC = 1, +KEY_1 = 2, +KEY_2 = 3, +KEY_3 = 4, +KEY_4 = 5, +KEY_5 = 6, +KEY_6 = 7, +KEY_7 = 8, +KEY_8 = 9, +KEY_9 = 10, +KEY_0 = 11, +KEY_MINUS = 12, +KEY_EQUAL = 13, +KEY_BACKSPACE = 14, +KEY_TAB = 15, +KEY_Q = 16, +KEY_W = 17, +KEY_E = 18, +KEY_R = 19, +KEY_T = 20, +KEY_Y = 21, +KEY_U = 22, +KEY_I = 23, +KEY_O = 24, +KEY_P = 25, +KEY_LEFTBRACE = 26, +KEY_RIGHTBRACE = 27, +KEY_ENTER = 28, +KEY_LEFTCTRL = 29, +KEY_A = 30, +KEY_S = 31, +KEY_D = 32, +KEY_F = 33, +KEY_G = 34, +KEY_H = 35, +KEY_J = 36, +KEY_K = 37, +KEY_L = 38, +KEY_SEMICOLON = 39, +KEY_APOSTROPHE = 40, +KEY_GRAVE = 41, +KEY_LEFTSHIFT = 42, +KEY_BACKSLASH = 43, +KEY_Z = 44, +KEY_X = 45, +KEY_C = 46, +KEY_V = 47, +KEY_B = 48, +KEY_N = 49, +KEY_M = 50, +KEY_COMMA = 51, +KEY_DOT = 52, +KEY_SLASH = 53, +KEY_RIGHTSHIFT = 54, +KEY_KPASTERISK = 55, +KEY_LEFTALT = 56, +KEY_SPACE = 57, +KEY_CAPSLOCK = 58, +KEY_F1 = 59, +KEY_F2 = 60, +KEY_F3 = 61, +KEY_F4 = 62, +KEY_F5 = 63, +KEY_F6 = 64, +KEY_F7 = 65, +KEY_F8 = 66, +KEY_F9 = 67, +KEY_F10 = 68, +KEY_NUMLOCK = 69, +KEY_SCROLLLOCK = 70, +KEY_KP7 = 71, +KEY_KP8 = 72, +KEY_KP9 = 73, +KEY_KPMINUS = 74, +KEY_KP4 = 75, +KEY_KP5 = 76, +KEY_KP6 = 77, +KEY_KPPLUS = 78, +KEY_KP1 = 79, +KEY_KP2 = 80, +KEY_KP3 = 81, +KEY_KP0 = 82, +KEY_KPDOT = 83, +KEY_84 = 84, +KEY_ZENKAKUHANKAKU = 85, +KEY_102ND = 86, +KEY_F11 = 87, +KEY_F12 = 88, +KEY_RO = 89, +KEY_KATAKANA = 90, +KEY_HIRAGANA = 91, +KEY_HENKAN = 92, +KEY_KATAKANAHIRAGANA = 93, +KEY_MUHENKAN = 94, +KEY_KPJPCOMMA = 95, +KEY_KPENTER = 96, +KEY_RIGHTCTRL = 97, +KEY_KPSLASH = 98, +KEY_SYSRQ = 99, +KEY_RIGHTALT = 100, +KEY_LINEFEED = 101, +KEY_HOME = 102, +KEY_UP = 103, +KEY_PAGEUP = 104, +KEY_LEFT = 105, +KEY_RIGHT = 106, +KEY_END = 107, +KEY_DOWN = 108, +KEY_PAGEDOWN = 109, +KEY_INSERT = 110, +KEY_DELETE = 111, +KEY_MACRO = 112, +KEY_MUTE = 113, +KEY_VOLUMEDOWN = 114, +KEY_VOLUMEUP = 115, +KEY_POWER = 116, +KEY_KPEQUAL = 117, +KEY_KPPLUSMINUS = 118, +KEY_PAUSE = 119, +KEY_SCALE = 120, +KEY_KPCOMMA = 121, +KEY_HANGEUL = 122, +KEY_HANJA = 123, +KEY_YEN = 124, +KEY_LEFTMETA = 125, +KEY_RIGHTMETA = 126, +KEY_COMPOSE = 127, +KEY_STOP = 128, +KEY_AGAIN = 129, +KEY_PROPS = 130, +KEY_UNDO = 131, +KEY_FRONT = 132, +KEY_COPY = 133, +KEY_OPEN = 134, +KEY_PASTE = 135, +KEY_FIND = 136, +KEY_CUT = 137, +KEY_HELP = 138, +KEY_MENU = 139, +KEY_CALC = 140, +KEY_SETUP = 141, +KEY_SLEEP = 142, +KEY_WAKEUP = 143, +KEY_FILE = 144, +KEY_SENDFILE = 145, +KEY_DELETEFILE = 146, +KEY_XFER = 147, +KEY_PROG1 = 148, +KEY_PROG2 = 149, +KEY_WWW = 150, +KEY_MSDOS = 151, +KEY_COFFEE = 152, +KEY_ROTATE_DISPLAY = 153, +KEY_CYCLEWINDOWS = 154, +KEY_MAIL = 155, +KEY_BOOKMARKS = 156, +KEY_COMPUTER = 157, +KEY_BACK = 158, +KEY_FORWARD = 159, +KEY_CLOSECD = 160, +KEY_EJECTCD = 161, +KEY_EJECTCLOSECD = 162, +KEY_NEXTSONG = 163, +KEY_PLAYPAUSE = 164, +KEY_PREVIOUSSONG = 165, +KEY_STOPCD = 166, +KEY_RECORD = 167, +KEY_REWIND = 168, +KEY_PHONE = 169, +KEY_ISO = 170, +KEY_CONFIG = 171, +KEY_HOMEPAGE = 172, +KEY_REFRESH = 173, +KEY_EXIT = 174, +KEY_MOVE = 175, +KEY_EDIT = 176, +KEY_SCROLLUP = 177, +KEY_SCROLLDOWN = 178, +KEY_KPLEFTPAREN = 179, +KEY_KPRIGHTPAREN = 180, +KEY_NEW = 181, +KEY_REDO = 182, +KEY_F13 = 183, +KEY_F14 = 184, +KEY_F15 = 185, +KEY_F16 = 186, +KEY_F17 = 187, +KEY_F18 = 188, +KEY_F19 = 189, +KEY_F20 = 190, +KEY_F21 = 191, +KEY_F22 = 192, +KEY_F23 = 193, +KEY_F24 = 194, +KEY_195 = 195, +KEY_196 = 196, +KEY_197 = 197, +KEY_198 = 198, +KEY_199 = 199, +KEY_PLAYCD = 200, +KEY_PAUSECD = 201, +KEY_PROG3 = 202, +KEY_PROG4 = 203, +KEY_DASHBOARD = 204, +KEY_SUSPEND = 205, +KEY_CLOSE = 206, +KEY_PLAY = 207, +KEY_FASTFORWARD = 208, +KEY_BASSBOOST = 209, +KEY_PRINT = 210, +KEY_HP = 211, +KEY_CAMERA = 212, +KEY_SOUND = 213, +KEY_QUESTION = 214, +KEY_EMAIL = 215, +KEY_CHAT = 216, +KEY_SEARCH = 217, +KEY_CONNECT = 218, +KEY_FINANCE = 219, +KEY_SPORT = 220, +KEY_SHOP = 221, +KEY_ALTERASE = 222, +KEY_CANCEL = 223, +KEY_BRIGHTNESSDOWN = 224, +KEY_BRIGHTNESSUP = 225, +KEY_MEDIA = 226, +KEY_SWITCHVIDEOMODE = 227, +KEY_KBDILLUMTOGGLE = 228, +KEY_KBDILLUMDOWN = 229, +KEY_KBDILLUMUP = 230, +KEY_SEND = 231, +KEY_REPLY = 232, +KEY_FORWARDMAIL = 233, +KEY_SAVE = 234, +KEY_DOCUMENTS = 235, +KEY_BATTERY = 236, +KEY_BLUETOOTH = 237, +KEY_WLAN = 238, +KEY_UWB = 239, +KEY_UNKNOWN = 240, +KEY_VIDEO_NEXT = 241, +KEY_VIDEO_PREV = 242, +KEY_BRIGHTNESS_CYCLE = 243, +KEY_BRIGHTNESS_AUTO = 244, +KEY_DISPLAY_OFF = 245, +KEY_WWAN = 246, +KEY_RFKILL = 247, +KEY_MICMUTE = 248, +KEY_249 = 249, +KEY_250 = 250, +KEY_251 = 251, +KEY_252 = 252, +KEY_253 = 253, +KEY_254 = 254, +KEY_255 = 255, +BTN_0 = 256, +BTN_1 = 257, +BTN_2 = 258, +BTN_3 = 259, +BTN_4 = 260, +BTN_5 = 261, +BTN_6 = 262, +BTN_7 = 263, +BTN_8 = 264, +BTN_9 = 265, +KEY_266 = 266, +KEY_267 = 267, +KEY_268 = 268, +KEY_269 = 269, +KEY_270 = 270, +KEY_271 = 271, +BTN_LEFT = 272, +BTN_RIGHT = 273, +BTN_MIDDLE = 274, +BTN_SIDE = 275, +BTN_EXTRA = 276, +BTN_FORWARD = 277, +BTN_BACK = 278, +BTN_TASK = 279, +KEY_280 = 280, +KEY_281 = 281, +KEY_282 = 282, +KEY_283 = 283, +KEY_284 = 284, +KEY_285 = 285, +KEY_286 = 286, +KEY_287 = 287, +BTN_TRIGGER = 288, +BTN_THUMB = 289, +BTN_THUMB2 = 290, +BTN_TOP = 291, +BTN_TOP2 = 292, +BTN_PINKIE = 293, +BTN_BASE = 294, +BTN_BASE2 = 295, +BTN_BASE3 = 296, +BTN_BASE4 = 297, +BTN_BASE5 = 298, +BTN_BASE6 = 299, +KEY_300 = 300, +KEY_301 = 301, +KEY_302 = 302, +BTN_DEAD = 303, +BTN_SOUTH = 304, +BTN_EAST = 305, +BTN_C = 306, +BTN_NORTH = 307, +BTN_WEST = 308, +BTN_Z = 309, +BTN_TL = 310, +BTN_TR = 311, +BTN_TL2 = 312, +BTN_TR2 = 313, +BTN_SELECT = 314, +BTN_START = 315, +BTN_MODE = 316, +BTN_THUMBL = 317, +BTN_THUMBR = 318, +KEY_319 = 319, +BTN_TOOL_PEN = 320, +BTN_TOOL_RUBBER = 321, +BTN_TOOL_BRUSH = 322, +BTN_TOOL_PENCIL = 323, +BTN_TOOL_AIRBRUSH = 324, +BTN_TOOL_FINGER = 325, +BTN_TOOL_MOUSE = 326, +BTN_TOOL_LENS = 327, +BTN_TOOL_QUINTTAP = 328, +BTN_STYLUS3 = 329, +BTN_TOUCH = 330, +BTN_STYLUS = 331, +BTN_STYLUS2 = 332, +BTN_TOOL_DOUBLETAP = 333, +BTN_TOOL_TRIPLETAP = 334, +BTN_TOOL_QUADTAP = 335, +BTN_GEAR_DOWN = 336, +BTN_GEAR_UP = 337, +KEY_338 = 338, +KEY_339 = 339, +KEY_340 = 340, +KEY_341 = 341, +KEY_342 = 342, +KEY_343 = 343, +KEY_344 = 344, +KEY_345 = 345, +KEY_346 = 346, +KEY_347 = 347, +KEY_348 = 348, +KEY_349 = 349, +KEY_350 = 350, +KEY_351 = 351, +KEY_OK = 352, +KEY_SELECT = 353, +KEY_GOTO = 354, +KEY_CLEAR = 355, +KEY_POWER2 = 356, +KEY_OPTION = 357, +KEY_INFO = 358, +KEY_TIME = 359, +KEY_VENDOR = 360, +KEY_ARCHIVE = 361, +KEY_PROGRAM = 362, +KEY_CHANNEL = 363, +KEY_FAVORITES = 364, +KEY_EPG = 365, +KEY_PVR = 366, +KEY_MHP = 367, +KEY_LANGUAGE = 368, +KEY_TITLE = 369, +KEY_SUBTITLE = 370, +KEY_ANGLE = 371, +KEY_FULL_SCREEN = 372, +KEY_MODE = 373, +KEY_KEYBOARD = 374, +KEY_ASPECT_RATIO = 375, +KEY_PC = 376, +KEY_TV = 377, +KEY_TV2 = 378, +KEY_VCR = 379, +KEY_VCR2 = 380, +KEY_SAT = 381, +KEY_SAT2 = 382, +KEY_CD = 383, +KEY_TAPE = 384, +KEY_RADIO = 385, +KEY_TUNER = 386, +KEY_PLAYER = 387, +KEY_TEXT = 388, +KEY_DVD = 389, +KEY_AUX = 390, +KEY_MP3 = 391, +KEY_AUDIO = 392, +KEY_VIDEO = 393, +KEY_DIRECTORY = 394, +KEY_LIST = 395, +KEY_MEMO = 396, +KEY_CALENDAR = 397, +KEY_RED = 398, +KEY_GREEN = 399, +KEY_YELLOW = 400, +KEY_BLUE = 401, +KEY_CHANNELUP = 402, +KEY_CHANNELDOWN = 403, +KEY_FIRST = 404, +KEY_LAST = 405, +KEY_AB = 406, +KEY_NEXT = 407, +KEY_RESTART = 408, +KEY_SLOW = 409, +KEY_SHUFFLE = 410, +KEY_BREAK = 411, +KEY_PREVIOUS = 412, +KEY_DIGITS = 413, +KEY_TEEN = 414, +KEY_TWEN = 415, +KEY_VIDEOPHONE = 416, +KEY_GAMES = 417, +KEY_ZOOMIN = 418, +KEY_ZOOMOUT = 419, +KEY_ZOOMRESET = 420, +KEY_WORDPROCESSOR = 421, +KEY_EDITOR = 422, +KEY_SPREADSHEET = 423, +KEY_GRAPHICSEDITOR = 424, +KEY_PRESENTATION = 425, +KEY_DATABASE = 426, +KEY_NEWS = 427, +KEY_VOICEMAIL = 428, +KEY_ADDRESSBOOK = 429, +KEY_MESSENGER = 430, +KEY_DISPLAYTOGGLE = 431, +KEY_SPELLCHECK = 432, +KEY_LOGOFF = 433, +KEY_DOLLAR = 434, +KEY_EURO = 435, +KEY_FRAMEBACK = 436, +KEY_FRAMEFORWARD = 437, +KEY_CONTEXT_MENU = 438, +KEY_MEDIA_REPEAT = 439, +KEY_10CHANNELSUP = 440, +KEY_10CHANNELSDOWN = 441, +KEY_IMAGES = 442, +KEY_443 = 443, +KEY_444 = 444, +KEY_445 = 445, +KEY_446 = 446, +KEY_447 = 447, +KEY_DEL_EOL = 448, +KEY_DEL_EOS = 449, +KEY_INS_LINE = 450, +KEY_DEL_LINE = 451, +KEY_452 = 452, +KEY_453 = 453, +KEY_454 = 454, +KEY_455 = 455, +KEY_456 = 456, +KEY_457 = 457, +KEY_458 = 458, +KEY_459 = 459, +KEY_460 = 460, +KEY_461 = 461, +KEY_462 = 462, +KEY_463 = 463, +KEY_FN = 464, +KEY_FN_ESC = 465, +KEY_FN_F1 = 466, +KEY_FN_F2 = 467, +KEY_FN_F3 = 468, +KEY_FN_F4 = 469, +KEY_FN_F5 = 470, +KEY_FN_F6 = 471, +KEY_FN_F7 = 472, +KEY_FN_F8 = 473, +KEY_FN_F9 = 474, +KEY_FN_F10 = 475, +KEY_FN_F11 = 476, +KEY_FN_F12 = 477, +KEY_FN_1 = 478, +KEY_FN_2 = 479, +KEY_FN_D = 480, +KEY_FN_E = 481, +KEY_FN_F = 482, +KEY_FN_S = 483, +KEY_FN_B = 484, +KEY_485 = 485, +KEY_486 = 486, +KEY_487 = 487, +KEY_488 = 488, +KEY_489 = 489, +KEY_490 = 490, +KEY_491 = 491, +KEY_492 = 492, +KEY_493 = 493, +KEY_494 = 494, +KEY_495 = 495, +KEY_496 = 496, +KEY_BRL_DOT1 = 497, +KEY_BRL_DOT2 = 498, +KEY_BRL_DOT3 = 499, +KEY_BRL_DOT4 = 500, +KEY_BRL_DOT5 = 501, +KEY_BRL_DOT6 = 502, +KEY_BRL_DOT7 = 503, +KEY_BRL_DOT8 = 504, +KEY_BRL_DOT9 = 505, +KEY_BRL_DOT10 = 506, +KEY_507 = 507, +KEY_508 = 508, +KEY_509 = 509, +KEY_510 = 510, +KEY_511 = 511, +KEY_NUMERIC_0 = 512, +KEY_NUMERIC_1 = 513, +KEY_NUMERIC_2 = 514, +KEY_NUMERIC_3 = 515, +KEY_NUMERIC_4 = 516, +KEY_NUMERIC_5 = 517, +KEY_NUMERIC_6 = 518, +KEY_NUMERIC_7 = 519, +KEY_NUMERIC_8 = 520, +KEY_NUMERIC_9 = 521, +KEY_NUMERIC_STAR = 522, +KEY_NUMERIC_POUND = 523, +KEY_NUMERIC_A = 524, +KEY_NUMERIC_B = 525, +KEY_NUMERIC_C = 526, +KEY_NUMERIC_D = 527, +KEY_CAMERA_FOCUS = 528, +KEY_WPS_BUTTON = 529, +KEY_TOUCHPAD_TOGGLE = 530, +KEY_TOUCHPAD_ON = 531, +KEY_TOUCHPAD_OFF = 532, +KEY_CAMERA_ZOOMIN = 533, +KEY_CAMERA_ZOOMOUT = 534, +KEY_CAMERA_UP = 535, +KEY_CAMERA_DOWN = 536, +KEY_CAMERA_LEFT = 537, +KEY_CAMERA_RIGHT = 538, +KEY_ATTENDANT_ON = 539, +KEY_ATTENDANT_OFF = 540, +KEY_ATTENDANT_TOGGLE = 541, +KEY_LIGHTS_TOGGLE = 542, +KEY_543 = 543, +BTN_DPAD_UP = 544, +BTN_DPAD_DOWN = 545, +BTN_DPAD_LEFT = 546, +BTN_DPAD_RIGHT = 547, +KEY_548 = 548, +KEY_549 = 549, +KEY_550 = 550, +KEY_551 = 551, +KEY_552 = 552, +KEY_553 = 553, +KEY_554 = 554, +KEY_555 = 555, +KEY_556 = 556, +KEY_557 = 557, +KEY_558 = 558, +KEY_559 = 559, +KEY_ALS_TOGGLE = 560, +KEY_ROTATE_LOCK_TOGGLE = 561, +KEY_562 = 562, +KEY_563 = 563, +KEY_564 = 564, +KEY_565 = 565, +KEY_566 = 566, +KEY_567 = 567, +KEY_568 = 568, +KEY_569 = 569, +KEY_570 = 570, +KEY_571 = 571, +KEY_572 = 572, +KEY_573 = 573, +KEY_574 = 574, +KEY_575 = 575, +KEY_BUTTONCONFIG = 576, +KEY_TASKMANAGER = 577, +KEY_JOURNAL = 578, +KEY_CONTROLPANEL = 579, +KEY_APPSELECT = 580, +KEY_SCREENSAVER = 581, +KEY_VOICECOMMAND = 582, +KEY_ASSISTANT = 583, +KEY_KBD_LAYOUT_NEXT = 584, +KEY_585 = 585, +KEY_586 = 586, +KEY_587 = 587, +KEY_588 = 588, +KEY_589 = 589, +KEY_590 = 590, +KEY_591 = 591, +KEY_BRIGHTNESS_MIN = 592, +KEY_BRIGHTNESS_MAX = 593, +KEY_594 = 594, +KEY_595 = 595, +KEY_596 = 596, +KEY_597 = 597, +KEY_598 = 598, +KEY_599 = 599, +KEY_600 = 600, +KEY_601 = 601, +KEY_602 = 602, +KEY_603 = 603, +KEY_604 = 604, +KEY_605 = 605, +KEY_606 = 606, +KEY_607 = 607, +KEY_KBDINPUTASSIST_PREV = 608, +KEY_KBDINPUTASSIST_NEXT = 609, +KEY_KBDINPUTASSIST_PREVGROUP = 610, +KEY_KBDINPUTASSIST_NEXTGROUP = 611, +KEY_KBDINPUTASSIST_ACCEPT = 612, +KEY_KBDINPUTASSIST_CANCEL = 613, +KEY_RIGHT_UP = 614, +KEY_RIGHT_DOWN = 615, +KEY_LEFT_UP = 616, +KEY_LEFT_DOWN = 617, +KEY_ROOT_MENU = 618, +KEY_MEDIA_TOP_MENU = 619, +KEY_NUMERIC_11 = 620, +KEY_NUMERIC_12 = 621, +KEY_AUDIO_DESC = 622, +KEY_3D_MODE = 623, +KEY_NEXT_FAVORITE = 624, +KEY_STOP_RECORD = 625, +KEY_PAUSE_RECORD = 626, +KEY_VOD = 627, +KEY_UNMUTE = 628, +KEY_FASTREVERSE = 629, +KEY_SLOWREVERSE = 630, +KEY_DATA = 631, +KEY_ONSCREEN_KEYBOARD = 632, +KEY_633 = 633, +KEY_634 = 634, +KEY_635 = 635, +KEY_636 = 636, +KEY_637 = 637, +KEY_638 = 638, +KEY_639 = 639, +KEY_640 = 640, +KEY_641 = 641, +KEY_642 = 642, +KEY_643 = 643, +KEY_644 = 644, +KEY_645 = 645, +KEY_646 = 646, +KEY_647 = 647, +KEY_648 = 648, +KEY_649 = 649, +KEY_650 = 650, +KEY_651 = 651, +KEY_652 = 652, +KEY_653 = 653, +KEY_654 = 654, +KEY_655 = 655, +KEY_656 = 656, +KEY_657 = 657, +KEY_658 = 658, +KEY_659 = 659, +KEY_660 = 660, +KEY_661 = 661, +KEY_662 = 662, +KEY_663 = 663, +KEY_664 = 664, +KEY_665 = 665, +KEY_666 = 666, +KEY_667 = 667, +KEY_668 = 668, +KEY_669 = 669, +KEY_670 = 670, +KEY_671 = 671, +KEY_672 = 672, +KEY_673 = 673, +KEY_674 = 674, +KEY_675 = 675, +KEY_676 = 676, +KEY_677 = 677, +KEY_678 = 678, +KEY_679 = 679, +KEY_680 = 680, +KEY_681 = 681, +KEY_682 = 682, +KEY_683 = 683, +KEY_684 = 684, +KEY_685 = 685, +KEY_686 = 686, +KEY_687 = 687, +KEY_688 = 688, +KEY_689 = 689, +KEY_690 = 690, +KEY_691 = 691, +KEY_692 = 692, +KEY_693 = 693, +KEY_694 = 694, +KEY_695 = 695, +KEY_696 = 696, +KEY_697 = 697, +KEY_698 = 698, +KEY_699 = 699, +KEY_700 = 700, +KEY_701 = 701, +KEY_702 = 702, +KEY_703 = 703, +BTN_TRIGGER_HAPPY1 = 704, +BTN_TRIGGER_HAPPY2 = 705, +BTN_TRIGGER_HAPPY3 = 706, +BTN_TRIGGER_HAPPY4 = 707, +BTN_TRIGGER_HAPPY5 = 708, +BTN_TRIGGER_HAPPY6 = 709, +BTN_TRIGGER_HAPPY7 = 710, +BTN_TRIGGER_HAPPY8 = 711, +BTN_TRIGGER_HAPPY9 = 712, +BTN_TRIGGER_HAPPY10 = 713, +BTN_TRIGGER_HAPPY11 = 714, +BTN_TRIGGER_HAPPY12 = 715, +BTN_TRIGGER_HAPPY13 = 716, +BTN_TRIGGER_HAPPY14 = 717, +BTN_TRIGGER_HAPPY15 = 718, +BTN_TRIGGER_HAPPY16 = 719, +BTN_TRIGGER_HAPPY17 = 720, +BTN_TRIGGER_HAPPY18 = 721, +BTN_TRIGGER_HAPPY19 = 722, +BTN_TRIGGER_HAPPY20 = 723, +BTN_TRIGGER_HAPPY21 = 724, +BTN_TRIGGER_HAPPY22 = 725, +BTN_TRIGGER_HAPPY23 = 726, +BTN_TRIGGER_HAPPY24 = 727, +BTN_TRIGGER_HAPPY25 = 728, +BTN_TRIGGER_HAPPY26 = 729, +BTN_TRIGGER_HAPPY27 = 730, +BTN_TRIGGER_HAPPY28 = 731, +BTN_TRIGGER_HAPPY29 = 732, +BTN_TRIGGER_HAPPY30 = 733, +BTN_TRIGGER_HAPPY31 = 734, +BTN_TRIGGER_HAPPY32 = 735, +BTN_TRIGGER_HAPPY33 = 736, +BTN_TRIGGER_HAPPY34 = 737, +BTN_TRIGGER_HAPPY35 = 738, +BTN_TRIGGER_HAPPY36 = 739, +BTN_TRIGGER_HAPPY37 = 740, +BTN_TRIGGER_HAPPY38 = 741, +BTN_TRIGGER_HAPPY39 = 742, +BTN_TRIGGER_HAPPY40 = 743, +BTN_MAX = 744, +MouseWheelUp = 745, +MouseWheelDown = 746, +MouseWheelLeft = 747, +MouseWheelRight = 748, +KEY_MAX = 767, + +=== all kcs +No, +ErrorRollOver, +PostFail, +ErrorUndefined, +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, +Kb1, +Kb2, +Kb3, +Kb4, +Kb5, +Kb6, +Kb7, +Kb8, +Kb9, +Kb0, +Enter, +Escape, +BSpace, +Tab, +Space, +Minus, +Equal, +LBracket, +RBracket, +Bslash, +NonUsHash, +SColon, +Quote, +Grave, +Comma, +Dot, +Slash, +CapsLock, +F1, +F2, +F3, +F4, +F5, +F6, +F7, +F8, +F9, +F10, +F11, +F12, +PScreen, +ScrollLock, +Pause, +Insert, +Home, +PgUp, +Delete, +End, +PgDown, +Right, +Left, +Down, +Up, +NumLock, +KpSlash, +KpAsterisk, +KpMinus, +KpPlus, +KpEnter, +Kp1, +Kp2, +Kp3, +Kp4, +Kp5, +Kp6, +Kp7, +Kp8, +Kp9, +Kp0, +KpDot, +NonUsBslash, +Application, +Power, +KpEqual, +F13, +F14, +F15, +F16, +F17, +F18, +F19, +F20, +F21, +F22, +F23, +F24, +Execute, +Help, +Menu, +Select, +Stop, +Again, +Undo, +Cut, +Copy, +Paste, +Find, +Mute, +VolUp, +VolDown, +LockingCapsLock, +LockingNumLock, +LockingScrollLock, +KpComma, +KpEqualSign, +Intl1, +Intl2, +Intl3, +Intl4, +Intl5, +Intl6, +Intl7, +Intl8, +Intl9, +Lang1, +Lang2, +Lang3, +Lang4, +Lang5, +Lang6, +Lang7, +Lang8, +Lang9, +AltErase, +SysReq, +Cancel, +Clear, +Prior, +Return, +Separator, +Out, +Oper, +ClearAgain, +CrSel, +ExSel, +Wakeup, +BrightnessUp, +BrightnessDown, +KbdIllumUp, +KbdIllumDown, +K0xAA, +K0xAB, +K0xAC, +K0xAD, +K0xAE, +K0xAF, +K0xB0, +K0xB1, +K0xB2, +K0xB3, +K0xB4, +K0xB5, +K0xB6, +K0xB7, +K0xB8, +K0xB9, +K0xBA, +K0xBB, +K0xBC, +K0xBD, +K0xBE, +K0xBF, +K0xC0, +K0xC1, +K0xC2, +K0xC3, +K0xC4, +K0xC5, +K0xC6, +K0xC7, +K0xC8, +K0xC9, +K0xCA, +K0xCB, +K0xCC, +K0xCD, +K0xCE, +K0xCF, +K0xD0, +K0xD1, +K0xD2, +K0xD3, +K0xD4, +K0xD5, +K0xD6, +K0xD7, +K0xD8, +K0xD9, +K0xDA, +K0xDB, +K0xDC, +K0xDD, +K0xDE, +K0xDF, +LCtrl, +LShift, +LAlt, +LGui, +RCtrl, +RShift, +RAlt, +RGui, +MediaPlayPause, +MediaStopCD, +MediaPreviousSong, +MediaNextSong, +MediaEjectCD, +MediaVolUp, +MediaVolDown, +MediaMute, +MediaWWW, +MediaBack, +MediaForward, +MediaStop, +MediaFind, +MediaScrollUp, +MediaScrollDown, +MediaEdit, +MediaSleep, +MediaCoffee, +MediaRefresh, +MediaCalc, +K252, +K253, +K254, +K255, +K256, +K257, +K258, +K259, +K260, +K261, +K262, +K263, +K264, +K265, +K266, +K267, +K268, +K269, +K270, +K271, +K272, +K273, +K274, +K275, +K276, +K277, +K278, +K279, +K280, +K281, +K282, +K283, +K284, +K285, +K286, +K287, +K288, +K289, +K290, +K291, +K292, +K293, +K294, +K295, +K296, +K297, +K298, +K299, +K300, +K301, +K302, +K303, +K304, +K305, +K306, +K307, +K308, +K309, +K310, +K311, +K312, +K313, +K314, +K315, +K316, +K317, +K318, +K319, +K320, +K321, +K322, +K323, +K324, +K325, +K326, +K327, +K328, +K329, +K330, +K331, +K332, +K333, +K334, +K335, +K336, +K337, +K338, +K339, +K340, +K341, +K342, +K343, +K344, +K345, +K346, +K347, +K348, +K349, +K350, +K351, +K352, +K353, +K354, +K355, +K356, +K357, +K358, +K359, +K360, +K361, +K362, +K363, +K364, +K365, +K366, +K367, +K368, +K369, +K370, +K371, +K372, +K373, +K374, +K375, +K376, +K377, +K378, +K379, +K380, +K381, +K382, +K383, +K384, +K385, +K386, +K387, +K388, +K389, +K390, +K391, +K392, +K393, +K394, +K395, +K396, +K397, +K398, +K399, +K400, +K401, +K402, +K403, +K404, +K405, +K406, +K407, +K408, +K409, +K410, +K411, +K412, +K413, +K414, +K415, +K416, +K417, +K418, +K419, +K420, +K421, +K422, +K423, +K424, +K425, +K426, +K427, +K428, +K429, +K430, +K431, +K432, +K433, +K434, +K435, +K436, +K437, +K438, +K439, +K440, +K441, +K442, +K443, +K444, +K445, +K446, +K447, +K448, +K449, +K450, +K451, +K452, +K453, +K454, +K455, +K456, +K457, +K458, +K459, +K460, +K461, +K462, +K463, +K464, +K465, +K466, +K467, +K468, +K469, +K470, +K471, +K472, +K473, +K474, +K475, +K476, +K477, +K478, +K479, +K480, +K481, +K482, +K483, +K484, +K485, +K486, +K487, +K488, +K489, +K490, +K491, +K492, +K493, +K494, +K495, +K496, +K497, +K498, +K499, +K500, +K501, +K502, +K503, +K504, +K505, +K506, +K507, +K508, +K509, +K510, +K511, +K512, +K513, +K514, +K515, +K516, +K517, +K518, +K519, +K520, +K521, +K522, +K523, +K524, +K525, +K526, +K527, +K528, +K529, +K530, +K531, +K532, +K533, +K534, +K535, +K536, +K537, +K538, +K539, +K540, +K541, +K542, +K543, +K544, +K545, +K546, +K547, +K548, +K549, +K550, +K551, +K552, +K553, +K554, +K555, +K556, +K557, +K558, +K559, +K560, +K561, +K562, +K563, +K564, +K565, +K566, +K567, +K568, +K569, +K570, +K571, +K572, +K573, +K574, +K575, +K576, +K577, +K578, +K579, +K580, +K581, +K582, +K583, +K584, +K585, +K586, +K587, +K588, +K589, +K590, +K591, +K592, +K593, +K594, +K595, +K596, +K597, +K598, +K599, +K600, +K601, +K602, +K603, +K604, +K605, +K606, +K607, +K608, +K609, +K610, +K611, +K612, +K613, +K614, +K615, +K616, +K617, +K618, +K619, +K620, +K621, +K622, +K623, +K624, +K625, +K626, +K627, +K628, +K629, +K630, +K631, +K632, +K633, +K634, +K635, +K636, +K637, +K638, +K639, +K640, +K641, +K642, +K643, +K644, +K645, +K646, +K647, +K648, +K649, +K650, +K651, +K652, +K653, +K654, +K655, +K656, +K657, +K658, +K659, +K660, +K661, +K662, +K663, +K664, +K665, +K666, +K667, +K668, +K669, +K670, +K671, +K672, +K673, +K674, +K675, +K676, +K677, +K678, +K679, +K680, +K681, +K682, +K683, +K684, +K685, +K686, +K687, +K688, +K689, +K690, +K691, +K692, +K693, +K694, +K695, +K696, +K697, +K698, +K699, +K700, +K701, +K702, +K703, +K704, +K705, +K706, +K707, +K708, +K709, +K710, +K711, +K712, +K713, +K714, +K715, +K716, +K717, +K718, +K719, +K720, +K721, +K722, +K723, +K724, +K725, +K726, +K727, +K728, +K729, +K730, +K731, +K732, +K733, +K734, +K735, +K736, +K737, +K738, +K739, +K740, +K741, +K742, +K743, +K744, +MWU, +MWD, +MWL, +MWR, +KeyMax, diff --git a/key-sort-add/src/main.rs b/key-sort-add/src/main.rs index bea45dbd3..648d4696c 100644 --- a/key-sort-add/src/main.rs +++ b/key-sort-add/src/main.rs @@ -1,3 +1,5 @@ +//! one: +//! //! Takes a file formatted as: //! //! KEY_RESERVED = 0, @@ -11,11 +13,21 @@ //! Outputs to stdout a sorted version of the file with numeric gaps filled in with: //! //! KEY_X = X, +//! +//! two: mapping.txt to ensure KeyCode and OsCode can simply be transmuted into each other. use std::io::Read; fn main() { - let mut f = std::fs::File::open(std::env::args().nth(1).expect("filename parameter")) + match std::env::args().nth(1).expect("function parameter").as_str() { + "one" => one(), + "two" => two(), + _ => panic!("unknown capabality"), + } +} + +fn one() { + let mut f = std::fs::File::open(std::env::args().nth(2).expect("filename parameter")) .expect("file open"); let mut s = String::new(); f.read_to_string(&mut s).expect("read file"); @@ -24,7 +36,14 @@ fn main() { .map(|l| { let mut segments = l.trim_end_matches(',').trim().split(" = "); let key = segments.next().expect("a string"); - let num: u16 = str::parse(segments.next().expect("string after =")).expect("u16"); + let num: u16 = u16::from_str_radix( + segments + .next() + .map(|s| s.trim_start_matches("0x")) + .expect("string after ="), + 10, + ) + .expect("u16"); (key.to_owned(), num) }) .collect::>(); @@ -36,7 +55,7 @@ fn main() { for cur in cur_key { let prev = prev_key.next().expect("lagging iterator is valid"); for missing in prev.1 + 1..cur.1 { - keys_to_add.push((format!("KEY_{missing}"), missing)); + keys_to_add.push((format!("K{missing}"), missing)); } } keys.append(&mut keys_to_add); @@ -45,3 +64,67 @@ fn main() { println!("{} = {},", key.0, key.1); } } + +fn two() { + use std::collections::HashMap; + + let mut f = std::fs::File::open(std::env::args().nth(2).expect("filename parameter")) + .expect("file open"); + let mut s = String::new(); + f.read_to_string(&mut s).expect("read file"); + let mut lines = s.lines(); + + // filter out useless lines + while let Some(line) = lines.next() { + if line == "=== kc to osc" { + break; + } + } + + // parse kc to osc + let mut kc_to_osc: HashMap<&str, &str> = HashMap::new(); + while let Some(line) = lines.next() { + if line.trim().is_empty() { + continue; + } + if line == "=== osc to u16" { + break; + } + let (kc, osc) = line.split_once(" => ").expect("arrow separator"); + let kc = kc.trim_start_matches("KeyCode::"); + let osc = osc.trim_end_matches(',') + .trim_start_matches("OsCode::"); + kc_to_osc.insert(kc, osc); + } + + // parse osc to u16 + let mut osc_vals: HashMap<&str, u16> = HashMap::new(); + while let Some(line) = lines.next() { + if line.trim().is_empty() { + continue; + } + if line == "=== all kcs" { + break; + } + let (kc, num) = line.split_once(" = ").expect("equal separator"); + let num = num.trim_end_matches(',').parse::().expect("u16"); + osc_vals.insert(kc, num); + } + + // parse kcs + let mut kc_vals: Vec<(&str, Option)> = vec![]; + while let Some(line) = lines.next() { + if line.trim().is_empty() { + continue; + } + let kc = line.trim_end_matches(','); + let val: Option = kc_to_osc.get(&kc) + .and_then(|osc| osc_vals.get(osc)) + .copied(); + kc_vals.push((kc, val)); + } + + for (kc, val) in kc_vals.iter() { + println!("{kc} = {},", val.unwrap_or(65535)); + } +} diff --git a/keyberon/Cargo.toml b/keyberon/Cargo.toml index 9ea485711..df47423d9 100644 --- a/keyberon/Cargo.toml +++ b/keyberon/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "kanata-keyberon" -version = "0.18.0" +version = "0.1120.1" authors = ["Guillaume Pinot ", "Robin Krahl ", "jtroo "] -edition = "2018" +edition = "2021" description = "Pure Rust keyboard firmware. Fork intended for use with kanata." documentation = "https://docs.rs/keyberon" repository = "https://github.com/TeXitoi/keyberon" @@ -11,7 +11,11 @@ categories = ["no-std"] license = "MIT" readme = "README.md" +[features] +tap_hold_tracker = [] + [dependencies] kanata-keyberon-macros = { version = "0.2.0" } heapless = "0.7.16" -arraydeque = { version = "0.4.5", default-features = false } +rustc-hash = "1.1.0" +arraydeque = { version = "0.5.1", default-features = false } diff --git a/keyberon/README.md b/keyberon/README.md index fc9c66ca7..72cd51939 100644 --- a/keyberon/README.md +++ b/keyberon/README.md @@ -4,3 +4,5 @@ This is a fork intended for use by the [kanata keyboard remapper software](https://github.com/jtroo/kanata). Please make contributions to the [original project](https://github.com/TeXitoi/keyberon) where applicable. + +This crate does not follow semver. It tracks the version of kanata. diff --git a/keyberon/src/action.rs b/keyberon/src/action.rs index 004c88184..6f52f27ac 100644 --- a/keyberon/src/action.rs +++ b/keyberon/src/action.rs @@ -1,9 +1,12 @@ //! The different actions that can be executed via any given key. use crate::key_code::KeyCode; -use crate::layout::{QueuedIter, WaitingAction}; +use crate::layout::{KCoord, QueuedIter, WaitingAction}; use core::fmt::Debug; +pub mod switch; +pub use switch::*; + /// The different types of actions we support for key sequences/macros #[non_exhaustive] #[derive(Clone, Copy, Eq, PartialEq)] @@ -28,7 +31,7 @@ pub enum SequenceEvent<'a, T: 'a> { Complete, } -impl<'a, T> Debug for SequenceEvent<'a, T> { +impl Debug for SequenceEvent<'_, T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::NoOp => write!(f, "NoOp"), @@ -58,6 +61,13 @@ pub enum HoldTapConfig<'a> { /// not used in the flow of typing, like escape for example. If /// you are annoyed by accidental tap, you can try this behavior. HoldOnOtherKeyPress, + /// Resolves based on release order after both keys are down. + /// If the other key releases first (modifier still held) → Hold. + /// If the modifier releases first (other key still held) → Tap. + /// The buffer field specifies a grace period in ticks (ms) after the + /// initial press during which release-order logic is ignored and fast + /// typing will resolve as Tap. + Order { buffer: u16 }, /// If there is a press and release of another key, the hold /// action is activated. /// @@ -69,11 +79,15 @@ pub enum HoldTapConfig<'a> { /// A custom configuration. Allows the behavior to be controlled by a caller /// supplied handler function. /// - /// The input to the custom handler will be an iterator that returns + /// The first argument to the custom handler will be an iterator that returns /// [Stacked] [Events](Event). The order of the events matches the order the /// corresponding key was pressed/released, i.e. the first event is the /// event first received after the HoldTap action key is pressed. /// + /// The second argument is the coordinate `(row, col)` of the key that + /// initiated the HoldTap action, allowing the handler to identify which + /// physical key is waiting for resolution. + /// /// The return value should be the intended action that should be used. A /// [Some] value will cause one of: [WaitingAction::Tap] for the configured /// tap action, [WaitingAction::Hold] for the hold action, and @@ -81,27 +95,33 @@ pub enum HoldTapConfig<'a> { /// value will cause a fallback to the timeout-based approach. If the /// timeout is not triggered, the next tick will call the custom handler /// again. - Custom(&'a (dyn Fn(QueuedIter) -> Option + Send + Sync)), + /// The bool value defines if the timeout check should be skipped at the + /// next tick. This should generally be false. This is used by `tap-hold- + /// except-keys` to handle presses even when the timeout has been reached. + #[allow(clippy::type_complexity)] + Custom(&'a (dyn Fn(QueuedIter, KCoord) -> (Option, bool) + Send + Sync)), } -impl<'a> Debug for HoldTapConfig<'a> { +impl Debug for HoldTapConfig<'_> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { HoldTapConfig::Default => f.write_str("Default"), HoldTapConfig::HoldOnOtherKeyPress => f.write_str("HoldOnOtherKeyPress"), + HoldTapConfig::Order { .. } => f.write_str("Order"), HoldTapConfig::PermissiveHold => f.write_str("PermissiveHold"), HoldTapConfig::Custom(_) => f.write_str("Custom"), } } } -impl<'a> PartialEq for HoldTapConfig<'a> { +impl PartialEq for HoldTapConfig<'_> { fn eq(&self, other: &Self) -> bool { #[allow(clippy::match_like_matches_macro)] match (self, other) { (HoldTapConfig::Default, HoldTapConfig::Default) | (HoldTapConfig::HoldOnOtherKeyPress, HoldTapConfig::HoldOnOtherKeyPress) | (HoldTapConfig::PermissiveHold, HoldTapConfig::PermissiveHold) => true, + (HoldTapConfig::Order { .. }, HoldTapConfig::Order { .. }) => true, _ => false, } } @@ -161,6 +181,15 @@ where /// /// To deactivate the functionality, set this to 0. pub tap_hold_interval: u16, + /// Specifically the `tap-hold-release-timeout` action variant + /// can benefit from resetting the timeout after a new press, + /// because a human might have a slow release but they did + /// indeed want a hold to activate. + pub on_press_reset_timeout_to: Option, + /// Per-action override for the global `tap_hold_require_prior_idle` setting. + /// If `Some(n)`, uses `n` instead of the global value (0 = disabled for this action). + /// If `None`, falls back to the global `defcfg` value. + pub require_prior_idle: Option, } /// Define one shot key behaviour. @@ -276,7 +305,7 @@ impl<'a, T> ChordsGroup<'a, T> { /// A set of virtual keys (represented as a bit mask) pressed together. /// The keys do not directly correspond to physical keys. They are unique to a given [ChordGroup] and their mapping from physical keys is definied in [ChordGroup.coords]. /// As such, each chord group can effectively have at most 32 different keys (though multiple physical keys may be mapped to the same virtual key). -pub type ChordKeys = u32; +pub type ChordKeys = u128; /// Defines the maximum number of (virtual) keys that can be used in a single chords group. pub const MAX_CHORD_KEYS: usize = ChordKeys::BITS as usize; @@ -352,6 +381,8 @@ where /// key will hold space until either another key is pressed or the timeout occurs, which will /// probably send many undesired space characters to your active application. OneShot(&'a OneShot<'a, T>), + /// An action to ignore processing of events for OneShot. + OneShotIgnoreEventsTicks(u16), /// Tap-dance key. When tapping the key N times in quck succession, activates the N'th action /// in `actions`. The action will activate in the following conditions: /// @@ -373,9 +404,18 @@ where /// Fork action that can activate one of two potential actions depending on what keys are /// currently active. Fork(&'a ForkConfig<'a, T>), + /// Action that can activate 0 to N actions based on what keys are currently + /// active and the boolean logic of each case. + /// + /// The maximum number of actions that can activate the same time is governed by + /// `ACTION_QUEUE_LEN`. + Switch(&'a Switch<'a, T>), + /// Disregard the entire layer stack, i.e. the current base layer and any while-held layers, + /// and select the action from `Layout.src_keys`. + Src, } -impl<'a, T> Action<'a, T> { +impl Action<'_, T> { /// Gets the layer number if the action is the `Layer` action. pub fn layer(self) -> Option { match self { diff --git a/keyberon/src/action/switch.rs b/keyberon/src/action/switch.rs new file mode 100644 index 000000000..54eb13429 --- /dev/null +++ b/keyberon/src/action/switch.rs @@ -0,0 +1,1539 @@ +//! Handle processing of the switch action for Keyberon. +//! +//! Limitations: +//! - Maximum opcode length: 4095 +//! - Maximum boolean expression depth: 8 +//! - Maximum key recency: 7, where 0 is the most recent key press +//! +//! The intended use is to build up a `Switch` struct and use that in the `Layout`. +//! +//! The `Layout` will use `Switch::actions` to iterate over the actions that should be activated +//! when the corresponding key is pressed. + +use super::*; +use crate::layout::{HistoricalEvent, KCoord}; + +use crate::key_code::*; +use std::num::NonZeroU8; + +use BooleanOperator::*; +use BreakOrFallthrough::*; + +pub const MAX_OPCODE_LEN: u16 = 0x0FFF; +pub const OP_MASK: u16 = 0xF000; +pub const MAX_BOOL_EXPR_DEPTH: usize = 8; +pub const MAX_KEY_RECENCY: u8 = 7; + +pub type Case<'a, T> = (&'a [OpCode], &'a Action<'a, T>, BreakOrFallthrough); + +#[derive(Clone, Copy)] +/// Behaviour of a switch action. Each case is a 3-tuple of: +/// +/// - the boolean expression (array of opcodes) +/// - the action to evaluate if the expression evaluates to true +/// - whether to break or fallthrough to the next case if the expression evaluates to true +pub struct Switch<'a, T: 'a> { + // A callback to optionally initialize some state for this switch. + pub init_fn: Option<&'a (dyn Fn() + Send + Sync)>, + pub cases: &'a [Case<'a, T>], + // Extra callbacks that can be indexed into to evaluate functions, + // e.g. based on state updated by init_fn. + pub callbacks: &'a [&'a (dyn Fn() -> bool + Send + Sync)], +} + +impl<'a, T: 'a + PartialEq> PartialEq for Switch<'a, T> { + fn eq(&self, other: &Self) -> bool { + self.cases == other.cases + } +} + +impl<'a, T: 'a + Debug> Debug for Switch<'a, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("Switch") + .field("cases", &self.cases) + .field("init_is_some", &self.init_fn.is_some()) + .field("callbacks_len", &self.callbacks.len()) + .finish() + } +} + +// NOTE: have exhausted our opcodes for u16! +// +// Future rewrite: do traditional u8 opcodes, with variable length for the total opcode depending +// on the first one encountered? Or could be lazy and use u32 and have 4 bytes for every opcode. +// This probably isn't that performance-sensitive anyway... it's triggering on every input. + +const OR_VAL: u16 = 0x1000; +const AND_VAL: u16 = 0x2000; +const NOT_VAL: u16 = 0x3000; + +const INPUT_VAL: u16 = 851; +const HISTORICAL_INPUT_VAL: u16 = 852; +const LAYER_VAL: u16 = 853; +const BASE_LAYER_VAL: u16 = 854; +const HISTORICAL_DEVICE_VAL: u16 = 855; +const CALLBACK_INDEX_VAL: u16 = 856; + +// Binary values: +// 0b0100 ... +// 0b0110 ... +// +// How-far-back are in bits 12-10 (3 bits) +// Time is compressed in bits 9-0 (10 bits) +const TICKS_SINCE_VAL_GT: u16 = 0x4000; +const TICKS_SINCE_VAL_LT: u16 = 0x6000; + +// Highest bit in u16. Lower 3 bits in the highest nibble are "how far back". This means that +// switch can look back up to 8 keys. +const HISTORICAL_KEYCODE_VAL: u16 = 0x8000; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// Boolean operator. Notably missing today is Not. +pub enum BooleanOperator { + Or, + And, + Not, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +/// OpCode for a switch case boolean expression. +pub struct OpCode(u16); + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// The more useful interpretion of an OpCode. +enum OpCodeType { + BooleanOp(OperatorAndEndIndex), + KeyCode(u16), + HistoricalKeyCode(HistoricalKeyCode), + Input(KCoord), + HistoricalInput(HistoricalInput), + TicksSinceLessThan(TicksSinceNthKey), + TicksSinceGreaterThan(TicksSinceNthKey), + Layer(u16), + BaseLayer(u16), + HistoricalDevice(HistoricalDevice), + CallbackIndex(u16), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// The operation type and the opcode index at which evaluating this type ends. +struct OperatorAndEndIndex { + pub op: BooleanOperator, + pub idx: usize, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// An op that checks specifically for a key that is a certain number of key presses back in +/// history. +struct HistoricalKeyCode { + key_code: u16, + how_far_back: u8, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// An op that checks specifically for a key that is a certain number of key presses back in +/// history. +struct HistoricalInput { + input: KCoord, + how_far_back: u8, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct HistoricalDevice { + device_id: NonZeroU8, + how_far_back: u8, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct TicksSinceNthKey { + nth_key: u8, + ticks_since: u16, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +/// Whether or not a case should break out of the switch if it evaluates to true or fallthrough to +/// the next case. +pub enum BreakOrFallthrough { + Break, + Fallthrough, +} + +impl<'a, T> Switch<'a, T> { + /// Iterates over the actions (if any) that are activated in the `Switch` based on its cases, + /// the currently active keys, and historically pressed keys. + /// + /// The `historical_keys` parameter should iterate in the order of most-recent-first. + #[allow(clippy::too_many_arguments)] + pub fn actions( + &self, + active_keys: A1, + active_positions: A2, + historical_keys: H1, + historical_positions: H2, + layers: L, + default_layer: u16, + device_history: D, + ) -> SwitchActions<'a, T, A1, A2, H1, H2, L, D> + where + A1: Iterator + Clone, + A2: Iterator + Clone, + H1: Iterator> + Clone, + H2: Iterator> + Clone, + L: Iterator + Clone, + D: Iterator> + Clone, + { + SwitchActions { + cases: self.cases, + active_keys, + active_positions, + historical_keys, + historical_positions, + layers, + default_layer, + device_history, + case_index: 0, + callbacks: self.callbacks, + } + } +} + +#[derive(Clone)] +/// Iterator returned by `Switch::actions`. +pub struct SwitchActions<'a, T, A1, A2, H1, H2, L, D> +where + A1: Iterator + Clone, + A2: Iterator + Clone, + H1: Iterator> + Clone, + H2: Iterator> + Clone, + L: Iterator + Clone, + D: Iterator> + Clone, +{ + cases: &'a [(&'a [OpCode], &'a Action<'a, T>, BreakOrFallthrough)], + active_keys: A1, + active_positions: A2, + historical_keys: H1, + historical_positions: H2, + layers: L, + default_layer: u16, + device_history: D, + case_index: usize, + callbacks: &'a [&'a (dyn Fn() -> bool + Send + Sync)], +} + +impl<'a, T, A1, A2, H1, H2, L, D> Iterator for SwitchActions<'a, T, A1, A2, H1, H2, L, D> +where + A1: Iterator + Clone, + A2: Iterator + Clone, + H1: Iterator> + Clone, + H2: Iterator> + Clone, + L: Iterator + Clone, + D: Iterator> + Clone, +{ + type Item = &'a Action<'a, T>; + + fn next(&mut self) -> Option { + while self.case_index < self.cases.len() { + let case = &self.cases[self.case_index]; + if evaluate_boolean( + case.0, + self.active_keys.clone(), + self.active_positions.clone(), + self.historical_keys.clone(), + self.historical_positions.clone(), + self.layers.clone(), + self.default_layer, + self.device_history.clone(), + self.callbacks, + ) { + let ret_ac = case.1; + match case.2 { + Break => self.case_index = self.cases.len(), + Fallthrough => self.case_index += 1, + } + return Some(ret_ac); + } + self.case_index += 1; + } + None + } +} + +impl BooleanOperator { + fn to_u16(self) -> u16 { + match self { + Or => OR_VAL, + And => AND_VAL, + Not => NOT_VAL, + } + } +} +fn lossy_compress_ticks(t: u16) -> u16 { + match t { + 0..=255 => t, + 256..=2303 => (t - 255) / 8 + 255, + _ => (t - 2303) / 128 + 511, + } +} + +fn lossy_decompress_ticks(t: u16) -> u16 { + match t { + 0..=255 => t, + 256..=511 => (t - 255) * 8 + 255, + _ => (t - 511) * 128 + 2303, + } +} + +impl OpCode { + /// Return a new OpCode that checks if the key active or not. + pub fn new_key(kc: KeyCode) -> Self { + assert!((kc as u16) <= KEY_MAX); + Self(kc as u16 & MAX_OPCODE_LEN) + } + + /// Return a new OpCode that checks if the n'th most recent key, defined by `key_recency`, + /// matches the input keycode. + pub fn new_key_history(kc: KeyCode, key_recency: u8) -> Self { + assert!((kc as u16) <= MAX_OPCODE_LEN); + assert!(key_recency <= MAX_KEY_RECENCY); + Self((kc as u16 & MAX_OPCODE_LEN) | HISTORICAL_KEYCODE_VAL | ((key_recency as u16) << 12)) + } + + /// Returns a new opcode that returns true if the n'th most recent key was pressed greater + /// than `ticks_since` ticks ago. + /// + /// At 256 ticks or above, this has a resolution of 8ms (rounded down). At 2304 ticks or + /// above, this has a resolution of 128 ms (rounded down). + pub fn new_ticks_since_gt(nth_key: u8, ticks_since: u16) -> Self { + assert!(nth_key <= MAX_KEY_RECENCY); + Self(TICKS_SINCE_VAL_GT | lossy_compress_ticks(ticks_since) | (u16::from(nth_key) << 10)) + } + + /// Returns a new opcode that returns true if the n'th most recent key was pressed greater + /// than `ticks_since` ticks ago. + /// + /// At 256 ticks or above, this has a resolution of 8ms (rounded down). At 2304 ticks or + /// above, this has a resolution of 128 ms (rounded down). + pub fn new_ticks_since_lt(nth_key: u8, ticks_since: u16) -> Self { + assert!(nth_key <= MAX_KEY_RECENCY); + Self(TICKS_SINCE_VAL_LT | lossy_compress_ticks(ticks_since) | (u16::from(nth_key) << 10)) + } + + /// Return a new OpCode for a boolean operation that ends (non-inclusive) at the specified + /// index. + pub fn new_bool(op: BooleanOperator, end_idx: u16) -> Self { + assert!(end_idx <= MAX_OPCODE_LEN); + Self((end_idx & MAX_OPCODE_LEN) + op.to_u16()) + } + + /// Return OpCodes specifying an active input check. + pub fn new_active_input(input: KCoord) -> (Self, Self) { + assert!(input.0 < 4); + assert!(input.1 < 0x0400); + ( + Self(INPUT_VAL), + Self((u16::from(input.0 & 3) << 14) + input.1), + ) + } + + /// Return OpCodes specifying an active input check. + pub fn new_historical_input(input: KCoord, key_recency: u8) -> (Self, Self) { + assert!(input.0 < 4); + assert!(input.1 < 0x0400); + assert!(key_recency < 0x8); + ( + Self(HISTORICAL_INPUT_VAL), + Self((u16::from(input.0 & 3) << 14) + (u16::from(key_recency) << 11) + input.1), + ) + } + + /// Return OpCodes specifying an active layer check. + pub fn new_layer(layer: u16) -> (Self, Self) { + assert!(usize::from(layer) < crate::layout::MAX_LAYERS); + (Self(LAYER_VAL), Self(layer)) + } + + /// Return OpCodes specifying an base layer check. + pub fn new_base_layer(base_layer: u16) -> (Self, Self) { + assert!(usize::from(base_layer) < crate::layout::MAX_LAYERS); + (Self(BASE_LAYER_VAL), Self(base_layer)) + } + + /// Return OpCodes specifying a device history check. + pub fn new_device_history(id: NonZeroU8, how_far_back: u8) -> (Self, Self) { + assert!(how_far_back <= MAX_KEY_RECENCY); + ( + Self(HISTORICAL_DEVICE_VAL), + Self(id.get() as u16 | ((how_far_back as u16) << 8)), + ) + } + + /// Returns OpCodes specifying a callback, referenced by index. + pub fn new_callback_index(index: u16) -> (Self, Self) { + (Self(CALLBACK_INDEX_VAL), Self(index)) + } + + /// Return the interpretation of this `OpCode`. + fn opcode_type(self, next: Option) -> OpCodeType { + if self.0 < KEY_MAX { + OpCodeType::KeyCode(self.0) + } else if self.0 <= MAX_OPCODE_LEN { + let op2 = next.expect("next should be some for opcode {self:?}"); + match self.0 { + INPUT_VAL => OpCodeType::Input((((op2.0 >> 14) & 0x3) as u8, op2.0 & 0x3FF)), + HISTORICAL_INPUT_VAL => OpCodeType::HistoricalInput(HistoricalInput { + input: (((op2.0 >> 14) & 0x3) as u8, op2.0 & 0x3FF), + how_far_back: (op2.0 >> 11) as u8 & 0x7, + }), + LAYER_VAL => OpCodeType::Layer(op2.0), + BASE_LAYER_VAL => OpCodeType::BaseLayer(op2.0), + HISTORICAL_DEVICE_VAL => OpCodeType::HistoricalDevice(HistoricalDevice { + device_id: NonZeroU8::new((op2.0 & 0xFF) as u8) + .expect("device ID must be nonzero"), + how_far_back: ((op2.0 >> 8) & 0x7) as u8, + }), + CALLBACK_INDEX_VAL => OpCodeType::CallbackIndex(op2.0), + _ => unreachable!("unexpected opcode {self:?}"), + } + } else { + match self.0 & 0xE000 { + TICKS_SINCE_VAL_LT => OpCodeType::TicksSinceLessThan(TicksSinceNthKey { + nth_key: ((self.0 & 0x1C00) >> 10) as u8, + ticks_since: lossy_decompress_ticks(self.0 & 0x03FF), + }), + TICKS_SINCE_VAL_GT => OpCodeType::TicksSinceGreaterThan(TicksSinceNthKey { + nth_key: ((self.0 & 0x1C00) >> 10) as u8, + ticks_since: lossy_decompress_ticks(self.0 & 0x03FF), + }), + 0x8000..=0xF000 => OpCodeType::HistoricalKeyCode(HistoricalKeyCode { + key_code: self.0 & 0x0FFF, + how_far_back: ((self.0 & 0x7000) >> 12) as u8, + }), + _ => OpCodeType::BooleanOp(OperatorAndEndIndex::from(self.0)), + } + } + } +} + +impl From for OperatorAndEndIndex { + fn from(value: u16) -> Self { + Self { + op: match value & OP_MASK { + OR_VAL => Or, + AND_VAL => And, + NOT_VAL => Not, + _ => unreachable!("public interface should protect from this"), + }, + idx: usize::from(value & MAX_OPCODE_LEN), + } + } +} + +/// Evaluate the return value of an expression evaluated on the given key codes. +#[allow(clippy::too_many_arguments)] +fn evaluate_boolean( + bool_expr: &[OpCode], + key_codes: impl Iterator + Clone, + inputs: impl Iterator + Clone, + historical_keys: impl Iterator> + Clone, + historical_inputs: impl Iterator> + Clone, + layers: impl Iterator + Clone, + default_layer: u16, + device_history: impl Iterator> + Clone, + callbacks: &[&(dyn Fn() -> bool + Send + Sync)], +) -> bool { + let mut ret = true; + let mut current_index = 0; + let mut current_end_index = bool_expr.len(); + let mut current_op = Or; + let mut stack: arraydeque::ArrayDeque< + OperatorAndEndIndex, + MAX_BOOL_EXPR_DEPTH, + arraydeque::behavior::Saturating, + > = Default::default(); + while current_index < bool_expr.len() { + if current_index >= current_end_index { + match stack.pop_back() { + Some(operator) => { + (current_op, current_end_index) = (operator.op, operator.idx); + } + None => break, + } + // Short-circuiting logic + if matches!((ret, current_op), (true, Or | Not) | (false, And)) + || current_index >= current_end_index + { + if current_op == Not { + ret = !ret; + } + current_index = current_end_index; + continue; + } + } + match bool_expr[current_index].opcode_type(bool_expr.get(current_index + 1).copied()) { + OpCodeType::BooleanOp(operator) => { + let res = stack.push_back(OperatorAndEndIndex { + op: current_op, + idx: current_end_index, + }); + assert!( + res.is_ok(), + "exceeded boolean op depth {MAX_BOOL_EXPR_DEPTH}" + ); + (current_op, current_end_index) = (operator.op, operator.idx); + current_index += 1; + continue; + } + OpCodeType::KeyCode(kc) => { + ret = key_codes.clone().any(|kc_input| kc_input as u16 == kc); + } + OpCodeType::HistoricalKeyCode(hkc) => { + ret = historical_keys + .clone() + .nth(hkc.how_far_back as usize) + .map(|he| he.event as u16 == hkc.key_code) + .unwrap_or(false); + } + OpCodeType::TicksSinceLessThan(tsnk) => { + ret = historical_keys + .clone() + .nth(tsnk.nth_key.into()) + .map(|he| he.ticks_since_occurrence <= tsnk.ticks_since) + .unwrap_or(false); + } + OpCodeType::TicksSinceGreaterThan(tsnk) => { + ret = historical_keys + .clone() + .nth(tsnk.nth_key.into()) + .map(|he| he.ticks_since_occurrence > tsnk.ticks_since) + .unwrap_or(false); + } + OpCodeType::Input(coord) => { + // opcode has size 2 + current_index += 1; + ret = inputs.clone().any(|c| c == coord) + } + OpCodeType::HistoricalInput(hki) => { + // opcode has size 2 + current_index += 1; + ret = historical_inputs + .clone() + .nth(hki.how_far_back as usize) + .map(|he| he.event == hki.input) + .unwrap_or(false) + } + OpCodeType::Layer(layer) => { + // opcode has size 2 + current_index += 1; + ret = layers.clone().next().map(|l| l == layer).unwrap_or(false) + } + OpCodeType::BaseLayer(base_layer) => { + // opcode has size 2 + current_index += 1; + ret = default_layer == base_layer; + } + OpCodeType::HistoricalDevice(hd) => { + // opcode has size 2 + current_index += 1; + ret = device_history + .clone() + .nth(hd.how_far_back as usize) + .and_then(|d| d.map(|d| d == hd.device_id)) + .unwrap_or(false); + } + OpCodeType::CallbackIndex(callback_index) => { + // opcode has size 2 + current_index += 1; + ret = callbacks[usize::from(callback_index)](); + } + }; + if current_op == Not { + ret = !ret; + } + if matches!((ret, current_op), (true, Or) | (false, And | Not)) { + current_index = current_end_index; + continue; + } + current_index += 1; + } + while let Some(OperatorAndEndIndex { op, .. }) = stack.pop_back() { + if op == Not { + ret = !ret; + } + } + ret +} + +#[cfg(test)] +mod test { + use super::*; + + fn svec(s: &[T]) -> Vec + where + T: Clone, + { + s.iter().cloned().collect() + } + + // Do not use anymore. + // Superceded by test_evaluate with SwitchTestCfg. + fn evaluate_bool_test(opcodes: &[OpCode], key_codes: &[KeyCode]) -> bool { + let opcodes = svec(opcodes); + let key_codes = svec(key_codes); + + let testcfg = SwitchTestCfg { + opcodes, + key_codes, + ..Default::default() + }; + testcfg.evaluate() + } + + struct SwitchTestCfg { + opcodes: Vec, + key_codes: Vec, + inputs: Vec, + historical_keys: Vec>, + historical_inputs: Vec>, + layers: Vec, + default_layer: u16, + device_history: Vec>, + callbacks: Vec<&'static (dyn Fn() -> bool + Send + Sync)>, + } + + impl Default for SwitchTestCfg { + fn default() -> Self { + Self { + opcodes: vec![], + key_codes: vec![], + inputs: vec![], + historical_keys: vec![], + historical_inputs: vec![], + layers: vec![], + default_layer: 0, + device_history: vec![], + callbacks: vec![], + } + } + } + + impl SwitchTestCfg { + fn new() -> Self { + Self::default() + } + + fn evaluate(&self) -> bool { + evaluate_boolean( + self.opcodes.as_slice(), + self.key_codes.iter().copied(), + self.inputs.iter().copied(), + self.historical_keys.iter().copied(), + self.historical_inputs.iter().copied(), + self.layers.iter().copied(), + self.default_layer, + self.device_history.iter().copied(), + self.callbacks.as_slice(), + ) + } + + fn opcodes(mut self, opcodes: &[OpCode]) -> Self { + self.opcodes = svec(opcodes); + self + } + + fn inputs(mut self, inputs: &[KCoord]) -> Self { + self.inputs = svec(inputs); + self + } + + fn historical_keys(mut self, historical_keys: &[HistoricalEvent]) -> Self { + self.historical_keys = svec(historical_keys); + self + } + + fn historical_inputs(mut self, historical_inputs: &[HistoricalEvent]) -> Self { + self.historical_inputs = svec(historical_inputs); + self + } + + fn device_history(mut self, device_history: &[Option]) -> Self { + self.device_history = svec(device_history); + self + } + } + + #[test] + fn bool_evaluation_test_0() { + let opcodes = &[ + OpCode::new_bool(And, 9), + OpCode::new_key(KeyCode::A), + OpCode::new_key(KeyCode::B), + OpCode::new_bool(Or, 6), + OpCode::new_key(KeyCode::C), + OpCode::new_key(KeyCode::D), + OpCode::new_bool(Or, 9), + OpCode::new_key(KeyCode::E), + OpCode::new_key(KeyCode::F), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert!(evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_1() { + let opcodes = &[ + OpCode::new_bool(And, 9), + OpCode::new_key(KeyCode::A), + OpCode::new_key(KeyCode::B), + OpCode::new_bool(Or, 6), + OpCode::new_key(KeyCode::C), + OpCode::new_key(KeyCode::D), + OpCode::new_bool(Or, 9), + OpCode::new_key(KeyCode::E), + OpCode::new_key(KeyCode::F), + ]; + let keycodes = &[ + KeyCode::A, + KeyCode::B, + KeyCode::C, + KeyCode::D, + KeyCode::E, + KeyCode::F, + ]; + assert!(evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_2() { + let opcodes = &[ + OpCode(0x2009), + OpCode(KeyCode::A as u16), + OpCode(KeyCode::B as u16), + OpCode(0x1006), + OpCode(KeyCode::C as u16), + OpCode(KeyCode::D as u16), + OpCode(0x1009), + OpCode(KeyCode::E as u16), + OpCode(KeyCode::F as u16), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::E, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_3() { + let opcodes = &[ + OpCode(0x2009), + OpCode(KeyCode::A as u16), + OpCode(KeyCode::B as u16), + OpCode(0x1006), + OpCode(KeyCode::C as u16), + OpCode(KeyCode::D as u16), + OpCode(0x1009), + OpCode(KeyCode::E as u16), + OpCode(KeyCode::F as u16), + ]; + let keycodes = &[KeyCode::B, KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_4() { + let opcodes = &[]; + let keycodes = &[]; + assert!(evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_5() { + let opcodes = &[]; + let keycodes = &[ + KeyCode::A, + KeyCode::B, + KeyCode::C, + KeyCode::D, + KeyCode::E, + KeyCode::F, + ]; + assert!(evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_6() { + let opcodes = &[OpCode(KeyCode::A as u16), OpCode(KeyCode::B as u16)]; + let keycodes = &[ + KeyCode::A, + KeyCode::B, + KeyCode::C, + KeyCode::D, + KeyCode::E, + KeyCode::F, + ]; + assert!(evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_7() { + let opcodes = &[OpCode(KeyCode::A as u16), OpCode(KeyCode::B as u16)]; + let keycodes = &[KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_9() { + let opcodes = &[ + OpCode(0x2003), + OpCode(KeyCode::A as u16), + OpCode(KeyCode::B as u16), + OpCode(KeyCode::C as u16), + ]; + let keycodes = &[KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; + assert!(evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_10() { + let opcodes = &[ + OpCode(0x2004), + OpCode(KeyCode::A as u16), + OpCode(KeyCode::B as u16), + OpCode(KeyCode::C as u16), + ]; + let keycodes = &[KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_11() { + let opcodes = &[ + OpCode(0x1003), + OpCode(KeyCode::A as u16), + OpCode(KeyCode::B as u16), + ]; + let keycodes = &[KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_12() { + let opcodes = &[ + OpCode(0x1005), + OpCode(0x2004), + OpCode(KeyCode::A as u16), + OpCode(KeyCode::B as u16), + OpCode(KeyCode::C as u16), + ]; + let keycodes = &[KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; + assert!(evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_max_depth_does_not_panic() { + let opcodes = &[ + OpCode(0x1008), + OpCode(0x1008), + OpCode(0x1008), + OpCode(0x1008), + OpCode(0x1008), + OpCode(0x1008), + OpCode(0x1008), + OpCode(0x1008), + ]; + let keycodes = &[]; + assert!(evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + #[should_panic] + fn bool_evaluation_test_more_than_max_depth_panics() { + let opcodes = &[ + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + ]; + let keycodes = &[]; + assert!(evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn switch_fallthrough() { + let sw = Switch { + cases: &[ + (&[], &Action::<()>::KeyCode(KeyCode::A), Fallthrough), + (&[], &Action::<()>::KeyCode(KeyCode::B), Fallthrough), + ], + init_fn: None, + callbacks: &[], + }; + let mut actions = sw.actions( + [].iter().copied(), + [].iter().copied(), + [].iter().copied(), + [].iter().copied(), + [].iter().copied(), + 0, + core::iter::empty(), + ); + assert_eq!(actions.next(), Some(&Action::<()>::KeyCode(KeyCode::A))); + assert_eq!(actions.next(), Some(&Action::<()>::KeyCode(KeyCode::B))); + assert_eq!(actions.next(), None); + } + + #[test] + fn switch_break() { + let sw = Switch { + cases: &[ + (&[], &Action::<()>::KeyCode(KeyCode::A), Break), + (&[], &Action::<()>::KeyCode(KeyCode::B), Break), + ], + init_fn: None, + callbacks: &[], + }; + let mut actions = sw.actions( + [].iter().copied(), + [].iter().copied(), + [].iter().copied(), + [].iter().copied(), + [].iter().copied(), + 0, + core::iter::empty(), + ); + assert_eq!(actions.next(), Some(&Action::<()>::KeyCode(KeyCode::A))); + assert_eq!(actions.next(), None); + } + + #[test] + fn switch_no_actions() { + let sw = Switch { + cases: &[ + ( + &[OpCode::new_key(KeyCode::A)], + &Action::<()>::KeyCode(KeyCode::A), + Break, + ), + ( + &[OpCode::new_key(KeyCode::A)], + &Action::<()>::KeyCode(KeyCode::B), + Break, + ), + ], + init_fn: None, + callbacks: &[], + }; + let mut actions = sw.actions( + [].iter().copied(), + [].iter().copied(), + [].iter().copied(), + [].iter().copied(), + [].iter().copied(), + 0, + core::iter::empty(), + ); + assert_eq!(actions.next(), None); + } + + #[test] + fn switch_device_history_match() { + let id1 = NonZeroU8::new(1).unwrap(); + let id2 = NonZeroU8::new(2).unwrap(); + let (op1, op2) = OpCode::new_device_history(id1, 0); + let opcodes = &[op1, op2]; + + // Matching device ID (most recent) + let testcfg = SwitchTestCfg::new() + .opcodes(opcodes) + .device_history(&[Some(id1)]); + assert_eq!(true, testcfg.evaluate()); + + // Non-matching device ID + let testcfg = SwitchTestCfg::new() + .opcodes(opcodes) + .device_history(&[Some(id2)]); + assert_eq!(false, testcfg.evaluate()); + + // Empty device history + let testcfg = SwitchTestCfg::new().opcodes(opcodes); + assert_eq!(false, testcfg.evaluate()); + } + + #[test] + fn switch_device_history_recency() { + let id1 = NonZeroU8::new(1).unwrap(); + let id2 = NonZeroU8::new(2).unwrap(); + let id3 = NonZeroU8::new(3).unwrap(); + let history = [Some(id3), Some(id2), Some(id1)]; // most recent first: 3, 2, 1 + // + // Check most recent (recency 1 → how_far_back 0) is id3 + let (op1, op2) = OpCode::new_device_history(id3, 0); + let testcfg = SwitchTestCfg::new() + .opcodes(&[op1, op2]) + .device_history(&history); + assert_eq!(true, testcfg.evaluate()); + + // Check second most recent (recency 2 → how_far_back 1) is id2 + let (op1, op2) = OpCode::new_device_history(id2, 1); + let testcfg = SwitchTestCfg::new() + .opcodes(&[op1, op2]) + .device_history(&history); + assert_eq!(true, testcfg.evaluate()); + + // Wrong device at recency 1 + let (op1, op2) = OpCode::new_device_history(id1, 0); + let testcfg = SwitchTestCfg::new() + .opcodes(&[op1, op2]) + .device_history(&history); + assert_eq!(false, testcfg.evaluate()); + } + + #[test] + fn switch_device_history_opcode_roundtrip() { + let id = NonZeroU8::new(42).unwrap(); + let (op1, op2) = OpCode::new_device_history(id, 3); + match op1.opcode_type(Some(op2)) { + OpCodeType::HistoricalDevice(hd) => { + assert_eq!(hd.device_id, id); + assert_eq!(hd.how_far_back, 3); + } + other => panic!("expected HistoricalDevice, got {other:?}"), + } + } + + #[test] + fn switch_device_history_unknown_device() { + let id1 = NonZeroU8::new(1).unwrap(); + let history = [Some(id1), None, Some(id1)]; // unknown device at position 1 + // + // Looking for id1 at position 0 should match + let (op1, op2) = OpCode::new_device_history(id1, 0); + let testcfg = SwitchTestCfg::new() + .opcodes(&[op1, op2]) + .device_history(&history); + assert_eq!(true, testcfg.evaluate()); + + // Looking for id1 at position 1 (where None is) should NOT match + let (op1, op2) = OpCode::new_device_history(id1, 1); + let testcfg = SwitchTestCfg::new() + .opcodes(&[op1, op2]) + .device_history(&history); + assert_eq!(false, testcfg.evaluate()); + } + + #[test] + fn switch_historical_1() { + let opcode_true = [OpCode(0x8000 | KeyCode::A as u16)]; + let opcode_true2 = [OpCode(0xF000 | KeyCode::H as u16)]; + let opcode_false = [OpCode(0x9000 | KeyCode::A as u16)]; + let opcode_false2 = [OpCode(0xE000 | KeyCode::H as u16)]; + assert_eq!( + OpCode::new_key_history(KeyCode::A, 0), + OpCode(0x8000 | KeyCode::A as u16) + ); + assert_eq!( + OpCode::new_key_history(KeyCode::H, 7), + OpCode(0xF000 | KeyCode::H as u16) + ); + let hist_keycodes = [ + HistoricalEvent { + event: KeyCode::A, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::B, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::C, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::D, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::E, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::F, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::G, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::H, + ticks_since_occurrence: 0, + }, + ]; + + let testcfg = SwitchTestCfg::new() + .opcodes(&opcode_true) + .historical_keys(&hist_keycodes); + assert_eq!(true, testcfg.evaluate()); + + let testcfg = SwitchTestCfg::new() + .opcodes(&opcode_true2) + .historical_keys(&hist_keycodes); + assert_eq!(true, testcfg.evaluate()); + + let testcfg = SwitchTestCfg::new() + .opcodes(&opcode_false) + .historical_keys(&hist_keycodes); + assert_eq!(false, testcfg.evaluate()); + + let testcfg = SwitchTestCfg::new() + .opcodes(&opcode_false2) + .historical_keys(&hist_keycodes); + assert_eq!(false, testcfg.evaluate()); + } + + #[test] + fn switch_historical_bools() { + let opcodes_true_and = [ + OpCode::new_bool(And, 3), + OpCode::new_key_history(KeyCode::A, 0), + OpCode::new_key_history(KeyCode::B, 1), + ]; + let opcodes_false_and1 = [ + OpCode::new_bool(And, 3), + OpCode::new_key_history(KeyCode::A, 0), + OpCode::new_key_history(KeyCode::B, 2), + ]; + let opcodes_false_and2 = [ + OpCode::new_bool(And, 3), + OpCode::new_key_history(KeyCode::B, 2), + OpCode::new_key_history(KeyCode::A, 0), + ]; + let opcodes_true_or1 = [ + OpCode::new_bool(Or, 3), + OpCode::new_key_history(KeyCode::A, 0), + OpCode::new_key_history(KeyCode::B, 1), + ]; + let opcodes_true_or2 = [ + OpCode::new_bool(Or, 3), + OpCode::new_key_history(KeyCode::A, 0), + OpCode::new_key_history(KeyCode::B, 2), + ]; + let opcodes_true_or3 = [ + OpCode::new_bool(Or, 3), + OpCode::new_key_history(KeyCode::B, 2), + OpCode::new_key_history(KeyCode::A, 0), + ]; + let opcodes_false_or = [ + OpCode::new_bool(Or, 3), + OpCode::new_key_history(KeyCode::A, 1), + OpCode::new_key_history(KeyCode::B, 2), + ]; + let hist_keycodes = [ + HistoricalEvent { + event: KeyCode::A, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::B, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::C, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::D, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::E, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::F, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::G, + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: KeyCode::H, + ticks_since_occurrence: 0, + }, + ]; + + let test = |opcodes: &[OpCode], expectation: bool| { + let testcfg = SwitchTestCfg::new() + .opcodes(opcodes) + .historical_keys(&hist_keycodes); + assert_eq!(expectation, testcfg.evaluate()) + }; + test(&opcodes_true_and, true); + test(&opcodes_true_or1, true); + test(&opcodes_true_or2, true); + test(&opcodes_true_or3, true); + test(&opcodes_false_and1, false); + test(&opcodes_false_and2, false); + test(&opcodes_false_or, false); + } + + #[test] + fn switch_historical_ticks_since() { + let opcodes_true_and = [ + OpCode::new_bool(And, 3), + OpCode::new_ticks_since_gt(0, 99), + OpCode::new_ticks_since_lt(0, 101), + ]; + let opcodes_false_and1 = [ + OpCode::new_bool(And, 3), + OpCode::new_ticks_since_gt(1, 200), + OpCode::new_ticks_since_lt(1, 240), + ]; + let opcodes_false_and2 = [ + OpCode::new_bool(And, 3), + OpCode::new_ticks_since_gt(2, 300), + OpCode::new_ticks_since_lt(2, 300), + ]; + let opcodes_true_or1 = [ + OpCode::new_bool(Or, 3), + OpCode::new_ticks_since_gt(3, 500), + OpCode::new_ticks_since_lt(3, 510), + ]; + let opcodes_true_or2 = [ + OpCode::new_bool(Or, 3), + OpCode::new_ticks_since_gt(4, 500), + OpCode::new_ticks_since_lt(4, 511), + ]; + let opcodes_true_or3 = [ + OpCode::new_bool(Or, 3), + OpCode::new_ticks_since_gt(5, 980), + OpCode::new_ticks_since_lt(5, 999), + ]; + let opcodes_false_or1 = [ + OpCode::new_bool(Or, 3), + OpCode::new_ticks_since_gt(6, 40200), + OpCode::new_ticks_since_lt(6, 39999), + ]; + let opcodes_false_or2 = [ + OpCode::new_bool(Or, 3), + OpCode::new_ticks_since_gt(5, 1030), + OpCode::new_ticks_since_lt(5, 999), + ]; + let opcodes_false_or3 = [ + OpCode::new_bool(Or, 3), + OpCode::new_ticks_since_gt(4, 520), + OpCode::new_ticks_since_lt(4, 511), + ]; + let opcodes_false_or4 = [ + OpCode::new_bool(Or, 3), + OpCode::new_ticks_since_gt(3, 520), + OpCode::new_ticks_since_lt(3, 510), + ]; + let opcodes_false_or5 = [ + OpCode::new_bool(Or, 3), + OpCode::new_ticks_since_gt(2, 265), + OpCode::new_ticks_since_lt(2, 255), + ]; + let opcodes_false_or6 = [ + OpCode::new_bool(Or, 3), + OpCode::new_ticks_since_gt(1, 256), + OpCode::new_ticks_since_lt(1, 254), + ]; + let hist_keycodes = [ + HistoricalEvent { + event: KeyCode::A, + ticks_since_occurrence: 100, + }, + HistoricalEvent { + event: KeyCode::B, + ticks_since_occurrence: 255, + }, + HistoricalEvent { + event: KeyCode::C, + ticks_since_occurrence: 256, + }, + HistoricalEvent { + event: KeyCode::D, + ticks_since_occurrence: 511, + }, + HistoricalEvent { + event: KeyCode::E, + ticks_since_occurrence: 512, + }, + HistoricalEvent { + event: KeyCode::F, + ticks_since_occurrence: 1000, + }, + HistoricalEvent { + event: KeyCode::G, + ticks_since_occurrence: 40000, + }, + ]; + + let test = |opcodes: &[OpCode], expectation: bool| { + let testcfg = SwitchTestCfg::new() + .opcodes(opcodes) + .historical_keys(&hist_keycodes); + assert_eq!(expectation, testcfg.evaluate()) + }; + test(&opcodes_true_and, true); + test(&opcodes_true_or1, true); + test(&opcodes_true_or2, true); + test(&opcodes_true_or3, true); + test(&opcodes_false_and1, false); + test(&opcodes_false_and2, false); + test(&opcodes_false_or1, false); + test(&opcodes_false_or2, false); + test(&opcodes_false_or3, false); + test(&opcodes_false_or4, false); + test(&opcodes_false_or5, false); + test(&opcodes_false_or6, false); + } + + #[test] + fn bool_evaluation_test_not_0() { + // Full inverse of a previous test + let opcodes = &[ + OpCode::new_bool(Not, 10), + OpCode::new_bool(And, 10), + OpCode::new_key(KeyCode::A), + OpCode::new_key(KeyCode::B), + OpCode::new_bool(Or, 7), + OpCode::new_key(KeyCode::C), + OpCode::new_key(KeyCode::D), + OpCode::new_bool(Or, 10), + OpCode::new_key(KeyCode::E), + OpCode::new_key(KeyCode::F), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_not_1() { + // Both A and B exist, should be false + let opcodes = &[ + OpCode::new_bool(Not, 3), + OpCode::new_key(KeyCode::A), + OpCode::new_key(KeyCode::B), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_not_2() { + // Neither X nor Y exist, should be false + let opcodes = &[ + OpCode::new_bool(Not, 3), + OpCode::new_key(KeyCode::X), + OpCode::new_key(KeyCode::Y), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert!(evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_not_3() { + let opcodes = &[ + OpCode::new_key(KeyCode::C), + OpCode::new_bool(Not, 3), + OpCode::new_key(KeyCode::D), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_not_4() { + let opcodes = &[ + OpCode::new_bool(And, 10), + OpCode::new_key(KeyCode::A), + OpCode::new_key(KeyCode::B), + OpCode::new_bool(Or, 7), + OpCode::new_key(KeyCode::C), + OpCode::new_bool(Not, 7), + OpCode::new_key(KeyCode::D), + OpCode::new_bool(Or, 10), + OpCode::new_key(KeyCode::E), + OpCode::new_key(KeyCode::F), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_not_5() { + let opcodes = &[ + OpCode::new_bool(Not, 4), + OpCode::new_key(KeyCode::C), + OpCode::new_bool(Not, 4), + OpCode::new_key(KeyCode::D), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert!(evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_not_6() { + // C does not exist, D does. Ensure C nonexistence does not short-circuit + // and existence of D is checked. + let opcodes = &[ + OpCode::new_bool(Not, 3), + OpCode::new_key(KeyCode::C), + OpCode::new_key(KeyCode::D), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_or_equivalency_not_6() { + let opcodes = &[ + OpCode::new_bool(Not, 4), + OpCode::new_bool(Or, 4), + OpCode::new_key(KeyCode::C), + OpCode::new_key(KeyCode::D), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_not_7() { + // A exists, make sure this short-circuits, and E nonexistence does not override the return. + let opcodes = &[ + OpCode::new_bool(Not, 3), + OpCode::new_key(KeyCode::A), + OpCode::new_key(KeyCode::E), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_or_equivalency_not_7() { + let opcodes = &[ + OpCode::new_bool(Not, 4), + OpCode::new_bool(Or, 4), + OpCode::new_key(KeyCode::A), + OpCode::new_key(KeyCode::E), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_not_8() { + let opcodes = &[ + OpCode::new_bool(Not, 4), + OpCode::new_bool(Not, 4), + OpCode::new_bool(Not, 4), + OpCode::new_key(KeyCode::A), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert!(!evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn bool_evaluation_test_not_9() { + let opcodes = &[ + OpCode::new_bool(Not, 4), + OpCode::new_bool(Not, 4), + OpCode::new_bool(Not, 4), + OpCode::new_key(KeyCode::C), + ]; + let keycodes = &[KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert!(evaluate_bool_test(opcodes, keycodes,)); + } + + #[test] + fn switch_inputs() { + let (op1, op2) = OpCode::new_active_input((0, 1)); + let (op3, op4) = OpCode::new_active_input((1, 2)); + let (op5, op6) = OpCode::new_active_input((1, 3)); + let (op7, op8) = OpCode::new_active_input((3, 3)); + let opcodes_true_and = [OpCode::new_bool(And, 5), op1, op2, op3, op4]; + let opcodes_false_and1 = [OpCode::new_bool(And, 5), op1, op2, op5, op6]; + let opcodes_false_and2 = [OpCode::new_bool(And, 5), op5, op6, op1, op2]; + let opcodes_false_or = [OpCode::new_bool(Or, 5), op7, op8, op5, op6]; + let opcodes_true_or1 = [OpCode::new_bool(Or, 5), op1, op2, op5, op6]; + let opcodes_true_or2 = [OpCode::new_bool(Or, 5), op7, op8, op3, op4]; + let active_inputs = [(0, 1), (1, 2), (2, 3), (3, 4)]; + let test = |opcodes: &[OpCode], expectation: bool| { + let testcfg = SwitchTestCfg::new().opcodes(opcodes).inputs(&active_inputs); + assert_eq!(expectation, testcfg.evaluate()) + }; + test(&opcodes_true_and, true); + test(&opcodes_false_and1, false); + test(&opcodes_false_and2, false); + test(&opcodes_false_or, false); + test(&opcodes_true_or1, true); + test(&opcodes_true_or2, true); + } + + #[test] + fn switch_historical_inputs() { + let (op1, op2) = OpCode::new_historical_input((0, 0), 0); + let (op3, op4) = OpCode::new_historical_input((3, 750), 7); + let (op5, op6) = OpCode::new_historical_input((1, 3), 0); + let (op7, op8) = OpCode::new_historical_input((3, 3), 7); + let opcodes_true_and = [OpCode::new_bool(And, 5), op1, op2, op3, op4]; + let opcodes_false_and1 = [OpCode::new_bool(And, 5), op1, op2, op5, op6]; + let opcodes_false_and2 = [OpCode::new_bool(And, 5), op5, op6, op1, op2]; + let opcodes_false_or = [OpCode::new_bool(Or, 5), op7, op8, op5, op6]; + let opcodes_true_or1 = [OpCode::new_bool(Or, 5), op1, op2, op5, op6]; + let opcodes_true_or2 = [OpCode::new_bool(Or, 5), op7, op8, op3, op4]; + let historical_inputs = [ + HistoricalEvent { + event: (0, 0), + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: (1, 750), + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: (2, 1), + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: (3, 749), + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: (0, 1), + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: (1, 2), + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: (2, 3), + ticks_since_occurrence: 0, + }, + HistoricalEvent { + event: (3, 750), + ticks_since_occurrence: 0, + }, + ]; + + let test = |opcodes: &[OpCode], expectation: bool| { + let testcfg = SwitchTestCfg::new() + .opcodes(opcodes) + .historical_inputs(&historical_inputs); + assert_eq!(expectation, testcfg.evaluate()) + }; + + test(&opcodes_true_and, true); + test(&opcodes_false_and1, false); + test(&opcodes_false_and2, false); + test(&opcodes_false_or, false); + test(&opcodes_true_or1, true); + test(&opcodes_true_or2, true); + } +} diff --git a/keyberon/src/chord.rs b/keyberon/src/chord.rs new file mode 100644 index 000000000..d079bdd96 --- /dev/null +++ b/keyberon/src/chord.rs @@ -0,0 +1,617 @@ +//! Module for chords v2 implementation. + +use std::cell::Cell; + +use arraydeque::ArrayDeque; +use heapless::Vec as HVec; +use rustc_hash::FxHashMap; + +use crate::{ + action::Action, + key_code::KEY_MAX, + layout::{Event, Queue, Queued, QueuedAction}, +}; + +// Macro to help with this boilerplate. +// $v should probably be `self` at points of use. +// Ownership rules make this difficult to do as a regular fn, +// because impl function calls don't understand split borrowing. +macro_rules! no_chord_activations { + ($v:expr) => {{ + $v.ticks_to_ignore_chord = $v.configured_ticks_to_ignore_chord; + }}; +} + +pub(crate) const TRIGGER_TAPHOLD_COORD: (u8, u16) = (0, 0); + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ReleaseBehaviour { + OnFirstRelease, + OnLastRelease, +} + +#[derive(Clone)] +pub struct ChordV2<'a, T> { + /// The action associated with this chord. + pub action: &'a Action<'a, T>, + /// The full set of keys that need to be pressed to activate this chord. + pub participating_keys: &'a [u16], + /// The number of ticks during which, after the first press of a participant, + /// this chord can be activated if all participants get pressed. + /// In other words, after the number of ticks defined by `pending_duration` + /// elapses, this chord can no longer be completed. + pub pending_duration: u16, + /// The layers on which this chord is disabled. + pub disabled_layers: &'a [u16], + /// When should the action for this chord be released. + pub release_behaviour: ReleaseBehaviour, +} + +impl<'a, T> std::fmt::Debug for ChordV2<'a, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("Point") + .field("participating_keys", &self.participating_keys) + .field("pending_duration", &self.pending_duration) + .field("disabled_layers", &self.disabled_layers) + .field("release_behaviour", &self.release_behaviour) + .finish() + } +} + +#[derive(Debug, Clone)] +pub struct ChordsForKey<'a, T> { + /// Chords that this key participates in. + pub chords: Vec<&'a ChordV2<'a, T>>, +} + +#[derive(Debug, Clone)] +pub struct ChordsForKeys<'a, T> { + pub mapping: FxHashMap>, +} + +const SMOL_Q_LEN: usize = 16; + +struct ActiveChord<'a, T> { + /// Chords uses a virtual coordinate in the keyberon state for an activated chord. + /// This field tracks which coordinate to release when the chord itself is released. + coordinate: u16, + /// Keys left to release. + /// For OnFirstRelease, this should have length 0. + remaining_keys_to_release: HVec, + /// Necessary to include here make sure that, for OnFirstRelease, + /// random other releases that are not part of this chord, + /// do not release this chord. + participating_keys: &'a [u16], + /// Action associated with the active chord. + /// This needs to be stored here + action: &'a Action<'a, T>, + /// In the case of Unread, this chord has not yet been consumed by the layout code. + /// This might happen for a while because of tap-hold-related delays. + /// In the Releasable status, the active chord has been consumed and can be released. + status: ActiveChordStatus, + /// Tracks how old an action is. + delay: u16, +} + +fn tick_ach(acc: &mut ActiveChord) { + acc.delay = acc.delay.saturating_add(1); +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ActiveChordStatus { + /// -> UnreadPendingRelease if chord released before being consumed + /// -> Releasable if consumed + Unread, + /// -> Released once consumed + UnreadReleased, + /// Can remove at any time. + /// -> Released once released + Releasable, + /// Remove on next tick_chv2 + Released, +} +use ActiveChordStatus::*; + +/// Like the layout Queue but smaller. +pub(crate) type SmolQueue = ArrayDeque; + +/// Global input chords configuration. +pub struct ChordsV2<'a, T> { + // Note: Interior fields do not need to be pub or mutable via impl pub fn. + // Like a layout, this should be destroyed and recreated on a live reload. + // + /// Queued inputs that can potentially activate a chord but have not yet. + /// Inputs will leave if they are determined that they will not activate a chord, + /// or if a chord activates. + queue: Queue, + /// Information about what chords are possible and what keys they are associated with. + chords: ChordsForKeys<'a, T>, + /// Chords that are active, i.e. ones that have not yet been released. + active_chords: HVec, 10>, + /// When a key leaves the combo queue without activating a chord, + /// this activates a timer during which keys cannot activate chords + /// and are always forwarded directly to the standard input queue. + /// + /// This keeps track of the timer. + ticks_to_ignore_chord: u16, + /// Initial value for the above when the appropriate event happens. + /// This must have a minimum value even if not configured by the user, + /// or if configured by the user to be zero. (maybe forbid that config) + configured_ticks_to_ignore_chord: u16, + /// Optimization: if there are no new inputs, the code can skip some processing work. + /// This tracks the next time that a change will happen, so that the processing work + /// is **not** skipped when something needs to be checked. + ticks_until_next_state_change: u16, + /// Optimization: the below is part of skipping processing work - if this is has changed, + /// then processing work cannot be skipped. + prev_active_layer: u16, + /// Optimization: the below is part of skipping processing work - if this is has changed, + /// then processing work cannot be skipped. + prev_queue_len: u8, + /// Virtual coordinate for use in the layout state. + next_coord: Cell, +} + +impl std::fmt::Debug for ChordsV2<'_, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ChordsV2") + } +} + +impl<'a, T> ChordsV2<'a, T> { + pub fn new(chords: ChordsForKeys<'a, T>, ticks_ignore_chord: u16) -> Self { + assert!(ticks_ignore_chord >= 5); + Self { + queue: Queue::new(), + chords, + active_chords: HVec::new(), + ticks_to_ignore_chord: 0, + configured_ticks_to_ignore_chord: ticks_ignore_chord, + ticks_until_next_state_change: 0, + prev_active_layer: u16::MAX, + prev_queue_len: u8::MAX, + next_coord: Cell::new(KEY_MAX + 1), + } + } + + pub fn is_idle_chv2(&self) -> bool { + self.queue.is_empty() && self.active_chords.is_empty() + } + + pub fn accepts_chords_chv2(&self) -> bool { + self.ticks_to_ignore_chord == 0 + } + + pub fn push_back_chv2(&mut self, item: Queued) -> Option { + self.queue.push_back(item) + } + + pub fn chords(&self) -> &ChordsForKeys<'a, T> { + &self.chords + } + + pub(crate) fn get_action_chv2(&mut self) -> QueuedAction<'a, T> { + self.active_chords + .iter_mut() + .find_map(|ach| match ach.status { + Unread => { + ach.status = Releasable; + // Note on LayerStack being default (empty): + // A chordv2 is not allowed to use transparency, + // so it does not need to handle this case. + Some(Some(( + (0, ach.coordinate), + ach.delay, + ach.action, + Default::default(), + ))) + } + UnreadReleased => { + ach.status = Released; + Some(Some(( + (0, ach.coordinate), + ach.delay, + ach.action, + Default::default(), + ))) + } + Releasable | Released => None, + }) + .unwrap_or_default() + } + + /// Update the times in the queue without activating any chords yet. + /// Returns queued events that are no longer usable in chords. + pub(crate) fn tick_chv2(&mut self, active_layer: u16) -> SmolQueue { + let mut q = SmolQueue::new(); + self.queue.iter_mut().for_each(Queued::tick_qd); + let prev_active_chord_len = self.active_chords.len(); + self.active_chords.iter_mut().for_each(tick_ach); + self.drain_inputs(&mut q, active_layer); + if self.active_chords.len() != prev_active_chord_len { + // A chord was activated. Forward a no-op press event to potentially trigger + // HoldOnOtherKeyPress or PermissiveHold. + // FLAW: this does not associate with the actual input keys and thus cannot correctly + // trigger the early tap for *-keys variants of kanata tap-hold. + q.push_back(Queued::new_press( + TRIGGER_TAPHOLD_COORD.0, + TRIGGER_TAPHOLD_COORD.1, + )); + } + if self + .active_chords + .iter() + .any(|ach| matches!(ach.status, UnreadReleased | Released)) + { + // A chord was released. Forward a no-op release event to potentially trigger + // PermissiveHold. + // FLAW: see above + q.push_back(Queued::new_release( + TRIGGER_TAPHOLD_COORD.0, + TRIGGER_TAPHOLD_COORD.1, + )); + } + self.clear_released_chords(&mut q); + self.ticks_to_ignore_chord = self.ticks_to_ignore_chord.saturating_sub(1); + q + } + + fn next_coord(&self) -> u16 { + let ret = self.next_coord.get(); + let mut new = ret + 1; + if new > KEY_MAX + 50 { + new = KEY_MAX + 1; + } + self.next_coord.set(new); + ret + } + + fn drain_inputs(&mut self, drainq: &mut SmolQueue, active_layer: u16) { + if self.ticks_to_ignore_chord > 0 { + self.drain_without_new_activations(drainq); + return; + } + if self.ticks_until_next_state_change > 0 + && self.prev_active_layer == active_layer + && usize::from(self.prev_queue_len) == self.queue.len() + { + self.ticks_until_next_state_change = + self.ticks_until_next_state_change.saturating_sub(1); + return; + } + self.ticks_until_next_state_change = 0; + self.prev_active_layer = active_layer; + debug_assert!(self.queue.capacity() < 255); + self.prev_queue_len = self.queue.len() as u8; + + self.drain_virtual_keys(drainq); + self.drain_releases(drainq); + self.process_presses(active_layer); + } + + /// Used to process keys while chordsv2 is in the disabled state from rapid typing. + /// Releases must still be processed to release already-activated chords. + fn drain_without_new_activations(&mut self, drainq: &mut SmolQueue) { + let achs = &mut self.active_chords; + for qd in self.queue.iter() { + if let Event::Release(_, j) = qd.event { + // Release the key from active chords. + achs.iter_mut().for_each(|ach| { + if !ach.participating_keys.contains(&j) { + return; + } + ach.remaining_keys_to_release.retain(|pk| *pk != j); + if ach.remaining_keys_to_release.is_empty() { + ach.status = match ach.status { + Unread | UnreadReleased => UnreadReleased, + Releasable | Released => Released, + } + } + }); + } + drainq.push_back(*qd); + } + self.queue.clear(); + } + + fn drain_virtual_keys(&mut self, drainq: &mut SmolQueue) { + self.queue.retain(|qd| { + match qd.event { + // Only row 0 is real inputs. + // Drain other rows (at the time of writing should only be index 1). + Event::Press(0, _) | Event::Release(0, _) => true, + _ => { + let overflow = drainq.push_back(*qd); + assert!(overflow.is_none(), "oops overflowed drain queue"); + false + } + } + }); + } + + fn drain_releases(&mut self, drainq: &mut SmolQueue) { + let achs = &mut self.active_chords; + let mut presses = HVec::<_, SMOL_Q_LEN>::new(); + self.queue.retain(|qd| match qd.event { + Event::Press(_, j) => { + let overflow = presses.push(j); + debug_assert!(overflow.is_ok()); + true + } + Event::Release(_, j) => { + if presses.is_empty() { + // Release the key from active chords. + achs.iter_mut().for_each(|ach| { + if !ach.participating_keys.contains(&j) { + return; + } + ach.remaining_keys_to_release.retain(|pk| *pk != j); + if ach.remaining_keys_to_release.is_empty() { + ach.status = match ach.status { + Unread | UnreadReleased => UnreadReleased, + Releasable | Released => Released, + } + } + }); + + drainq.push_back(*qd); + false + } else { + true + } + } + }) + } + + fn process_presses(&mut self, active_layer: u16) { + #[derive(Copy, Clone, Debug)] + struct PressWithTime { + key: u16, + since: u16, + } + + let mut presses = HVec::::new(); + let mut presses_with_time = HVec::::new(); + let mut relevant_release_found = false; + for qd in self.queue.iter() { + match qd.event { + Event::Press(_, j) => { + let overflowed = presses.push(j); + debug_assert!(overflowed.is_ok(), "too many presses in queue"); + let _ = presses_with_time.push(PressWithTime { + key: j, + since: qd.since, + }); + } + Event::Release(_, j) => { + if presses.contains(&j) { + relevant_release_found = true; + break; + } + } + } + } + let prev_active_chords_len = self.active_chords.len(); + let Some(starting_press) = presses.first() else { + return; + }; + let Some(possible_chords) = self.chords.mapping.get(starting_press) else { + no_chord_activations!(self); + return; + }; + + // For subsequent keypresses, + // all must fit into a single chord for chord state to remain pending + // instead of activating a chord, + // and there must also be a longer chord that can still potentially be activated. + // + // Prioritization of chord activation: + // 1. Timed out chord + // 2. Longer chord + let mut accumulated_presses = HVec::::new(); + let mut chord_candidates = HVec::<&ChordV2<'a, T>, SMOL_Q_LEN>::new(); + let mut prev_count = usize::MAX; + let mut min_timeout; + + assert!(!presses_with_time.is_empty()); + let since = self.queue.iter().next().unwrap().since; + + for press in presses_with_time.iter().copied() { + min_timeout = u16::MAX; + let _ = accumulated_presses.push(press); + + let count_possible = if prev_count == chord_candidates.len() { + // optimization: no longer need to check the whole list. + // chord_candidates will keep getting shrunk. + chord_candidates.retain(|chc| { + chc.participating_keys.contains(&press.key) + && accumulated_presses[0].since - press.since <= chc.pending_duration + }); + for chc in chord_candidates.iter() { + if chc.pending_duration > since { + min_timeout = std::cmp::min(min_timeout, chc.pending_duration); + } + } + chord_candidates.len() + } else { + chord_candidates.clear(); + possible_chords + .chords + .iter() + .filter(|pch| !pch.disabled_layers.contains(&active_layer)) + .filter(|pch| { + if accumulated_presses + .iter() + .all(|acp| pch.participating_keys.contains(&acp.key)) + && accumulated_presses[0].since - press.since <= pch.pending_duration + { + // If full, can't run the optimization above, but not fatal. + // Can ignore the overflow. + let _overflow = chord_candidates.push(pch); + if pch.pending_duration > since { + min_timeout = std::cmp::min(min_timeout, pch.pending_duration); + } + true + } else { + false + } + }) + .count() + }; + + match count_possible { + 1 => { + // Found a chord that is not fully overlapped by another. + // Activate the chord if it is completed + let coord = self.next_coord(); + let cch = chord_candidates[0]; + if cch + .participating_keys + .iter() + .all(|pk| accumulated_presses.iter().any(|ap| ap.key == *pk)) + { + let ach = get_active_chord(cch, since, coord, relevant_release_found); + let overflow = self.active_chords.push(ach); + assert!(overflow.is_ok(), "active chords has room"); + break; + } + } + 0 => { + // If reached this, it means we went from 2+ -> 0, + // or we got to zero at the first iteration. + // Backtrack one accumulated press then: + // - activate a chord if one completed + // - clear the input queue otherwise + let _ = accumulated_presses.pop(); + chord_candidates.clear(); + let completed_chord = possible_chords + .chords + .iter() + .filter(|pch| !pch.disabled_layers.contains(&active_layer)) + .find( + // Ensure the two lists have the same set of keys + |pch| { + accumulated_presses + .iter() + .all(|acp| pch.participating_keys.contains(&acp.key)) + && pch.participating_keys.iter().all(|pk| { + accumulated_presses.iter().any(|ap| ap.key == *pk) + }) + }, + ); + match completed_chord { + Some(cch) => { + let coord = self.next_coord(); + let ach = get_active_chord(cch, since, coord, relevant_release_found); + let overflow = self.active_chords.push(ach); + assert!(overflow.is_ok(), "active chords has room"); + } + None => no_chord_activations!(self), + } + break; + } + _ => {} + } + self.ticks_until_next_state_change = match min_timeout { + u16::MAX => 0, + t => t.saturating_sub(since), + }; + prev_count = count_possible; + } + if self.ticks_until_next_state_change == 0 || relevant_release_found { + // Find a chord that matches exactly and activate that, + // otherwise clear the input queue. + let completed_chord = + if chord_candidates.is_full() { + possible_chords + .chords + .iter() + .filter(|pch| !pch.disabled_layers.contains(&active_layer)) + .find( + // Ensure the two lists have the same set of keys + |pch| { + accumulated_presses + .iter() + .all(|acp| pch.participating_keys.contains(&acp.key)) + && pch.participating_keys.iter().all(|pk| { + accumulated_presses.iter().any(|ap| ap.key == *pk) + }) + }, + ) + } else { + chord_candidates + .iter() + .filter(|pch| !pch.disabled_layers.contains(&active_layer)) + .find( + // Ensure the two lists have the same set of keys + |pch| { + accumulated_presses + .iter() + .all(|acp| pch.participating_keys.contains(&acp.key)) + && pch.participating_keys.iter().all(|pk| { + accumulated_presses.iter().any(|ap| ap.key == *pk) + }) + }, + ) + }; + match completed_chord { + Some(cch) => { + let ach = + get_active_chord(cch, since, self.next_coord(), relevant_release_found); + let overflow = self.active_chords.push(ach); + assert!(overflow.is_ok(), "active chords has room"); + } + None => { + no_chord_activations!(self) + } + } + } + + // Clear presses from the queue if they were consumed by a chord. + if self.active_chords.len() > prev_active_chords_len { + self.queue.retain(|qd| match qd.event { + Event::Press(_, j) => !accumulated_presses.iter().any(|ap| ap.key == j), + _ => true, + }); + } + } + + fn clear_released_chords(&mut self, drainq: &mut SmolQueue) { + self.active_chords.retain(|ach| { + if ach.status == Released { + let overflow = drainq.push_back(Queued { + event: Event::Release(0, ach.coordinate), + since: 0, + }); + assert!(overflow.is_none(), "oops overflowed drain queue"); + false + } else { + true + } + }); + } +} + +fn get_active_chord<'a, T>( + cch: &ChordV2<'a, T>, + since: u16, + coord: u16, + release_found: bool, +) -> ActiveChord<'a, T> { + let mut remaining_keys_to_release = HVec::new(); + if cch.release_behaviour == ReleaseBehaviour::OnLastRelease { + remaining_keys_to_release.extend(cch.participating_keys.iter().copied()); + }; + ActiveChord { + coordinate: coord, + remaining_keys_to_release, + participating_keys: cch.participating_keys, + action: cch.action, + status: if release_found && cch.release_behaviour == ReleaseBehaviour::OnFirstRelease { + ActiveChordStatus::UnreadReleased + } else { + ActiveChordStatus::Unread + }, + delay: since, + } +} diff --git a/keyberon/src/key_code.rs b/keyberon/src/key_code.rs index 50784fd17..60aed7eed 100644 --- a/keyberon/src/key_code.rs +++ b/keyberon/src/key_code.rs @@ -1,877 +1,873 @@ //! Key code definitions. +/// Used for switch opcode purposes. Keys should not exceed this amount. +pub const KEY_MAX: u16 = 850; + +#[test] +fn keycode_max_test() { + assert!((KeyCode::KeyMax as u16) < KEY_MAX); +} + #[allow(missing_docs)] /// Define a key code according to the HID specification. Their names /// correspond to the american QWERTY layout. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(u16)] pub enum KeyCode { - /// The "no" key, a placeholder to express nothing. - No = 0x00, - /// Error if too much keys are pressed at the same time. - ErrorRollOver, - /// The POST fail error. - PostFail, - /// An undefined error occured. - ErrorUndefined, - /// `a` and `A`. - A, - B, - C, - D, - E, - F, - G, - H, - I, - J, - K, - L, - M, // 0x10 - N, - O, - P, - Q, - R, - S, - T, - U, - V, - W, - X, - Y, - Z, - /// `1` and `!`. - Kb1, - /// `2` and `@`. - Kb2, - /// `3` and `#`. - Kb3, // 0x20 - /// `4` and `$`. - Kb4, - /// `5` and `%`. - Kb5, - /// `6` and `^`. - Kb6, - /// `7` and `&`. - Kb7, - /// `8` and `*`. - Kb8, - /// `9` and `(`. - Kb9, - /// `0` and `)`. - Kb0, - Enter, - Escape, - BSpace, - Tab, - Space, - /// `-` and `_`. - Minus, - /// `=` and `+`. - Equal, - /// `[` and `{`. - LBracket, - /// `]` and `}`. - RBracket, // 0x30 - /// `\` and `|`. - Bslash, - /// Non-US `#` and `~` (Typically near the Enter key). - NonUsHash, - /// `;` and `:`. - SColon, - /// `'` and `"`. - Quote, - // How to have ` as code? - /// \` and `~`. - Grave, - /// `,` and `<`. - Comma, - /// `.` and `>`. - Dot, - /// `/` and `?`. - Slash, - CapsLock, - F1, - F2, - F3, - F4, - F5, - F6, - F7, // 0x40 - F8, - F9, - F10, - F11, - F12, - PScreen, - ScrollLock, - Pause, - Insert, - Home, - PgUp, - Delete, - End, - PgDown, - Right, - Left, // 0x50 - Down, - Up, - NumLock, - /// Keypad `/` - KpSlash, - /// Keypad `*` - KpAsterisk, - /// Keypad `-`. - KpMinus, - /// Keypad `+`. - KpPlus, - /// Keypad enter. - KpEnter, - /// Keypad 1. - Kp1, - Kp2, - Kp3, - Kp4, - Kp5, - Kp6, - Kp7, - Kp8, // 0x60 - Kp9, - Kp0, - KpDot, - /// Non-US `\` and `|` (Typically near the Left-Shift key) - NonUsBslash, - Application, // 0x65 - /// not a key, used for errors - Power, - /// Keypad `=`. - KpEqual, - F13, - F14, - F15, - F16, - F17, - F18, - F19, - F20, - F21, // 0x70 - F22, - F23, - F24, - Execute, - Help, - Menu, - Select, - Stop, - Again, - Undo, - Cut, - Copy, - Paste, - Find, - Mute, - VolUp, // 0x80 - VolDown, - /// Deprecated. - LockingCapsLock, - /// Deprecated. - LockingNumLock, - /// Deprecated. - LockingScrollLock, - /// Keypad `,`, also used for the brazilian keypad period (.) key. - KpComma, - /// Used on AS/400 keyboard - KpEqualSign, - Intl1, - Intl2, - Intl3, - Intl4, - Intl5, - Intl6, - Intl7, - Intl8, - Intl9, - Lang1, // 0x90 - Lang2, - Lang3, - Lang4, - Lang5, - Lang6, - Lang7, - Lang8, - Lang9, - AltErase, - SysReq, - Cancel, - Clear, - Prior, - Return, - Separator, - Out, // 0xA0 - Oper, - ClearAgain, - CrSel, - ExSel, - - // According to QMK, 0xA5-0xDF are not usable on modern keyboards. The keys below are - // not real keys for USB codes; they are used only for kanata. - Wakeup, // 0xA5 - BrightnessUp, - BrightnessDown, - KbdIllumUp, - KbdIllumDown, - K0xAA, - K0xAB, - K0xAC, - K0xAD, - K0xAE, - K0xAF, - K0xB0, - K0xB1, - K0xB2, - K0xB3, - K0xB4, - K0xB5, - K0xB6, - K0xB7, - K0xB8, - K0xB9, - K0xBA, - K0xBB, - K0xBC, - K0xBD, - K0xBE, - K0xBF, - K0xC0, - K0xC1, - K0xC2, - K0xC3, - K0xC4, - K0xC5, - K0xC6, - K0xC7, - K0xC8, - K0xC9, - K0xCA, - K0xCB, - K0xCC, - K0xCD, - K0xCE, - K0xCF, - K0xD0, - K0xD1, - K0xD2, - K0xD3, - K0xD4, - K0xD5, - K0xD6, - K0xD7, - K0xD8, - K0xD9, - K0xDA, - K0xDB, - K0xDC, - K0xDD, - K0xDE, - K0xDF, - - // Modifiers - /// Left Control. - LCtrl = 0xE0, - /// Left Shift. - LShift, - /// Left Alt. - LAlt, - /// Left GUI (the Windows key). - LGui, - /// Right Control. - RCtrl, - /// Right Shift. - RShift, - /// Right Alt (or Alt Gr). - RAlt, - /// Right GUI (the Windows key). - RGui, // 0xE7 - - // Unofficial - MediaPlayPause = 0xE8, - MediaStopCD, - MediaPreviousSong, - MediaNextSong, - MediaEjectCD, - MediaVolUp, - MediaVolDown, - MediaMute, - MediaWWW, // 0xF0 - MediaBack, - MediaForward, - MediaStop, - MediaFind, - MediaScrollUp, - MediaScrollDown, - MediaEdit, - MediaSleep, - MediaCoffee, - MediaRefresh, - MediaCalc, // 0xFB - - K252, - K253, - K254, - K255, - K256, - K257, - K258, - K259, - K260, - K261, - K262, - K263, - K264, - K265, - K266, - K267, - K268, - K269, - K270, - K271, - K272, - K273, - K274, - K275, - K276, - K277, - K278, - K279, - K280, - K281, - K282, - K283, - K284, - K285, - K286, - K287, - K288, - K289, - K290, - K291, - K292, - K293, - K294, - K295, - K296, - K297, - K298, - K299, - K300, - K301, - K302, - K303, - K304, - K305, - K306, - K307, - K308, - K309, - K310, - K311, - K312, - K313, - K314, - K315, - K316, - K317, - K318, - K319, - K320, - K321, - K322, - K323, - K324, - K325, - K326, - K327, - K328, - K329, - K330, - K331, - K332, - K333, - K334, - K335, - K336, - K337, - K338, - K339, - K340, - K341, - K342, - K343, - K344, - K345, - K346, - K347, - K348, - K349, - K350, - K351, - K352, - K353, - K354, - K355, - K356, - K357, - K358, - K359, - K360, - K361, - K362, - K363, - K364, - K365, - K366, - K367, - K368, - K369, - K370, - K371, - K372, - K373, - K374, - K375, - K376, - K377, - K378, - K379, - K380, - K381, - K382, - K383, - K384, - K385, - K386, - K387, - K388, - K389, - K390, - K391, - K392, - K393, - K394, - K395, - K396, - K397, - K398, - K399, - K400, - K401, - K402, - K403, - K404, - K405, - K406, - K407, - K408, - K409, - K410, - K411, - K412, - K413, - K414, - K415, - K416, - K417, - K418, - K419, - K420, - K421, - K422, - K423, - K424, - K425, - K426, - K427, - K428, - K429, - K430, - K431, - K432, - K433, - K434, - K435, - K436, - K437, - K438, - K439, - K440, - K441, - K442, - K443, - K444, - K445, - K446, - K447, - K448, - K449, - K450, - K451, - K452, - K453, - K454, - K455, - K456, - K457, - K458, - K459, - K460, - K461, - K462, - K463, - K464, - K465, - K466, - K467, - K468, - K469, - K470, - K471, - K472, - K473, - K474, - K475, - K476, - K477, - K478, - K479, - K480, - K481, - K482, - K483, - K484, - K485, - K486, - K487, - K488, - K489, - K490, - K491, - K492, - K493, - K494, - K495, - K496, - K497, - K498, - K499, - K500, - K501, - K502, - K503, - K504, - K505, - K506, - K507, - K508, - K509, - K510, - K511, - K512, - K513, - K514, - K515, - K516, - K517, - K518, - K519, - K520, - K521, - K522, - K523, - K524, - K525, - K526, - K527, - K528, - K529, - K530, - K531, - K532, - K533, - K534, - K535, - K536, - K537, - K538, - K539, - K540, - K541, - K542, - K543, - K544, - K545, - K546, - K547, - K548, - K549, - K550, - K551, - K552, - K553, - K554, - K555, - K556, - K557, - K558, - K559, - K560, - K561, - K562, - K563, - K564, - K565, - K566, - K567, - K568, - K569, - K570, - K571, - K572, - K573, - K574, - K575, - K576, - K577, - K578, - K579, - K580, - K581, - K582, - K583, - K584, - K585, - K586, - K587, - K588, - K589, - K590, - K591, - K592, - K593, - K594, - K595, - K596, - K597, - K598, - K599, - K600, - K601, - K602, - K603, - K604, - K605, - K606, - K607, - K608, - K609, - K610, - K611, - K612, - K613, - K614, - K615, - K616, - K617, - K618, - K619, - K620, - K621, - K622, - K623, - K624, - K625, - K626, - K627, - K628, - K629, - K630, - K631, - K632, - K633, - K634, - K635, - K636, - K637, - K638, - K639, - K640, - K641, - K642, - K643, - K644, - K645, - K646, - K647, - K648, - K649, - K650, - K651, - K652, - K653, - K654, - K655, - K656, - K657, - K658, - K659, - K660, - K661, - K662, - K663, - K664, - K665, - K666, - K667, - K668, - K669, - K670, - K671, - K672, - K673, - K674, - K675, - K676, - K677, - K678, - K679, - K680, - K681, - K682, - K683, - K684, - K685, - K686, - K687, - K688, - K689, - K690, - K691, - K692, - K693, - K694, - K695, - K696, - K697, - K698, - K699, - K700, - K701, - K702, - K703, - K704, - K705, - K706, - K707, - K708, - K709, - K710, - K711, - K712, - K713, - K714, - K715, - K716, - K717, - K718, - K719, - K720, - K721, - K722, - K723, - K724, - K725, - K726, - K727, - K728, - K729, - K730, - K731, - K732, - K733, - K734, - K735, - K736, - K737, - K738, - K739, - K740, - K741, - K742, - K743, - K744, + ErrorUndefined = 0, + Escape = 1, + Kb1 = 2, + Kb2 = 3, + Kb3 = 4, + Kb4 = 5, + Kb5 = 6, + Kb6 = 7, + Kb7 = 8, + Kb8 = 9, + Kb9 = 10, + Kb0 = 11, + Minus = 12, + Equal = 13, + BSpace = 14, + Tab = 15, + Q = 16, + W = 17, + E = 18, + R = 19, + T = 20, + Y = 21, + U = 22, + I = 23, + O = 24, + P = 25, + LBracket = 26, + RBracket = 27, + Enter = 28, + LCtrl = 29, + A = 30, + S = 31, + D = 32, + F = 33, + G = 34, + H = 35, + J = 36, + K = 37, + L = 38, + SColon = 39, + Quote = 40, + Grave = 41, + LShift = 42, + Bslash = 43, + Z = 44, + X = 45, + C = 46, + V = 47, + B = 48, + N = 49, + M = 50, + Comma = 51, + Dot = 52, + Slash = 53, + RShift = 54, + KpAsterisk = 55, + LAlt = 56, + Space = 57, + CapsLock = 58, + F1 = 59, + F2 = 60, + F3 = 61, + F4 = 62, + F5 = 63, + F6 = 64, + F7 = 65, + F8 = 66, + F9 = 67, + F10 = 68, + NumLock = 69, + ScrollLock = 70, + Kp7 = 71, + Kp8 = 72, + Kp9 = 73, + KpMinus = 74, + Kp4 = 75, + Kp5 = 76, + Kp6 = 77, + KpPlus = 78, + Kp1 = 79, + Kp2 = 80, + Kp3 = 81, + Kp0 = 82, + KpDot = 83, + K0xBF = 84, + K0xC0 = 85, + NonUsBslash = 86, + F11 = 87, + F12 = 88, + Intl1 = 89, + K0xB1 = 90, + K0xB3 = 91, + K0xB0 = 92, + K0xB2 = 93, + K0xAF = 94, + NonUsHash = 95, + KpEnter = 96, + RCtrl = 97, + KpSlash = 98, + SysReq = 99, + RAlt = 100, + K0xC1 = 101, + Home = 102, + Up = 103, + PgUp = 104, + Left = 105, + Right = 106, + End = 107, + Down = 108, + PgDown = 109, + Insert = 110, + Delete = 111, + K0xC2 = 112, + Mute = 113, + VolDown = 114, + VolUp = 115, + Power = 116, + KpEqual = 117, + K0xC3 = 118, + Pause = 119, + K0xC4 = 120, + KpComma = 121, + Lang1 = 122, + Lang2 = 123, + Intl3 = 124, + LGui = 125, + RGui = 126, + Application = 127, + Stop = 128, + Again = 129, + K0xC5 = 130, + Undo = 131, + K0xC6 = 132, + Copy = 133, + K0xC7 = 134, + Paste = 135, + Find = 136, + Cut = 137, + Help = 138, + Menu = 139, + MediaCalc = 140, + K0xC8 = 141, + MediaSleep = 142, + Wakeup = 143, + K0xC9 = 144, + K0xCA = 145, + K0xCB = 146, + K0xCC = 147, + K0xCD = 148, + K0xCE = 149, + MediaWWW = 150, + K0xCF = 151, + MediaCoffee = 152, + K0xD0 = 153, + K0xD1 = 154, + K0xAE = 155, + K0xD2 = 156, + K0xD3 = 157, + MediaBack = 158, + MediaForward = 159, + MediaStop = 160, + MediaEjectCD = 161, + MediaFind = 162, + MediaNextSong = 163, + MediaPlayPause = 164, + MediaPreviousSong = 165, + MediaStopCD = 166, + K0xD4 = 167, + K0xD5 = 168, + K0xD6 = 169, + K0xD7 = 170, + K0xD8 = 171, + K0xAD = 172, + MediaRefresh = 173, + K0xD9 = 174, + K0xDA = 175, + MediaEdit = 176, + MediaScrollUp = 177, + MediaScrollDown = 178, + K0xDB = 179, + K0xDC = 180, + K0xDD = 181, + K0xDE = 182, + F13 = 183, + F14 = 184, + F15 = 185, + F16 = 186, + F17 = 187, + F18 = 188, + F19 = 189, + F20 = 190, + F21 = 191, + F22 = 192, + F23 = 193, + F24 = 194, + Execute = 195, + LockingCapsLock = 196, + LockingNumLock = 197, + LockingScrollLock = 198, + KpEqualSign = 199, + Intl2 = 200, + Intl4 = 201, + Intl5 = 202, + Intl6 = 203, + Intl7 = 204, + Intl8 = 205, + Intl9 = 206, + Select = 207, + Lang3 = 208, + Lang4 = 209, + PScreen = 210, + Lang5 = 211, + Lang6 = 212, + Lang7 = 213, + Lang8 = 214, + K0xAB = 215, + Lang9 = 216, + K0xDF = 217, + K0xBE = 218, + Clear = 219, + K220 = 220, + K0xAC = 221, + AltErase = 222, + Cancel = 223, + BrightnessDown = 224, + BrightnessUp = 225, + K0xAA = 226, + Prior = 227, + Return = 228, + KbdIllumDown = 229, + KbdIllumUp = 230, + Separator = 231, + Out = 232, + Oper = 233, + ClearAgain = 234, + CrSel = 235, + ExSel = 236, + K0xB4 = 237, + K0xB5 = 238, + K0xB6 = 239, + No = 240, + K0xB7 = 241, + K0xB8 = 242, + K0xB9 = 243, + K0xBA = 244, + K0xBB = 245, + K0xBC = 246, + K0xBD = 247, + MediaMute = 248, + K249 = 249, + PostFail = 250, + ErrorRollOver = 251, + K252 = 252, + K253 = 253, + K254 = 254, + K255 = 255, + K256 = 256, + K257 = 257, + K258 = 258, + K259 = 259, + K260 = 260, + K261 = 261, + K262 = 262, + K263 = 263, + K264 = 264, + K265 = 265, + K266 = 266, + K267 = 267, + K268 = 268, + K269 = 269, + K270 = 270, + K271 = 271, + K272 = 272, + K273 = 273, + K274 = 274, + K275 = 275, + K276 = 276, + K277 = 277, + K278 = 278, + K279 = 279, + K280 = 280, + K281 = 281, + K282 = 282, + K283 = 283, + K284 = 284, + K285 = 285, + K286 = 286, + K287 = 287, + K288 = 288, + K289 = 289, + K290 = 290, + K291 = 291, + K292 = 292, + K293 = 293, + K294 = 294, + K295 = 295, + K296 = 296, + K297 = 297, + K298 = 298, + K299 = 299, + K300 = 300, + K301 = 301, + K302 = 302, + K303 = 303, + K304 = 304, + K305 = 305, + K306 = 306, + K307 = 307, + K308 = 308, + K309 = 309, + K310 = 310, + K311 = 311, + K312 = 312, + K313 = 313, + K314 = 314, + K315 = 315, + K316 = 316, + K317 = 317, + K318 = 318, + K319 = 319, + K320 = 320, + K321 = 321, + K322 = 322, + K323 = 323, + K324 = 324, + K325 = 325, + K326 = 326, + K327 = 327, + K328 = 328, + K329 = 329, + K330 = 330, + K331 = 331, + K332 = 332, + K333 = 333, + K334 = 334, + K335 = 335, + K336 = 336, + K337 = 337, + K338 = 338, + K339 = 339, + K340 = 340, + K341 = 341, + K342 = 342, + K343 = 343, + K344 = 344, + K345 = 345, + K346 = 346, + K347 = 347, + K348 = 348, + K349 = 349, + K350 = 350, + K351 = 351, + K352 = 352, + K353 = 353, + K354 = 354, + K355 = 355, + K356 = 356, + K357 = 357, + K358 = 358, + K359 = 359, + K360 = 360, + K361 = 361, + K362 = 362, + K363 = 363, + K364 = 364, + K365 = 365, + K366 = 366, + K367 = 367, + K368 = 368, + K369 = 369, + K370 = 370, + K371 = 371, + K372 = 372, + K373 = 373, + K374 = 374, + K375 = 375, + K376 = 376, + K377 = 377, + K378 = 378, + K379 = 379, + K380 = 380, + K381 = 381, + K382 = 382, + K383 = 383, + K384 = 384, + K385 = 385, + K386 = 386, + K387 = 387, + K388 = 388, + K389 = 389, + K390 = 390, + K391 = 391, + K392 = 392, + K393 = 393, + K394 = 394, + K395 = 395, + K396 = 396, + K397 = 397, + K398 = 398, + K399 = 399, + K400 = 400, + K401 = 401, + K402 = 402, + K403 = 403, + K404 = 404, + K405 = 405, + K406 = 406, + K407 = 407, + K408 = 408, + K409 = 409, + K410 = 410, + K411 = 411, + K412 = 412, + K413 = 413, + K414 = 414, + K415 = 415, + K416 = 416, + K417 = 417, + K418 = 418, + K419 = 419, + K420 = 420, + K421 = 421, + K422 = 422, + K423 = 423, + K424 = 424, + K425 = 425, + K426 = 426, + K427 = 427, + K428 = 428, + K429 = 429, + K430 = 430, + K431 = 431, + K432 = 432, + K433 = 433, + K434 = 434, + K435 = 435, + K436 = 436, + K437 = 437, + K438 = 438, + K439 = 439, + K440 = 440, + K441 = 441, + K442 = 442, + K443 = 443, + K444 = 444, + K445 = 445, + K446 = 446, + K447 = 447, + K448 = 448, + K449 = 449, + K450 = 450, + K451 = 451, + K452 = 452, + K453 = 453, + K454 = 454, + K455 = 455, + K456 = 456, + K457 = 457, + K458 = 458, + K459 = 459, + K460 = 460, + K461 = 461, + K462 = 462, + K463 = 463, + K464 = 464, + K465 = 465, + K466 = 466, + K467 = 467, + K468 = 468, + K469 = 469, + K470 = 470, + K471 = 471, + K472 = 472, + K473 = 473, + K474 = 474, + K475 = 475, + K476 = 476, + K477 = 477, + K478 = 478, + K479 = 479, + K480 = 480, + K481 = 481, + K482 = 482, + K483 = 483, + K484 = 484, + K485 = 485, + K486 = 486, + K487 = 487, + K488 = 488, + K489 = 489, + K490 = 490, + K491 = 491, + K492 = 492, + K493 = 493, + K494 = 494, + K495 = 495, + K496 = 496, + K497 = 497, + K498 = 498, + K499 = 499, + K500 = 500, + K501 = 501, + K502 = 502, + K503 = 503, + K504 = 504, + K505 = 505, + K506 = 506, + K507 = 507, + K508 = 508, + K509 = 509, + K510 = 510, + K511 = 511, + K512 = 512, + K513 = 513, + K514 = 514, + K515 = 515, + K516 = 516, + K517 = 517, + K518 = 518, + K519 = 519, + K520 = 520, + K521 = 521, + K522 = 522, + K523 = 523, + K524 = 524, + K525 = 525, + K526 = 526, + K527 = 527, + K528 = 528, + K529 = 529, + K530 = 530, + K531 = 531, + K532 = 532, + K533 = 533, + K534 = 534, + K535 = 535, + K536 = 536, + K537 = 537, + K538 = 538, + K539 = 539, + K540 = 540, + K541 = 541, + K542 = 542, + K543 = 543, + K544 = 544, + K545 = 545, + K546 = 546, + K547 = 547, + K548 = 548, + K549 = 549, + K550 = 550, + K551 = 551, + K552 = 552, + K553 = 553, + K554 = 554, + K555 = 555, + K556 = 556, + K557 = 557, + K558 = 558, + K559 = 559, + K560 = 560, + K561 = 561, + K562 = 562, + K563 = 563, + K564 = 564, + K565 = 565, + K566 = 566, + K567 = 567, + K568 = 568, + K569 = 569, + K570 = 570, + K571 = 571, + K572 = 572, + K573 = 573, + K574 = 574, + K575 = 575, + K576 = 576, + K577 = 577, + K578 = 578, + K579 = 579, + K580 = 580, + K581 = 581, + K582 = 582, + K583 = 583, + K584 = 584, + K585 = 585, + K586 = 586, + K587 = 587, + K588 = 588, + K589 = 589, + K590 = 590, + K591 = 591, + K592 = 592, + K593 = 593, + K594 = 594, + K595 = 595, + K596 = 596, + K597 = 597, + K598 = 598, + K599 = 599, + K600 = 600, + K601 = 601, + K602 = 602, + K603 = 603, + K604 = 604, + K605 = 605, + K606 = 606, + K607 = 607, + K608 = 608, + K609 = 609, + K610 = 610, + K611 = 611, + K612 = 612, + K613 = 613, + K614 = 614, + K615 = 615, + K616 = 616, + K617 = 617, + K618 = 618, + K619 = 619, + K620 = 620, + K621 = 621, + K622 = 622, + K623 = 623, + K624 = 624, + K625 = 625, + K626 = 626, + K627 = 627, + K628 = 628, + K629 = 629, + K630 = 630, + K631 = 631, + K632 = 632, + K633 = 633, + K634 = 634, + K635 = 635, + K636 = 636, + K637 = 637, + K638 = 638, + K639 = 639, + K640 = 640, + K641 = 641, + K642 = 642, + K643 = 643, + K644 = 644, + K645 = 645, + K646 = 646, + K647 = 647, + K648 = 648, + K649 = 649, + K650 = 650, + K651 = 651, + K652 = 652, + K653 = 653, + K654 = 654, + K655 = 655, + K656 = 656, + K657 = 657, + K658 = 658, + K659 = 659, + K660 = 660, + K661 = 661, + K662 = 662, + K663 = 663, + K664 = 664, + K665 = 665, + K666 = 666, + K667 = 667, + K668 = 668, + K669 = 669, + K670 = 670, + K671 = 671, + K672 = 672, + K673 = 673, + K674 = 674, + K675 = 675, + K676 = 676, + K677 = 677, + K678 = 678, + K679 = 679, + K680 = 680, + K681 = 681, + K682 = 682, + K683 = 683, + K684 = 684, + K685 = 685, + K686 = 686, + K687 = 687, + K688 = 688, + K689 = 689, + K690 = 690, + K691 = 691, + K692 = 692, + K693 = 693, + K694 = 694, + K695 = 695, + K696 = 696, + K697 = 697, + K698 = 698, + K699 = 699, + K700 = 700, + K701 = 701, + K702 = 702, + K703 = 703, + K704 = 704, + K705 = 705, + K706 = 706, + K707 = 707, + K708 = 708, + K709 = 709, + K710 = 710, + K711 = 711, + K712 = 712, + K713 = 713, + K714 = 714, + K715 = 715, + K716 = 716, + K717 = 717, + K718 = 718, + K719 = 719, + K720 = 720, + K721 = 721, + K722 = 722, + K723 = 723, + K724 = 724, + K725 = 725, + K726 = 726, + K727 = 727, + K728 = 728, + K729 = 729, + K730 = 730, + K731 = 731, + K732 = 732, + K733 = 733, + K734 = 734, + K735 = 735, + K736 = 736, + K737 = 737, + K738 = 738, + K739 = 739, + K740 = 740, + K741 = 741, + K742 = 742, + K743 = 743, + K744 = 744, + MWU = 745, + MWD = 746, + MWL = 747, + MWR = 748, + K749 = 749, + K750 = 750, + K751 = 751, + K752 = 752, + K753 = 753, + K754 = 754, + K755 = 755, + K756 = 756, + K757 = 757, + K758 = 758, + K759 = 759, + K760 = 760, + K761 = 761, + K762 = 762, + K763 = 763, + K764 = 764, + K765 = 765, + K766 = 766, + KeyMax = 767, } impl KeyCode { - /// Returns `true` if the key code corresponds to a modifier (sent - /// separately on the USB HID report). - pub fn is_modifier(self) -> bool { - KeyCode::LCtrl <= self && self <= KeyCode::RGui - } - - /// Returns the byte with the bit corresponding to the USB HID - /// modifier bitfield set. - pub fn as_modifier_bit(self) -> u8 { - if self.is_modifier() { - 1 << (self as u8 - KeyCode::LCtrl as u8) - } else { - 0 - } - } -} - -/// A standard keyboard USB HID report. -/// -/// It can handle any modifier and 6 keys. -#[derive(Default, Debug, Clone, Eq, PartialEq)] -pub struct KbHidReport([u8; 8]); - -impl core::iter::FromIterator for KbHidReport { - fn from_iter(iter: T) -> Self - where - T: IntoIterator, - { - let mut res = Self::default(); - for kc in iter { - res.pressed(kc); - } - res + pub fn is_mod(self) -> bool { + use KeyCode::*; + matches!( + self, + LShift | RShift | LCtrl | RCtrl | LAlt | RAlt | LGui | RGui + ) } } -impl KbHidReport { - /// Returns the byte slice corresponding to the report. - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } - - /// Add the given key code to the report. If the report is full, - /// it will be set to `ErrorRollOver`. - pub fn pressed(&mut self, kc: KeyCode) { - use KeyCode::*; - match kc { - No => (), - ErrorRollOver | PostFail | ErrorUndefined => self.set_all(kc), - kc if kc.is_modifier() => self.0[0] |= kc.as_modifier_bit(), - _ => self.0[2..] - .iter_mut() - .find(|c| **c == 0) - .map(|c| *c = kc as u8) - .unwrap_or_else(|| self.set_all(ErrorRollOver)), - } - } - fn set_all(&mut self, kc: KeyCode) { - for c in &mut self.0[2..] { - *c = kc as u8; +use core::fmt; +impl fmt::Display for KeyCode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + KeyCode::Kb1 => write!(f, "1"), + KeyCode::Kb2 => write!(f, "2"), + KeyCode::Kb3 => write!(f, "3"), + KeyCode::Kb4 => write!(f, "4"), + KeyCode::Kb5 => write!(f, "5"), + KeyCode::Kb6 => write!(f, "6"), + KeyCode::Kb7 => write!(f, "7"), + KeyCode::Kb8 => write!(f, "8"), + KeyCode::Kb9 => write!(f, "9"), + KeyCode::Kb0 => write!(f, "0"), + KeyCode::LCtrl => write!(f, "‹⎈"), + KeyCode::RCtrl => write!(f, "⎈›"), + KeyCode::LShift => write!(f, "‹⇧"), + KeyCode::RShift => write!(f, "⇧›"), + KeyCode::LAlt => write!(f, "‹⎇"), + KeyCode::RAlt => write!(f, "⎇›"), + KeyCode::LGui => write!(f, "‹◆"), + KeyCode::RGui => write!(f, "◆›"), + KeyCode::Enter => write!(f, "⏎"), + KeyCode::Escape => write!(f, "⎋"), + KeyCode::BSpace => write!(f, "␈"), + KeyCode::Tab => write!(f, "⭾"), + KeyCode::Space => write!(f, "␠"), + KeyCode::Minus => write!(f, "−"), + KeyCode::Equal => write!(f, "="), + KeyCode::LBracket => write!(f, "["), + KeyCode::RBracket => write!(f, "]"), + KeyCode::Bslash => write!(f, "\\"), + KeyCode::NonUsHash => write!(f, "#"), + KeyCode::SColon => write!(f, ";"), + KeyCode::Quote => write!(f, "'"), + KeyCode::Grave => write!(f, "`"), + KeyCode::Comma => write!(f, ","), + KeyCode::Dot => write!(f, "."), + KeyCode::Slash => write!(f, "/"), + KeyCode::CapsLock => write!(f, "⇪"), + KeyCode::Insert => write!(f, "⎀"), + KeyCode::Delete => write!(f, "␡"), + KeyCode::Home => write!(f, "⇤"), + KeyCode::End => write!(f, "⇥"), + KeyCode::PgDown => write!(f, "⇟"), + KeyCode::PgUp => write!(f, "⇞"), + KeyCode::Down => write!(f, "▼"), + KeyCode::Up => write!(f, "▲"), + KeyCode::Right => write!(f, "▶"), + KeyCode::Left => write!(f, "◀"), + KeyCode::NumLock => write!(f, "⇭"), + KeyCode::KpSlash => write!(f, "🔢/"), + KeyCode::KpAsterisk => write!(f, "🔢*"), + KeyCode::KpMinus => write!(f, "🔢−"), + KeyCode::KpPlus => write!(f, "🔢+"), + KeyCode::KpEnter => write!(f, "🔢⏎"), + KeyCode::Kp0 => write!(f, "🔢0"), + KeyCode::Kp1 => write!(f, "🔢1"), + KeyCode::Kp2 => write!(f, "🔢2"), + KeyCode::Kp3 => write!(f, "🔢3"), + KeyCode::Kp4 => write!(f, "🔢4"), + KeyCode::Kp5 => write!(f, "🔢5"), + KeyCode::Kp6 => write!(f, "🔢6"), + KeyCode::Kp7 => write!(f, "🔢7"), + KeyCode::Kp8 => write!(f, "🔢8"), + KeyCode::Kp9 => write!(f, "🔢9"), + KeyCode::KpDot => write!(f, "🔢."), + KeyCode::KpEqual => write!(f, "🔢="), + KeyCode::NonUsBslash => write!(f, "|"), + KeyCode::Application => write!(f, "☰"), + KeyCode::Mute => write!(f, "🔇"), + KeyCode::VolUp => write!(f, "🔊"), + KeyCode::VolDown => write!(f, "🔉"), + _ => write!(f, "{self:?}"), } } } diff --git a/keyberon/src/layout.rs b/keyberon/src/layout.rs index 9ba8391ee..93bf0d6a0 100644 --- a/keyberon/src/layout.rs +++ b/keyberon/src/layout.rs @@ -22,14 +22,22 @@ /// to do when not using a macro. pub use kanata_keyberon_macros::*; -use crate::action::*; +mod contextual_execution; +use contextual_execution::*; + +use std::num::NonZeroU16; + +use crate::chord::*; use crate::key_code::KeyCode; +use crate::{action::*, multikey_buffer::MultiKeyBuffer}; use arraydeque::ArrayDeque; use heapless::Vec; use State::*; /// The coordinate type. +/// First item is either 0 or 1 denoting real key or virtual key, respectively. +/// Second item is the position in layout. pub type KCoord = (u8, u16); /// The Layers type. @@ -39,45 +47,154 @@ pub type KCoord = (u8, u16); /// corresponds to the key on the first layer, row 2, column 3. /// The generic parameters are in order: the number of columns, rows and layers, /// and the type contained in custom actions. -pub type Layers<'a, const C: usize, const R: usize, const L: usize, T = core::convert::Infallible> = - [[[Action<'a, T>; C]; R]; L]; +pub type Layers<'a, const C: usize, const R: usize, T = core::convert::Infallible> = + &'a [[[Action<'a, T>; C]; R]]; const QUEUE_SIZE: usize = 32; +pub type QueueLen = u8; + +#[test] +fn check_queue_size() { + use std::convert::TryFrom; + let _v = QueueLen::try_from(QUEUE_SIZE).unwrap(); +} /// The current event queue. /// /// Events can be retrieved by iterating over this struct and calling [Queued::event]. -type Queue = ArrayDeque<[Queued; QUEUE_SIZE], arraydeque::behavior::Wrapping>; +pub(crate) type Queue = ArrayDeque; /// A list of queued press events. Used for special handling of potentially multiple press events /// that occur during a Waiting event. -type PressedQueue = ArrayDeque<[KCoord; QUEUE_SIZE]>; +type PressedQueue = ArrayDeque; + +/// The maximum number of actions that can be activated concurrently via chord decomposition or +/// activation of multiple switch cases using fallthrough. +pub const ACTION_QUEUE_LEN: usize = 8; + +pub const CUSTOM_EVENT_RELEASE_QUEUE_LEN: usize = 16; /// The queue is currently only used for chord decomposition when a longer chord does not result in /// an action, but splitting it into smaller chords would. The buffer size of 8 should be more than /// enough for real world usage, but if one wanted to be extra safe, this should be ChordKeys::BITS /// since that should guarantee that all potentially queueable actions can fit. -type ActionQueue<'a, T> = ArrayDeque<[QueuedAction<'a, T>; 8], arraydeque::behavior::Wrapping>; -type QueuedAction<'a, T> = Option<(KCoord, &'a Action<'a, T>)>; +type ActionQueue<'a, T> = + ArrayDeque, ACTION_QUEUE_LEN, arraydeque::behavior::Wrapping>; +type CustomEventReleaseQueue<'a, T> = + ArrayDeque<&'a T, CUSTOM_EVENT_RELEASE_QUEUE_LEN, arraydeque::behavior::Wrapping>; +type Delay = u16; +pub(crate) type QueuedAction<'a, T> = Option<(KCoord, Delay, &'a Action<'a, T>, LayerStack)>; + +pub const REAL_KEY_ROW: u8 = 0; + +const HISTORICAL_EVENT_LEN: usize = 8; +const EXTRA_WAITING_LEN: usize = 8; +#[test] +fn extra_waiting_size_constraint() { + assert!(EXTRA_WAITING_LEN < i8::MAX as usize); +} /// The layout manager. It takes `Event`s and `tick`s as input, and /// generate keyboard reports. -pub struct Layout<'a, const C: usize, const R: usize, const L: usize, T = core::convert::Infallible> +pub struct Layout<'a, const C: usize, const R: usize, T = core::convert::Infallible> where T: 'a + std::fmt::Debug, { - pub layers: &'a [[[Action<'a, T>; C]; R]; L], + /// Fallback for transparent keys inside actions that are on `default_layer`. + pub src_keys: &'a [Action<'a, T>; C], + pub layers: &'a [[[Action<'a, T>; C]; R]], pub default_layer: usize, /// Key states. pub states: Vec, 64>, pub waiting: Option>, + pub extra_waiting: + ArrayDeque, EXTRA_WAITING_LEN, arraydeque::behavior::Wrapping>, pub tap_dance_eager: Option>, pub queue: Queue, pub oneshot: OneShotState, + pub keys_to_suppress_for_one_cycle: Vec, pub last_press_tracker: LastPressTracker, - pub active_sequences: ArrayDeque<[SequenceState<'a, T>; 4], arraydeque::behavior::Wrapping>, + pub active_sequences: ArrayDeque, 4, arraydeque::behavior::Wrapping>, pub action_queue: ActionQueue<'a, T>, - pub prev_action: Option<&'a Action<'a, T>>, + pub custom_event_release_queue: CustomEventReleaseQueue<'a, T>, + pub rpt_action: Option<&'a Action<'a, T>>, + pub historical_keys: History, + pub historical_inputs: History, + /// Historical inputs where tap-holds that resolve to a hold or timeout are deleted. + /// Used for prior-idle calculations. + /// Ideally whether timeout behaviour is excluded would be configurable such that + /// timeout activations are only excluded when the timeout would resolve + /// to the same action as hold. + /// For now avoid that complexity, such behaviour can be added later via a flag on the + /// TapHoldConfiguration without much concern if it is desired. + pub historical_inputs_sans_holds_or_timeouts: History, + pub quick_tap_hold_timeout: bool, + /// If a different key was pressed within this many ticks before a HoldTap key, + /// immediately resolve as tap (typing streak detection). 0 = disabled. + pub tap_hold_require_prior_idle: u16, + pub chords_v2: Option>, + /// History of device IDs that sent events, most-recent-first. + /// Used by `(device-history N recency)` switch conditions. + pub device_history: ArrayDeque, 8, arraydeque::behavior::Wrapping>, + rpt_multikey_key_buffer: MultiKeyBuffer<'a, T>, + trans_resolution_behavior_v2: bool, + delegate_to_first_layer: bool, + contextual_execution: ContextualExecution, + /// Tracks tap-hold activation events (hold/tap resolved). + /// Only stores data when the `tap_hold_tracker` feature is enabled; + /// otherwise this is a zero-sized no-op. + pub tap_hold_tracker: crate::tap_hold_tracker::TapHoldTracker, +} + +pub use crate::tap_hold_tracker::{HoldActivatedInfo, TapActivatedInfo}; + +#[derive(Debug)] +pub struct History { + events: ArrayDeque, + ticks_since_occurrences: ArrayDeque, +} + +#[derive(Copy, Clone, Debug)] +pub struct HistoricalEvent { + pub event: T, + pub ticks_since_occurrence: u16, +} + +impl History +where + T: Copy, +{ + fn new() -> Self { + Self { + ticks_since_occurrences: ArrayDeque::new(), + events: ArrayDeque::new(), + } + } + + fn tick_hist(&mut self) { + let ticks = self.ticks_since_occurrences.as_uninit_slice_mut(); + for tick_count in ticks { + unsafe { + *tick_count.assume_init_mut() = tick_count.assume_init().saturating_add(1); + } + } + } + + fn push_front(&mut self, event: T) { + self.ticks_since_occurrences.push_front(0); + self.events.push_front(event); + } + + pub fn iter_hevents(&self) -> impl Iterator> + '_ + Clone { + self.events + .iter() + .copied() + .zip(self.ticks_since_occurrences.iter().copied()) + .map(|(event, ticks_since_occurrence)| HistoricalEvent { + event, + ticks_since_occurrence, + }) + } } /// An event on the key matrix. @@ -149,7 +266,7 @@ pub enum CustomEvent<'a, T: 'a> { /// The given custom action key is released. Release(&'a T), } -impl<'a, T> CustomEvent<'a, T> { +impl CustomEvent<'_, T> { /// Update an event according to a new event. /// ///The event can only be modified in the order `NoEvent < Press < @@ -164,11 +281,28 @@ impl<'a, T> CustomEvent<'a, T> { } } +/// Metadata about normal key flags. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct NormalKeyFlags(pub u8); + +pub const NORMAL_KEY_FLAG_CLEAR_ON_NEXT_ACTION: u8 = 0x01; +pub const NORMAL_KEY_FLAG_CLEAR_ON_NEXT_RELEASE: u8 = 0x02; + +impl NormalKeyFlags { + pub fn nkf_clear_on_next_action(self) -> bool { + (self.0 & NORMAL_KEY_FLAG_CLEAR_ON_NEXT_ACTION) == NORMAL_KEY_FLAG_CLEAR_ON_NEXT_ACTION + } + pub fn nkf_clear_on_next_release(self) -> bool { + (self.0 & NORMAL_KEY_FLAG_CLEAR_ON_NEXT_RELEASE) == NORMAL_KEY_FLAG_CLEAR_ON_NEXT_RELEASE + } +} + #[derive(Debug, Eq, PartialEq)] pub enum State<'a, T: 'a> { NormalKey { keycode: KeyCode, coord: KCoord, + flags: NormalKeyFlags, }, LayerModifier { value: usize, @@ -188,36 +322,71 @@ pub enum State<'a, T: 'a> { SeqCustomPending(&'a T), SeqCustomActive(&'a T), Tombstone, + NoOpInput { + coord: KCoord, + }, } -impl<'a, T> Copy for State<'a, T> {} -impl<'a, T> Clone for State<'a, T> { +impl Copy for State<'_, T> {} +impl Clone for State<'_, T> { fn clone(&self) -> Self { *self } } impl<'a, T: 'a> State<'a, T> { - fn keycode(&self) -> Option { + pub fn keycode(&self) -> Option { match self { NormalKey { keycode, .. } => Some(*keycode), FakeKey { keycode } => Some(*keycode), _ => None, } } - fn tick(&self) -> Option { - Some(*self) + pub fn coord(&self) -> Option { + match self { + NormalKey { coord, .. } + | LayerModifier { coord, .. } + | Custom { coord, .. } + | NoOpInput { coord } + | RepeatingSequence { coord, .. } => Some(*coord), + _ => None, + } + } + fn keycode_in_coords(&self, coords: &OneShotCoords) -> Option { + match self { + NormalKey { keycode, coord, .. } => { + if coords.contains(coord) { + Some(*keycode) + } else { + None + } + } + _ => None, + } } /// Returns None if the key has been released and Some otherwise. - pub fn release(&self, c: KCoord, custom: &mut CustomEvent<'a, T>) -> Option { + pub fn release( + &self, + c: KCoord, + custom: &mut CustomEvent<'a, T>, + custom_release_queue: &mut CustomEventReleaseQueue<'a, T>, + ) -> Option { match *self { NormalKey { coord, .. } | LayerModifier { coord, .. } | RepeatingSequence { coord, .. } + | NoOpInput { coord } if coord == c => { None } Custom { value, coord } if coord == c => { - custom.update(CustomEvent::Release(value)); + match custom { + CustomEvent::NoEvent => custom.update(CustomEvent::Release(value)), + _ => { + if custom_release_queue.push_back(value).is_some() { + panic!("overflowed custom action release queue"); + } + } + } None } _ => Some(*self), @@ -225,7 +394,10 @@ impl<'a, T: 'a> State<'a, T> { } pub fn release_state(&self, s: ReleasableState) -> Option { match (*self, s) { - (NormalKey { keycode: k1, .. }, ReleasableState::KeyCode(k2)) => { + ( + NormalKey { keycode: k1, .. } | FakeKey { keycode: k1 }, + ReleasableState::KeyCode(k2), + ) => { if k1 == k2 { None } else { @@ -254,10 +426,19 @@ impl<'a, T: 'a> State<'a, T> { _ => None, } } + pub fn clear_on_next_release(&self) -> bool { + match self { + NormalKey { flags, .. } => { + (flags.0 & NORMAL_KEY_FLAG_CLEAR_ON_NEXT_RELEASE) + == NORMAL_KEY_FLAG_CLEAR_ON_NEXT_RELEASE + } + _ => false, + } + } } #[derive(Copy, Clone, Debug)] -struct TapDanceState<'a, T: 'a> { +pub(crate) struct TapDanceState<'a, T: 'a> { actions: &'a [&'a Action<'a, T>], timeout: u16, num_taps: u16, @@ -272,8 +453,8 @@ pub struct TapDanceEagerState<'a, T: 'a> { num_taps: u16, } -impl<'a, T> TapDanceEagerState<'a, T> { - fn tick(&mut self) { +impl TapDanceEagerState<'_, T> { + fn tick_tde(&mut self) { self.timeout = self.timeout.saturating_sub(1); } @@ -292,7 +473,7 @@ impl<'a, T> TapDanceEagerState<'a, T> { } #[derive(Debug)] -enum WaitingConfig<'a, T: 'a + std::fmt::Debug> { +pub(crate) enum WaitingConfig<'a, T: 'a + std::fmt::Debug> { HoldTap(HoldTapConfig<'a>), TapDance(TapDanceState<'a, T>), Chord(&'a ChordsGroup<'a, T>), @@ -302,12 +483,15 @@ enum WaitingConfig<'a, T: 'a + std::fmt::Debug> { pub struct WaitingState<'a, T: 'a + std::fmt::Debug> { coord: KCoord, timeout: u16, + on_press_reset_timeout_to: Option, delay: u16, ticks: u16, hold: &'a Action<'a, T>, tap: &'a Action<'a, T>, timeout_action: &'a Action<'a, T>, config: WaitingConfig<'a, T>, + layer_stack: LayerStack, + prev_queue_len: QueueLen, } /// Actions that can be triggered for a key configured for HoldTap. @@ -324,7 +508,7 @@ pub enum WaitingAction { } impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { - fn tick( + fn tick_wt( &mut self, queued: &mut Queue, action_queue: &mut ActionQueue<'a, T>, @@ -337,6 +521,7 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { WaitingConfig::TapDance(ref tds) => { let (ret, num_taps) = self.handle_tap_dance(tds.num_taps, tds.actions.len(), queued); + self.prev_queue_len = queued.len() as u8; // Due to ownership issues, handle_tap_dance can't contain all of the necessary // logic. if ret.is_some() { @@ -368,6 +553,19 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { } fn handle_hold_tap(&mut self, cfg: HoldTapConfig, queued: &Queue) -> Option { + if queued.len() as u8 == self.prev_queue_len && self.timeout > 0 { + // Fast path: nothing has changed since last tick and we haven't timed out yet. + return None; + } + if let Some(timeout_reset_val) = self.on_press_reset_timeout_to { + if let Some(last) = queued.iter().next_back() { + if last.event.is_press() { + self.timeout = timeout_reset_val.into(); + } + } + } + self.prev_queue_len = queued.len() as u8; + let mut skip_timeout = false; match cfg { HoldTapConfig::Default => (), HoldTapConfig::HoldOnOtherKeyPress => { @@ -375,6 +573,30 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { return Some(WaitingAction::Hold); } } + HoldTapConfig::Order { buffer, .. } => { + // Like PermissiveHold: if another key was pressed AND released + // (while modifier is still held), resolve as Hold. + // If modifier is released first, the fallthrough below handles Tap. + // + // Buffer: key presses that occurred within `buffer` ticks of the + // hold-tap key press are ignored by release-order logic, allowing + // fast typing to resolve as Tap regardless of release order. + let mut queued = queued.iter(); + while let Some(q) = queued.next() { + if q.event.is_press() { + // Elapsed ticks since this key entered the queue, compared against buffer window. + let press_tick = self.ticks.saturating_sub(q.since); + if press_tick < buffer { + continue; + } + let (i, j) = q.event.coord(); + let target = Event::Release(i, j); + if queued.clone().any(|q| q.event == target) { + return Some(WaitingAction::Hold); + } + } + } + } HoldTapConfig::PermissiveHold => { let mut queued = queued.iter(); while let Some(q) = queued.next() { @@ -388,21 +610,26 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { } } HoldTapConfig::Custom(func) => { - if let waiting_action @ Some(_) = (func)(QueuedIter(queued.iter())) { + let (waiting_action, local_skip) = (func)(QueuedIter(queued.iter()), self.coord); + if waiting_action.is_some() { return waiting_action; } + skip_timeout = local_skip; } } - if let Some(&Queued { since, .. }) = queued + if let Some(&Queued { + since: since_release, + .. + }) = queued .iter() .find(|s| self.is_corresponding_release(&s.event)) { - if self.timeout >= self.delay.saturating_sub(since) { + if self.timeout >= self.delay.saturating_sub(since_release) { Some(WaitingAction::Tap) } else { Some(WaitingAction::Timeout) } - } else if self.timeout == 0 { + } else if self.timeout == 0 && (!skip_timeout) { Some(WaitingAction::Timeout) } else { None @@ -415,6 +642,10 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { max_taps: usize, queued: &mut Queue, ) -> (Option, u16) { + if queued.len() as u8 == self.prev_queue_len && self.timeout > 0 { + // Fast path: nothing has changed since last tick and we haven't timed out yet. + return (None, num_taps); + } // Evict events with the same coordinates except for the final release. E.g. if 3 taps have // occurred, this will remove all `Press` events and 2 `Release` events. This is done so // that the state machine processes the entire tap dance sequence as a single press and @@ -434,7 +665,7 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { do_retain }); }; - if self.timeout == 0 || usize::from(num_taps) >= max_taps { + if self.timeout == 0 { evict_same_coord_events(num_taps, queued); return (Some(WaitingAction::Tap), num_taps); } @@ -449,6 +680,10 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { Ok(same_tap_count) } }) { + Ok(num_taps) if usize::from(num_taps) >= max_taps => { + evict_same_coord_events(num_taps, queued); + (Some(WaitingAction::Tap), num_taps) + } Ok(num_taps) => (None, num_taps), Err((num_taps, _)) => { evict_same_coord_events(num_taps, queued); @@ -463,8 +698,16 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { queued: &mut Queue, action_queue: &mut ActionQueue<'a, T>, ) -> Option<(WaitingAction, &'a Action<'a, T>, PressedQueue)> { + if queued.len() as u8 == self.prev_queue_len && self.timeout.saturating_sub(self.delay) > 0 + { + // Fast path: nothing has changed since last tick and we haven't timed out yet. + return None; + } + self.prev_queue_len = queued.len() as u8; + // need to keep track of how many Press events we handled so we can filter them out later let mut handled_press_events = 0; + let start_chord_coord = self.coord; let mut released_coord = None; // Compute the set of chord keys that are currently pressed @@ -529,6 +772,7 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { }; let mut pq = PressedQueue::new(); + let _ = pq.push_back(start_chord_coord); // Return all press events that were logically handled by this chording event queued.retain(|s| { @@ -551,15 +795,15 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { fn decompose_chord_into_action_queue( &mut self, config: &'a ChordsGroup<'a, T>, - queued: &mut Queue, + queued: &Queue, action_queue: &mut ActionQueue<'a, T>, ) { - let mut chord_key_order = [0u32; ChordKeys::BITS as usize]; + let mut chord_key_order = [0u128; ChordKeys::BITS as usize]; // Default to the initial coordinate. But if a key is released early (before the timeout // occurs), use that key for action releases. That way the chord is released as early as // possible. - let mut action_queue_coord = self.coord; + let mut default_associated_coord = self.coord; let starting_mask = config.get_keys(self.coord).unwrap_or(0); let mut mask_bits_set = 1; @@ -569,7 +813,7 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { Ok(active) } else if let Some(chord_keys) = config.get_keys(s.event.coord()) { match s.event { - Event::Press(_, _) => { + Event::Press(..) => { if active | chord_keys != active { chord_key_order[mask_bits_set] = chord_keys; mask_bits_set += 1; @@ -577,7 +821,7 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { Ok(active | chord_keys) } Event::Release(i, j) => { - action_queue_coord = (i, j); + default_associated_coord = (i, j); Err(active) // released a chord key, abort } } @@ -590,6 +834,32 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { let len = mask_bits_set; let chord_keys = &chord_key_order[0..len]; + let get_coord_for_chord = |mask: ChordKeys| -> (u8, u16) { + if config.get_keys(default_associated_coord).unwrap_or(0) & mask > 0 { + // This might be a release. + // If it belongs to the associated action, prefer to use it. + return default_associated_coord; + } + if self.coord != default_associated_coord + && config.get_keys(self.coord).unwrap_or(0) & mask > 0 + { + // The first coordinate not in queued + // so must be explicitly checked if it is not the default coord. + return self.coord; + } + queued + .iter() + .find_map(|q| { + let coord = q.event.coord(); + let qmask = config.get_keys(coord).unwrap_or(0); + match qmask & mask { + 0 => None, + _ => Some(coord), + } + }) + .unwrap_or(default_associated_coord) + }; + // Compute actions using the following description: // // Let's say we have a chord group with keys (h j k l). The full set (h j k l) is not @@ -620,6 +890,7 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { let mut start = 0; let mut end = len; + let delay = self.delay + self.ticks; while start < len { let sub_chord = &chord_keys[start..end]; let chord_mask = sub_chord @@ -628,7 +899,13 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { .reduce(|acc, e| acc | e) .unwrap_or(0); if let Some(action) = config.get_chord(chord_mask) { - let _ = action_queue.push_back(Some((action_queue_coord, action))); + let coord = get_coord_for_chord(chord_mask); + // Note on LayerStack being default (empty): + // A chordv1 allows transparency, so this is broken right now, + // and could result in an infinite loop, because + // the queue activating code falls back to top-level resolution order + // if the stored layer stack is empty. + let _ = action_queue.push_back(Some((coord, delay, action, Default::default()))); } else { end -= 1; // shrink from end until something is found, or have checked up to and including @@ -641,7 +918,13 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { .reduce(|acc, e| acc | e) .unwrap_or(0); if let Some(action) = config.get_chord(chord_mask) { - let _ = action_queue.push_back(Some((action_queue_coord, action))); + let coord = get_coord_for_chord(chord_mask); + let _ = action_queue.push_back(Some(( + coord, + delay, + action, + Default::default(), + ))); break; } end -= 1; @@ -661,6 +944,8 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { } } +type OneShotCoords = ArrayDeque; + #[derive(Debug, Copy, Clone)] pub struct SequenceState<'a, T: 'a> { cur_event: Option>, @@ -669,23 +954,80 @@ pub struct SequenceState<'a, T: 'a> { remaining_events: &'a [SequenceEvent<'a, T>], } -type OneShotKeys = [KCoord; ONE_SHOT_MAX_ACTIVE]; type ReleasedOneShotKeys = Vec; +// Using a u16 for indices instead of usize. +// Need to check against this value in code that creates layers. +pub const MAX_LAYERS: usize = 60000; + +// Use heapless Vec for perf - avoid pointer indirections. +// Use u16 for more efficient cache. 12*u16 = 3*u64 = 24 bytes. +// Then there is a usize for the length, totaling 32 bytes. +// Cache line is typically 64 bytes, so this takes half a cache line. +// Above all assumes x86-64. +pub const MAX_ACTIVE_LAYERS: usize = 12; + +/// Because we only need a read-only stack and efficient iteration over contained +/// items, LayerStack items are in reverse order over usual back-to-front order +/// of items in array-based stack implementations. +type LayerStack = Vec; + /// Contains the state of one shot keys that are currently active. pub struct OneShotState { /// KCoordinates of one shot keys that are active - pub keys: ArrayDeque, + pub keys: ArrayDeque, /// KCoordinates of one shot keys that have been released - pub released_keys: ArrayDeque, + pub released_keys: ArrayDeque, + /// Fix #1874: + /// Represents the one-shot state that must not be released on physical key release. + /// Consider the case `(multi a (one-shot 100 b))`, + /// when only tracking coordinates (which this used to in the past), + /// the `a` would not release a even upon releasing the action key + /// because its state falls on the same coordinate as the oneshot b. + /// The fix is to explicitly know which key/layer states + /// — which are the only actions allowed within one-shot — + /// should be kept, and normally release others. + pub state_to_retain_on_release: + ArrayDeque, /// Used to keep track of already-pressed keys for the release variants. - pub other_pressed_keys: ArrayDeque, + pub other_pressed_keys: ArrayDeque, /// Timeout (ms) after which all one shot keys expire pub timeout: u16, /// Contains the end config of the most recently pressed one shot key pub end_config: OneShotEndConfig, /// Marks if release of the one shot keys should be done on the next tick pub release_on_next_tick: bool, + /// The number of ticks to delay the release of the one-shot activation + /// for EndOnFirstPress(OrRepress). + /// This used to not exist and effectively be 1 (1ms), + /// but that is too short for some environments. + /// When too short, applications or desktop environments process + /// the key release before the next press, + /// even if temporally the release was sent after. + pub pause_input_processing_delay: u16, + /// If pause_input_processing_delay is used, this will be >0, + /// meaning input processing should be paused to prevent extra presses + /// from coming in while OneShot has not yet been released. + /// + /// May also be reused for other purposes... + pub pause_input_processing_ticks: u16, + + /// Number of ticks to ignore press events for. + pub ticks_to_ignore_events: u16, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum OneShotRetainableState { + KeyCode { coord: KCoord, kc: KeyCode }, + Layer { coord: KCoord, layer: u16 }, +} + +impl OneShotRetainableState { + pub fn coord(&self) -> KCoord { + match self { + Self::KeyCode { coord, .. } | Self::Layer { coord, .. } => *coord, + } + } } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] @@ -695,25 +1037,35 @@ enum OneShotHandlePressKey { } impl OneShotState { - fn tick(&mut self) -> Option { + fn tick_osh(&mut self) -> Option { if self.keys.is_empty() { return None; } + self.ticks_to_ignore_events = self.ticks_to_ignore_events.saturating_sub(1); self.timeout = self.timeout.saturating_sub(1); if self.release_on_next_tick || self.timeout == 0 { self.release_on_next_tick = false; self.timeout = 0; + self.pause_input_processing_ticks = 0; + self.ticks_to_ignore_events = 0; self.keys.clear(); self.other_pressed_keys.clear(); + self.state_to_retain_on_release.clear(); Some(self.released_keys.drain(..).collect()) } else { None } } - fn handle_press(&mut self, key: OneShotHandlePressKey) { - if self.keys.is_empty() { - return; + /// Returns the coordinates associated with an active oneshot. + /// The intended use of this is for `rpt-any` to be able to repeat + /// the output chord of a one-shot+normal key, + /// which are two separate actions that users + /// would likely want to combine under a repeat condition. + fn handle_press(&mut self, key: OneShotHandlePressKey) -> OneShotCoords { + let mut oneshot_coords = ArrayDeque::new(); + if self.keys.is_empty() || self.ticks_to_ignore_events > 0 { + return oneshot_coords; } match key { OneShotHandlePressKey::OneShotKey(pressed_coord) => { @@ -724,7 +1076,9 @@ impl OneShotState { ) && self.keys.contains(&pressed_coord) { self.release_on_next_tick = true; + oneshot_coords.extend(self.keys.iter().copied()); } + self.released_keys.retain(|coord| *coord != pressed_coord); } OneShotHandlePressKey::Other(pressed_coord) => { @@ -732,12 +1086,15 @@ impl OneShotState { self.end_config, OneShotEndConfig::EndOnFirstPress | OneShotEndConfig::EndOnFirstPressOrRepress ) { - self.release_on_next_tick = true; + self.timeout = core::cmp::min(self.pause_input_processing_delay, self.timeout); + self.pause_input_processing_ticks = self.pause_input_processing_delay; } else { let _ = self.other_pressed_keys.push_back(pressed_coord); } + oneshot_coords.extend(self.keys.iter().copied()); } - } + }; + oneshot_coords } /// Returns true if the caller should handle the release normally and false otherwise. @@ -761,6 +1118,12 @@ impl OneShotState { (false, self.released_keys.push_back((i, j))) } } + + fn add_state_to_retain(&mut self, state: OneShotRetainableState) { + if !self.state_to_retain_on_release.contains(&state) { + self.state_to_retain_on_release.push_back(state); + } + } } /// An iterator over the currently queued events. @@ -782,8 +1145,8 @@ impl<'a> Iterator for QueuedIter<'a> { /// An event, waiting in a queue to be processed. #[derive(Debug, Copy, Clone)] pub struct Queued { - event: Event, - since: u16, + pub(crate) event: Event, + pub(crate) since: u16, } impl From for Queued { fn from(event: Event) -> Self { @@ -791,7 +1154,21 @@ impl From for Queued { } } impl Queued { - fn tick(&mut self) { + pub(crate) fn new_press(i: u8, j: u16) -> Self { + Self { + since: 0, + event: Event::Press(i, j), + } + } + + pub(crate) fn new_release(i: u8, j: u16) -> Self { + Self { + since: 0, + event: Event::Release(i, j), + } + } + + pub(crate) fn tick_qd(&mut self) { self.since = self.since.saturating_add(1); } @@ -808,21 +1185,28 @@ pub struct LastPressTracker { } impl LastPressTracker { - fn tick(&mut self) { + fn tick_lpt(&mut self) { self.tap_hold_timeout = self.tap_hold_timeout.saturating_sub(1); } + fn update_coord(&mut self, coord: KCoord) { + if coord.0 == REAL_KEY_ROW { + // Only update if it's a real key press. + self.coord = coord; + } + } } -impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt::Debug> - Layout<'a, C, R, L, T> -{ +impl<'a, const C: usize, const R: usize, T: 'a + Copy + std::fmt::Debug> Layout<'a, C, R, T> { /// Creates a new `Layout` object. - pub fn new(layers: &'a [[[Action; C]; R]; L]) -> Self { + fn new(layers: &'a [[[Action; C]; R]]) -> Self { + assert!(layers.len() < MAX_LAYERS); Self { + src_keys: &[Action::NoOp; C], layers, default_layer: 0, states: Vec::new(), waiting: None, + extra_waiting: ArrayDeque::new(), tap_dance_eager: None, queue: ArrayDeque::new(), oneshot: OneShotState { @@ -830,82 +1214,243 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt end_config: OneShotEndConfig::EndOnFirstPress, keys: ArrayDeque::new(), released_keys: ArrayDeque::new(), + state_to_retain_on_release: ArrayDeque::new(), other_pressed_keys: ArrayDeque::new(), release_on_next_tick: false, + pause_input_processing_delay: 0, + pause_input_processing_ticks: 0, + ticks_to_ignore_events: 0, }, + keys_to_suppress_for_one_cycle: Vec::new(), last_press_tracker: Default::default(), active_sequences: ArrayDeque::new(), action_queue: ArrayDeque::new(), - prev_action: None, + custom_event_release_queue: ArrayDeque::new(), + rpt_action: None, + historical_keys: History::new(), + historical_inputs: History::new(), + historical_inputs_sans_holds_or_timeouts: History::new(), + rpt_multikey_key_buffer: unsafe { MultiKeyBuffer::new() }, + quick_tap_hold_timeout: false, + tap_hold_require_prior_idle: 0, + trans_resolution_behavior_v2: true, + delegate_to_first_layer: false, + chords_v2: None, + device_history: ArrayDeque::new(), + contextual_execution: ContextualExecution::new(), + tap_hold_tracker: Default::default(), } } + pub fn new_with_trans_action_settings( + src_keys: &'a [Action; C], + layers: &'a [[[Action; C]; R]], + trans_resolution_behavior_v2: bool, + delegate_to_first_layer: bool, + ) -> Self { + let mut new = Self::new(layers); + new.src_keys = src_keys; + new.trans_resolution_behavior_v2 = trans_resolution_behavior_v2; + new.delegate_to_first_layer = delegate_to_first_layer; + new + } + /// Iterates on the key codes of the current state. - pub fn keycodes(&self) -> impl Iterator + '_ { - self.states.iter().filter_map(State::keycode) + pub fn keycodes(&self) -> impl Iterator + Clone + '_ { + let keys_to_suppress_for_one_cycle = self.keys_to_suppress_for_one_cycle.clone(); + self.states + .iter() + .filter_map(State::keycode) + .filter(move |kc| !keys_to_suppress_for_one_cycle.contains(kc)) + } + + fn exclude_hold_or_timeout_from_history(coord: KCoord, history: &mut History) { + if let Some((i, _)) = history + .events + .iter() + .copied() + .enumerate() + .find(|(_i, ev)| *ev == coord) + { + history.ticks_since_occurrences.remove(i); + history.events.remove(i); + } } - fn waiting_into_hold(&mut self) -> CustomEvent<'a, T> { - if let Some(w) = &self.waiting { + + fn waiting_into_hold(&mut self, idx: i8) -> CustomEvent<'a, T> { + let waiting = if idx < 0 { + self.waiting.as_ref() + } else { + self.extra_waiting.get(idx as usize) + }; + if let Some(w) = waiting { let hold = w.hold; let coord = w.coord; + + Self::exclude_hold_or_timeout_from_history( + coord, + &mut self.historical_inputs_sans_holds_or_timeouts, + ); + let delay = match w.config { WaitingConfig::HoldTap(..) | WaitingConfig::Chord(_) => w.delay + w.ticks, WaitingConfig::TapDance(_) => 0, }; - self.waiting = None; + let layer_stack = w.layer_stack.clone(); + self.tap_hold_tracker.set_hold_activated(coord, &w.config); + if idx < 0 { + self.waiting = None; + } else { + self.extra_waiting.remove(idx as usize); + } if coord == self.last_press_tracker.coord { self.last_press_tracker.tap_hold_timeout = 0; } - self.do_action(hold, coord, delay, false) + // Similar issue happens for the quick tap-hold tap as with on-press release; + // the rapidity of the release can cause issues. See pause_input_processing_delay + // comments for more detail. + self.oneshot.pause_input_processing_ticks = self.oneshot.pause_input_processing_delay; + let mut custom_activation_count = 0; + self.do_action( + hold, + coord, + delay, + false, + &mut layer_stack.into_iter(), + &mut custom_activation_count, + ) } else { CustomEvent::NoEvent } } - fn waiting_into_tap(&mut self, pq: Option) -> CustomEvent<'a, T> { - if let Some(w) = &self.waiting { + fn waiting_into_tap(&mut self, pq: Option, idx: i8) -> CustomEvent<'a, T> { + let waiting = if idx < 0 { + self.waiting.as_ref() + } else { + self.extra_waiting.get(idx as usize) + }; + if let Some(w) = waiting { let tap = w.tap; let coord = w.coord; let delay = match w.config { WaitingConfig::HoldTap(..) | WaitingConfig::Chord(_) => w.delay + w.ticks, WaitingConfig::TapDance(_) => 0, }; - self.waiting = None; - let ret = self.do_action(tap, coord, delay, false); + let layer_stack = w.layer_stack.clone(); + self.tap_hold_tracker.set_tap_activated(coord, &w.config); + if idx < 0 { + self.waiting = None; + } else { + self.extra_waiting.remove(idx as usize); + } + let mut custom_activation_count = 0; + let ret = self.do_action( + tap, + coord, + delay, + false, + &mut layer_stack.clone().into_iter(), + &mut custom_activation_count, + ); + if let Some(pq) = pq { - if matches!( - tap, + let mut custom_activation_count = 0; + self.contextual_execution.pause_historical_keys_updates = true; + match tap { Action::KeyCode(_) - | Action::MultipleKeyCodes(_) - | Action::OneShot(_) - | Action::Layer(_) - ) { - // The current intent of this block is to ensure that simple actions like - // key presses or layer-while-held remain pressed as long as a single key from - // the input chord remains held. The behaviour of these actions is correct in - // the case of repeating do_action, so there is currently no harm in doing - // this. Other action types are more problematic though. - for other_coord in pq.iter().copied() { - self.do_action(tap, other_coord, delay, false); + | Action::MultipleKeyCodes(_) + | Action::OneShot(_) + | Action::Layer(_) => { + // The current intent of this block is to ensure that simple actions like + // key presses or layer-while-held remain pressed as long as a single key from + // the input chord remains held. The behaviour of these actions is correct in + // the case of repeating do_action, so there is currently no harm in doing + // this. Other action types are more problematic though. + for other_coord in pq.iter().copied() { + self.do_action( + tap, + other_coord, + delay, + false, + &mut layer_stack.clone().into_iter(), + &mut custom_activation_count, + ); + } + } + Action::MultipleActions(acs) => { + // Like above block, but for the same simple actions within MultipleActions + for ac in acs.iter() { + if matches!( + ac, + Action::KeyCode(_) + | Action::MultipleKeyCodes(_) + | Action::OneShot(_) + | Action::Layer(_) + ) { + for other_coord in pq.iter().copied() { + self.do_action( + ac, + other_coord, + delay, + false, + &mut layer_stack.clone().into_iter(), + &mut custom_activation_count, + ); + } + } + } } + _ => {} } + self.contextual_execution.pause_historical_keys_updates = false; } + + // Similar issue happens for the quick tap-hold tap as with on-press release; + // the rapidity of the release can cause issues. See pause_input_processing_delay + // comments for more detail. + self.oneshot.pause_input_processing_ticks = self.oneshot.pause_input_processing_delay; ret } else { CustomEvent::NoEvent } } - fn waiting_into_timeout(&mut self) -> CustomEvent<'a, T> { - if let Some(w) = &self.waiting { + fn waiting_into_timeout(&mut self, idx: i8) -> CustomEvent<'a, T> { + let waiting = if idx < 0 { + self.waiting.as_ref() + } else { + self.extra_waiting.get(idx as usize) + }; + if let Some(w) = waiting { let timeout_action = w.timeout_action; let coord = w.coord; + + Self::exclude_hold_or_timeout_from_history( + coord, + &mut self.historical_inputs_sans_holds_or_timeouts, + ); + let delay = match w.config { WaitingConfig::HoldTap(..) | WaitingConfig::Chord(_) => w.delay + w.ticks, WaitingConfig::TapDance(_) => 0, }; - self.waiting = None; + let layer_stack = w.layer_stack.clone(); + self.tap_hold_tracker.set_hold_activated(coord, &w.config); + if idx < 0 { + self.waiting = None; + } else { + self.extra_waiting.remove(idx as usize); + } if coord == self.last_press_tracker.coord { self.last_press_tracker.tap_hold_timeout = 0; } - self.do_action(timeout_action, coord, delay, false) + let mut custom_activation_count = 0; + self.do_action( + timeout_action, + coord, + delay, + false, + &mut layer_stack.into_iter(), + &mut custom_activation_count, + ) } else { CustomEvent::NoEvent } @@ -921,24 +1466,55 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt /// Returns the corresponding `CustomEvent`, allowing to manage /// custom actions thanks to the `Action::Custom` variant. pub fn tick(&mut self) -> CustomEvent<'a, T> { - if let Some(Some((coord, action))) = self.action_queue.pop_front() { + self.tick_layout() + } + + fn tick_layout(&mut self) -> CustomEvent<'a, T> { + // Eagerly process released queued custom actions + // then return; other items will be processed later! + if let Some(released_custom_event) = self.custom_event_release_queue.pop_front() { + return CustomEvent::Release(released_custom_event); + } + + let active_layer = self.current_layer() as u16; + if let Some(chv2) = self.chords_v2.as_mut() { + self.queue.extend(chv2.tick_chv2(active_layer).drain(0..)); + if let chord_action @ Some(_) = chv2.get_action_chv2() { + self.action_queue.push_back(chord_action); + self.oneshot.pause_input_processing_ticks = + self.oneshot.pause_input_processing_delay; + } + } + self.keys_to_suppress_for_one_cycle.clear(); + if let Some(Some((coord, delay, action, layer_stack))) = self.action_queue.pop_front() { // If there's anything in the action queue, don't process anything else yet - execute // everything. Otherwise an action may never be released. - return self.do_action(action, coord, 0, false); + let mut custom_activation_count = 0; + return self.do_action( + action, + coord, + delay, + false, + &mut layer_stack.into_iter(), + &mut custom_activation_count, + ); } - self.states = self.states.iter().filter_map(State::tick).collect(); - self.queue.iter_mut().for_each(Queued::tick); - self.last_press_tracker.tick(); + self.queue.iter_mut().for_each(Queued::tick_qd); + self.last_press_tracker.tick_lpt(); if let Some(ref mut tde) = self.tap_dance_eager { - tde.tick(); + tde.tick_tde(); if tde.is_expired() { self.tap_dance_eager = None; } } self.process_sequences(); + self.historical_keys.tick_hist(); + self.historical_inputs.tick_hist(); + self.historical_inputs_sans_holds_or_timeouts.tick_hist(); + let mut custom = CustomEvent::NoEvent; - if let Some(released_keys) = self.oneshot.tick() { + if let Some(released_keys) = self.oneshot.tick_osh() { for key in released_keys.iter() { custom.update(self.dequeue(Queued { event: Event::Release(key.0, key.1), @@ -948,18 +1524,37 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt } custom.update(match &mut self.waiting { - Some(w) => match w.tick(&mut self.queue, &mut self.action_queue) { - Some((WaitingAction::Hold, _)) => self.waiting_into_hold(), - Some((WaitingAction::Tap, pq)) => self.waiting_into_tap(pq), - Some((WaitingAction::Timeout, _)) => self.waiting_into_timeout(), + Some(w) => match w.tick_wt(&mut self.queue, &mut self.action_queue) { + Some((WaitingAction::Hold, _)) => self.waiting_into_hold(-1), + Some((WaitingAction::Tap, pq)) => self.waiting_into_tap(pq, -1), + Some((WaitingAction::Timeout, _)) => self.waiting_into_timeout(-1), Some((WaitingAction::NoOp, _)) => self.drop_waiting(), None => CustomEvent::NoEvent, }, - None => match self.queue.pop_front() { - Some(s) => self.dequeue(s), - None => CustomEvent::NoEvent, - }, + None => { + if self.extra_waiting.is_empty() { + // Due to the possible delay in the key release for EndOnFirstPress + // because some apps/DEs do not handle it properly if done too quickly, + // undesirable behaviour of extra presses making it in before + // the release happens might occur. + // + // A mitigation against that is to pause input processing. + if self.oneshot.pause_input_processing_ticks > 0 { + self.oneshot.pause_input_processing_ticks = + self.oneshot.pause_input_processing_ticks.saturating_sub(1); + CustomEvent::NoEvent + } else { + match self.queue.pop_front() { + Some(s) => self.dequeue(s), + None => CustomEvent::NoEvent, + } + } + } else { + CustomEvent::NoEvent + } + } }); + let custom = self.process_extra_waitings(custom); self.process_sequence_custom(custom) } /// Takes care of draining and populating the `active_sequences` ArrayDeque, @@ -978,26 +1573,20 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt seq.tapped = None; } else { // Pull the next SequenceEvent - match seq.remaining_events { - [e, tail @ ..] => { - seq.cur_event = Some(*e); - seq.remaining_events = tail; - } - [] => (), + if let [e, tail @ ..] = seq.remaining_events { + seq.cur_event = Some(*e); + seq.remaining_events = tail; } // Process it (SequenceEvent) match seq.cur_event { Some(SequenceEvent::Complete) => { - for fake_key in self.states.clone().iter() { - if let FakeKey { keycode } = *fake_key { - self.states.retain(|s| s.seq_release(keycode).is_some()); - } - } seq.remaining_events = &[]; } Some(SequenceEvent::Press(keycode)) => { // Start tracking this fake key Press() event let _ = self.states.push(FakeKey { keycode }); + self.contextual_execution + .push_historical_key(&mut self.historical_keys, keycode); // Fine to fake (0, 0). This is sequences anyway. In Kanata, nothing // valid should be at (0, 0) that this would interfere with. self.oneshot @@ -1006,6 +1595,8 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt Some(SequenceEvent::Tap(keycode)) => { // Same as Press() except we track it for one tick via seq.tapped: let _ = self.states.push(FakeKey { keycode }); + self.contextual_execution + .push_historical_key(&mut self.historical_keys, keycode); self.oneshot .handle_press(OneShotHandlePressKey::Other((0, 0))); seq.tapped = Some(keycode); @@ -1015,12 +1606,10 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.oneshot.handle_release((0, 0)); self.states.retain(|s| s.seq_release(keycode).is_some()); } - Some(SequenceEvent::Delay { duration }) => { + Some(SequenceEvent::Delay { duration }) if duration > 0 => { // Setup a delay that will be decremented once per tick until 0 - if duration > 0 { - // -1 to start since this tick counts - seq.delay = duration - 1; - } + // -1 to start since this tick counts + seq.delay = duration - 1; } Some(SequenceEvent::Custom(custom)) => { let _ = self.states.push(State::SeqCustomPending(custom)); @@ -1051,6 +1640,38 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt } } } + + fn process_extra_waitings(&mut self, current_custom: CustomEvent<'a, T>) -> CustomEvent<'a, T> { + if !matches!(current_custom, CustomEvent::NoEvent) { + return current_custom; + } + let mut waiting_action = (0, None); + for (i, w) in self.extra_waiting.iter_mut().enumerate() { + match w.tick_wt(&mut self.queue, &mut self.action_queue) { + None => {} + wa => { + waiting_action = (i as isize, wa); + // break - only complete one at a time even if potentially multiple have + // completed, so that only one custom event is returned. + // + // Theoretically if we could call the waiting_into_* functions, we could do that + // here and break only if custom is None, but that runs into mutability + // problems. I don't expect any perceptible degradation between from not doing + // the above. + break; + } + } + } + let i = waiting_action.0; + match waiting_action.1 { + Some((WaitingAction::Hold, _)) => self.waiting_into_hold(i as i8), + Some((WaitingAction::Tap, pq)) => self.waiting_into_tap(pq, i as i8), + Some((WaitingAction::Timeout, _)) => self.waiting_into_timeout(i as i8), + Some((WaitingAction::NoOp, _)) => self.drop_waiting(), + None => current_custom, + } + } + fn process_sequence_custom( &mut self, mut current_custom: CustomEvent<'a, T>, @@ -1058,6 +1679,11 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt if self.states.is_empty() || !matches!(current_custom, CustomEvent::NoEvent) { return current_custom; } + // It is important to note that this code cannot simply be replaced by `retain_mut`. + // The `retain_mut` function is not chosen + // because it is important to break on the first `SeqCustom` that is discovered. + // Such functionality could be replaced by a marker to ignore processing, + // but for now that is not necessary. self.states.retain(|s| !matches!(s, State::Tombstone)); for state in self.states.iter_mut() { match state { @@ -1083,72 +1709,164 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt let mut custom = CustomEvent::NoEvent; let (do_release, overflow_key) = self.oneshot.handle_release((i, j)); if do_release { - self.states - .retain(|s| s.release((i, j), &mut custom).is_some()); + self.states.retain(|s| { + !s.clear_on_next_release() + && s.release((i, j), &mut custom, &mut self.custom_event_release_queue) + .is_some() + }); + } else { + // Fix #1874: + // Might still need to apply release, + // but need to check against states on same coordinate + // that aren't part of a OneShot. + self.states.retain(|s| { + match s { + NormalKey { coord, keycode, .. } => { + // NormalKey is a valid oneshot state, + // may need to keep. + *coord != (i, j) + || self.oneshot.state_to_retain_on_release.contains( + &OneShotRetainableState::KeyCode { + coord: *coord, + kc: *keycode, + }, + ) + } + LayerModifier { coord, value } => { + // LayerModifier is a valid oneshot state, + // may need to keep. + *coord != (i, j) + || self.oneshot.state_to_retain_on_release.contains( + &OneShotRetainableState::Layer { + coord: *coord, + layer: *value as u16, + }, + ) + } + // Everything else is not a valid oneshot state, + // if it falls on the same coordinate + // as a oneshot key, it should still be released here. + _ => { + !s.clear_on_next_release() + && s.release( + (i, j), + &mut custom, + &mut self.custom_event_release_queue, + ) + .is_some() + } + } + }); } if let Some((i2, j2)) = overflow_key { - self.states - .retain(|s| s.release((i2, j2), &mut custom).is_some()); + self.states.retain(|s| { + s.release((i2, j2), &mut custom, &mut self.custom_event_release_queue) + .is_some() + }); } custom } Press(i, j) => { - if let Some(tde) = self.tap_dance_eager { + let mut layer_stack = self.trans_resolution_layer_order().into_iter(); + let mut custom_activation_count = 0; + if let Some(tde) = &mut self.tap_dance_eager { if (i, j) == self.last_press_tracker.coord && !tde.is_expired() { + let tde_action = tde.actions[usize::from(tde.num_taps)]; + tde.incr_taps(); let custom = self.do_action( - tde.actions[usize::from(tde.num_taps)], + tde_action, (i, j), queue.since, false, + &mut layer_stack.skip(1), + &mut custom_activation_count, ); - // unwrap is here because tde cannot be ref mut - self.tap_dance_eager.as_mut().expect("some").incr_taps(); custom - - // i == 0 means real key, i == 1 means fake key. Let fake keys do whatever, but - // interrupt tap-dance-eager if real key. - } else if i == 0 { - // unwrap is here because tde cannot be ref mut - self.tap_dance_eager.as_mut().expect("some").set_expired(); - let action = self.press_as_action((i, j), self.current_layer()); - self.do_action(action, (i, j), queue.since, false) } else { - let action = self.press_as_action((i, j), self.current_layer()); - self.do_action(action, (i, j), queue.since, false) + // i == 0 means real key, i == 1 means fake key. Let fake keys do whatever, but + // interrupt tap-dance-eager if real key. + if i == REAL_KEY_ROW { + tde.set_expired(); + } + self.do_action( + &Action::Trans, + (i, j), + queue.since, + false, + &mut layer_stack, + &mut custom_activation_count, + ) } } else { - let action = self.press_as_action((i, j), self.current_layer()); - self.do_action(action, (i, j), queue.since, false) + self.do_action( + &Action::Trans, + (i, j), + queue.since, + false, + &mut layer_stack, + &mut custom_activation_count, + ) } } } } /// Register a key event. pub fn event(&mut self, event: Event) { - if let Some(queued) = self.queue.push_back(event.into()) { - self.waiting_into_hold(); - self.dequeue(queued); + if let Event::Press(x, y) = event { + self.historical_inputs.push_front((x, y)); + self.historical_inputs_sans_holds_or_timeouts + .push_front((x, y)); + } + if let Some(overflow) = if let Some(ch) = self.chords_v2.as_mut() { + ch.push_back_chv2(event.into()) + } else { + self.queue.push_back(event.into()) + } { + for i in -1..(EXTRA_WAITING_LEN as i8) { + self.waiting_into_hold(i); + } + self.dequeue(overflow); + } + } + + /// Put a key event at the front instead of back. + /// These events will not participate in chordsv2. + pub fn event_to_front(&mut self, event: Event) { + if let Event::Press(x, y) = event { + self.historical_inputs.push_front((x, y)); + self.historical_inputs_sans_holds_or_timeouts + .push_front((x, y)); + } + if let Some(overflow) = self.queue.push_front(event.into()) { + for i in -1..(EXTRA_WAITING_LEN as i8) { + self.waiting_into_hold(i); + } + self.dequeue(overflow); } } - fn press_as_action(&self, coord: KCoord, layer: usize) -> &'a Action<'a, T> { + + /// Resolve coordinate to first non-Trans actions. + /// Trans on base layer, resolves to key from defsrc. + fn resolve_coord( + &self, + coord: KCoord, + layer_stack: &mut (impl Iterator + Clone), + ) -> &'a Action<'a, T> { use crate::action::Action::*; - let action = self - .layers - .get(layer) - .and_then(|l| l.get(coord.0 as usize)) - .and_then(|l| l.get(coord.1 as usize)); - match action { - None => &NoOp, - Some(Trans) => { - if layer != self.default_layer { - self.press_as_action(coord, self.default_layer) - } else { - &NoOp - } + let x = coord.0 as usize; + let y = coord.1 as usize; + assert!(x <= self.layers[0].len()); + assert!(y <= self.layers[0][0].len()); + for layer in layer_stack { + assert!(usize::from(layer) <= self.layers.len()); + let action = &self.layers[usize::from(layer)][x][y]; + match action { + Trans => continue, + action => return action, } - Some(action) => action, } + if x == 0 { &self.src_keys[y] } else { &NoOp } } fn do_action( &mut self, @@ -1156,25 +1874,107 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt coord: KCoord, delay: u16, is_oneshot: bool, + layer_stack: &mut (impl Iterator + Clone), // used to resolve Trans action + custom_activation_count: &mut u8, ) -> CustomEvent<'a, T> { - assert!(self.waiting.is_none() || matches!(action, Action::Custom(..))); - if self.last_press_tracker.coord != coord { + let mut action = action; + if let Trans = action { + action = self.resolve_coord(coord, layer_stack); + } + let action = action; + + if self.last_press_tracker.coord != coord && coord.0 == REAL_KEY_ROW { self.last_press_tracker.tap_hold_timeout = 0; } use Action::*; - if !matches!(action, Repeat | Layer(..) | DefaultLayer(..)) { - self.prev_action = Some(action); - } + self.states.retain(|s| match s { + // Need to solve a problem here - if the output chord `S-=` is active, and then a + // different keypress `=` happens, the `lsft =` states are cleared; but the `=` + // is immediately re-added again. This means the release is never observed.. + // + // Bug introduced: + // + // If doing something like typing parentheses using output chords, + // e.g. S-9 followed by S-0, + // the trivial fix will suppress the shift key for a bit + // but the `0` still gets output, + // resulting in an unshifted `0` which is incorrect. + // + // Fix added: + // + // Do not apply the suppression to modifiers. + // Modifiers typically don't have a usage pattern similar to + // the real use case of `S-=` followed by `=` example, + // such as `S-=` followed by only `lsft`, + // with a desire for the lone `lsft` to actually activate something. + NormalKey { flags, keycode, .. } => match flags.nkf_clear_on_next_action() { + true => { + self.oneshot.pause_input_processing_ticks += 2; + if !keycode.is_mod() { + let _ = self.keys_to_suppress_for_one_cycle.push(*keycode); + } + false + } + false => true, + }, + _ => true, + }); match action { - NoOp | Trans => { - if !is_oneshot { + NoOp => { + // There is an interaction between oneshot and chordsv2 here. + // chordsv2 sends fake queued press/release events at the coordinate level in order + // to trigger other "waiting" style actions, namely tap-hold. However, these can + // potentially interfere with oneshot by triggering early oneshot activation. This + // is resolved by ignoring actions at the coordinate at which the fake events are + // sent. + if !is_oneshot && coord != TRIGGER_TAPHOLD_COORD { self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + self.rpt_action = Some(action); + let _ = self.states.push(NoOpInput { coord }); + } + Src => { + let action = &self.src_keys[usize::from(coord.1)]; + // Risk: infinite recursive resulting in stack overflow. + // In practice this is not expected to happen. + // The `src_keys` actions are all expected to be `KeyCode` or `NoOp` actions. + self.do_action( + action, + coord, + delay, + is_oneshot, + &mut std::iter::empty(), + custom_activation_count, + ); + } + Trans => { + // Transparent action should be resolved to non-transparent one near the top + // of `do_action`. + unreachable!("Trans action should have been resolved earlier") } Repeat => { - if let Some(ac) = self.prev_action { - self.do_action(ac, coord, delay, is_oneshot); + // Notes around repeat: + // + // Though this action seems conceptually simple, in reality there are a lot of + // decisions to be made around how exactly actions repeat. For example: in a + // tap-dance action, would one expect the tap-dance to be repeated or the inner + // action that was most activated within the tap-dance? + // + // Currently the answer to these questions is: what is easy/possible to do? + // It is not consistent whether it is the outer or inner action. + // The actions tap-dance and tap-hold will repeat the inner action and + // not the outer (tap-dance|hold), + // but multi will repeat the entire outer multi action. + if let Some(ac) = self.rpt_action { + self.do_action( + ac, + coord, + delay, + is_oneshot, + &mut std::iter::empty(), + custom_activation_count, + ); } } HoldTap(HoldTapAction { @@ -1184,35 +1984,102 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt timeout_action, config, tap_hold_interval, + on_press_reset_timeout_to, + require_prior_idle, }) => { + // Typing streak detection: if a different physical key was pressed + // recently, resolve as tap immediately without entering WaitingState. + // Per-action override takes precedence over the global defcfg value. + let idle_threshold = require_prior_idle.unwrap_or(self.tap_hold_require_prior_idle); + if idle_threshold > 0 { + let prior_idle_tap = self + .historical_inputs_sans_holds_or_timeouts + .iter_hevents() + .find(|prior| { + prior.event.0 == REAL_KEY_ROW + // Disregard this same press event + && prior.event != coord + // Disregard any presses that are actually still in the queue and + // unresolved. + && !self.queue.iter().any(|q| { + q.since == prior.ticks_since_occurrence + && match q.event { + Event::Press(0, j) => j == prior.event.1, + _ => false + } + }) + }) + .is_some_and(|prior| prior.ticks_since_occurrence <= idle_threshold); + if prior_idle_tap { + let custom = self.do_action( + tap, + coord, + delay, + is_oneshot, + layer_stack, + custom_activation_count, + ); + self.last_press_tracker.update_coord(coord); + return custom; + } + } let mut custom = CustomEvent::NoEvent; if *tap_hold_interval == 0 || coord != self.last_press_tracker.coord || self.last_press_tracker.tap_hold_timeout == 0 { + let ticks = match self.quick_tap_hold_timeout { + // Leave 1 tick to timeout as it will be consumed in the next processing cycle + true => delay.min(timeout.saturating_sub(1)), + false => 0, + }; let waiting: WaitingState = WaitingState { coord, - timeout: *timeout, - delay, - ticks: 0, + timeout: timeout.saturating_sub(ticks), + delay: delay.saturating_sub(ticks), + ticks, + on_press_reset_timeout_to: *on_press_reset_timeout_to, hold, tap, timeout_action, config: WaitingConfig::HoldTap(*config), + layer_stack: layer_stack.collect(), + prev_queue_len: QueueLen::MAX, }; - self.waiting = Some(waiting); + if self.waiting.is_some() { + self.extra_waiting.push_back(waiting); + } else { + self.waiting = Some(waiting); + } self.last_press_tracker.tap_hold_timeout = *tap_hold_interval; } else { self.last_press_tracker.tap_hold_timeout = 0; - custom.update(self.do_action(tap, coord, delay, is_oneshot)); + custom.update(self.do_action( + tap, + coord, + delay, + is_oneshot, + layer_stack, + custom_activation_count, + )); } // Need to set tap_hold_tracker coord AFTER the checks. - self.last_press_tracker.coord = coord; + self.last_press_tracker.update_coord(coord); return custom; } &OneShot(oneshot) => { - self.last_press_tracker.coord = coord; - let custom = self.do_action(oneshot.action, coord, delay, true); + self.last_press_tracker.update_coord(coord); + let custom = self.do_action( + oneshot.action, + coord, + delay, + true, + &mut std::iter::empty(), + custom_activation_count, + ); + // Note - set rpt_action after doing the inner oneshot action. This means that the + // whole oneshot will be repeated by rpt-any rather than only the inner action. + self.rpt_action = Some(action); self.oneshot .handle_press(OneShotHandlePressKey::OneShotKey(coord)); self.oneshot.timeout = oneshot.timeout; @@ -1222,8 +2089,13 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt } return custom; } + &OneShotIgnoreEventsTicks(ticks) => { + self.last_press_tracker.update_coord(coord); + self.rpt_action = Some(action); + self.oneshot.ticks_to_ignore_events = ticks; + } &TapDance(td) => { - self.last_press_tracker.coord = coord; + self.last_press_tracker.update_coord(coord); match td.config { TapDanceConfig::Lazy => { self.waiting = Some(WaitingState { @@ -1234,11 +2106,14 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt hold: &Action::NoOp, tap: &Action::NoOp, timeout_action: &Action::NoOp, + on_press_reset_timeout_to: None, config: WaitingConfig::TapDance(TapDanceState { actions: td.actions, timeout: td.timeout, num_taps: 1, }), + layer_stack: layer_stack.collect(), + prev_queue_len: QueueLen::MAX, }); } TapDanceConfig::Eager => { @@ -1264,12 +2139,19 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt } } }; - self.do_action(td.actions[0], coord, delay, false); + return self.do_action( + td.actions[0], + coord, + delay, + false, + layer_stack, + custom_activation_count, + ); } } } &Chords(chords) => { - self.last_press_tracker.coord = coord; + self.last_press_tracker.update_coord(coord); self.waiting = Some(WaitingState { coord, timeout: chords.timeout, @@ -1278,42 +2160,137 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt hold: &Action::NoOp, tap: &Action::NoOp, timeout_action: &Action::NoOp, + on_press_reset_timeout_to: None, config: WaitingConfig::Chord(chords), + layer_stack: layer_stack.collect(), + prev_queue_len: QueueLen::MAX, }); } &KeyCode(keycode) => { - self.last_press_tracker.coord = coord; - let _ = self.states.push(NormalKey { coord, keycode }); + self.last_press_tracker.update_coord(coord); + // Most-recent-first! + self.contextual_execution + .push_historical_key(&mut self.historical_keys, keycode); + let _ = self.states.push(NormalKey { + coord, + keycode, + flags: NormalKeyFlags(0), + }); + let mut oneshot_coords = ArrayDeque::new(); if !is_oneshot { - self.oneshot + oneshot_coords = self + .oneshot .handle_press(OneShotHandlePressKey::Other(coord)); + } else { + self.oneshot + .add_state_to_retain(OneShotRetainableState::KeyCode { + coord, + kc: keycode, + }); + } + if oneshot_coords.is_empty() { + self.rpt_action = Some(action); + } else { + self.rpt_action = None; + unsafe { + self.rpt_multikey_key_buffer.clear(); + for kc in self + .states + .iter() + .filter_map(|kc| State::keycode_in_coords(kc, &oneshot_coords)) + { + self.rpt_multikey_key_buffer.push(kc); + } + self.rpt_multikey_key_buffer.push(keycode); + self.rpt_action = Some(self.rpt_multikey_key_buffer.get_ref()); + } } } &MultipleKeyCodes(v) => { - self.last_press_tracker.coord = coord; + self.last_press_tracker.update_coord(coord); for &keycode in *v { - let _ = self.states.push(NormalKey { coord, keycode }); + // BUG: + // In the original implementation, activating an action sequence such as b -> + // S-b will not type a separate instance of "B" because b is already held and + // wont be re-sent by the outer processing loop. + // + // FIX: + // If `keycode` is not a mod, suppress it for a cycle if it exists in the + // status. This is not expected to have any negative perceptible effects. + if !keycode.is_mod() && self.keycodes().any(|kc| kc == keycode) { + let _ = self.keys_to_suppress_for_one_cycle.push(keycode); + } + self.contextual_execution + .push_historical_key(&mut self.historical_keys, keycode); + let _ = self.states.push(NormalKey { + coord, + keycode, + // In Kanata, this action is only ever used with output chords. Output + // chords within a one-shot are ignored because someone might do something + // like (one-shot C-S-lalt to get 3 modifiers. These are probably intended + // to remain held. However, other output chords are usually used to type + // symbols or accented characters, e.g. S-1 or RA-a. Clearing chord keys on + // the next action allows a subsequent typed key to not have modifiers + // alongside it. But if the symbol or accented character is held down, key + // repeat works just fine. + flags: NormalKeyFlags(if is_oneshot { + 0 + } else { + NORMAL_KEY_FLAG_CLEAR_ON_NEXT_ACTION + }), + }); } + + let mut oneshot_coords = ArrayDeque::new(); if !is_oneshot { - self.oneshot + oneshot_coords = self + .oneshot .handle_press(OneShotHandlePressKey::Other(coord)); + } else { + for &keycode in *v { + self.oneshot + .add_state_to_retain(OneShotRetainableState::KeyCode { + coord, + kc: keycode, + }); + } + } + if oneshot_coords.is_empty() { + self.rpt_action = Some(action); + } else { + self.rpt_action = None; + unsafe { + self.rpt_multikey_key_buffer.clear(); + for kc in self + .states + .iter() + .filter_map(|s| s.keycode_in_coords(&oneshot_coords)) + { + self.rpt_multikey_key_buffer.push(kc); + } + for &keycode in *v { + self.rpt_multikey_key_buffer.push(keycode); + } + self.rpt_action = Some(self.rpt_multikey_key_buffer.get_ref()); + } } } &MultipleActions(v) => { - self.last_press_tracker.coord = coord; + self.last_press_tracker.update_coord(coord); let mut custom = CustomEvent::NoEvent; for action in *v { - custom.update(self.do_action(action, coord, delay, is_oneshot)); - } - // Multi is probably the one action where it is desirable to repeat the top-level - // action instead of the final action. The final action meaning a hold action in - // `tap-hold` or the 3rd tap of a `tap-dance`. - // - // Set the prev_action again since it was probably overwritten by the - // `do_action` recursion. - if !matches!(action, Action::Repeat) { - self.prev_action = Some(action); + custom.update(self.do_action( + action, + coord, + delay, + is_oneshot, + &mut layer_stack.clone(), + custom_activation_count, + )); } + // Save the whole multi action instead of the final action in multi so that Repeat + // repeats all of the actions in this multi. + self.rpt_action = Some(action); return custom; } Sequence { events } => { @@ -1327,6 +2304,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + self.rpt_action = Some(action); } RepeatableSequence { events } => { self.active_sequences.push_back(SequenceState { @@ -1343,6 +2321,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + self.rpt_action = Some(action); } CancelSequences => { // Clear any and all running sequences then clean up any leftover FakeKey events @@ -1356,17 +2335,27 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + self.rpt_action = Some(action); } &Layer(value) => { - self.last_press_tracker.coord = coord; + self.last_press_tracker.update_coord(coord); let _ = self.states.push(LayerModifier { value, coord }); if !is_oneshot { self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); + } else { + self.oneshot + .add_state_to_retain(OneShotRetainableState::Layer { + coord, + layer: value as u16, + }); } + // Notably missing in Layer and below in DefaultLayer is setting rpt_action. This + // is so that if the Repeat key is on a different layer than the base, it can still + // be used to repeat the previous non-layer-changing action. } DefaultLayer(value) => { - self.last_press_tracker.coord = coord; + self.last_press_tracker.update_coord(coord); self.set_default_layer(*value); if !is_oneshot { self.oneshot @@ -1374,14 +2363,28 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt } } Custom(value) => { - self.last_press_tracker.coord = coord; - if self.states.push(State::Custom { value, coord }).is_ok() { - return CustomEvent::Press(value); - } + self.last_press_tracker.update_coord(coord); if !is_oneshot { self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + *custom_activation_count = custom_activation_count.saturating_add(1); + self.rpt_action = Some(action); + return match custom_activation_count { + 0 | 1 => { + let _ = self.states.push(State::Custom { value, coord }); + CustomEvent::Press(value) + } + _ => { + self.action_queue.push_back(Some(( + coord, + delay, + action, + layer_stack.clone().collect(), + ))); + CustomEvent::NoEvent + } + }; } ReleaseState(rs) => { self.states.retain(|s| s.release_state(*rs).is_some()); @@ -1389,17 +2392,75 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + self.rpt_action = Some(action); } Fork(fcfg) => { - return match self.states.iter().any(|s| match s { + let ret = match self.states.iter().any(|s| match s { NormalKey { keycode, .. } | FakeKey { keycode } => { fcfg.right_triggers.contains(keycode) } _ => false, }) { - false => self.do_action(&fcfg.left, coord, delay, false), - true => self.do_action(&fcfg.right, coord, delay, false), + false => self.do_action( + &fcfg.left, + coord, + delay, + false, + &mut layer_stack.clone(), + custom_activation_count, + ), + true => self.do_action( + &fcfg.right, + coord, + delay, + false, + &mut layer_stack.clone(), + custom_activation_count, + ), }; + // Repeat the fork rather than the terminal action. + self.rpt_action = Some(action); + return ret; + } + Switch(sw) => { + let active_keys = self.states.iter().filter_map(State::keycode); + let active_coords = self.states.iter().filter_map(State::coord); + let historical_keys = self.historical_keys.iter_hevents(); + let historical_coords = self.historical_inputs.iter_hevents(); + let layers = self.trans_resolution_layer_order().into_iter(); + let mut action_queue: ActionQueue = Default::default(); + if let Some(f) = sw.init_fn { + f(); + } + for ac in sw.actions( + active_keys, + active_coords, + historical_keys, + historical_coords, + layers, + // Note on truncating cast: I expect default layer to be in range by other + // assertions. + self.default_layer as u16, + self.device_history.iter().copied(), + ) { + action_queue.push_back(Some((coord, delay, ac, layer_stack.clone().collect()))); + } + + let mut custom = CustomEvent::NoEvent; + while let Some(Some((coord, delay, action, layer_stack))) = action_queue.pop_front() + { + custom.update(self.do_action( + action, + coord, + delay, + is_oneshot, + &mut layer_stack.into_iter(), + custom_activation_count, + )); + } + + self.rpt_action = Some(action); + return custom; } } CustomEvent::NoEvent @@ -1414,6 +2475,33 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt .unwrap_or(self.default_layer) } + pub fn active_held_layers(&self) -> impl Iterator + Clone + '_ { + self.states + .iter() + .filter_map(|s| State::get_layer(s).map(|l| l as u16)) + .rev() + } + + /// Returns a list indices of layers that should be used for [`Action::Trans`] resolution. + pub fn trans_resolution_layer_order(&self) -> LayerStack { + let current_layer = self.current_layer(); + if self.trans_resolution_behavior_v2 { + let mut v = self.active_held_layers().collect::(); + let _ = v.push(self.default_layer as u16); + if self.delegate_to_first_layer && current_layer != 0 && self.default_layer != 0 { + let _ = v.push(0); + } + v + } else { + let mut v = Vec::new(); + let _ = v.push(current_layer as u16); + if self.delegate_to_first_layer && current_layer != 0 { + let _ = v.push(0); + } + v + } + } + /// Sets the default layer for the layout pub fn set_default_layer(&mut self, value: usize) { if value < self.layers.len() { @@ -1442,9 +2530,11 @@ mod test { #[test] fn basic_hold_tap() { - static LAYERS: Layers<2, 1, 2> = [ + static LAYERS: Layers<2, 1> = &[ [[ HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: l(1), tap: k(Space), @@ -1453,6 +2543,8 @@ mod test { tap_hold_interval: 0, }), HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: k(LCtrl), timeout_action: k(LShift), @@ -1463,7 +2555,7 @@ mod test { ]], [[Trans, MultipleKeyCodes(&[LCtrl, Enter].as_slice())]], ]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 1)); @@ -1493,10 +2585,12 @@ mod test { } #[test] - fn basic_hold_tap_timeout() { - static LAYERS: Layers<2, 1, 2> = [ + fn basic_hold_tap_repress_timeout() { + static LAYERS: Layers<2, 1> = &[ [[ HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: l(1), tap: k(Space), @@ -1505,6 +2599,8 @@ mod test { tap_hold_interval: 0, }), HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: k(LCtrl), timeout_action: k(LCtrl), @@ -1515,7 +2611,7 @@ mod test { ]], [[Trans, MultipleKeyCodes(&[LCtrl, Enter].as_slice())]], ]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 1)); @@ -1546,8 +2642,10 @@ mod test { #[test] fn hold_tap_interleaved_timeout() { - static LAYERS: Layers<2, 1, 1> = [[[ + static LAYERS: Layers<2, 1> = &[[[ HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: k(LAlt), timeout_action: k(LAlt), @@ -1556,6 +2654,8 @@ mod test { tap_hold_interval: 0, }), HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 20, hold: k(LCtrl), timeout_action: k(LCtrl), @@ -1564,7 +2664,7 @@ mod test { tap_hold_interval: 0, }), ]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 0)); @@ -1595,8 +2695,10 @@ mod test { #[test] fn hold_on_press() { - static LAYERS: Layers<2, 1, 1> = [[[ + static LAYERS: Layers<2, 1> = &[[[ HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: k(LAlt), timeout_action: k(LAlt), @@ -1606,7 +2708,7 @@ mod test { }), k(Enter), ]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); // Press another key before timeout assert_eq!(CustomEvent::NoEvent, layout.tick()); @@ -1651,10 +2753,213 @@ mod test { assert_keys(&[], layout.keycodes()); } + #[test] + fn order_clean_tap() { + // Press and release modifier with no other keys → Tap. + static LAYERS: Layers<2, 1> = &[[[ + HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + timeout: u16::MAX, + hold: k(LAlt), + timeout_action: k(Space), + tap: k(Space), + config: HoldTapConfig::Order { buffer: 0 }, + tap_hold_interval: 0, + require_prior_idle: None, + }), + k(Enter), + ]]]; + let mut layout = Layout::new(LAYERS); + + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + for _ in 0..50 { + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + layout.event(Release(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[Space], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + #[test] + fn order_hold() { + // Modifier down → other down → other up first → Hold. + static LAYERS: Layers<2, 1> = &[[[ + HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + timeout: u16::MAX, + hold: k(LAlt), + timeout_action: k(Space), + tap: k(Space), + config: HoldTapConfig::Order { buffer: 0 }, + tap_hold_interval: 0, + require_prior_idle: None, + }), + k(Enter), + ]]]; + let mut layout = Layout::new(LAYERS); + + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + // Other key releases first → Hold + layout.event(Release(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt, Enter], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt], layout.keycodes()); + // Release modifier + layout.event(Release(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + #[test] + fn order_tap() { + // Modifier down → other down → modifier up first → Tap. + static LAYERS: Layers<2, 1> = &[[[ + HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + timeout: u16::MAX, + hold: k(LAlt), + timeout_action: k(Space), + tap: k(Space), + config: HoldTapConfig::Order { buffer: 0 }, + tap_hold_interval: 0, + require_prior_idle: None, + }), + k(Enter), + ]]]; + let mut layout = Layout::new(LAYERS); + + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + // Modifier releases first → Tap + layout.event(Release(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[Space], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[Space, Enter], layout.keycodes()); + } + + #[test] + fn order_multi_key_hold() { + // TH down → A down → B down → A up (while B still held) → TH up. + // A's press+release cycle completes while TH is held → Hold. + static LAYERS: Layers<3, 1> = &[[[ + HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + timeout: u16::MAX, + hold: k(LAlt), + timeout_action: k(Space), + tap: k(Space), + config: HoldTapConfig::Order { buffer: 0 }, + tap_hold_interval: 0, + require_prior_idle: None, + }), + k(Enter), + k(Tab), + ]]]; + let mut layout = Layout::new(LAYERS); + + // TH down + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + // A down + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + // B down + layout.event(Press(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + // A up — A's press+release cycle is complete → Hold resolves + layout.event(Release(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt], layout.keycodes()); + // Queued keys replay: Enter press, Tab press, Enter release + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt, Enter], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt, Enter, Tab], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt, Tab], layout.keycodes()); + // Release B + layout.event(Release(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt], layout.keycodes()); + // Release TH + layout.event(Release(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + #[test] + fn order_buffer_ignores_press_within_window() { + // TH down (buffer=50) → other key pressed+released within 50 ticks. + // Without buffer this would be Hold (other key's press+release cycle + // completes while TH held). With buffer=50, the press is ignored by + // release-order logic, so TH remains unresolved. Releasing TH → Tap. + static LAYERS: Layers<2, 1> = &[[[ + HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, + timeout: u16::MAX, + hold: k(LAlt), + timeout_action: k(Space), + tap: k(Space), + config: HoldTapConfig::Order { buffer: 50 }, + tap_hold_interval: 0, + }), + k(Enter), + ]]]; + let mut layout = Layout::new(LAYERS); + + // TH down + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + // Other key pressed at tick ~1 (well within 50-tick buffer) + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + // Other key released — would normally trigger Hold, but press is buffered + layout.event(Release(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + // TH released → Tap (buffered press was ignored). + // Space activates, then queued Enter press+release replays. + layout.event(Release(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[Space], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[Space, Enter], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[Space], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + #[test] fn permissive_hold() { - static LAYERS: Layers<2, 1, 1> = [[[ + static LAYERS: Layers<2, 1> = &[[[ HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: k(LAlt), timeout_action: k(LAlt), @@ -1664,7 +2969,7 @@ mod test { }), k(Enter), ]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); // Press and release another key before timeout assert_eq!(CustomEvent::NoEvent, layout.tick()); @@ -1691,13 +2996,79 @@ mod test { assert_keys(&[], layout.keycodes()); } + #[test] + fn simultaneous_hold() { + static LAYERS: Layers<3, 1> = &[[[ + HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, + timeout: 200, + hold: k(LAlt), + timeout_action: k(LAlt), + tap: k(Space), + config: HoldTapConfig::Default, + tap_hold_interval: 0, + }), + HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, + timeout: 200, + hold: k(RAlt), + timeout_action: k(RAlt), + tap: k(A), + config: HoldTapConfig::Default, + tap_hold_interval: 0, + }), + HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, + timeout: 200, + hold: k(LCtrl), + timeout_action: k(LCtrl), + tap: k(A), + config: HoldTapConfig::Default, + tap_hold_interval: 0, + }), + ]]]; + let mut layout = Layout::new(LAYERS); + layout.quick_tap_hold_timeout = true; + + // Press and release another key before timeout + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + + for _ in 0..196 { + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt, RAlt], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt, RAlt], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt, RAlt, LCtrl], layout.keycodes()); + } + #[test] fn multiple_actions() { - static LAYERS: Layers<2, 1, 2> = [ + static LAYERS: Layers<2, 1> = &[ [[MultipleActions(&[l(1), k(LShift)].as_slice()), k(F)]], [[Trans, k(E)]], ]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 0)); @@ -1716,8 +3087,8 @@ mod test { #[test] fn custom() { - static LAYERS: Layers<1, 1, 1, u8> = [[[Action::Custom(42)]]]; - let mut layout = Layout::new(&LAYERS); + static LAYERS: Layers<1, 1, i32> = &[[[Action::Custom(42)]]]; + let mut layout = Layout::new(LAYERS); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); @@ -1738,13 +3109,13 @@ mod test { #[test] fn multiple_layers() { - static LAYERS: Layers<2, 1, 4> = [ + static LAYERS: Layers<2, 1> = &[ [[l(1), l(2)]], [[k(A), l(3)]], [[l(0), k(B)]], [[k(C), k(D)]], ]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_eq!(0, layout.current_layer()); assert_keys(&[], layout.keycodes()); @@ -1801,20 +3172,22 @@ mod test { #[test] fn custom_handler() { - fn always_tap(_: QueuedIter) -> Option { - Some(WaitingAction::Tap) + fn always_tap(_: QueuedIter, _: KCoord) -> (Option, bool) { + (Some(WaitingAction::Tap), false) } - fn always_hold(_: QueuedIter) -> Option { - Some(WaitingAction::Hold) + fn always_hold(_: QueuedIter, _: KCoord) -> (Option, bool) { + (Some(WaitingAction::Hold), false) } - fn always_nop(_: QueuedIter) -> Option { - Some(WaitingAction::NoOp) + fn always_nop(_: QueuedIter, _: KCoord) -> (Option, bool) { + (Some(WaitingAction::NoOp), false) } - fn always_none(_: QueuedIter) -> Option { - None + fn always_none(_: QueuedIter, _: KCoord) -> (Option, bool) { + (None, false) } - static LAYERS: Layers<4, 1, 1> = [[[ + static LAYERS: Layers<4, 1> = &[[[ HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: k(Kb1), timeout_action: k(Kb1), @@ -1823,6 +3196,8 @@ mod test { tap_hold_interval: 0, }), HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: k(Kb3), timeout_action: k(Kb3), @@ -1831,6 +3206,8 @@ mod test { tap_hold_interval: 0, }), HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: k(Kb5), timeout_action: k(Kb5), @@ -1839,6 +3216,8 @@ mod test { tap_hold_interval: 0, }), HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: k(Kb7), timeout_action: k(Kb7), @@ -1847,7 +3226,7 @@ mod test { tap_hold_interval: 0, }), ]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); @@ -1910,8 +3289,10 @@ mod test { #[test] fn tap_hold_interval() { - static LAYERS: Layers<2, 1, 1> = [[[ + static LAYERS: Layers<2, 1> = &[[[ HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: k(LAlt), timeout_action: k(LAlt), @@ -1921,7 +3302,7 @@ mod test { }), k(Enter), ]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); // press and release the HT key, expect tap action assert_eq!(CustomEvent::NoEvent, layout.tick()); @@ -1965,8 +3346,10 @@ mod test { #[test] fn tap_hold_interval_interleave() { - static LAYERS: Layers<3, 1, 1> = [[[ + static LAYERS: Layers<3, 1> = &[[[ HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: k(LAlt), timeout_action: k(LAlt), @@ -1976,6 +3359,8 @@ mod test { }), k(Enter), HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: k(LAlt), timeout_action: k(LAlt), @@ -1984,7 +3369,7 @@ mod test { tap_hold_interval: 200, }), ]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); // press and release the HT key, expect tap action assert_eq!(CustomEvent::NoEvent, layout.tick()); @@ -2088,7 +3473,9 @@ mod test { #[test] fn tap_hold_interval_short_hold() { - static LAYERS: Layers<1, 1, 1> = [[[HoldTap(&HoldTapAction { + static LAYERS: Layers<1, 1> = &[[[HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 50, hold: k(LAlt), timeout_action: k(LAlt), @@ -2096,7 +3483,7 @@ mod test { config: HoldTapConfig::Default, tap_hold_interval: 200, })]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); // press and hold the HT key, expect hold action assert_eq!(CustomEvent::NoEvent, layout.tick()); @@ -2130,8 +3517,10 @@ mod test { #[test] fn tap_hold_interval_different_hold() { - static LAYERS: Layers<2, 1, 1> = [[[ + static LAYERS: Layers<2, 1> = &[[[ HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 50, hold: k(LAlt), timeout_action: k(LAlt), @@ -2140,6 +3529,8 @@ mod test { tap_hold_interval: 200, }), HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 200, hold: k(RAlt), timeout_action: k(RAlt), @@ -2148,7 +3539,7 @@ mod test { tap_hold_interval: 200, }), ]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); // press HT1, press HT2, release HT1 after hold timeout, release HT2, press HT2 layout.event(Press(0, 0)); @@ -2180,7 +3571,7 @@ mod test { #[test] fn one_shot() { - static LAYERS: Layers<3, 1, 1> = [[[ + static LAYERS: Layers<3, 1> = &[[[ OneShot(&crate::action::OneShot { timeout: 100, action: &k(LShift), @@ -2189,7 +3580,8 @@ mod test { k(A), k(B), ]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); + layout.oneshot.pause_input_processing_delay = 1; // Test: // 1. press one-shot @@ -2291,7 +3683,7 @@ mod test { #[test] fn one_shot_end_press_or_repress() { - static LAYERS: Layers<3, 1, 1> = [[[ + static LAYERS: Layers<3, 1> = &[[[ OneShot(&crate::action::OneShot { timeout: 100, action: &k(LShift), @@ -2300,7 +3692,8 @@ mod test { k(A), k(B), ]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); + layout.oneshot.pause_input_processing_delay = 1; // Test: // 1. press one-shot @@ -2444,7 +3837,7 @@ mod test { #[test] fn one_shot_end_on_release() { - static LAYERS: Layers<3, 1, 1> = [[[ + static LAYERS: Layers<3, 1> = &[[[ OneShot(&crate::action::OneShot { timeout: 100, action: &k(LShift), @@ -2453,7 +3846,7 @@ mod test { k(A), k(B), ]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); // Test: // 1. press one-shot @@ -2589,7 +3982,7 @@ mod test { #[test] fn one_shot_multi() { - static LAYERS: Layers<4, 1, 2> = [ + static LAYERS: Layers<4, 1> = &[ [[ OneShot(&crate::action::OneShot { timeout: 100, @@ -2610,7 +4003,8 @@ mod test { ]], [[k(A), k(B), k(C), k(D)]], ]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); + layout.oneshot.pause_input_processing_delay = 1; layout.event(Press(0, 0)); layout.event(Release(0, 0)); @@ -2646,7 +4040,7 @@ mod test { #[test] fn one_shot_tap_hold() { - static LAYERS: Layers<3, 1, 2> = [ + static LAYERS: Layers<3, 1> = &[ [[ OneShot(&crate::action::OneShot { timeout: 200, @@ -2654,6 +4048,8 @@ mod test { end_config: OneShotEndConfig::EndOnFirstPress, }), HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 100, hold: k(LAlt), timeout_action: k(LAlt), @@ -2665,7 +4061,8 @@ mod test { ]], [[k(A), k(B), k(C)]], ]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); + layout.oneshot.pause_input_processing_delay = 1; layout.event(Press(0, 0)); layout.event(Release(0, 0)); @@ -2705,8 +4102,8 @@ mod test { } #[test] - fn tap_dance() { - static LAYERS: Layers<2, 2, 1> = [[ + fn tap_dance_uneager() { + static LAYERS: Layers<2, 2> = &[[ [ TapDance(&crate::action::TapDance { timeout: 100, @@ -2718,6 +4115,8 @@ mod test { end_config: OneShotEndConfig::EndOnFirstPress, }), &HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, timeout: 100, hold: k(LAlt), timeout_action: k(LAlt), @@ -2732,7 +4131,7 @@ mod test { ], [k(B), k(C)], ]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); // Test: tap-dance first key, timeout layout.event(Press(0, 0)); @@ -2809,8 +4208,6 @@ mod test { assert_keys(&[], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); - assert_keys(&[], layout.keycodes()); - assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Space], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); @@ -2833,7 +4230,7 @@ mod test { assert_keys(&[], layout.keycodes()); } layout.event(Press(0, 0)); - for _ in 0..101 { + for _ in 0..100 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } @@ -2848,7 +4245,7 @@ mod test { #[test] fn tap_dance_eager() { - static LAYERS: Layers<2, 2, 1> = [[ + static LAYERS: Layers<2, 2> = &[[ [ TapDance(&crate::action::TapDance { timeout: 100, @@ -2859,7 +4256,7 @@ mod test { ], [k(B), k(C)], ]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); // Test: tap-dance-eager first key layout.event(Press(0, 0)); @@ -2935,7 +4332,7 @@ mod test { #[test] fn release_state() { - static LAYERS: Layers<2, 1, 2> = [ + static LAYERS: Layers<2, 1> = &[ [[ MultipleActions(&(&[KeyCode(LCtrl), Layer(1)] as _)), MultipleActions(&(&[KeyCode(LAlt), Layer(1)] as _)), @@ -2948,7 +4345,7 @@ mod test { ]], ]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); @@ -2994,7 +4391,7 @@ mod test { ], timeout: 100, }; - static LAYERS: Layers<6, 1, 1> = [[[ + static LAYERS: Layers<6, 1> = &[[[ NoOp, NoOp, Chords(&GROUP), @@ -3003,7 +4400,7 @@ mod test { Chords(&GROUP), ]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); layout.event(Press(0, 2)); // timeout on non-terminal chord for _ in 0..50 { @@ -3047,10 +4444,14 @@ mod test { assert_keys(&[Kb5, Kb3], layout.keycodes()); layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); - assert_keys(&[], layout.keycodes()); + assert_keys(&[Kb3], layout.keycodes()); layout.event(Release(0, 3)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[Kb3], layout.keycodes()); layout.event(Release(0, 4)); - + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + // release terminal chord with no action associated // combo like (h j k) -> (h j) (k) layout.event(Press(0, 2)); @@ -3074,9 +4475,11 @@ mod test { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb5, Kb3], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); - assert_keys(&[], layout.keycodes()); - layout.event(Release(0, 3)); + assert_keys(&[Kb5], layout.keycodes()); layout.event(Release(0, 2)); + layout.event(Release(0, 3)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); @@ -3100,7 +4503,7 @@ mod test { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb3, Kb6], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); - assert_keys(&[], layout.keycodes()); + assert_keys(&[Kb3], layout.keycodes()); } #[test] @@ -3117,7 +4520,7 @@ mod test { ], timeout: 100, }; - static LAYERS: Layers<6, 1, 1> = [[[ + static LAYERS: Layers<6, 1> = &[[[ NoOp, k(A), Chords(&GROUP), @@ -3126,7 +4529,7 @@ mod test { Chords(&GROUP), ]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); layout.event(Press(0, 2)); // timeout on non-terminal chord for _ in 0..50 { @@ -3146,9 +4549,69 @@ mod test { assert_keys(&[], layout.keycodes()); } + #[test] + fn test_chord_multi_waiting_decomposition() { + const GROUP: ChordsGroup = ChordsGroup { + coords: &[((0, 0), 1), ((0, 1), 2)], + chords: &[ + ( + 1, + &HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, + timeout: 100, + hold: k(A), + timeout_action: k(A), + tap: k(Kb1), + config: HoldTapConfig::Default, + tap_hold_interval: 0, + }), + ), + ( + 2, + &HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, + timeout: 100, + hold: k(B), + timeout_action: k(B), + tap: k(Kb2), + config: HoldTapConfig::Default, + tap_hold_interval: 0, + }), + ), + ], + timeout: 100, + }; + static LAYERS: Layers<2, 1> = &[[[Chords(&GROUP), Chords(&GROUP)]]]; + + let mut layout = Layout::new(LAYERS); + layout.quick_tap_hold_timeout = true; + layout.event(Press(0, 0)); + layout.event(Press(0, 1)); + // Why does this take 103 ticks? + // 0: chord begin + // 1: chord decompose + // 2: action queue dequeue + // 3: action queue dequeue + // 4-103: timeout ticks + for _ in 0..102 { + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[A, B], layout.keycodes()); + layout.event(Release(0, 0)); + layout.event(Release(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[B], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + #[test] fn test_fork() { - static LAYERS: Layers<2, 1, 1> = [[[ + static LAYERS: Layers<2, 1> = &[[[ Fork(&ForkConfig { left: k(Kb1), right: k(Kb2), @@ -3156,7 +4619,7 @@ mod test { }), k(Space), ]]]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); @@ -3181,7 +4644,7 @@ mod test { #[test] fn test_repeat() { - static LAYERS: Layers<5, 1, 2> = [ + static LAYERS: Layers<5, 1> = &[ [[ k(A), MultipleKeyCodes(&[LShift, B].as_slice()), @@ -3197,7 +4660,7 @@ mod test { Layer(1), ]], ]; - let mut layout = Layout::new(&LAYERS); + let mut layout = Layout::new(LAYERS); // Press a key layout.event(Press(0, 0)); @@ -3283,4 +4746,684 @@ mod test { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } + + #[test] + fn test_clear_multiple_keycodes() { + static LAYERS: Layers<2, 1> = &[[[k(A), MultipleKeyCodes(&[LCtrl, Enter].as_slice())]]]; + let mut layout = Layout::new(LAYERS); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LCtrl, Enter], layout.keycodes()); + // Cancel chord keys on next keypress. + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[A], layout.keycodes()); + } + + // Tests the new Trans behavior. + // https://github.com/jtroo/kanata/issues/738 + #[test] + fn test_trans_in_stacked_held_layers() { + static LAYERS: Layers<4, 1> = &[ + [[Layer(1), NoOp, NoOp, k(A)]], + [[NoOp, Layer(2), NoOp, k(B)]], + [[NoOp, NoOp, Layer(3), Trans]], + [[NoOp, NoOp, NoOp, Trans]], + ]; + let mut layout = Layout::new(LAYERS); + + // change to layer 2 + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + // change to layer 3 + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + // change to layer 4 + layout.event(Press(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + // pressing Trans should press a key in layer 2, compared to previous behavior, + // where a key in layer 1 would be pressed + layout.event(Press(0, 3)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[B], layout.keycodes()); + layout.event(Release(0, 3)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + #[test] + fn test_trans_in_action_on_first_layer() { + static DEFSRC_LAYER: [Action; 2] = [NoOp, k(X)]; + static LAYERS: Layers<2, 1> = &[ + [[Layer(1), Trans]], + [[NoOp, MultipleActions(&[Trans].as_slice())]], + ]; + let mut layout = Layout::new_with_trans_action_settings(&DEFSRC_LAYER, LAYERS, true, true); + + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[X], layout.keycodes()); + layout.event(Release(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + #[test] + fn test_trans_in_taphold_tap() { + static LAYERS: Layers<3, 1> = &[ + [[Layer(1), NoOp, k(A)]], + [[NoOp, Layer(2), k(B)]], + [[ + NoOp, + NoOp, + HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, + timeout: 50, + hold: k(Space), + timeout_action: k(Space), + tap: Trans, + config: HoldTapConfig::Default, + tap_hold_interval: 200, + }), + ]], + ]; + let mut layout = Layout::new(LAYERS); + + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 2)); // press th + for _ in 0..10 { + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + layout.event(Release(0, 2)); // release th + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[B], layout.keycodes()); // B is resolved from Trans + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + // test tap action repeat + layout.event(Press(0, 2)); + for _ in 0..30 { + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[B], layout.keycodes()); + } + layout.event(Release(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + #[test] + fn test_trans_in_taphold_hold() { + static LAYERS: Layers<3, 1> = &[ + [[Layer(1), NoOp, k(A)]], + [[NoOp, Layer(2), k(B)]], + [[ + NoOp, + NoOp, + HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, + timeout: 50, + hold: Trans, + timeout_action: Trans, + tap: k(Space), + config: HoldTapConfig::Default, + tap_hold_interval: 200, + }), + ]], + ]; + let mut layout = Layout::new(LAYERS); + + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 2)); // press th + for _ in 0..50 { + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + for _ in 0..70 { + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[B], layout.keycodes()); // B is resolved from Trans + } + layout.event(Release(0, 2)); // release th + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + #[test] + fn test_trans_in_tapdance_lazy() { + static LAYERS: Layers<3, 1> = &[ + [[Layer(1), NoOp, k(A)]], + [[NoOp, Layer(2), k(B)]], + [[ + NoOp, + NoOp, + TapDance(&crate::action::TapDance { + timeout: 100, + actions: &[&Trans, &k(X)], + config: TapDanceConfig::Lazy, + }), + ]], + ]; + let mut layout = Layout::new(LAYERS); + + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 2)); + for _ in 0..10 { + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + layout.event(Release(0, 2)); + for _ in 0..90 { + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[B], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + #[test] + fn test_trans_in_tapdance_eager() { + static LAYERS: Layers<3, 1> = &[ + [[Layer(1), NoOp, k(A)]], + [[NoOp, Layer(2), k(B)]], + [[ + NoOp, + NoOp, + TapDance(&crate::action::TapDance { + timeout: 100, + actions: &[&Trans, &k(X)], + config: TapDanceConfig::Eager, + }), + ]], + ]; + let mut layout = Layout::new(LAYERS); + + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 2)); + for _ in 0..10 { + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[B], layout.keycodes()); + } + layout.event(Release(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + #[test] + fn test_trans_in_multi() { + static LAYERS: Layers<3, 1> = &[ + [[Layer(1), NoOp, k(A)]], + [[NoOp, Layer(2), k(B)]], + [[NoOp, NoOp, MultipleActions(&[Trans, k(X)].as_slice())]], + ]; + let mut layout = Layout::new(LAYERS); + + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 2)); + for _ in 0..10 { + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[B, X], layout.keycodes()); + } + layout.event(Release(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + #[test] + fn test_trans_in_chords() { + const GROUP: ChordsGroup = ChordsGroup { + coords: &[((0, 2), 1), ((0, 3), 2)], + chords: &[(1, &Trans), (2, &Trans), (3, &KeyCode(X))], + timeout: 100, + }; + static LAYERS: Layers<4, 1> = &[ + [[Layer(1), NoOp, k(A), k(B)]], + [[NoOp, Layer(2), k(C), k(D)]], + [[NoOp, NoOp, Chords(&GROUP), Chords(&GROUP)]], + ]; + let mut layout = Layout::new(LAYERS); + + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 2)); + for _ in 0..10 { + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + layout.event(Release(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[C], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + #[test] + fn test_trans_in_fork() { + static LAYERS: Layers<3, 1> = &[ + [[Layer(1), NoOp, k(A)]], + [[NoOp, Layer(2), k(B)]], + [[ + NoOp, + NoOp, + Fork(&ForkConfig { + left: Trans, + right: Trans, + right_triggers: &[Space], + }), + ]], + ]; + let mut layout = Layout::new(LAYERS); + + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[B], layout.keycodes()); + layout.event(Release(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + #[test] + fn test_trans_in_switch() { + static LAYERS: Layers<3, 1> = &[ + [[Layer(1), NoOp, k(A)]], + [[NoOp, Layer(2), k(B)]], + [[ + NoOp, + NoOp, + Switch(&switch::Switch { + cases: &[(&[], &Trans, BreakOrFallthrough::Break)], + init_fn: None, + callbacks: &[], + }), + ]], + ]; + let mut layout = Layout::new(LAYERS); + layout.trans_resolution_behavior_v2 = true; + + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + + layout.event(Press(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[B], layout.keycodes()); + + layout.event(Release(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + #[test] + fn test_multiple_taphold_trans() { + static LAYERS: Layers<4, 1> = &[ + [[Layer(1), NoOp, NoOp, k(A)]], + [[ + NoOp, + Layer(2), + NoOp, + HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, + timeout: 50, + hold: k(B), + timeout_action: k(B), + tap: Trans, + config: HoldTapConfig::Default, + tap_hold_interval: 200, + }), + ]], + [[ + NoOp, + NoOp, + Layer(3), + HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, + timeout: 50, + hold: k(C), + timeout_action: k(C), + tap: Trans, + config: HoldTapConfig::Default, + tap_hold_interval: 200, + }), + ]], + [[ + NoOp, + NoOp, + NoOp, + HoldTap(&HoldTapAction { + on_press_reset_timeout_to: None, + require_prior_idle: None, + timeout: 50, + hold: k(D), + timeout_action: k(D), + tap: Trans, + config: HoldTapConfig::Default, + tap_hold_interval: 200, + }), + ]], + ]; + let mut layout = Layout::new(LAYERS); + + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 3)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Release(0, 3)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Release(0, 3)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[A], layout.keycodes()); + } + + #[test] + fn trans_in_multi_works_with_all_trans_settings() { + let permutations: &[(bool, bool)] = + &[(false, false), (false, true), (true, false), (true, true)]; + + for &(trans_v2, delegate_to_1st) in permutations { + static DEFSRC_LAYER: [Action; 3] = [NoOp, NoOp, k(X)]; + static LAYERS: Layers<3, 1> = &[ + [[ + Layer(1), + DefaultLayer(1), + MultipleActions(&[Trans, k(Y)].as_slice()), + ]], + [[NoOp, Layer(2), k(B)]], + [[NoOp, NoOp, Trans]], + ]; + for &do_layer_switch in &[false, true] { + let mut layout = Layout::new_with_trans_action_settings( + &DEFSRC_LAYER, + LAYERS, + trans_v2, + delegate_to_1st, + ); + + layout.event(Press(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[X, Y], layout.keycodes()); + layout.event(Release(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + + if do_layer_switch { + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Release(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + assert_eq!(layout.default_layer, 1); + } else { + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + + layout.event(Press(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[B], layout.keycodes()); + layout.event(Release(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys( + match (trans_v2, delegate_to_1st) { + (false, false) => &[X], + (false, true) => &[X, Y], + (true, _) => &[B], + }, + layout.keycodes(), + ); + layout.event(Release(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + } + } + + #[cfg(feature = "tap_hold_tracker")] + #[test] + fn hold_activated_is_set_on_hold_timeout() { + static LAYERS: Layers<1, 1> = &[[[HoldTap(&HoldTapAction { + timeout: 5, + hold: k(LAlt), + timeout_action: k(LAlt), + tap: k(Space), + config: HoldTapConfig::Default, + tap_hold_interval: 0, + on_press_reset_timeout_to: None, + require_prior_idle: None, + })]]]; + let mut layout = Layout::new(LAYERS); + // Nothing set initially. + assert!(layout.tap_hold_tracker.take_hold_activated().is_none()); + + layout.event(Press(0, 0)); + for _ in 0..6 { + let _ = layout.tick(); + } + // Hold action should be active. + assert_keys(&[LAlt], layout.keycodes()); + + // Flag should be set exactly once. + let info = layout + .tap_hold_tracker + .take_hold_activated() + .expect("hold_activated should be set"); + assert_eq!(info.coord, (0, 0)); + assert!(layout.tap_hold_tracker.take_hold_activated().is_none()); + } + + #[cfg(feature = "tap_hold_tracker")] + #[test] + fn tap_activated_is_set_on_tap_release() { + static LAYERS: Layers<2, 1> = &[[[ + HoldTap(&HoldTapAction { + timeout: 200, + hold: k(LAlt), + timeout_action: k(LAlt), + tap: k(Space), + config: HoldTapConfig::Default, + tap_hold_interval: 0, + on_press_reset_timeout_to: None, + require_prior_idle: None, + }), + k(A), + ]]]; + let mut layout = Layout::new(LAYERS); + assert!(layout.tap_hold_tracker.take_tap_activated().is_none()); + + // Quick press and release triggers tap. + layout.event(Press(0, 0)); + let _ = layout.tick(); + layout.event(Release(0, 0)); + let _ = layout.tick(); + + assert_keys(&[Space], layout.keycodes()); + let info = layout + .tap_hold_tracker + .take_tap_activated() + .expect("tap_activated should be set"); + assert_eq!(info.coord, (0, 0)); + assert!(layout.tap_hold_tracker.take_tap_activated().is_none()); + } + + #[cfg(feature = "tap_hold_tracker")] + #[test] + fn hold_activated_is_set_on_permissive_hold() { + static LAYERS: Layers<2, 1> = &[[[ + HoldTap(&HoldTapAction { + timeout: 200, + hold: k(LAlt), + timeout_action: k(LAlt), + tap: k(Space), + config: HoldTapConfig::PermissiveHold, + tap_hold_interval: 0, + on_press_reset_timeout_to: None, + require_prior_idle: None, + }), + k(A), + ]]]; + let mut layout = Layout::new(LAYERS); + assert!(layout.tap_hold_tracker.take_hold_activated().is_none()); + + // PermissiveHold: press hold-tap key, press another key, release it => hold. + layout.event(Press(0, 0)); + let _ = layout.tick(); + layout.event(Press(0, 1)); + let _ = layout.tick(); + layout.event(Release(0, 1)); + let _ = layout.tick(); + + assert_keys(&[LAlt], layout.keycodes()); + let info = layout + .tap_hold_tracker + .take_hold_activated() + .expect("hold_activated should be set via waiting_into_hold"); + assert_eq!(info.coord, (0, 0)); + assert!(layout.tap_hold_tracker.take_tap_activated().is_none()); + } + + #[cfg(feature = "tap_hold_tracker")] + #[test] + fn hold_activated_is_set_on_hold_on_other_key_press() { + static LAYERS: Layers<2, 1> = &[[[ + HoldTap(&HoldTapAction { + timeout: 200, + hold: k(LAlt), + timeout_action: k(LAlt), + tap: k(Space), + config: HoldTapConfig::HoldOnOtherKeyPress, + tap_hold_interval: 0, + on_press_reset_timeout_to: None, + require_prior_idle: None, + }), + k(A), + ]]]; + let mut layout = Layout::new(LAYERS); + assert!(layout.tap_hold_tracker.take_hold_activated().is_none()); + + // HoldOnOtherKeyPress: press hold-tap key, then press another key => hold. + layout.event(Press(0, 0)); + let _ = layout.tick(); + layout.event(Press(0, 1)); + let _ = layout.tick(); + + assert_keys(&[LAlt], layout.keycodes()); + let info = layout + .tap_hold_tracker + .take_hold_activated() + .expect("hold_activated should be set via waiting_into_hold"); + assert_eq!(info.coord, (0, 0)); + assert!(layout.tap_hold_tracker.take_tap_activated().is_none()); + } + + #[test] + fn chord_does_not_set_tap_hold_activated() { + const GROUP: ChordsGroup = ChordsGroup { + coords: &[((0, 0), 1), ((0, 1), 2)], + chords: &[(3, &KeyCode(Kb5))], + timeout: 100, + }; + static LAYERS: Layers<2, 1> = &[[[Chords(&GROUP), Chords(&GROUP)]]]; + + let mut layout = Layout::new(LAYERS); + layout.event(Press(0, 0)); + for _ in 0..50 { + let _ = layout.tick(); + } + layout.event(Press(0, 1)); + for _ in 0..50 { + let _ = layout.tick(); + } + assert_keys(&[Kb5], layout.keycodes()); + // Chord resolution must not set tap or hold activated flags. + assert!(layout.tap_hold_tracker.take_hold_activated().is_none()); + assert!(layout.tap_hold_tracker.take_tap_activated().is_none()); + } + + #[test] + fn tap_dance_does_not_set_tap_hold_activated() { + static LAYERS: Layers<2, 1> = &[[[ + TapDance(&crate::action::TapDance { + timeout: 100, + actions: &[&k(LShift), &k(LCtrl)], + config: TapDanceConfig::Lazy, + }), + k(A), + ]]]; + + let mut layout = Layout::new(LAYERS); + // Single tap, wait for timeout. + layout.event(Press(0, 0)); + for _ in 0..101 { + let _ = layout.tick(); + } + assert_keys(&[LShift], layout.keycodes()); + // Tap-dance resolution must not set tap or hold activated flags. + assert!(layout.tap_hold_tracker.take_hold_activated().is_none()); + assert!(layout.tap_hold_tracker.take_tap_activated().is_none()); + } } diff --git a/keyberon/src/layout/contextual_execution.rs b/keyberon/src/layout/contextual_execution.rs new file mode 100644 index 000000000..8049868a8 --- /dev/null +++ b/keyberon/src/layout/contextual_execution.rs @@ -0,0 +1,26 @@ +//! Information about what state the keyberon layout is in +//! and handling conditional execution based on state. + +use super::*; + +#[derive(Clone, Copy, Debug)] +pub(super) struct ContextualExecution { + /// Known pause case: + /// - When replicating output keys during chordv1 activation. + pub(super) pause_historical_keys_updates: bool, +} + +impl ContextualExecution { + pub(super) fn new() -> Self { + Self { + pause_historical_keys_updates: false, + } + } + + /// Push into historical keys while checking the pause state. + pub(super) fn push_historical_key(&self, h: &mut History, e: T) { + if !self.pause_historical_keys_updates { + h.push_front(e); + } + } +} diff --git a/keyberon/src/lib.rs b/keyberon/src/lib.rs index 78258ea05..bacf39abf 100644 --- a/keyberon/src/lib.rs +++ b/keyberon/src/lib.rs @@ -2,5 +2,8 @@ //! Please make contributions to the original project. pub mod action; +pub mod chord; pub mod key_code; pub mod layout; +mod multikey_buffer; +pub mod tap_hold_tracker; diff --git a/keyberon/src/multikey_buffer.rs b/keyberon/src/multikey_buffer.rs new file mode 100644 index 000000000..85dec84b1 --- /dev/null +++ b/keyberon/src/multikey_buffer.rs @@ -0,0 +1,87 @@ +//! Module for `MultiKeyBuffer`. + +use std::{array, slice}; + +use crate::action::{Action, ONE_SHOT_MAX_ACTIVE}; +use crate::key_code::KeyCode; + +// Presumably this should be plenty. +// ONE_SHOT_MAX_ACTIVE is already likely unreasonably large enough. +// This buffer capacity adds more onto that, +// just in case somebody finds a way to use all of the one-shot capacity. +const BUFCAP: usize = ONE_SHOT_MAX_ACTIVE + 4; + +/// This is an unsafe container that enables a mutable Action::MultipleKeyCodes. +pub(crate) struct MultiKeyBuffer<'a, T> { + buf: [KeyCode; BUFCAP], + size: usize, + ptr: *mut &'static [KeyCode], + ac: *mut Action<'a, T>, +} + +unsafe impl Send for MultiKeyBuffer<'_, T> {} + +impl<'a, T> MultiKeyBuffer<'a, T> { + /// Create a new instance of `MultiKeyBuffer`. + /// + /// # Safety + /// + /// The program should not have any references to the inner buffer when the struct is dropped. + pub(crate) unsafe fn new() -> Self { + Self { + buf: array::from_fn(|_| KeyCode::Escape), + size: 0, + ptr: Box::leak(Box::new(slice::from_raw_parts( + core::ptr::NonNull::dangling().as_ptr(), + 0, + ))), + ac: Box::leak(Box::new(Action::NoOp)), + } + } + + /// Set the current size of the buffer to zero. + /// + /// # Safety + /// + /// The program should not have any references to the inner buffer. + pub(crate) unsafe fn clear(&mut self) { + self.size = 0; + } + + /// Push to the end of the buffer. If the buffer is full, this silently fails. + /// + /// # Safety + /// + /// The program should not have any references to the inner buffer. + pub(crate) unsafe fn push(&mut self, kc: KeyCode) { + if self.size < BUFCAP { + self.buf[self.size] = kc; + self.size += 1; + } + } + + /// Get a reference to the inner buffer in the form of an `Action`. + /// The `Action` will be the variant `MultipleKeyCodes`, + /// containing all keys that have been pushed. + /// + /// # Safety + /// + /// The program should not have any references to the inner buffer before calling. + /// The program should not mutate the buffer after calling this function until after the + /// returned reference is dropped. + pub(crate) unsafe fn get_ref(&self) -> &'a Action<'a, T> { + *self.ac = Action::NoOp; + *self.ptr = slice::from_raw_parts(self.buf.as_ptr(), self.size); + *self.ac = Action::MultipleKeyCodes(&*self.ptr); + &*self.ac + } +} + +impl Drop for MultiKeyBuffer<'_, T> { + fn drop(&mut self) { + unsafe { + drop(Box::from_raw(self.ac)); + drop(Box::from_raw(self.ptr)); + } + } +} diff --git a/keyberon/src/tap_hold_tracker.rs b/keyberon/src/tap_hold_tracker.rs new file mode 100644 index 000000000..e3797f063 --- /dev/null +++ b/keyberon/src/tap_hold_tracker.rs @@ -0,0 +1,119 @@ +//! Tracks tap-hold activation events for external consumers (e.g. TCP broadcast). +//! +//! When the `tap_hold_tracker` feature is enabled, this module stores the +//! coordinate of the most recent hold/tap activation so that higher-level code +//! can relay it over the network. When the feature is disabled the tracker is +//! a zero-sized no-op — all setters are empty and all getters return `None`. +//! +//! The `config` parameter on the setters accepts a `&WaitingConfig` reference; +//! the `matches!` guard lives inside the method body so that the no-op stub's +//! empty body causes the compiler to eliminate the call entirely. + +#[cfg(feature = "tap_hold_tracker")] +mod inner { + use crate::layout::{KCoord, WaitingConfig}; + + /// Information about a tap-hold key that just transitioned to hold state. + #[derive(Debug, Clone, Copy)] + pub struct HoldActivatedInfo { + /// The key coordinate (row, column). + pub coord: KCoord, + } + + /// Information about a tap-hold key that just triggered its tap action. + #[derive(Debug, Clone, Copy)] + pub struct TapActivatedInfo { + /// The key coordinate (row, column). + pub coord: KCoord, + } + + /// Records the most recent tap-hold activation event. + #[derive(Debug, Default)] + pub struct TapHoldTracker { + hold_activated: Option, + tap_activated: Option, + } + + impl TapHoldTracker { + pub(crate) fn set_hold_activated<'a, T: std::fmt::Debug>( + &mut self, + coord: KCoord, + config: &WaitingConfig<'a, T>, + ) { + if matches!(config, WaitingConfig::HoldTap(..)) { + self.hold_activated = Some(HoldActivatedInfo { coord }); + } + } + + pub(crate) fn set_tap_activated<'a, T: std::fmt::Debug>( + &mut self, + coord: KCoord, + config: &WaitingConfig<'a, T>, + ) { + if matches!(config, WaitingConfig::HoldTap(..)) { + self.tap_activated = Some(TapActivatedInfo { coord }); + } + } + + pub fn take_hold_activated(&mut self) -> Option { + self.hold_activated.take() + } + + pub fn take_tap_activated(&mut self) -> Option { + self.tap_activated.take() + } + } +} + +#[cfg(not(feature = "tap_hold_tracker"))] +mod inner { + use crate::layout::{KCoord, WaitingConfig}; + + /// Stub: no coordinate data stored when the feature is disabled. + #[derive(Debug, Clone, Copy)] + pub struct HoldActivatedInfo { + /// The key coordinate (row, column). + pub coord: KCoord, + } + + /// Stub: no coordinate data stored when the feature is disabled. + #[derive(Debug, Clone, Copy)] + pub struct TapActivatedInfo { + /// The key coordinate (row, column). + pub coord: KCoord, + } + + /// Zero-sized no-op tracker when the feature is disabled. + #[derive(Debug, Default)] + pub struct TapHoldTracker; + + impl TapHoldTracker { + #[inline(always)] + pub(crate) fn set_hold_activated<'a, T: std::fmt::Debug>( + &mut self, + _coord: KCoord, + _config: &WaitingConfig<'a, T>, + ) { + } + + #[inline(always)] + pub(crate) fn set_tap_activated<'a, T: std::fmt::Debug>( + &mut self, + _coord: KCoord, + _config: &WaitingConfig<'a, T>, + ) { + } + + #[inline(always)] + pub fn take_hold_activated(&mut self) -> Option { + None + } + + #[inline(always)] + pub fn take_tap_activated(&mut self) -> Option { + None + } + } +} + +pub use inner::*; diff --git a/myfile b/myfile new file mode 100644 index 000000000..e9944e6b6 Binary files /dev/null and b/myfile differ diff --git a/parser/.gitignore b/parser/.gitignore index 62e2af0b8..cb4f2dd90 100644 --- a/parser/.gitignore +++ b/parser/.gitignore @@ -1,2 +1,3 @@ # Ignore Cargo.lock since this is a library crate Cargo.lock +target \ No newline at end of file diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 91c3c20c7..699ed5ba5 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,32 +1,38 @@ [package] name = "kanata-parser" -version = "0.1.0" +version = "0.1120.1" authors = ["jtroo "] description = "A parser for configuration language of kanata, a keyboard remapper." keywords = ["kanata", "parser"] homepage = "https://github.com/jtroo/kanata" repository = "https://github.com/jtroo/kanata" readme = "README.md" -license = "LGPL-3.0" +license = "LGPL-3.0-only" edition = "2021" [dependencies] -log = { version = "0.4.8", default_features = false } anyhow = "1" -parking_lot = "0.12" +bitflags = "2.5.0" +bytemuck = "1.15.0" +log = { version = "0.4.8", default-features = false } +miette = { version = "5.7.0", features = ["fancy"] } once_cell = "1" -radix_trie = "0.2" +ordered-float = "5.1.0" +parking_lot = "0.12" +patricia_tree = "0.9" rustc-hash = "1.1.0" -miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -kanata-keyberon = { path = "../keyberon" } +kanata-keyberon = { path = "../keyberon", version = "0.1120.1" } + +[dev-dependencies] +simplelog = "0.12.0" [features] cmd = [] interception_driver = [] - -[target.'cfg(target_os = "linux")'.dependencies] -evdev = "=0.12.0" - -[target.'cfg(target_os = "windows")'.dependencies] +gui = [] +lsp = [] +win_llhook_read_scancodes = [] +win_sendinput_send_scancodes = [] +zippychord = [] diff --git a/parser/README.md b/parser/README.md index 9e0b759a4..3297d6fbf 100644 --- a/parser/README.md +++ b/parser/README.md @@ -1,3 +1,5 @@ # kanata-parser A parser for configuration language of [kanata](https://github.com/jtroo/kanata). + +This crate does not follow semver. It tracks the version of kanata. diff --git a/parser/src/cfg/alloc.rs b/parser/src/cfg/alloc.rs index f0d520a6f..333ae35d7 100644 --- a/parser/src/cfg/alloc.rs +++ b/parser/src/cfg/alloc.rs @@ -11,8 +11,19 @@ use std::sync::Arc; /// /// In practice, this is not difficult to do in the `cfg` module which only exposes a single public /// method. +/// +/// To avoid leaks, types transformed to &'static by this struct +/// should not contain nested allocations, +/// or if they do, the nested allocations should also +/// be managed by this struct. pub(crate) struct Allocations { - allocations: Mutex>, + allocations: Mutex>, +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct Allocation { + ptr: usize, + len: usize, } impl std::fmt::Debug for Allocations { @@ -27,8 +38,14 @@ impl Drop for Allocations { "freeing allocations of length {}", self.allocations.lock().len() ); - for p in self.allocations.lock().iter().rev().copied() { - unsafe { drop(Box::from_raw(p as *mut usize)) }; + for a in self.allocations.lock().iter().rev().copied() { + log::debug!("freeing ptr 0x{:x} len{}", a.ptr, a.len); + unsafe { + drop(Box::<[u8]>::from_raw(std::ptr::slice_from_raw_parts_mut( + a.ptr as *mut u8, + a.len, + ))) + }; } } } @@ -40,48 +57,63 @@ impl Allocations { /// /// Ensure that all associated allocations are no longer referenced before dropping all /// clones of the `Arc`. - pub(super) unsafe fn new() -> Arc { + pub(crate) unsafe fn new() -> Arc { Arc::new(Self { allocations: Mutex::new(vec![]), }) } - /// Returns a `&'static T` by leaking the existing box. - pub(super) fn bref(&self, v: Box) -> &'static T { - let p = Box::into_raw(v); - if (p as usize) < 16 { - panic!("bref bad ptr"); - } - self.allocations.lock().push(p as usize); - Box::leak(unsafe { Box::from_raw(p) }) - } - /// Returns a `&'static T` by leaking a newly created Box of `v`. - pub(super) fn sref(&self, v: T) -> &'static T { + pub(crate) fn sref(&self, v: T) -> &'static T { let p = Box::into_raw(Box::new(v)); - if (p as usize) < 16 { - panic!("sref bad ptr"); - } - self.allocations.lock().push(p as usize); + log::debug!( + "sref type: {}, ptr:{p:?} sz:{}", + std::any::type_name::(), + std::mem::size_of::() + ); + self.allocations.lock().push(Allocation { + ptr: p as usize, + len: std::mem::size_of::(), + }); Box::leak(unsafe { Box::from_raw(p) }) } - fn bref_slice(&self, v: Box<[T]>) -> &'static [T] { + pub(crate) fn bref_slice(&self, v: Box<[T]>) -> &'static [T] { // An empty slice has no backing allocation. `Box<[T]>` is a fat pointer so the leaked return // will contain a length of 0 and an invalid pointer. if !v.is_empty() { - self.allocations.lock().push(v.as_ptr() as usize); + let p = v.as_ptr(); + log::debug!( + "bref_slice type: {}, ptr:{p:?} sz:{}", + std::any::type_name::(), + std::mem::size_of::() + ); + self.allocations.lock().push(Allocation { + ptr: p as usize, + len: std::mem::size_of::() * v.len(), + }); } Box::leak(v) } /// Returns a &'static [&'static T] from a `Vec` by converting to a boxed slice and leaking it. - pub(super) fn sref_vec(&self, v: Vec) -> &'static [T] { + pub(crate) fn sref_vec(&self, v: Vec) -> &'static [T] { + log::debug!("sref_vec {}", std::any::type_name::()); self.bref_slice(v.into_boxed_slice()) } - /// Returns a `&'static [&'static T]` by leaking a newly created box and boxed slice of `v`. - pub(super) fn sref_slice(&self, v: T) -> &'static [&'static T] { - self.bref_slice(vec![self.sref(v)].into_boxed_slice()) + /// Returns a `&'static str` by leaking a String. + pub(crate) fn sref_str(&self, v: String) -> &'static str { + if !v.capacity() == 0 { + "" + } else { + let len = v.len(); + let s = v.leak(); + self.allocations.lock().push(Allocation { + ptr: s.as_ptr() as usize, + len, + }); + s + } } } diff --git a/parser/src/cfg/arbitrary_code.rs b/parser/src/cfg/arbitrary_code.rs new file mode 100644 index 000000000..118f4029f --- /dev/null +++ b/parser/src/cfg/arbitrary_code.rs @@ -0,0 +1,20 @@ +use super::*; + +use crate::bail; +use anyhow::anyhow; + +pub(crate) fn parse_arbitrary_code( + ac_params: &[SExpr], + s: &ParserState, +) -> Result<&'static KanataAction> { + const ERR_MSG: &str = "arbitrary code expects one parameter: "; + if ac_params.len() != 1 { + bail!("{ERR_MSG}"); + } + let code = ac_params[0] + .atom(s.vars()) + .map(str::parse::) + .and_then(|c| c.ok()) + .ok_or_else(|| anyhow!("{ERR_MSG}: got {:?}", ac_params[0]))?; + custom(CustomAction::SendArbitraryCode(code), &s.a) +} diff --git a/parser/src/cfg/caps_word.rs b/parser/src/cfg/caps_word.rs new file mode 100644 index 000000000..5c88a04aa --- /dev/null +++ b/parser/src/cfg/caps_word.rs @@ -0,0 +1,110 @@ +use super::*; + +use crate::bail; + +pub(crate) fn parse_caps_word( + ac_params: &[SExpr], + repress_behaviour: CapsWordRepressBehaviour, + s: &ParserState, +) -> Result<&'static KanataAction> { + const ERR_STR: &str = "caps-word expects 1 param: "; + if ac_params.len() != 1 { + bail!("{ERR_STR}\nFound {} params instead of 1", ac_params.len()); + } + let timeout = parse_non_zero_u16(&ac_params[0], s, "timeout")?; + custom( + CustomAction::CapsWord(CapsWordCfg { + repress_behaviour, + keys_to_capitalize: &[ + KeyCode::A, + KeyCode::B, + KeyCode::C, + KeyCode::D, + KeyCode::E, + KeyCode::F, + KeyCode::G, + KeyCode::H, + KeyCode::I, + KeyCode::J, + KeyCode::K, + KeyCode::L, + KeyCode::M, + KeyCode::N, + KeyCode::O, + KeyCode::P, + KeyCode::Q, + KeyCode::R, + KeyCode::S, + KeyCode::T, + KeyCode::U, + KeyCode::V, + KeyCode::W, + KeyCode::X, + KeyCode::Y, + KeyCode::Z, + KeyCode::Minus, + ], + keys_nonterminal: &[ + KeyCode::Kb0, + KeyCode::Kb1, + KeyCode::Kb2, + KeyCode::Kb3, + KeyCode::Kb4, + KeyCode::Kb5, + KeyCode::Kb6, + KeyCode::Kb7, + KeyCode::Kb8, + KeyCode::Kb9, + KeyCode::Kp0, + KeyCode::Kp1, + KeyCode::Kp2, + KeyCode::Kp3, + KeyCode::Kp4, + KeyCode::Kp5, + KeyCode::Kp6, + KeyCode::Kp7, + KeyCode::Kp8, + KeyCode::Kp9, + KeyCode::BSpace, + KeyCode::Delete, + KeyCode::Up, + KeyCode::Down, + KeyCode::Left, + KeyCode::Right, + ], + timeout, + }), + &s.a, + ) +} + +pub(crate) fn parse_caps_word_custom( + ac_params: &[SExpr], + repress_behaviour: CapsWordRepressBehaviour, + s: &ParserState, +) -> Result<&'static KanataAction> { + const ERR_STR: &str = "caps-word-custom expects 3 param: "; + if ac_params.len() != 3 { + bail!("{ERR_STR}\nFound {} params instead of 3", ac_params.len()); + } + let timeout = parse_non_zero_u16(&ac_params[0], s, "timeout")?; + custom( + CustomAction::CapsWord(CapsWordCfg { + repress_behaviour, + keys_to_capitalize: s.a.sref_vec( + parse_key_list(&ac_params[1], s, "keys-to-capitalize")? + .into_iter() + .map(KeyCode::from) + .collect(), + ), + keys_nonterminal: s.a.sref_vec( + parse_key_list(&ac_params[2], s, "extra-non-terminal-keys")? + .into_iter() + .map(KeyCode::from) + .collect(), + ), + timeout, + }), + &s.a, + ) +} diff --git a/parser/src/cfg/chord.rs b/parser/src/cfg/chord.rs new file mode 100644 index 000000000..9c88bb760 --- /dev/null +++ b/parser/src/cfg/chord.rs @@ -0,0 +1,342 @@ +use kanata_keyberon::chord::{ChordV2, ChordsForKey, ChordsForKeys, ReleaseBehaviour}; +use rustc_hash::{FxHashMap, FxHashSet}; + +use std::fs; + +use crate::{anyhow_expr, bail_expr}; + +use super::*; + +pub(crate) fn parse_defchordv2( + exprs: &[SExpr], + s: &ParserState, +) -> Result> { + if exprs[0].atom(None).expect("should be atom") == "defchordsv2-experimental" { + log::warn!( + "You should replace defchordsv2-experimental with defchordsv2.\n\ + Using -experimental will be invalid in the future." + ); + } + + let mut chunks = exprs[1..].chunks_exact(5); + let mut chords_container = ChordsForKeys::<'static, KanataCustom> { + mapping: FxHashMap::default(), + }; + + let mut all_participating_key_sets = FxHashSet::default(); + + let all_chords = chunks + .by_ref() + .flat_map(|chunk| match chunk[0] { + // Match a line like + // (include filename.txt) () 100 all-released (layer1 layer2) + SExpr::List(Spanned { + t: ref exprs, + span: _, + }) if matches!(exprs.first(), Some(SExpr::Atom(a)) if a.t == "include") => { + let file_name = exprs[1].atom(s.vars()).unwrap(); + let chord_translation = ChordTranslation::create( + file_name, + &chunk[2], + &chunk[3], + &chunk[4], + &s.layers[0][0], + ); + let chord_definitions = parse_chord_file(file_name).unwrap(); + let processed = chord_definitions.iter().map(|chord_def| { + let chunk = chord_translation.translate_chord(chord_def); + parse_single_chord(&chunk, s, &mut all_participating_key_sets) + }); + Ok::<_, ParseError>(processed.collect::>()) + } + _ => Ok(vec![parse_single_chord( + chunk, + s, + &mut all_participating_key_sets, + )]), + }) + .flat_map(|vec_result| vec_result.into_iter()) + .collect::>>(); + let unsuccessful = all_chords + .iter() + .filter_map(|r| r.as_ref().err()) + .collect::>(); + if let Some(e) = unsuccessful.first() { + return Err((*e).clone()); + } + + let successful = all_chords + .into_iter() + .filter_map(Result::ok) + .collect::>(); + for chord in successful { + for pkey in chord.participating_keys.iter().copied() { + //log::trace!("chord for key:{pkey:?} > {chord:?}"); + chords_container + .mapping + .entry(pkey) + .or_insert(ChordsForKey { chords: vec![] }) + .chords + .push(s.a.sref(chord.clone())); + } + } + let rem = chunks.remainder(); + if !rem.is_empty() { + bail_expr!( + rem.last().unwrap(), + "Incomplete chord entry. Each chord entry must have 5 items:\n\ + participating-keys, action, timeout, release-type, disabled-layers" + ); + } + Ok(chords_container) +} + +fn parse_single_chord( + chunk: &[SExpr], + s: &ParserState, + all_participating_key_sets: &mut FxHashSet>, +) -> Result> { + let participants = parse_participating_keys(&chunk[0], s)?; + if !all_participating_key_sets.insert(participants.clone()) { + bail_expr!( + &chunk[0], + "Duplicate participating-keys, key sets may be used only once." + ); + } + let action = parse_action(&chunk[1], s)?; + let timeout = parse_timeout(&chunk[2], s)?; + let release_behaviour = parse_release_behaviour(&chunk[3], s)?; + let disabled_layers = parse_disabled_layers(&chunk[4], s)?; + let chord: ChordV2<'static, KanataCustom> = ChordV2 { + action, + participating_keys: s.a.sref_vec(participants.clone()), + pending_duration: timeout, + disabled_layers: s.a.sref_vec(disabled_layers), + release_behaviour, + }; + Ok(s.a.sref(chord).clone()) +} + +fn parse_participating_keys(keys: &SExpr, s: &ParserState) -> Result> { + let mut participants = keys + .list(s.vars()) + .map(|l| { + l.iter() + .try_fold(vec![], |mut keys, key| -> Result> { + let k = key.atom(s.vars()).and_then(str_to_oscode).ok_or_else(|| { + anyhow_expr!( + key, + "The first chord item must be a list of keys.\nInvalid key name." + ) + })?; + keys.push(k.into()); + Ok(keys) + }) + }) + .ok_or_else(|| anyhow_expr!(keys, "The first chord item must be a list of keys."))??; + if participants.len() < 2 { + bail_expr!(keys, "The minimum number of participating chord keys is 2"); + } + participants.sort(); + Ok(participants) +} + +fn parse_timeout(chunk: &SExpr, s: &ParserState) -> Result { + let timeout = parse_non_zero_u16(chunk, s, "chord timeout")?; + Ok(timeout) +} + +fn parse_release_behaviour( + release_behaviour_string: &SExpr, + s: &ParserState, +) -> Result { + let release_behaviour = release_behaviour_string + .atom(s.vars()) + .and_then(|r| { + Some(match r { + "first-release" => ReleaseBehaviour::OnFirstRelease, + "all-released" => ReleaseBehaviour::OnLastRelease, + _ => return None, + }) + }) + .ok_or_else(|| { + anyhow_expr!( + release_behaviour_string, + "Chord release behaviour must be one of:\n\ + first-release | all-released" + ) + })?; + Ok(release_behaviour) +} + +fn parse_disabled_layers(disabled_layers: &SExpr, s: &ParserState) -> Result> { + let disabled_layers = disabled_layers + .list(s.vars()) + .map(|dl| { + dl.iter() + .try_fold(vec![], |mut layers, layer| -> Result> { + let l_idx = layer + .atom(s.vars()) + .and_then(|l| s.layer_idxs.get(l)) + .ok_or_else(|| anyhow_expr!(layer, "Not a known layer name."))?; + layers.push((*l_idx) as u16); + Ok(layers) + }) + }) + .ok_or_else(|| { + anyhow_expr!( + disabled_layers, + "Disabled layers must be a list of layer names" + ) + })??; + Ok(disabled_layers) +} + +fn parse_chord_file(file_name: &str) -> Result> { + let input_data = + fs::read_to_string(file_name).unwrap_or_else(|_| panic!("Unable to read file {file_name}")); + let parsed_chords = parse_input(&input_data).unwrap(); + Ok(parsed_chords) +} + +fn parse_input(input: &str) -> Result> { + input + .lines() + .filter(|line| !line.trim().is_empty() && !line.trim().starts_with("//")) + .map(|line| { + let mut caps = line.split('\t'); + let error_message = format!( + "Each line needs to have an action separated by a tab character, got '{line}'" + ); + let keys = caps.next().expect(&error_message); + let action = caps.next().expect(&error_message); + Ok(ChordDefinition { + keys: keys.to_string(), + action: action.to_string(), + }) + }) + .collect() +} + +#[derive(Debug)] +struct ChordDefinition { + keys: String, + action: String, +} + +struct ChordTranslation<'a> { + file_name: &'a str, + target_map: FxHashMap, + postprocess_map: FxHashMap, + timeout: &'a SExpr, + release_behaviour: &'a SExpr, + disabled_layers: &'a SExpr, +} + +impl<'a> ChordTranslation<'a> { + fn create( + file_name: &'a str, + timeout: &'a SExpr, + release_behaviour: &'a SExpr, + disabled_layers: &'a SExpr, + first_layer: &[Action<'static, KanataCustom>], + ) -> Self { + let postprocess_map: FxHashMap = [ + ("semicolon", ";"), + ("colon", "S-."), + ("slash", "/"), + ("apostrophe", "'"), + ("dot", "."), + (" ", "spc"), + ] + .iter() + .cloned() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let target_map = first_layer + .iter() + .enumerate() + .filter_map(|(idx, layout)| { + layout + .key_codes() + .next() + .map(|kc| kc.to_string().to_lowercase()) + .zip( + idx.try_into() + .ok() + .and_then(OsCode::from_u16) + .map(|osc| osc.to_string().to_lowercase()), + ) + }) + .collect::>() + .into_iter() + .chain(vec![(" ".to_string(), "spc".to_string())]) + .collect::>(); + ChordTranslation { + file_name, + target_map, + postprocess_map, + timeout, + release_behaviour, + disabled_layers, + } + } + + fn post_process(&self, converted: &str) -> String { + self.postprocess_map + .get(converted) + .map(|c| c.to_string()) + .unwrap_or_else(|| { + if converted.chars().all(|c| c.is_uppercase()) { + format!("S-{}", converted.to_lowercase()) + } else { + converted.to_string() + } + }) + } + + fn participant_keys(&self, keys: &str) -> Vec { + keys.chars() + .map(|key| { + self.target_map + .get(key.to_string().to_lowercase().as_str()) + .map(|c| self.postprocess_map.get(c).unwrap_or(c).to_string()) + .unwrap_or_else(|| key.to_string()) + }) + .collect::>() + } + + fn action(&self, action: &str) -> Vec { + let mut action_strings = action + .chars() + .map(|c| self.post_process(&c.to_string())) + .collect::>(); + // Wait 50ms for one-shot Shift to release + // TODO: This would be better handled by a (multi (release-key lsft)(release-key rsft)) + // but I haven't gotten that to work yet. + action_strings.insert(1, "50".to_string()); + action_strings.extend_from_slice(&[ + "sldr".to_string(), + "spc".to_string(), + "nop0".to_string(), + ]); + action_strings + } + + fn translate_chord(&self, chord_def: &ChordDefinition) -> Vec { + let sexpr_string = format!( + "(({}) (macro {}))", + self.participant_keys(&chord_def.keys).join(" "), + self.action(&chord_def.action).join(" ") + ); + let mut participant_action = sexpr::parse(&sexpr_string, self.file_name).unwrap()[0] + .t + .clone(); + participant_action.extend_from_slice(&[ + self.timeout.clone(), + self.release_behaviour.clone(), + self.disabled_layers.clone(), + ]); + participant_action + } +} diff --git a/parser/src/cfg/chord_v1.rs b/parser/src/cfg/chord_v1.rs new file mode 100644 index 000000000..534dca843 --- /dev/null +++ b/parser/src/cfg/chord_v1.rs @@ -0,0 +1,352 @@ +use super::*; + +use crate::anyhow_expr; +use crate::anyhow_span; +use crate::bail; +use crate::bail_expr; +use crate::bail_span; +use crate::err_expr; + +#[derive(Debug, Clone)] +pub(crate) struct ChordGroup { + id: u16, + name: String, + keys: Vec, + coords: Vec<((u8, u16), ChordKeys)>, + chords: HashMap, + timeout: u16, +} + +pub(crate) fn parse_chord(ac_params: &[SExpr], s: &ParserState) -> Result<&'static KanataAction> { + const ERR_MSG: &str = "Action chord expects a chords group name followed by an identifier"; + if ac_params.len() != 2 { + bail!(ERR_MSG); + } + + let name = ac_params[0] + .atom(s.vars()) + .ok_or_else(|| anyhow_expr!(&ac_params[0], "{ERR_MSG}"))?; + let group = match s.chord_groups.get(name) { + Some(t) => t, + None => bail_expr!(&ac_params[0], "Referenced unknown chord group: {}.", name), + }; + let chord_key_index = ac_params[1] + .atom(s.vars()) + .map(|s| match group.keys.iter().position(|e| e == s) { + Some(i) => Ok(i), + None => err_expr!( + &ac_params[1], + r#"Identifier "{}" is not used in chord group "{}"."#, + &s, + name, + ), + }) + .ok_or_else(|| anyhow_expr!(&ac_params[0], "{ERR_MSG}"))??; + let chord_keys: u128 = 1 << chord_key_index; + + // We don't yet know at this point what the entire chords group will look like nor at which + // coords this action will end up. So instead we store a dummy action which will be properly + // resolved in `resolve_chord_groups`. + Ok(s.a.sref(Action::Chords(s.a.sref(ChordsGroup { + timeout: group.timeout, + coords: s.a.sref_vec(vec![((0, group.id), chord_keys)]), + chords: s.a.sref_vec(vec![]), + })))) +} + +pub(crate) fn parse_chord_groups( + exprs: &[&Spanned>], + s: &mut ParserState, +) -> Result<()> { + const MSG: &str = "Incorrect number of elements found in defchords.\nThere should be the group name, followed by timeout, followed by keys-action pairs"; + for expr in exprs { + let mut subexprs = check_first_expr(expr.t.iter(), "defchords")?; + let name = subexprs + .next() + .and_then(|e| e.atom(s.vars())) + .ok_or_else(|| anyhow_span!(expr, "{MSG}"))? + .to_owned(); + let timeout = match subexprs.next() { + Some(e) => parse_non_zero_u16(e, s, "timeout")?, + None => bail_span!(expr, "{MSG}"), + }; + let id = match s.chord_groups.len().try_into() { + Ok(id) => id, + Err(_) => bail_span!(expr, "Maximum number of chord groups exceeded."), + }; + let mut group = ChordGroup { + id, + name: name.clone(), + keys: Vec::new(), + coords: Vec::new(), + chords: HashMap::default(), + timeout, + }; + // Read k-v pairs from the configuration + while let Some(keys_expr) = subexprs.next() { + let action = match subexprs.next() { + Some(v) => v, + None => bail_expr!( + keys_expr, + "Key list found without action - add an action for this chord" + ), + }; + let mut keys = keys_expr + .list(s.vars()) + .map(|keys| { + keys.iter().map(|key| { + key.atom(s.vars()).ok_or_else(|| { + anyhow_expr!( + key, + "Chord keys cannot be lists. Invalid key name: {:?}", + key + ) + }) + }) + }) + .ok_or_else(|| anyhow_expr!(keys_expr, "Chord must be a list/set of keys"))?; + let mask: u128 = keys.try_fold(0, |mask, key| { + let key = key?; + let index = match group.keys.iter().position(|k| k == key) { + Some(i) => i, + None => { + let i = group.keys.len(); + if i + 1 > MAX_CHORD_KEYS { + bail_expr!(keys_expr, "Maximum number of keys in a chords group ({MAX_CHORD_KEYS}) exceeded - found {}", i + 1); + } + group.keys.push(key.to_owned()); + i + } + }; + Ok(mask | (1 << index)) + })?; + if group.chords.insert(mask, action.clone()).is_some() { + bail_expr!(keys_expr, "Duplicate chord in group {name}"); + } + } + if s.chord_groups.insert(name.to_owned(), group).is_some() { + bail_span!(expr, "Duplicate chords group: {}", name); + } + } + Ok(()) +} + +pub(crate) fn resolve_chord_groups(layers: &mut IntermediateLayers, s: &ParserState) -> Result<()> { + let mut chord_groups = s.chord_groups.values().cloned().collect::>(); + chord_groups.sort_by_key(|group| group.id); + + for layer in layers.iter() { + for (i, row) in layer.iter().enumerate() { + for (j, cell) in row.iter().enumerate() { + find_chords_coords(&mut chord_groups, (i as u8, j as u16), cell); + } + } + } + + let chord_groups = chord_groups.into_iter().map(|group| { + // Check that all keys in the chord group have been assigned to some coordinate + for (key_index, key) in group.keys.iter().enumerate() { + let key_mask = 1 << key_index; + if !group.coords.iter().any(|(_, keys)| keys & key_mask != 0) { + bail!("coord group `{0}` defines unused key `{1}`, did you forget to bind `(chord {0} {1})`?", group.name, key) + } + } + + let chords = group.chords.iter().map(|(mask, action)| { + Ok((*mask, parse_action(action, s)?)) + }).collect::>>()?; + + Ok(s.a.sref(ChordsGroup { + coords: s.a.sref_vec(group.coords), + chords: s.a.sref_vec(chords), + timeout: group.timeout, + })) + }).collect::>>()?; + + for layer in layers.iter_mut() { + for row in layer.iter_mut() { + for cell in row.iter_mut() { + if let Some(action) = fill_chords(&chord_groups, cell, s) { + *cell = action; + } + } + } + } + + Ok(()) +} + +pub(crate) fn find_chords_coords( + chord_groups: &mut [ChordGroup], + coord: (u8, u16), + action: &KanataAction, +) { + match action { + Action::Chords(ChordsGroup { coords, .. }) => { + for ((_, group_id), chord_keys) in coords.iter() { + let group = &mut chord_groups[*group_id as usize]; + group.coords.push((coord, *chord_keys)); + } + } + Action::NoOp + | Action::Trans + | Action::Src + | Action::Repeat + | Action::KeyCode(_) + | Action::MultipleKeyCodes(_) + | Action::Layer(_) + | Action::DefaultLayer(_) + | Action::Sequence { .. } + | Action::RepeatableSequence { .. } + | Action::CancelSequences + | Action::ReleaseState(_) + | Action::OneShotIgnoreEventsTicks(_) + | Action::Custom(_) => {} + Action::HoldTap(HoldTapAction { tap, hold, .. }) => { + find_chords_coords(chord_groups, coord, tap); + find_chords_coords(chord_groups, coord, hold); + } + Action::OneShot(OneShot { action: ac, .. }) => { + find_chords_coords(chord_groups, coord, ac); + } + Action::MultipleActions(actions) => { + for ac in actions.iter() { + find_chords_coords(chord_groups, coord, ac); + } + } + Action::TapDance(TapDance { actions, .. }) => { + for ac in actions.iter() { + find_chords_coords(chord_groups, coord, ac); + } + } + Action::Fork(ForkConfig { left, right, .. }) => { + find_chords_coords(chord_groups, coord, left); + find_chords_coords(chord_groups, coord, right); + } + Action::Switch(Switch { cases, .. }) => { + for case in cases.iter() { + find_chords_coords(chord_groups, coord, case.1); + } + } + } +} + +pub(crate) fn fill_chords( + chord_groups: &[&'static ChordsGroup], + action: &KanataAction, + s: &ParserState, +) -> Option { + match action { + Action::Chords(ChordsGroup { coords, .. }) => { + let ((_, group_id), _) = coords + .iter() + .next() + .expect("unresolved chords should have exactly one entry"); + Some(Action::Chords(chord_groups[*group_id as usize])) + } + Action::NoOp + | Action::Trans + | Action::Repeat + | Action::Src + | Action::KeyCode(_) + | Action::MultipleKeyCodes(_) + | Action::Layer(_) + | Action::DefaultLayer(_) + | Action::Sequence { .. } + | Action::RepeatableSequence { .. } + | Action::CancelSequences + | Action::ReleaseState(_) + | Action::OneShotIgnoreEventsTicks(_) + | Action::Custom(_) => None, + Action::HoldTap(&hta @ HoldTapAction { tap, hold, .. }) => { + let new_tap = fill_chords(chord_groups, &tap, s); + let new_hold = fill_chords(chord_groups, &hold, s); + if new_tap.is_some() || new_hold.is_some() { + Some(Action::HoldTap(s.a.sref(HoldTapAction { + hold: new_hold.unwrap_or(hold), + tap: new_tap.unwrap_or(tap), + ..hta + }))) + } else { + None + } + } + Action::OneShot(&os @ OneShot { action: ac, .. }) => { + fill_chords(chord_groups, ac, s).map(|ac| { + Action::OneShot(s.a.sref(OneShot { + action: s.a.sref(ac), + ..os + })) + }) + } + Action::MultipleActions(actions) => { + let new_actions = actions + .iter() + .map(|ac| fill_chords(chord_groups, ac, s)) + .collect::>(); + if new_actions.iter().any(|it| it.is_some()) { + let new_actions = new_actions + .iter() + .zip(**actions) + .map(|(new_ac, ac)| new_ac.unwrap_or(*ac)) + .collect::>(); + Some(Action::MultipleActions(s.a.sref(s.a.sref_vec(new_actions)))) + } else { + None + } + } + Action::TapDance(&td @ TapDance { actions, .. }) => { + let new_actions = actions + .iter() + .map(|ac| fill_chords(chord_groups, ac, s)) + .collect::>(); + if new_actions.iter().any(|it| it.is_some()) { + let new_actions = new_actions + .iter() + .zip(actions) + .map(|(new_ac, ac)| new_ac.map(|v| s.a.sref(v)).unwrap_or(*ac)) + .collect::>(); + Some(Action::TapDance(s.a.sref(TapDance { + actions: s.a.sref_vec(new_actions), + ..td + }))) + } else { + None + } + } + Action::Fork(&fcfg @ ForkConfig { left, right, .. }) => { + let new_left = fill_chords(chord_groups, &left, s); + let new_right = fill_chords(chord_groups, &right, s); + if new_left.is_some() || new_right.is_some() { + Some(Action::Fork(s.a.sref(ForkConfig { + left: new_left.unwrap_or(left), + right: new_right.unwrap_or(right), + ..fcfg + }))) + } else { + None + } + } + Action::Switch(Switch { + cases, + init_fn, + callbacks, + }) => { + let mut new_cases = vec![]; + for case in cases.iter() { + new_cases.push(( + case.0, + fill_chords(chord_groups, case.1, s) + .map(|ac| s.a.sref(ac)) + .unwrap_or(case.1), + case.2, + )); + } + Some(Action::Switch(s.a.sref(Switch { + cases: s.a.sref_vec(new_cases), + init_fn: *init_fn, + callbacks, + }))) + } + } +} diff --git a/parser/src/cfg/clipboard.rs b/parser/src/cfg/clipboard.rs new file mode 100644 index 000000000..1e06b6d66 --- /dev/null +++ b/parser/src/cfg/clipboard.rs @@ -0,0 +1,83 @@ +use super::*; + +use crate::anyhow_expr; +use crate::bail; +use crate::bail_expr; + +pub(crate) fn parse_clipboard_set( + ac_params: &[SExpr], + s: &ParserState, +) -> Result<&'static KanataAction> { + const ERR_MSG: &str = "expects 1 parameter: "; + if ac_params.len() != 1 { + bail!("{CLIPBOARD_SET} {ERR_MSG}, found {}", ac_params.len()); + } + let expr = &ac_params[0]; + let clip_string = match expr { + SExpr::Atom(filepath) => filepath, + SExpr::List(_) => { + bail_expr!(&expr, "Clipboard string cannot be a list") + } + }; + let clip_string = clip_string.t.trim_atom_quotes(); + custom( + CustomAction::ClipboardSet(s.a.sref_str(clip_string.to_string())), + &s.a, + ) +} + +pub(crate) fn parse_clipboard_save( + ac_params: &[SExpr], + s: &ParserState, +) -> Result<&'static KanataAction> { + const ERR_MSG: &str = "expects 1 parameter: "; + if ac_params.len() != 1 { + bail!("{CLIPBOARD_SAVE} {ERR_MSG}, found {}", ac_params.len()); + } + let id = parse_u16(&ac_params[0], s, "clipboard save ID")?; + custom(CustomAction::ClipboardSave(id), &s.a) +} + +pub(crate) fn parse_clipboard_restore( + ac_params: &[SExpr], + s: &ParserState, +) -> Result<&'static KanataAction> { + const ERR_MSG: &str = "expects 1 parameter: "; + if ac_params.len() != 1 { + bail!("{CLIPBOARD_RESTORE} {ERR_MSG}, found {}", ac_params.len()); + } + let id = parse_u16(&ac_params[0], s, "clipboard save ID")?; + custom(CustomAction::ClipboardRestore(id), &s.a) +} + +pub(crate) fn parse_clipboard_save_swap( + ac_params: &[SExpr], + s: &ParserState, +) -> Result<&'static KanataAction> { + const ERR_MSG: &str = + "expects 2 parameters: "; + if ac_params.len() != 2 { + bail!("{CLIPBOARD_SAVE_SWAP} {ERR_MSG}, found {}", ac_params.len()); + } + let id1 = parse_u16(&ac_params[0], s, "clipboard save ID")?; + let id2 = parse_u16(&ac_params[1], s, "clipboard save ID")?; + custom(CustomAction::ClipboardSaveSwap(id1, id2), &s.a) +} + +pub(crate) fn parse_clipboard_save_set( + ac_params: &[SExpr], + s: &ParserState, +) -> Result<&'static KanataAction> { + const ERR_MSG: &str = "expects 2 parameters: "; + if ac_params.len() != 2 { + bail!("{CLIPBOARD_SAVE_SET} {ERR_MSG}, found {}", ac_params.len()); + } + let id = parse_u16(&ac_params[0], s, "clipboard save ID")?; + let save_content = ac_params[1] + .atom(s.vars()) + .ok_or_else(|| anyhow_expr!(&ac_params[1], "save content must be a string"))?; + custom( + CustomAction::ClipboardSaveSet(id, s.a.sref_str(save_content.into())), + &s.a, + ) +} diff --git a/parser/src/cfg/cmd.rs b/parser/src/cfg/cmd.rs new file mode 100644 index 000000000..b9d40574b --- /dev/null +++ b/parser/src/cfg/cmd.rs @@ -0,0 +1,140 @@ +use super::*; + +use crate::bail; +use crate::bail_expr; + +pub(crate) enum CmdType { + /// Execute command in own thread. + Standard, + /// Execute command synchronously and output stdout as macro-like SExpr. + OutputKeys, + /// Execute command and set clipboard to output. Clipboard content is passed as stdin to the + /// command. + ClipboardSet, + /// Execute command and set clipboard save id to output. + /// Clipboard save id content is passed as stdin to the command. + ClipboardSaveSet, +} + +// Parse cmd, but there are 2 arguments before specifying normal log and error log +pub(crate) fn parse_cmd_log(ac_params: &[SExpr], s: &ParserState) -> Result<&'static KanataAction> { + const ERR_STR: &str = + "cmd-log expects at least 3 strings, "; + if !s.is_cmd_enabled { + bail!( + "cmd is not enabled for this kanata executable (did you use 'cmd_allowed' variants?), but is set in the configuration" + ); + } + if ac_params.len() < 3 { + bail!(ERR_STR); + } + let mut cmd = vec![]; + let log_level = + if let Some(Ok(input_mode)) = ac_params[0].atom(s.vars()).map(LogLevel::try_from_str) { + input_mode + } else { + bail_expr!(&ac_params[0], "{ERR_STR}\n{}", LogLevel::err_msg()); + }; + let error_log_level = + if let Some(Ok(input_mode)) = ac_params[1].atom(s.vars()).map(LogLevel::try_from_str) { + input_mode + } else { + bail_expr!(&ac_params[1], "{ERR_STR}\n{}", LogLevel::err_msg()); + }; + collect_strings(&ac_params[2..], &mut cmd, s); + if cmd.is_empty() { + bail!(ERR_STR); + } + let cmds = cmd.into_iter().map(|v| s.a.sref_str(v)).collect(); + custom( + CustomAction::CmdLog(log_level, error_log_level, s.a.sref_vec(cmds)), + &s.a, + ) +} + +#[allow(unused_variables)] +pub(crate) fn parse_cmd( + ac_params: &[SExpr], + s: &ParserState, + cmd_type: CmdType, +) -> Result<&'static KanataAction> { + #[cfg(not(feature = "cmd"))] + { + bail!( + "cmd is not enabled for this kanata executable. Use a cmd_allowed prebuilt executable or compile with the feature: cmd." + ); + } + #[cfg(feature = "cmd")] + { + if matches!(cmd_type, CmdType::ClipboardSaveSet) { + const ERR_STR: &str = "expects a save ID and at least one string"; + if !s.is_cmd_enabled { + bail!("To use cmd you must put in defcfg: danger-enable-cmd yes."); + } + if ac_params.len() < 2 { + bail!("{CLIPBOARD_SAVE_CMD_SET} {ERR_STR}"); + } + let mut cmd = vec![]; + let save_id = parse_u16(&ac_params[0], s, "clipboard save ID")?; + collect_strings(&ac_params[1..], &mut cmd, s); + if cmd.is_empty() { + bail_expr!(&ac_params[1], "{CLIPBOARD_SAVE_CMD_SET} {ERR_STR}"); + } + let cmds = cmd.into_iter().map(|v| s.a.sref_str(v)).collect(); + return custom( + CustomAction::ClipboardSaveCmdSet(save_id, s.a.sref_vec(cmds)), + &s.a, + ); + } + + const ERR_STR: &str = "cmd expects at least one string"; + if !s.is_cmd_enabled { + bail!("To use cmd you must put in defcfg: danger-enable-cmd yes."); + } + let mut cmd = vec![]; + collect_strings(ac_params, &mut cmd, s); + if cmd.is_empty() { + bail!(ERR_STR); + } + let cmds = cmd.into_iter().map(|v| s.a.sref_str(v)).collect(); + let cmds = s.a.sref_vec(cmds); + custom( + match cmd_type { + CmdType::Standard => CustomAction::Cmd(cmds), + CmdType::OutputKeys => CustomAction::CmdOutputKeys(cmds), + CmdType::ClipboardSet => CustomAction::ClipboardCmdSet(cmds), + CmdType::ClipboardSaveSet => unreachable!(), + }, + &s.a, + ) + } +} + +/// Recurse through all levels of list nesting and collect into a flat list of strings. +/// Recursion is DFS, which matches left-to-right reading of the strings as they appear, +/// if everything was on a single line. +pub(crate) fn collect_strings(params: &[SExpr], strings: &mut Vec, s: &ParserState) { + for param in params { + if let Some(a) = param.atom(s.vars()) { + strings.push(a.trim_atom_quotes().to_owned()); + } else { + // unwrap: this must be a list, since it's not an atom. + let l = param.list(s.vars()).unwrap(); + collect_strings(l, strings, s); + } + } +} + +#[test] +pub(crate) fn test_collect_strings() { + let params = r#"(gah (squish "squash" (splish splosh) "bah mah") dah)"#; + let params = sexpr::parse(params, "noexist").unwrap(); + let mut strings = vec![]; + collect_strings(¶ms[0].t, &mut strings, &ParserState::default()); + assert_eq!( + &strings, + &[ + "gah", "squish", "squash", "splish", "splosh", "bah mah", "dah" + ] + ); +} diff --git a/parser/src/cfg/custom_tap_hold.rs b/parser/src/cfg/custom_tap_hold.rs index c46603523..b7fd6de31 100644 --- a/parser/src/cfg/custom_tap_hold.rs +++ b/parser/src/cfg/custom_tap_hold.rs @@ -1,32 +1,380 @@ -use kanata_keyberon::layout::{Event, QueuedIter, WaitingAction}; +use kanata_keyberon::layout::{Event, KCoord, QueuedIter, REAL_KEY_ROW, WaitingAction}; use crate::keys::OsCode; use super::alloc::Allocations; +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum Hand { + Left, + Right, + Neutral, +} + +/// Compact mapping from key codes to hand assignments. +/// Stores only keys that have an explicit left/right assignment; +/// any key not present is treated as `Hand::Neutral`. +#[derive(Clone, Copy, Debug)] +pub(crate) struct HandMap { + pub(crate) keys: &'static [u16], + pub(crate) hands: &'static [Hand], +} + +impl HandMap { + pub(crate) fn get(&self, key_code: u16) -> Hand { + self.keys + .iter() + .position(|&k| k == key_code) + .map(|i| self.hands[i]) + .unwrap_or(Hand::Neutral) + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum DecisionBehavior { + Tap, + Hold, + Ignore, +} + +/// The function-trait object stored inside `HoldTapConfig::Custom`. +pub(crate) type CustomTapHoldFn = + dyn Fn(QueuedIter, KCoord) -> (Option, bool) + Send + Sync; + /// Returns a closure that can be used in `HoldTapConfig::Custom`, which will return early with a /// Tap action in the case that any of `keys` are pressed. Otherwise it behaves as /// `HoldTapConfig::PermissiveHold` would. pub(crate) fn custom_tap_hold_release( keys: &[OsCode], a: &Allocations, -) -> &'static (dyn Fn(QueuedIter) -> Option + Send + Sync) { +) -> &'static CustomTapHoldFn { let keys = a.sref_vec(Vec::from_iter(keys.iter().copied())); - a.sref(move |mut queued: QueuedIter| -> Option { - while let Some(q) = queued.next() { - if q.event().is_press() { + a.sref( + move |mut queued: QueuedIter, _coord: KCoord| -> (Option, bool) { + while let Some(q) = queued.next() { + if q.event().is_press() { + let (i, j) = q.event().coord(); + // If any key matches the input, do a tap right away. + if i == REAL_KEY_ROW && keys.iter().copied().map(u16::from).any(|j2| j2 == j) { + return (Some(WaitingAction::Tap), false); + } + // Otherwise do the PermissiveHold algorithm. + let target = Event::Release(i, j); + if queued.clone().copied().any(|q| q.event() == target) { + return (Some(WaitingAction::Hold), false); + } + } + } + (None, false) + }, + ) +} + +/// Returns a closure that can be used in `HoldTapConfig::Custom`, which will return early with a +/// Tap action in the case that any of `keys_press_then_release_trigger_tap` are pressed and +/// released, or if any in `keys_press_trigger_tap` are pressed (no release needed). Otherwise it +/// behaves as `HoldTapConfig::PermissiveHold` would. +pub(crate) fn custom_tap_hold_release_trigger_tap_release( + keys_press_trigger_tap: &[OsCode], + keys_press_then_release_trigger_tap: &[OsCode], + a: &Allocations, +) -> &'static CustomTapHoldFn { + let keys_press_then_release_trigger_tap = a.sref_vec(Vec::from_iter( + keys_press_then_release_trigger_tap + .iter() + .copied() + .map(u16::from), + )); + let keys_press_trigger_tap = a.sref_vec(Vec::from_iter( + keys_press_trigger_tap.iter().copied().map(u16::from), + )); + a.sref( + move |mut queued: QueuedIter, _coord: KCoord| -> (Option, bool) { + while let Some(q) = queued.next() { + if q.event().is_press() { + let (i, j) = q.event().coord(); + if i != REAL_KEY_ROW { + continue; + } + // If any pressed key matches the press list and has been released, do + // a tap right away. + if keys_press_trigger_tap.iter().copied().any(|j2| j2 == j) { + return (Some(WaitingAction::Tap), false); + } + // If any pressed key matches the press-release list and has been released, do + // a tap right away. + if keys_press_then_release_trigger_tap + .iter() + .copied() + .any(|j2| j2 == j) + { + let target = Event::Release(i, j); + if queued.clone().copied().any(|q| q.event() == target) { + return (Some(WaitingAction::Tap), false); + } + } + // Otherwise do the PermissiveHold algorithm. + let target = Event::Release(i, j); + if queued.clone().copied().any(|q| q.event() == target) { + return (Some(WaitingAction::Hold), false); + } + } + } + (None, false) + }, + ) +} + +/// Returns a closure for `tap-hold-keys` with three optional key lists: +/// - `keys_tap_on_press`: trigger tap immediately on press +/// - `keys_tap_on_press_release`: trigger tap when pressed then released +/// - `keys_hold_on_press`: trigger hold immediately on press +/// +/// For any other key, falls back to PermissiveHold behavior. +/// +/// Priority when a key appears in multiple lists (checked in order): +/// tap-on-press > hold-on-press > tap-on-press-release > PermissiveHold +pub(crate) fn custom_tap_hold_keys( + keys_tap_on_press: &[OsCode], + keys_tap_on_press_release: &[OsCode], + keys_hold_on_press: &[OsCode], + a: &Allocations, +) -> &'static CustomTapHoldFn { + let keys_tap_on_press = a.sref_vec(keys_tap_on_press.iter().copied().map(u16::from).collect()); + let keys_tap_on_press_release = a.sref_vec( + keys_tap_on_press_release + .iter() + .copied() + .map(u16::from) + .collect(), + ); + let keys_hold_on_press = + a.sref_vec(keys_hold_on_press.iter().copied().map(u16::from).collect()); + a.sref( + move |mut queued: QueuedIter, _coord: KCoord| -> (Option, bool) { + while let Some(q) = queued.next() { + if q.event().is_press() { + let (i, j) = q.event().coord(); + if i != REAL_KEY_ROW { + continue; + } + // If key is in tap-on-press list, trigger tap immediately. + if keys_tap_on_press.iter().copied().any(|j2| j2 == j) { + return (Some(WaitingAction::Tap), false); + } + // If key is in hold-on-press list, trigger hold immediately. + if keys_hold_on_press.iter().copied().any(|j2| j2 == j) { + return (Some(WaitingAction::Hold), false); + } + // If key is in tap-on-press-release list and has been released, + // trigger tap. + if keys_tap_on_press_release.iter().copied().any(|j2| j2 == j) { + let target = Event::Release(i, j); + if queued.clone().copied().any(|q| q.event() == target) { + return (Some(WaitingAction::Tap), false); + } + } + // Otherwise do the PermissiveHold algorithm: + // if another key was pressed and released, trigger hold. + let target = Event::Release(i, j); + if queued.clone().copied().any(|q| q.event() == target) { + return (Some(WaitingAction::Hold), false); + } + } + } + (None, false) + }, + ) +} + +pub(crate) fn custom_tap_hold_except(keys: &[OsCode], a: &Allocations) -> &'static CustomTapHoldFn { + let keys = a.sref_vec(Vec::from_iter(keys.iter().copied())); + a.sref( + move |mut queued: QueuedIter, _coord: KCoord| -> (Option, bool) { + for q in queued.by_ref() { + if q.event().is_press() { + let (_i, j) = q.event().coord(); + // If any key matches the input, do a tap. + if keys.iter().copied().map(u16::from).any(|j2| j2 == j) { + return (Some(WaitingAction::Tap), false); + } + // Otherwise continue with default behavior + return (None, false); + } + } + // Otherwise skip timeout + (None, true) + }, + ) +} + +/// Returns a closure that can be used in `HoldTapConfig::Custom`, which will return early with a +/// Tap action in the case that any of `keys` are pressed. Unlike `custom_tap_hold_except`, if no +/// matching key is pressed, this waits for timeout instead of skipping it. +pub(crate) fn custom_tap_hold_tap_keys( + keys: &[OsCode], + a: &Allocations, +) -> &'static CustomTapHoldFn { + let keys = a.sref_vec(Vec::from_iter(keys.iter().copied())); + a.sref( + move |mut queued: QueuedIter, _coord: KCoord| -> (Option, bool) { + for q in queued.by_ref() { + if q.event().is_press() { + let (_i, j) = q.event().coord(); + // If any key matches the input, do a tap. + if keys.iter().copied().map(u16::from).any(|j2| j2 == j) { + return (Some(WaitingAction::Tap), false); + } + // Otherwise continue with default behavior (no early hold activation) + } + } + // Wait for timeout (key difference from custom_tap_hold_except which returns true) + (None, false) + }, + ) +} + +pub(crate) fn custom_tap_hold_opposite_hand( + hand_map: &'static HandMap, + same_hand: DecisionBehavior, + neutral_behavior: DecisionBehavior, + unknown_hand: DecisionBehavior, + neutral_keys: &'static [OsCode], + a: &Allocations, +) -> &'static CustomTapHoldFn { + a.sref( + move |queued: QueuedIter, coord: KCoord| -> (Option, bool) { + let (_row, col) = coord; + let waiting_hand = hand_map.get(col); + + for q in queued { + if !q.event().is_press() { + continue; + } + let (i, j) = q.event().coord(); + if i != REAL_KEY_ROW { + continue; + } + + // Check neutral-keys first (takes precedence over defhands) + if let Some(osc) = OsCode::from_u16(j) { + if neutral_keys.contains(&osc) { + match neutral_behavior { + DecisionBehavior::Tap => return (Some(WaitingAction::Tap), false), + DecisionBehavior::Hold => return (Some(WaitingAction::Hold), false), + DecisionBehavior::Ignore => continue, + } + } + } + + let pressed_hand = hand_map.get(j); + + match (waiting_hand, pressed_hand) { + (Hand::Left, Hand::Right) | (Hand::Right, Hand::Left) => { + return (Some(WaitingAction::Hold), false); + } + (Hand::Left, Hand::Left) | (Hand::Right, Hand::Right) => match same_hand { + DecisionBehavior::Tap => return (Some(WaitingAction::Tap), false), + DecisionBehavior::Hold => return (Some(WaitingAction::Hold), false), + DecisionBehavior::Ignore => continue, + }, + _ => { + // At least one key is Neutral (not in defhands) + match unknown_hand { + DecisionBehavior::Tap => return (Some(WaitingAction::Tap), false), + DecisionBehavior::Hold => return (Some(WaitingAction::Hold), false), + DecisionBehavior::Ignore => continue, + } + } + } + } + (None, false) + }, + ) +} + +/// Like `custom_tap_hold_opposite_hand` but waits for the interrupting key's +/// press+release before committing to Hold. This avoids misfires on fast +/// cross-hand overlaps where keystrokes briefly overlap. +/// +/// The `-release` requirement only applies to opposite-hand and unknown-hand +/// keys. Same-hand keys resolve immediately on press (no release needed), +/// because requiring release would cause same-hand keys to be skipped when +/// still held, allowing a later opposite-hand key+release to incorrectly +/// trigger Hold. +pub(crate) fn custom_tap_hold_opposite_hand_release( + hand_map: &'static HandMap, + same_hand: DecisionBehavior, + neutral_behavior: DecisionBehavior, + unknown_hand: DecisionBehavior, + neutral_keys: &'static [OsCode], + a: &Allocations, +) -> &'static CustomTapHoldFn { + a.sref( + move |mut queued: QueuedIter, coord: KCoord| -> (Option, bool) { + let (_row, col) = coord; + let waiting_hand = hand_map.get(col); + + while let Some(q) = queued.next() { + if !q.event().is_press() { + continue; + } let (i, j) = q.event().coord(); - // If any key matches the input, do a tap right away. - if keys.iter().copied().map(u16::from).any(|j2| j2 == j) { - return Some(WaitingAction::Tap); + if i != REAL_KEY_ROW { + continue; } - // Otherwise do the PermissiveHold algorithm. - let target = Event::Release(i, j); - if queued.clone().copied().any(|q| q.event() == target) { - return Some(WaitingAction::Hold); + + // Check neutral-keys first (takes precedence over defhands) + if let Some(osc) = OsCode::from_u16(j) { + if neutral_keys.contains(&osc) { + // Neutral keys require release before deciding + let release = Event::Release(i, j); + if !queued.clone().copied().any(|q| q.event() == release) { + continue; + } + match neutral_behavior { + DecisionBehavior::Tap => return (Some(WaitingAction::Tap), false), + DecisionBehavior::Hold => return (Some(WaitingAction::Hold), false), + DecisionBehavior::Ignore => continue, + } + } + } + + let pressed_hand = hand_map.get(j); + + match (waiting_hand, pressed_hand) { + // Same hand: resolve immediately on press (no release needed). + // This prevents same-hand keys from being skipped while held, + // which would let a later opposite-hand release trigger Hold. + (Hand::Left, Hand::Left) | (Hand::Right, Hand::Right) => match same_hand { + DecisionBehavior::Tap => return (Some(WaitingAction::Tap), false), + DecisionBehavior::Hold => return (Some(WaitingAction::Hold), false), + DecisionBehavior::Ignore => continue, + }, + // Opposite hand: require release before committing to Hold + (Hand::Left, Hand::Right) | (Hand::Right, Hand::Left) => { + let release = Event::Release(i, j); + if !queued.clone().copied().any(|q| q.event() == release) { + continue; + } + return (Some(WaitingAction::Hold), false); + } + _ => { + // At least one key is Neutral (not in defhands): + // require release before deciding + let release = Event::Release(i, j); + if !queued.clone().copied().any(|q| q.event() == release) { + continue; + } + match unknown_hand { + DecisionBehavior::Tap => return (Some(WaitingAction::Tap), false), + DecisionBehavior::Hold => return (Some(WaitingAction::Hold), false), + DecisionBehavior::Ignore => continue, + } + } } } - } - None - }) + (None, false) + }, + ) } diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs new file mode 100644 index 000000000..2b5e70035 --- /dev/null +++ b/parser/src/cfg/defcfg.rs @@ -0,0 +1,1158 @@ +use super::HashSet; +use super::sexpr::SExpr; +use super::{TrimAtomQuotes, error::*}; +use crate::cfg::check_first_expr; +use crate::custom_action::*; +use crate::keys::*; +#[allow(unused)] +use crate::{anyhow_expr, anyhow_span, bail, bail_expr, bail_span}; + +#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum DeviceDetectMode { + KeyboardOnly, + KeyboardMice, + Any, +} +#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))] +impl std::fmt::Display for DeviceDetectMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))] +#[derive(Debug, Clone)] +pub struct CfgLinuxOptions { + pub linux_dev: Vec, + pub linux_dev_names_include: Option>, + pub linux_dev_names_exclude: Option>, + pub linux_continue_if_no_devs_found: bool, + pub linux_unicode_u_code: crate::keys::OsCode, + pub linux_unicode_termination: UnicodeTermination, + pub linux_x11_repeat_delay_rate: Option, + pub linux_use_trackpoint_property: bool, + pub linux_output_name: String, + pub linux_output_bus_type: LinuxCfgOutputBusType, + pub linux_device_detect_mode: Option, +} +#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))] +impl Default for CfgLinuxOptions { + fn default() -> Self { + Self { + linux_dev: vec![], + linux_dev_names_include: None, + linux_dev_names_exclude: None, + linux_continue_if_no_devs_found: false, + // historically was the only option, so make KEY_U the default + linux_unicode_u_code: crate::keys::OsCode::KEY_U, + // historically was the only option, so make Enter the default + linux_unicode_termination: UnicodeTermination::Enter, + linux_x11_repeat_delay_rate: None, + linux_use_trackpoint_property: false, + linux_output_name: "kanata".to_owned(), + linux_output_bus_type: LinuxCfgOutputBusType::BusI8042, + linux_device_detect_mode: None, + } + } +} +#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))] +#[derive(Debug, Clone, Copy)] +pub enum LinuxCfgOutputBusType { + BusUsb, + BusI8042, + BusVirtual, +} + +#[cfg(any(target_os = "macos", target_os = "unknown"))] +#[derive(Debug, Default, Clone)] +pub struct CfgMacosOptions { + pub macos_dev_names_include: Option>, + pub macos_dev_names_exclude: Option>, + pub macos_continue_if_no_devs_found: bool, +} + +#[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" +))] +#[derive(Debug, Clone, Default)] +pub struct CfgWinterceptOptions { + pub windows_interception_mouse_hwids: Option>, + pub windows_interception_mouse_hwids_exclude: Option>, + pub windows_interception_keyboard_hwids: Option>, + pub windows_interception_keyboard_hwids_exclude: Option>, +} + +#[cfg(any(target_os = "windows", target_os = "unknown"))] +#[derive(Debug, Clone, Default)] +pub struct CfgWindowsOptions { + pub windows_altgr: AltGrBehaviour, + pub sync_keystates: bool, +} + +#[cfg(all(any(target_os = "windows", target_os = "unknown"), feature = "gui"))] +#[derive(Debug, Clone)] +pub struct CfgOptionsGui { + /// File name / path to the tray icon file. + pub tray_icon: Option, + /// Whether to match layer names to icon files without an explicit 'icon' field + pub icon_match_layer_name: bool, + /// Show tooltip on layer changes showing layer icons + pub tooltip_layer_changes: bool, + /// Show tooltip on layer changes for the default/base layer + pub tooltip_no_base: bool, + /// Show tooltip on layer changes even for layers without an icon + pub tooltip_show_blank: bool, + /// Show tooltip on layer changes for this duration (ms) + pub tooltip_duration: u16, + /// Show system notification message on config reload + pub notify_cfg_reload: bool, + /// Disable sound for the system notification message on config reload + pub notify_cfg_reload_silent: bool, + /// Show system notification message on errors + pub notify_error: bool, + /// Set tooltip size (width, height) + pub tooltip_size: (u16, u16), +} +#[cfg(all(any(target_os = "windows", target_os = "unknown"), feature = "gui"))] +impl Default for CfgOptionsGui { + fn default() -> Self { + Self { + tray_icon: None, + icon_match_layer_name: true, + tooltip_layer_changes: false, + tooltip_show_blank: false, + tooltip_no_base: true, + tooltip_duration: 500, + notify_cfg_reload: true, + notify_cfg_reload_silent: false, + notify_error: true, + tooltip_size: (24, 24), + } + } +} + +#[derive(Debug)] +pub struct CfgOptions { + pub process_unmapped_keys: bool, + pub process_unmapped_keys_exceptions: Option>, + pub block_unmapped_keys: bool, + pub allow_hardware_repeat: bool, + pub start_alias: Option, + pub enable_cmd: bool, + pub sequence_timeout: u16, + pub sequence_input_mode: SequenceInputMode, + pub sequence_backtrack_modcancel: bool, + pub sequence_always_on: bool, + pub log_layer_changes: bool, + pub delegate_to_first_layer: bool, + pub movemouse_inherit_accel_state: bool, + pub movemouse_smooth_diagonals: bool, + pub override_release_on_activation: bool, + pub dynamic_macro_max_presses: u16, + pub dynamic_macro_replay_delay_behaviour: ReplayDelayBehaviour, + pub concurrent_tap_hold: bool, + pub rapid_event_delay: u16, + pub trans_resolution_behavior_v2: bool, + pub chords_v2_min_idle: u16, + pub tap_hold_require_prior_idle: u16, + #[cfg(any( + all(target_os = "windows", feature = "interception_driver"), + target_os = "linux", + target_os = "android", + target_os = "macos", + target_os = "unknown" + ))] + pub mouse_movement_key: Option, + #[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))] + pub linux_opts: CfgLinuxOptions, + #[cfg(any(target_os = "macos", target_os = "unknown"))] + pub macos_opts: CfgMacosOptions, + #[cfg(any(target_os = "windows", target_os = "unknown"))] + pub windows_opts: CfgWindowsOptions, + #[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" + ))] + pub wintercept_opts: CfgWinterceptOptions, + #[cfg(all(any(target_os = "windows", target_os = "unknown"), feature = "gui"))] + pub gui_opts: CfgOptionsGui, +} + +impl Default for CfgOptions { + fn default() -> Self { + Self { + process_unmapped_keys: false, + process_unmapped_keys_exceptions: None, + block_unmapped_keys: false, + allow_hardware_repeat: true, + start_alias: None, + enable_cmd: false, + sequence_timeout: 1000, + sequence_input_mode: SequenceInputMode::HiddenSuppressed, + sequence_backtrack_modcancel: true, + sequence_always_on: false, + log_layer_changes: true, + delegate_to_first_layer: false, + movemouse_inherit_accel_state: false, + movemouse_smooth_diagonals: false, + override_release_on_activation: false, + dynamic_macro_max_presses: 128, + dynamic_macro_replay_delay_behaviour: ReplayDelayBehaviour::Recorded, + concurrent_tap_hold: false, + rapid_event_delay: 5, + trans_resolution_behavior_v2: true, + chords_v2_min_idle: 5, + tap_hold_require_prior_idle: 0, + #[cfg(any( + all(target_os = "windows", feature = "interception_driver"), + target_os = "linux", + target_os = "android", + target_os = "macos", + target_os = "unknown" + ))] + mouse_movement_key: None, + #[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))] + linux_opts: Default::default(), + #[cfg(any(target_os = "windows", target_os = "unknown"))] + windows_opts: Default::default(), + #[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" + ))] + wintercept_opts: Default::default(), + #[cfg(any(target_os = "macos", target_os = "unknown"))] + macos_opts: Default::default(), + #[cfg(all(any(target_os = "windows", target_os = "unknown"), feature = "gui"))] + gui_opts: Default::default(), + } + } +} + +/// Parse configuration entries from an expression starting with defcfg. +pub fn parse_defcfg(expr: &[SExpr]) -> Result { + let mut seen_keys = HashSet::default(); + let mut cfg = CfgOptions::default(); + let mut exprs = check_first_expr(expr.iter(), "defcfg")?; + let mut is_process_unmapped_keys_defined = false; + // Read k-v pairs from the configuration + loop { + let key = match exprs.next() { + Some(k) => k, + None => { + if !is_process_unmapped_keys_defined { + log::warn!( + "The item process-unmapped-keys is not defined in defcfg. Consider whether process-unmapped-keys should be yes vs. no." + ); + } + return Ok(cfg); + } + }; + let val = match exprs.next() { + Some(v) => v, + None => bail_expr!(key, "Found a defcfg option missing a value"), + }; + match key { + SExpr::Atom(k) => { + let label = k.t.as_str(); + if !seen_keys.insert(label) { + bail_expr!(key, "Duplicate defcfg option {}", label); + } + match label { + "sequence-timeout" => { + cfg.sequence_timeout = parse_cfg_val_u16(val, label, true)?; + } + "sequence-input-mode" => { + let v = sexpr_to_str_or_err(val, label)?; + cfg.sequence_input_mode = SequenceInputMode::try_from_str(v) + .map_err(|e| anyhow_expr!(val, "{}", e.to_string()))?; + } + "sequence-always-on" => { + cfg.sequence_always_on = parse_defcfg_val_bool(val, label)? + } + "dynamic-macro-max-presses" => { + cfg.dynamic_macro_max_presses = parse_cfg_val_u16(val, label, false)?; + } + "dynamic-macro-replay-delay-behaviour" => { + cfg.dynamic_macro_replay_delay_behaviour = val + .atom(None) + .map(|v| match v { + "constant" => Ok(ReplayDelayBehaviour::Constant), + "recorded" => Ok(ReplayDelayBehaviour::Recorded), + _ => bail_expr!( + val, + "this option must be one of: constant | recorded" + ), + }) + .ok_or_else(|| { + anyhow_expr!(val, "this option must be one of: constant | recorded") + })??; + } + "linux-dev" => { + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "unknown" + ))] + { + cfg.linux_opts.linux_dev = parse_dev(val)?; + if cfg.linux_opts.linux_dev.is_empty() { + bail_expr!( + val, + "device list is empty, no devices will be intercepted" + ); + } + } + } + "linux-dev-names-include" => { + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "unknown" + ))] + { + let dev_names = parse_dev(val)?; + if dev_names.is_empty() { + log::warn!("linux-dev-names-include is empty"); + } + cfg.linux_opts.linux_dev_names_include = Some(dev_names); + } + } + "linux-dev-names-exclude" => { + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "unknown" + ))] + { + cfg.linux_opts.linux_dev_names_exclude = Some(parse_dev(val)?); + } + } + "linux-unicode-u-code" => { + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "unknown" + ))] + { + let v = sexpr_to_str_or_err(val, label)?; + cfg.linux_opts.linux_unicode_u_code = crate::keys::str_to_oscode(v) + .ok_or_else(|| { + anyhow_expr!(val, "unknown code for {label}: {}", v) + })?; + } + } + "linux-unicode-termination" => { + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "unknown" + ))] + { + let v = sexpr_to_str_or_err(val, label)?; + cfg.linux_opts.linux_unicode_termination = match v { + "enter" => UnicodeTermination::Enter, + "space" => UnicodeTermination::Space, + "enter-space" => UnicodeTermination::EnterSpace, + "space-enter" => UnicodeTermination::SpaceEnter, + _ => bail_expr!( + val, + "{label} got {}. It accepts: enter|space|enter-space|space-enter", + v + ), + } + } + } + "linux-x11-repeat-delay-rate" => { + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "unknown" + ))] + { + let v = sexpr_to_str_or_err(val, label)?; + let delay_rate = v.split(',').collect::>(); + const ERRMSG: &str = "Invalid value for linux-x11-repeat-delay-rate.\nExpected two numbers 0-65535 separated by a comma, e.g. 200,25"; + if delay_rate.len() != 2 { + bail_expr!(val, "{}", ERRMSG) + } + cfg.linux_opts.linux_x11_repeat_delay_rate = Some(KeyRepeatSettings { + delay: match str::parse::(delay_rate[0]) { + Ok(delay) => delay, + Err(_) => bail_expr!(val, "{}", ERRMSG), + }, + rate: match str::parse::(delay_rate[1]) { + Ok(rate) => rate, + Err(_) => bail_expr!(val, "{}", ERRMSG), + }, + }); + } + } + "linux-use-trackpoint-property" => { + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "unknown" + ))] + { + cfg.linux_opts.linux_use_trackpoint_property = + parse_defcfg_val_bool(val, label)? + } + } + "linux-output-device-name" => { + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "unknown" + ))] + { + let device_name = sexpr_to_str_or_err(val, label)?; + if device_name.is_empty() { + log::warn!( + "linux-output-device-name is empty, using kanata as default value" + ); + } else { + cfg.linux_opts.linux_output_name = device_name.to_owned(); + } + } + } + "linux-output-device-bus-type" => { + let bus_type = sexpr_to_str_or_err(val, label)?; + match bus_type { + "USB" | "I8042" | "virtual" => {} + _ => bail_expr!( + val, + "Invalid value for linux-output-device-bus-type.\nExpected one of: USB | I8042 | virtual" + ), + }; + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "unknown" + ))] + { + let bus_type = match bus_type { + "USB" => LinuxCfgOutputBusType::BusUsb, + "I8042" => LinuxCfgOutputBusType::BusI8042, + "virtual" => LinuxCfgOutputBusType::BusVirtual, + _ => unreachable!("validated earlier"), + }; + cfg.linux_opts.linux_output_bus_type = bus_type; + } + } + "linux-device-detect-mode" => { + let detect_mode = sexpr_to_str_or_err(val, label)?; + match detect_mode { + "any" | "keyboard-only" | "keyboard-mice" => {} + _ => bail_expr!( + val, + "Invalid value for linux-device-detect-mode.\nExpected one of: any | keyboard-only | keyboard-mice" + ), + }; + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "unknown" + ))] + { + let detect_mode = Some(match detect_mode { + "any" => DeviceDetectMode::Any, + "keyboard-only" => DeviceDetectMode::KeyboardOnly, + "keyboard-mice" => DeviceDetectMode::KeyboardMice, + _ => unreachable!("validated earlier"), + }); + cfg.linux_opts.linux_device_detect_mode = detect_mode; + } + } + "windows-altgr" => { + #[cfg(any(target_os = "windows", target_os = "unknown"))] + { + const CANCEL: &str = "cancel-lctl-press"; + const ADD: &str = "add-lctl-release"; + let v = sexpr_to_str_or_err(val, label)?; + cfg.windows_opts.windows_altgr = match v { + CANCEL => AltGrBehaviour::CancelLctlPress, + ADD => AltGrBehaviour::AddLctlRelease, + _ => bail_expr!( + val, + "Invalid value for {label}: {}. Valid values are {},{}", + v, + CANCEL, + ADD + ), + } + } + } + "windows-sync-keystates" => { + #[cfg(any(target_os = "windows", target_os = "unknown"))] + { + cfg.windows_opts.sync_keystates = parse_defcfg_val_bool(val, label)?; + } + } + "windows-interception-mouse-hwid" => { + #[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" + ))] + { + if cfg + .wintercept_opts + .windows_interception_mouse_hwids_exclude + .is_some() + { + bail_expr!( + val, + "{label} and windows-interception-mouse-hwid-exclude cannot both be included" + ); + } + let v = sexpr_to_str_or_err(val, label)?; + let hwid = v; + log::trace!("win hwid: {hwid}"); + let hwid_vec = hwid + .split(',') + .try_fold(vec![], |mut hwid_bytes, hwid_byte| { + hwid_byte.trim_matches(' ').parse::().map(|b| { + hwid_bytes.push(b); + hwid_bytes + }) + }).map_err(|_| anyhow_expr!(val, "{label} format is invalid. It should consist of numbers [0,255] separated by commas"))?; + let hwid_slice = hwid_vec.iter().copied().enumerate() + .try_fold([0u8; HWID_ARR_SZ], |mut hwid, idx_byte| { + let (i, b) = idx_byte; + if i > HWID_ARR_SZ { + bail_expr!(val, "{label} is too long; it should be up to {HWID_ARR_SZ} numbers [0,255]") + } + hwid[i] = b; + Ok(hwid) + })?; + match cfg + .wintercept_opts + .windows_interception_mouse_hwids + .as_mut() + { + Some(v) => { + v.push(hwid_slice); + } + None => { + cfg.wintercept_opts.windows_interception_mouse_hwids = + Some(vec![hwid_slice]); + } + } + cfg.wintercept_opts + .windows_interception_mouse_hwids + .as_mut() + .unwrap() + .shrink_to_fit(); + } + } + "windows-interception-mouse-hwids" => { + #[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" + ))] + { + if cfg + .wintercept_opts + .windows_interception_mouse_hwids_exclude + .is_some() + { + bail_expr!( + val, + "{label} and windows-interception-mouse-hwid-exclude cannot both be included" + ); + } + let parsed_hwids = sexpr_to_hwids_vec( + val, + label, + "entry in windows-interception-mouse-hwids", + )?; + match cfg + .wintercept_opts + .windows_interception_mouse_hwids + .as_mut() + { + Some(v) => { + v.extend(parsed_hwids); + } + None => { + cfg.wintercept_opts.windows_interception_mouse_hwids = + Some(parsed_hwids); + } + } + cfg.wintercept_opts + .windows_interception_mouse_hwids + .as_mut() + .unwrap() + .shrink_to_fit(); + } + } + "windows-interception-mouse-hwids-exclude" => { + #[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" + ))] + { + if cfg + .wintercept_opts + .windows_interception_mouse_hwids + .is_some() + { + bail_expr!( + val, + "{label} and windows-interception-mouse-hwid(s) cannot both be used" + ); + } + let parsed_hwids = sexpr_to_hwids_vec( + val, + label, + "entry in windows-interception-mouse-hwids-exclude", + )?; + cfg.wintercept_opts.windows_interception_mouse_hwids_exclude = + Some(parsed_hwids); + } + } + "windows-interception-keyboard-hwids" => { + #[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" + ))] + { + if cfg + .wintercept_opts + .windows_interception_keyboard_hwids_exclude + .is_some() + { + bail_expr!( + val, + "{label} and windows-interception-keyboard-hwid-exclude cannot both be used" + ); + } + let parsed_hwids = sexpr_to_hwids_vec( + val, + label, + "entry in windows-interception-keyboard-hwids", + )?; + cfg.wintercept_opts.windows_interception_keyboard_hwids = + Some(parsed_hwids); + } + } + "windows-interception-keyboard-hwids-exclude" => { + #[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" + ))] + { + if cfg + .wintercept_opts + .windows_interception_keyboard_hwids + .is_some() + { + bail_expr!( + val, + "{label} and windows-interception-keyboard-hwid cannot both be used" + ); + } + let parsed_hwids = sexpr_to_hwids_vec( + val, + label, + "entry in windows-interception-keyboard-hwids-exclude", + )?; + cfg.wintercept_opts + .windows_interception_keyboard_hwids_exclude = Some(parsed_hwids); + } + } + "macos-dev-names-include" => { + #[cfg(any(target_os = "macos", target_os = "unknown"))] + { + let dev_names = parse_dev(val)?; + if dev_names.is_empty() { + log::warn!("macos-dev-names-include is empty"); + } + cfg.macos_opts.macos_dev_names_include = Some(dev_names); + } + } + "macos-dev-names-exclude" => { + #[cfg(any(target_os = "macos", target_os = "unknown"))] + { + let dev_names = parse_dev(val)?; + if dev_names.is_empty() { + log::warn!("macos-dev-names-exclude is empty"); + } + cfg.macos_opts.macos_dev_names_exclude = Some(dev_names); + } + } + "macos-continue-if-no-devs-found" => { + #[cfg(any(target_os = "macos", target_os = "unknown"))] + { + cfg.macos_opts.macos_continue_if_no_devs_found = + parse_defcfg_val_bool(val, label)? + } + } + "tray-icon" => { + #[cfg(all( + any(target_os = "windows", target_os = "unknown"), + feature = "gui" + ))] + { + let icon_path = sexpr_to_str_or_err(val, label)?; + if icon_path.is_empty() { + log::warn!("tray-icon is empty"); + } + cfg.gui_opts.tray_icon = Some(icon_path.to_string()); + } + } + "icon-match-layer-name" => { + #[cfg(all( + any(target_os = "windows", target_os = "unknown"), + feature = "gui" + ))] + { + cfg.gui_opts.icon_match_layer_name = parse_defcfg_val_bool(val, label)? + } + } + "tooltip-layer-changes" => { + #[cfg(all( + any(target_os = "windows", target_os = "unknown"), + feature = "gui" + ))] + { + cfg.gui_opts.tooltip_layer_changes = parse_defcfg_val_bool(val, label)? + } + } + "tooltip-show-blank" => { + #[cfg(all( + any(target_os = "windows", target_os = "unknown"), + feature = "gui" + ))] + { + cfg.gui_opts.tooltip_show_blank = parse_defcfg_val_bool(val, label)? + } + } + "tooltip-no-base" => { + #[cfg(all( + any(target_os = "windows", target_os = "unknown"), + feature = "gui" + ))] + { + cfg.gui_opts.tooltip_no_base = parse_defcfg_val_bool(val, label)? + } + } + "tooltip-duration" => { + #[cfg(all( + any(target_os = "windows", target_os = "unknown"), + feature = "gui" + ))] + { + cfg.gui_opts.tooltip_duration = parse_cfg_val_u16(val, label, false)? + } + } + "notify-cfg-reload" => { + #[cfg(all( + any(target_os = "windows", target_os = "unknown"), + feature = "gui" + ))] + { + cfg.gui_opts.notify_cfg_reload = parse_defcfg_val_bool(val, label)? + } + } + "notify-cfg-reload-silent" => { + #[cfg(all( + any(target_os = "windows", target_os = "unknown"), + feature = "gui" + ))] + { + cfg.gui_opts.notify_cfg_reload_silent = + parse_defcfg_val_bool(val, label)? + } + } + "notify-error" => { + #[cfg(all( + any(target_os = "windows", target_os = "unknown"), + feature = "gui" + ))] + { + cfg.gui_opts.notify_error = parse_defcfg_val_bool(val, label)? + } + } + "tooltip-size" => { + #[cfg(all( + any(target_os = "windows", target_os = "unknown"), + feature = "gui" + ))] + { + let v = sexpr_to_str_or_err(val, label)?; + let tooltip_size = v.split(',').collect::>(); + const ERRMSG: &str = "Invalid value for tooltip-size.\nExpected two numbers 0-65535 separated by a comma, e.g. 24,24"; + if tooltip_size.len() != 2 { + bail_expr!(val, "{}", ERRMSG) + } + cfg.gui_opts.tooltip_size = ( + match str::parse::(tooltip_size[0]) { + Ok(w) => w, + Err(_) => bail_expr!(val, "{}", ERRMSG), + }, + match str::parse::(tooltip_size[1]) { + Ok(h) => h, + Err(_) => bail_expr!(val, "{}", ERRMSG), + }, + ); + } + } + + "process-unmapped-keys" => { + is_process_unmapped_keys_defined = true; + if let Some(list) = val.list(None) { + let err = "Expected (all-except key1 ... keyN)."; + if list.len() < 2 { + bail_expr!(val, "{err}"); + } + match list[0].atom(None) { + Some("all-except") => {} + _ => { + bail_expr!(val, "{err}"); + } + }; + // Note: deflocalkeys should already be parsed when parsing defcfg, + // so can use safely use str_to_oscode here; it will include user + // configurations already. + let mut key_exceptions: Vec<(OsCode, SExpr)> = vec![]; + for key_expr in list[1..].iter() { + let key = key_expr.atom(None).and_then(str_to_oscode).ok_or_else( + || anyhow_expr!(key_expr, "Expected a known key name."), + )?; + if key_exceptions.iter().any(|k_exc| k_exc.0 == key) { + bail_expr!(key_expr, "Duplicate key name is not allowed."); + } + key_exceptions.push((key, key_expr.clone())); + } + cfg.process_unmapped_keys = true; + cfg.process_unmapped_keys_exceptions = Some(key_exceptions); + } else { + cfg.process_unmapped_keys = parse_defcfg_val_bool(val, label)? + } + } + + "block-unmapped-keys" => { + cfg.block_unmapped_keys = parse_defcfg_val_bool(val, label)? + } + "allow-hardware-repeat" => { + cfg.allow_hardware_repeat = parse_defcfg_val_bool(val, label)? + } + "alias-to-trigger-on-load" => { + cfg.start_alias = parse_defcfg_val_string(val, label)? + } + "danger-enable-cmd" => cfg.enable_cmd = parse_defcfg_val_bool(val, label)?, + "sequence-backtrack-modcancel" => { + cfg.sequence_backtrack_modcancel = parse_defcfg_val_bool(val, label)? + } + "log-layer-changes" => { + cfg.log_layer_changes = parse_defcfg_val_bool(val, label)? + } + "delegate-to-first-layer" => { + cfg.delegate_to_first_layer = parse_defcfg_val_bool(val, label)?; + if cfg.delegate_to_first_layer { + log::info!( + "delegating transparent keys on other layers to first defined layer" + ); + } + } + "linux-continue-if-no-devs-found" => { + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "unknown" + ))] + { + cfg.linux_opts.linux_continue_if_no_devs_found = + parse_defcfg_val_bool(val, label)? + } + } + "movemouse-smooth-diagonals" => { + cfg.movemouse_smooth_diagonals = parse_defcfg_val_bool(val, label)? + } + "movemouse-inherit-accel-state" => { + cfg.movemouse_inherit_accel_state = parse_defcfg_val_bool(val, label)? + } + "override-release-on-activation" => { + cfg.override_release_on_activation = parse_defcfg_val_bool(val, label)? + } + "concurrent-tap-hold" => { + cfg.concurrent_tap_hold = parse_defcfg_val_bool(val, label)? + } + "rapid-event-delay" => { + cfg.rapid_event_delay = parse_cfg_val_u16(val, label, false)? + } + "transparent-key-resolution" => { + let v = sexpr_to_str_or_err(val, label)?; + cfg.trans_resolution_behavior_v2 = match v { + "to-base-layer" => false, + "layer-stack" => true, + _ => bail_expr!( + val, + "{label} got {}. It accepts: 'to-base-layer' or 'layer-stack'", + v + ), + }; + } + "chords-v2-min-idle" | "chords-v2-min-idle-experimental" => { + if label == "chords-v2-min-idle-experimental" { + log::warn!( + "You should replace chords-v2-min-idle-experimental with chords-v2-min-idle\n\ + Using -experimental will be invalid in the future." + ) + } + let min_idle = parse_cfg_val_u16(val, label, true)?; + if min_idle < 5 { + bail_expr!(val, "{label} must be 5-65535"); + } + cfg.chords_v2_min_idle = min_idle; + } + "tap-hold-require-prior-idle" => { + cfg.tap_hold_require_prior_idle = parse_cfg_val_u16(val, label, false)?; + } + "mouse-movement-key" => { + #[cfg(any( + all(target_os = "windows", feature = "interception_driver"), + target_os = "linux", + target_os = "android", + target_os = "macos", + target_os = "unknown" + ))] + { + if let Some(keystr) = parse_defcfg_val_string(val, label)? { + if let Some(key) = str_to_oscode(&keystr) { + cfg.mouse_movement_key = Some(key); + } else { + bail_expr!(val, "{label} not a recognised key code"); + } + } else { + bail_expr!(val, "{label} not a string for a key code"); + } + } + } + _ => bail_expr!(key, "Unknown defcfg option {}", label), + }; + } + SExpr::List(_) => { + bail_expr!(key, "Lists are not allowed in as keys in defcfg"); + } + } + } +} + +fn parse_defcfg_val_string(expr: &SExpr, _label: &str) -> Result> { + match expr { + SExpr::Atom(v) => Ok(Some(v.t.clone())), + _ => Ok(None), + } +} + +pub const FALSE_VALUES: [&str; 3] = ["no", "false", "0"]; +pub const TRUE_VALUES: [&str; 3] = ["yes", "true", "1"]; +pub const BOOLEAN_VALUES: [&str; 6] = ["yes", "true", "1", "no", "false", "0"]; + +fn parse_defcfg_val_bool(expr: &SExpr, label: &str) -> Result { + match &expr { + SExpr::Atom(v) => { + let val = v.t.trim_atom_quotes().to_ascii_lowercase(); + if TRUE_VALUES.contains(&val.as_str()) { + Ok(true) + } else if FALSE_VALUES.contains(&val.as_str()) { + Ok(false) + } else { + bail_expr!( + expr, + "The value for {label} must be one of: {}", + BOOLEAN_VALUES.join(", ") + ); + } + } + SExpr::List(_) => { + bail_expr!( + expr, + "The value for {label} cannot be a list, it must be one of: {}", + BOOLEAN_VALUES.join(", "), + ) + } + } +} + +fn parse_cfg_val_u16(expr: &SExpr, label: &str, exclude_zero: bool) -> Result { + let start = if exclude_zero { 1 } else { 0 }; + match &expr { + SExpr::Atom(v) => Ok(str::parse::(v.t.trim_atom_quotes()) + .ok() + .and_then(|u| { + if exclude_zero && u == 0 { + None + } else { + Some(u) + } + }) + .ok_or_else(|| anyhow_expr!(expr, "{label} must be {start}-65535"))?), + SExpr::List(_) => { + bail_expr!( + expr, + "The value for {label} cannot be a list, it must be a number {start}-65535", + ) + } + } +} + +pub fn parse_colon_separated_text(paths: &str) -> Vec { + let mut all_paths = vec![]; + let mut full_dev_path = String::new(); + let mut dev_path_iter = paths.split(':').peekable(); + while let Some(dev_path) = dev_path_iter.next() { + if dev_path.ends_with('\\') && dev_path_iter.peek().is_some() { + full_dev_path.push_str(dev_path.trim_end_matches('\\')); + full_dev_path.push(':'); + continue; + } else { + full_dev_path.push_str(dev_path); + } + all_paths.push(full_dev_path.clone()); + full_dev_path.clear(); + } + all_paths.shrink_to_fit(); + all_paths +} + +#[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "macos", + target_os = "unknown" +))] +pub fn parse_dev(val: &SExpr) -> Result> { + Ok(match val { + SExpr::Atom(a) => { + let devs = parse_colon_separated_text(a.t.trim_atom_quotes()); + if devs.len() == 1 && devs[0].is_empty() { + bail_expr!(val, "an empty string is not a valid device name or path") + } + devs + } + SExpr::List(l) => { + let r: Result> = + l.t.iter() + .try_fold(Vec::with_capacity(l.t.len()), |mut acc, expr| match expr { + SExpr::Atom(path) => { + let trimmed_path = path.t.trim_atom_quotes().to_string(); + if trimmed_path.is_empty() { + bail_span!( + path, + "an empty string is not a valid device name or path" + ) + } + acc.push(trimmed_path); + Ok(acc) + } + SExpr::List(inner_list) => { + bail_span!(inner_list, "expected strings, found a list") + } + }); + + r? + } + }) +} + +fn sexpr_to_str_or_err<'a>(expr: &'a SExpr, label: &str) -> Result<&'a str> { + match expr { + SExpr::Atom(a) => Ok(a.t.trim_atom_quotes()), + SExpr::List(_) => bail_expr!(expr, "The value for {label} can't be a list"), + } +} + +#[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" +))] +fn sexpr_to_list_or_err<'a>(expr: &'a SExpr, label: &str) -> Result<&'a [SExpr]> { + match expr { + SExpr::Atom(_) => bail_expr!(expr, "The value for {label} must be a list"), + SExpr::List(l) => Ok(&l.t), + } +} + +#[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" +))] +fn sexpr_to_hwids_vec( + val: &SExpr, + label: &str, + entry_label: &str, +) -> Result> { + let hwids = sexpr_to_list_or_err(val, label)?; + let mut parsed_hwids = vec![]; + for hwid_expr in hwids.iter() { + let hwid = sexpr_to_str_or_err(hwid_expr, entry_label)?; + log::trace!("win hwid: {hwid}"); + let hwid_vec = hwid + .split(',') + .try_fold(vec![], |mut hwid_bytes, hwid_byte| { + hwid_byte.trim_matches(' ').parse::().map(|b| { + hwid_bytes.push(b); + hwid_bytes + }) + }).map_err(|_| anyhow_expr!(hwid_expr, "Entry in {label} is invalid. Entries should be numbers [0,255] separated by commas"))?; + let hwid_slice = hwid_vec.iter().copied().enumerate() + .try_fold([0u8; HWID_ARR_SZ], |mut hwid, idx_byte| { + let (i, b) = idx_byte; + if i > HWID_ARR_SZ { + bail_expr!(hwid_expr, "entry in {label} is too long; it should be up to {HWID_ARR_SZ} 8-bit unsigned integers") + } + hwid[i] = b; + Ok(hwid) + }); + parsed_hwids.push(hwid_slice?); + } + parsed_hwids.shrink_to_fit(); + Ok(parsed_hwids) +} + +#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct KeyRepeatSettings { + pub delay: u16, + pub rate: u16, +} + +#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum UnicodeTermination { + Enter, + Space, + SpaceEnter, + EnterSpace, +} + +#[cfg(any(target_os = "windows", target_os = "unknown"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum AltGrBehaviour { + #[default] + DoNothing, + CancelLctlPress, + AddLctlRelease, +} + +#[cfg(any(target_os = "windows", target_os = "unknown"))] +#[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" +))] +pub const HWID_ARR_SZ: usize = 1024; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ReplayDelayBehaviour { + /// Always use a fixed number of ticks between presses and releases. + /// This is the original kanata behaviour. + /// This means that held action activations like in tap-hold do not behave as intended. + Constant, + /// Use the recorded number of ticks between presses and releases. + /// This is newer behaviour. + Recorded, +} diff --git a/parser/src/cfg/defhands.rs b/parser/src/cfg/defhands.rs new file mode 100644 index 000000000..3229cfd07 --- /dev/null +++ b/parser/src/cfg/defhands.rs @@ -0,0 +1,426 @@ +use super::sexpr::*; +use super::*; +use crate::{anyhow_expr, bail, bail_expr}; + +pub(super) fn parse_defhands(expr: &[SExpr], s: &ParserState) -> Result { + use custom_tap_hold::Hand; + + let exprs_iter = check_first_expr(expr.iter(), "defhands")?; + let mut keys: Vec = Vec::new(); + let mut hands: Vec = Vec::new(); + let mut seen_left = false; + let mut seen_right = false; + + for group_expr in exprs_iter { + let group = group_expr + .list(s.vars()) + .ok_or_else(|| anyhow_expr!(group_expr, "expected (left ...) or (right ...)"))?; + if group.is_empty() { + bail_expr!(group_expr, "expected (left ...) or (right ...)"); + } + let hand_name = group[0] + .atom(s.vars()) + .ok_or_else(|| anyhow_expr!(&group[0], "expected 'left' or 'right'"))?; + let hand = match hand_name { + "left" => { + if seen_left { + bail_expr!(&group[0], "duplicate 'left' group in defhands"); + } + seen_left = true; + Hand::Left + } + "right" => { + if seen_right { + bail_expr!(&group[0], "duplicate 'right' group in defhands"); + } + seen_right = true; + Hand::Right + } + _ => bail_expr!(&group[0], "expected 'left' or 'right', got '{}'", hand_name), + }; + for key_expr in &group[1..] { + let key_name = key_expr + .atom(s.vars()) + .ok_or_else(|| anyhow_expr!(key_expr, "expected a key name, found list"))?; + let osc = str_to_oscode(key_name) + .ok_or_else(|| anyhow_expr!(key_expr, "unknown key '{}'", key_name))?; + let code = u16::from(osc); + if let Some(pos) = keys.iter().position(|&k| k == code) { + let existing_name = if hands[pos] == Hand::Left { + "left" + } else { + "right" + }; + bail_expr!( + key_expr, + "Key already assigned to '{}' hand, cannot also be in '{}'", + existing_name, + hand_name + ); + } + keys.push(code); + hands.push(hand); + } + } + + let keys_static = s.a.sref_vec(keys); + let hands_static = s.a.sref_vec(hands); + Ok(custom_tap_hold::HandMap { + keys: keys_static, + hands: hands_static, + }) +} + +pub(super) fn parse_tap_hold_opposite_hand( + ac_params: &[SExpr], + s: &ParserState, +) -> Result<&'static KanataAction> { + use custom_tap_hold::{DecisionBehavior, custom_tap_hold_opposite_hand}; + + const ARITY_MSG: &str = "tap-hold-opposite-hand expects at least 3 items: \ + [options...]"; + if ac_params.is_empty() { + bail!(ARITY_MSG); + } + if ac_params.len() < 3 { + bail_expr!(&ac_params[0], "{}", ARITY_MSG); + } + let hand_map = s.hand_map.ok_or_else(|| { + anyhow_expr!( + &ac_params[0], + "tap-hold-opposite-hand requires defhands to be defined" + ) + })?; + + let hold_timeout = parse_non_zero_u16(&ac_params[0], s, "timeout")?; + let tap_action = parse_action(&ac_params[1], s)?; + let hold_action = parse_action(&ac_params[2], s)?; + if matches!(tap_action, Action::HoldTap { .. }) { + bail_expr!( + &ac_params[1], + "tap-hold does not work in the tap-action of tap-hold" + ); + } + + let mut timeout_behavior = DecisionBehavior::Tap; + let mut same_hand = DecisionBehavior::Tap; + let mut neutral_behavior = DecisionBehavior::Ignore; + let mut unknown_hand = DecisionBehavior::Ignore; + let mut neutral_keys: Vec = Vec::new(); + let mut require_prior_idle: Option = None; + let mut tap_repress_timeout: u16 = 0; + let mut seen_options: HashSet<&str> = HashSet::default(); + + for option_expr in &ac_params[3..] { + let Some(option) = option_expr.list(s.vars()) else { + bail_expr!( + option_expr, + "expected option list, e.g. `(timeout hold)` or `(neutral-keys spc tab)`" + ); + }; + if option.is_empty() { + bail_expr!(option_expr, "option list cannot be empty"); + } + let kw = option[0] + .atom(s.vars()) + .ok_or_else(|| anyhow_expr!(&option[0], "option name must be a string"))?; + if !seen_options.insert(kw) { + bail_expr!( + &option[0], + "duplicate option '{}' in tap-hold-opposite-hand", + kw + ); + } + match kw { + "timeout" => { + if option.len() != 2 { + bail_expr!( + option_expr, + "option must contain exactly 2 items: `(name value)`" + ); + } + timeout_behavior = parse_decision_behavior_tap_hold(&option[1], s)?; + } + "same-hand" => { + if option.len() != 2 { + bail_expr!( + option_expr, + "option must contain exactly 2 items: `(name value)`" + ); + } + same_hand = parse_decision_behavior(&option[1], s)?; + } + "neutral" => { + if option.len() != 2 { + bail_expr!( + option_expr, + "option must contain exactly 2 items: `(name value)`" + ); + } + neutral_behavior = parse_decision_behavior(&option[1], s)?; + } + "unknown-hand" => { + if option.len() != 2 { + bail_expr!( + option_expr, + "option must contain exactly 2 items: `(name value)`" + ); + } + unknown_hand = parse_decision_behavior(&option[1], s)?; + } + "neutral-keys" => { + if option.len() < 2 { + bail_expr!( + option_expr, + "neutral-keys expects one or more key atoms, e.g. `(neutral-keys spc tab)`" + ); + } + neutral_keys = parse_key_atoms(&option[1..], s, "neutral-keys")?; + } + "require-prior-idle" => { + require_prior_idle = Some(tap_hold::parse_require_prior_idle_option( + option, + option_expr, + s, + )?); + } + "tap-repress-timeout" => { + tap_repress_timeout = + tap_hold::parse_tap_repress_timeout_option(option, option_expr, s)?; + } + _ => bail_expr!( + &option[0], + "unknown option '{}' for tap-hold-opposite-hand. \ + Valid options: timeout, same-hand, neutral, unknown-hand, neutral-keys, \ + require-prior-idle, tap-repress-timeout", + kw + ), + } + } + + let timeout_action = match timeout_behavior { + DecisionBehavior::Tap => tap_action, + DecisionBehavior::Hold => hold_action, + DecisionBehavior::Ignore => unreachable!(), + }; + + let neutral_keys_static = s.a.sref_vec(neutral_keys); + + Ok(s.a.sref(Action::HoldTap(s.a.sref(HoldTapAction { + config: HoldTapConfig::Custom(custom_tap_hold_opposite_hand( + hand_map, + same_hand, + neutral_behavior, + unknown_hand, + neutral_keys_static, + &s.a, + )), + tap_hold_interval: tap_repress_timeout, + timeout: hold_timeout, + tap: *tap_action, + hold: *hold_action, + timeout_action: *timeout_action, + on_press_reset_timeout_to: None, + require_prior_idle, + })))) +} + +pub(super) fn parse_tap_hold_opposite_hand_release( + ac_params: &[SExpr], + s: &ParserState, +) -> Result<&'static KanataAction> { + use custom_tap_hold::{DecisionBehavior, custom_tap_hold_opposite_hand_release}; + + const ARITY_MSG: &str = "tap-hold-opposite-hand-release expects at least 3 items: \ + [options...]"; + if ac_params.is_empty() { + bail!(ARITY_MSG); + } + if ac_params.len() < 3 { + bail_expr!(&ac_params[0], "{}", ARITY_MSG); + } + let hand_map = s.hand_map.ok_or_else(|| { + anyhow_expr!( + &ac_params[0], + "tap-hold-opposite-hand-release requires defhands to be defined" + ) + })?; + + let hold_timeout = parse_non_zero_u16(&ac_params[0], s, "timeout")?; + let tap_action = parse_action(&ac_params[1], s)?; + let hold_action = parse_action(&ac_params[2], s)?; + if matches!(tap_action, Action::HoldTap { .. }) { + bail_expr!( + &ac_params[1], + "tap-hold does not work in the tap-action of tap-hold" + ); + } + + let mut timeout_behavior = DecisionBehavior::Tap; + let mut same_hand = DecisionBehavior::Tap; + let mut neutral_behavior = DecisionBehavior::Ignore; + let mut unknown_hand = DecisionBehavior::Ignore; + let mut neutral_keys: Vec = Vec::new(); + let mut require_prior_idle: Option = None; + let mut tap_repress_timeout: u16 = 0; + let mut seen_options: HashSet<&str> = HashSet::default(); + + for option_expr in &ac_params[3..] { + let Some(option) = option_expr.list(s.vars()) else { + bail_expr!( + option_expr, + "expected option list, e.g. `(timeout hold)` or `(neutral-keys spc tab)`" + ); + }; + if option.is_empty() { + bail_expr!(option_expr, "option list cannot be empty"); + } + let kw = option[0] + .atom(s.vars()) + .ok_or_else(|| anyhow_expr!(&option[0], "option name must be a string"))?; + if !seen_options.insert(kw) { + bail_expr!( + &option[0], + "duplicate option '{}' in tap-hold-opposite-hand-release", + kw + ); + } + match kw { + "timeout" => { + if option.len() != 2 { + bail_expr!( + option_expr, + "option must contain exactly 2 items: `(name value)`" + ); + } + timeout_behavior = parse_decision_behavior_tap_hold(&option[1], s)?; + } + "same-hand" => { + if option.len() != 2 { + bail_expr!( + option_expr, + "option must contain exactly 2 items: `(name value)`" + ); + } + same_hand = parse_decision_behavior(&option[1], s)?; + } + "neutral" => { + if option.len() != 2 { + bail_expr!( + option_expr, + "option must contain exactly 2 items: `(name value)`" + ); + } + neutral_behavior = parse_decision_behavior(&option[1], s)?; + } + "unknown-hand" => { + if option.len() != 2 { + bail_expr!( + option_expr, + "option must contain exactly 2 items: `(name value)`" + ); + } + unknown_hand = parse_decision_behavior(&option[1], s)?; + } + "neutral-keys" => { + if option.len() < 2 { + bail_expr!( + option_expr, + "neutral-keys expects one or more key atoms, e.g. `(neutral-keys spc tab)`" + ); + } + neutral_keys = parse_key_atoms(&option[1..], s, "neutral-keys")?; + } + "require-prior-idle" => { + require_prior_idle = Some(tap_hold::parse_require_prior_idle_option( + option, + option_expr, + s, + )?); + } + "tap-repress-timeout" => { + tap_repress_timeout = + tap_hold::parse_tap_repress_timeout_option(option, option_expr, s)?; + } + _ => bail_expr!( + &option[0], + "unknown option '{}' for tap-hold-opposite-hand-release. \ + Valid options: timeout, same-hand, neutral, unknown-hand, neutral-keys, \ + require-prior-idle, tap-repress-timeout", + kw + ), + } + } + + let timeout_action = match timeout_behavior { + DecisionBehavior::Tap => tap_action, + DecisionBehavior::Hold => hold_action, + DecisionBehavior::Ignore => unreachable!(), + }; + + let neutral_keys_static = s.a.sref_vec(neutral_keys); + + Ok(s.a.sref(Action::HoldTap(s.a.sref(HoldTapAction { + config: HoldTapConfig::Custom(custom_tap_hold_opposite_hand_release( + hand_map, + same_hand, + neutral_behavior, + unknown_hand, + neutral_keys_static, + &s.a, + )), + tap_hold_interval: tap_repress_timeout, + timeout: hold_timeout, + tap: *tap_action, + hold: *hold_action, + timeout_action: *timeout_action, + on_press_reset_timeout_to: None, + require_prior_idle, + })))) +} + +fn parse_key_atoms(exprs: &[SExpr], s: &ParserState, label: &str) -> Result> { + exprs + .iter() + .map(|key_expr| { + let key_name = key_expr + .atom(s.vars()) + .ok_or_else(|| anyhow_expr!(key_expr, "{label} expects key atoms, found list"))?; + str_to_oscode(key_name) + .ok_or_else(|| anyhow_expr!(key_expr, "unknown key '{key_name}'")) + }) + .collect() +} + +fn parse_decision_behavior( + expr: &SExpr, + s: &ParserState, +) -> Result { + use custom_tap_hold::DecisionBehavior; + + match expr + .atom(s.vars()) + .ok_or_else(|| anyhow_expr!(expr, "expected tap, hold, or ignore"))? + { + "tap" => Ok(DecisionBehavior::Tap), + "hold" => Ok(DecisionBehavior::Hold), + "ignore" => Ok(DecisionBehavior::Ignore), + v => bail_expr!(expr, "expected tap, hold, or ignore; got '{}'", v), + } +} + +fn parse_decision_behavior_tap_hold( + expr: &SExpr, + s: &ParserState, +) -> Result { + use custom_tap_hold::DecisionBehavior; + + match expr + .atom(s.vars()) + .ok_or_else(|| anyhow_expr!(expr, "expected tap or hold"))? + { + "tap" => Ok(DecisionBehavior::Tap), + "hold" => Ok(DecisionBehavior::Hold), + v => bail_expr!(expr, "expected tap or hold for timeout; got '{}'", v), + } +} diff --git a/parser/src/cfg/definputdevices.rs b/parser/src/cfg/definputdevices.rs new file mode 100644 index 000000000..8aaf57150 --- /dev/null +++ b/parser/src/cfg/definputdevices.rs @@ -0,0 +1,132 @@ +use super::*; +use crate::{anyhow_expr, bail_expr}; +use std::num::NonZeroU8; + +#[derive(Debug, Clone, Default)] +pub struct InputDeviceMatcher { + pub name: Option, + pub hash: Option, + pub vendor_id: Option, + pub product_id: Option, +} + +pub fn parse_definputdevices(expr: &[SExpr]) -> Result> { + let mut exprs = check_first_expr(expr.iter(), "definputdevices")?; + let mut seen_ids = HashSet::default(); + let mut devices = vec![]; + while let Some(id_expr) = exprs.next() { + let Some(matchers_expr) = exprs.next() else { + bail_expr!( + id_expr, + "definputdevices expects pairs: .\n\ + Missing matcher list for this device ID." + ); + }; + let id_str = id_expr + .atom(None) + .ok_or_else(|| anyhow_expr!(id_expr, "device ID must be a number (1-255)"))?; + let id_num: u8 = id_str + .parse() + .map_err(|_| anyhow_expr!(id_expr, "device ID must be a number (1-255)"))?; + let id = NonZeroU8::new(id_num) + .ok_or_else(|| anyhow_expr!(id_expr, "device ID must be nonzero (1-255)"))?; + if !seen_ids.insert(id) { + bail_expr!(id_expr, "duplicate device ID: {id_num}"); + } + let matcher_list = matchers_expr + .list(None) + .ok_or_else(|| anyhow_expr!(matchers_expr, "device matchers must be a list"))?; + if matcher_list.is_empty() { + bail_expr!( + matchers_expr, + "device matcher list must not be empty; \ + specify at least one of: name, hash, vendor_id, product_id" + ); + } + let mut matcher = InputDeviceMatcher::default(); + for m in matcher_list.iter() { + let props = m.list(None).ok_or_else(|| { + anyhow_expr!(m, "each matcher must be a list, e.g. (name \"...\")") + })?; + if props.len() != 2 { + bail_expr!( + m, + "each matcher must have exactly 2 items: (property value)" + ); + } + let prop_name = props[0] + .atom(None) + .ok_or_else(|| anyhow_expr!(&props[0], "matcher property must be an atom"))?; + let prop_val = props[1] + .atom(None) + .ok_or_else(|| anyhow_expr!(&props[1], "matcher value must be an atom"))?; + match prop_name { + "name" => { + if matcher.name.is_some() { + bail_expr!(m, "duplicate property: name"); + } + matcher.name = Some(prop_val.to_string()); + } + "hash" => { + if matcher.hash.is_some() { + bail_expr!(m, "duplicate property: hash"); + } + let stripped = prop_val + .strip_prefix("0x") + .or_else(|| prop_val.strip_prefix("0X")) + .unwrap_or(prop_val); + if stripped.is_empty() || !stripped.chars().all(|c| c.is_ascii_hexdigit()) { + bail_expr!( + &props[1], + "hash must be a valid hex string (e.g. \"a1b2c3def4\")" + ); + } + // Store lowercase, without 0x prefix, for case-insensitive matching. + matcher.hash = Some(stripped.to_ascii_lowercase()); + } + "vendor_id" => { + if matcher.vendor_id.is_some() { + bail_expr!(m, "duplicate property: vendor_id"); + } + let v = parse_hex_or_decimal_u16(prop_val).map_err(|_| { + anyhow_expr!( + &props[1], + "vendor_id must be a number 0-65535 or hex (e.g. 0x1D50)" + ) + })?; + matcher.vendor_id = Some(v); + } + "product_id" => { + if matcher.product_id.is_some() { + bail_expr!(m, "duplicate property: product_id"); + } + let v = parse_hex_or_decimal_u16(prop_val).map_err(|_| { + anyhow_expr!( + &props[1], + "product_id must be a number 0-65535 or hex (e.g. 0x615E)" + ) + })?; + matcher.product_id = Some(v); + } + _ => { + bail_expr!( + &props[0], + "unknown matcher property: {prop_name}\n\ + valid properties: name, hash, vendor_id, product_id" + ); + } + } + } + devices.push((id, matcher)); + } + Ok(devices) +} + +fn parse_hex_or_decimal_u16(s: &str) -> std::result::Result> { + let val: u64 = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { + u64::from_str_radix(hex, 16)? + } else { + s.parse()? + }; + u16::try_from(val).map_err(|e| e.into()) +} diff --git a/parser/src/cfg/deflayer.rs b/parser/src/cfg/deflayer.rs new file mode 100644 index 000000000..5aad47e59 --- /dev/null +++ b/parser/src/cfg/deflayer.rs @@ -0,0 +1,278 @@ +use super::*; + +use crate::anyhow_expr; +use crate::anyhow_span; +use crate::bail; +use crate::bail_expr; +use crate::bail_span; + +pub(crate) type LayerIndexes = HashMap; + +pub(crate) const DEFLAYER: &str = "deflayer"; +pub(crate) const DEFLAYER_MAPPED: &str = "deflayermap"; + +/// Returns layer names and their indexes into the keyberon layout. This also checks that: +/// - All layers have the same number of items as the defsrc, +/// - There are no duplicate layer names +/// - Parentheses weren't used directly or kmonad-style escapes for parentheses weren't used. +pub(crate) fn parse_layer_indexes( + exprs: &[SpannedLayerExprs], + expected_len: usize, + vars: &HashMap, + _lsp_hints: &mut LspHints, +) -> Result<(LayerIndexes, LayerIcons)> { + let mut layer_indexes = HashMap::default(); + let mut layer_icons = HashMap::default(); + for (i, expr_type) in exprs.iter().enumerate() { + let (mut subexprs, expr, do_element_count_check, deflayer_keyword) = match expr_type { + SpannedLayerExprs::DefsrcMapping(e) => { + (check_first_expr(e.t.iter(), DEFLAYER)?, e, true, DEFLAYER) + } + SpannedLayerExprs::CustomMapping(e) => ( + check_first_expr(e.t.iter(), DEFLAYER_MAPPED)?, + e, + false, + DEFLAYER_MAPPED, + ), + }; + let layer_expr = subexprs.next().ok_or_else(|| { + anyhow_span!( + expr, + "{deflayer_keyword} requires a layer name after `{deflayer_keyword}` token" + ) + })?; + let (layer_name, _layer_name_span, icon) = { + let name = layer_expr.atom(Some(vars)); + match name { + Some(name) => (name.to_owned(), layer_expr.span(), None), + None => { + // unwrap: this **must** be a list due to atom() call above. + let list = layer_expr.list(Some(vars)).unwrap(); + let first = list.first().ok_or_else(|| anyhow_expr!( + layer_expr, + "{deflayer_keyword} requires a string name within this pair of parentheses (or a string name without any)" + ))?; + let name = first.atom(Some(vars)).ok_or_else(|| anyhow_expr!( + layer_expr, + "layer name after {deflayer_keyword} must be a string when enclosed within one pair of parentheses" + ))?; + let layer_opts = parse_layer_opts(&list[1..])?; + let icon = layer_opts + .get(DEFLAYER_ICON[0]) + .map(|icon_s| icon_s.trim_atom_quotes().to_owned()); + (name.to_owned(), first.span(), icon) + } + } + }; + if layer_indexes.contains_key(&layer_name) { + bail_expr!(layer_expr, "duplicate layer name: {}", layer_name); + } + // Check if user tried to use parentheses directly - `(` and `)` + // or escaped them like in kmonad - `\(` and `\)`. + for subexpr in subexprs { + if let Some(list) = subexpr.list(None) { + if list.is_empty() { + bail_expr!( + subexpr, + "You can't put parentheses in deflayer directly, because they are special characters for delimiting lists.\n\ + To get `(` and `)` in US layout, you should use `S-9` and `S-0` respectively.\n\ + For more context, see: https://github.com/jtroo/kanata/issues/459" + ) + } + if list.len() == 1 + && list + .first() + .is_some_and(|s| s.atom(None).is_some_and(|atom| atom == "\\")) + { + bail_expr!( + subexpr, + "Escaping shifted characters with `\\` is currently not supported in kanata.\n\ + To get `(` and `)` in US layout, you should use `S-9` and `S-0` respectively.\n\ + For more context, see: https://github.com/jtroo/kanata/issues/163" + ) + } + } + } + if do_element_count_check { + let num_actions = expr.t.len() - 2; + if num_actions != expected_len { + bail_span!( + expr, + "Layer {} has {} item(s), but requires {} to match defsrc", + layer_name, + num_actions, + expected_len + ) + } + } + + #[cfg(feature = "lsp")] + _lsp_hints + .definition_locations + .layer + .insert(layer_name.clone(), _layer_name_span.clone()); + + layer_indexes.insert(layer_name.clone(), i); + layer_icons.insert(layer_name, icon); + } + + Ok((layer_indexes, layer_icons)) +} + +pub(crate) fn parse_layers( + s: &ParserState, + mapped_keys: &mut MappedKeys, + defcfg: &CfgOptions, +) -> Result { + let mut layers_cfg = new_layers(s.layer_exprs.len()); + if s.layer_exprs.len() > MAX_LAYERS { + bail!("Maximum number of layers ({}) exceeded.", MAX_LAYERS); + } + let mut defsrc_layer = s.defsrc_layer; + for (layer_level, layer) in s.layer_exprs.iter().enumerate() { + match layer { + // The skip is done to skip the `deflayer` and layer name tokens. + LayerExprs::DefsrcMapping(layer) => { + // Parse actions in the layer and place them appropriately according + // to defsrc mapping order. + for (i, ac) in layer.iter().skip(2).enumerate() { + let ac = parse_action(ac, s)?; + layers_cfg[layer_level][0][s.mapping_order[i]] = *ac; + } + } + LayerExprs::CustomMapping(layer) => { + // Parse actions as input output pairs + let mut pairs = layer[2..].chunks_exact(2); + let mut layer_mapped_keys = HashSet::default(); + let mut defsrc_anykey_used = false; + let mut unmapped_anykey_used = false; + let mut both_anykey_used = false; + for pair in pairs.by_ref() { + let input = &pair[0]; + let action = &pair[1]; + + let action = parse_action(action, s)?; + if input.atom(s.vars()).is_some_and(|x| x == "_") { + if defsrc_anykey_used { + bail_expr!(input, "must have only one use of _ within a layer") + } + if both_anykey_used { + bail_expr!(input, "must either use _ or ___ within a layer, not both") + } + for i in 0..s.mapping_order.len() { + if layers_cfg[layer_level][0][s.mapping_order[i]] == DEFAULT_ACTION { + layers_cfg[layer_level][0][s.mapping_order[i]] = *action; + } + } + defsrc_anykey_used = true; + } else if input.atom(s.vars()).is_some_and(|x| x == "__") { + if unmapped_anykey_used { + bail_expr!(input, "must have only one use of __ within a layer") + } + if !defcfg.process_unmapped_keys { + bail_expr!( + input, + "must set process-unmapped-keys to yes to use __ to map unmapped keys" + ); + } + if both_anykey_used { + bail_expr!(input, "must either use __ or ___ within a layer, not both") + } + for i in 0..layers_cfg[0][0].len() { + if layers_cfg[layer_level][0][i] == DEFAULT_ACTION + && !s.mapping_order.contains(&i) + { + layers_cfg[layer_level][0][i] = *action; + } + } + unmapped_anykey_used = true; + } else if input.atom(s.vars()).is_some_and(|x| x == "___") { + if both_anykey_used { + bail_expr!(input, "must have only one use of ___ within a layer") + } + if defsrc_anykey_used { + bail_expr!(input, "must either use _ or ___ within a layer, not both") + } + if unmapped_anykey_used { + bail_expr!(input, "must either use __ or ___ within a layer, not both") + } + if !defcfg.process_unmapped_keys { + bail_expr!( + input, + "must set process-unmapped-keys to yes to use ___ to also map unmapped keys" + ); + } + for i in 0..layers_cfg[0][0].len() { + if layers_cfg[layer_level][0][i] == DEFAULT_ACTION { + layers_cfg[layer_level][0][i] = *action; + } + } + both_anykey_used = true; + } else { + let input_key = input + .atom(s.vars()) + .and_then(str_to_oscode) + .ok_or_else(|| anyhow_expr!(input, "input must be a key name"))?; + mapped_keys.insert(input_key); + if !layer_mapped_keys.insert(input_key) { + bail_expr!(input, "input key must not be repeated within a layer") + } + layers_cfg[layer_level][0][usize::from(input_key)] = *action; + } + } + let rem = pairs.remainder(); + if !rem.is_empty() { + bail_expr!(&rem[0], "input must by followed by an action"); + } + } + } + for (osc, layer_action) in layers_cfg[layer_level][0].iter_mut().enumerate() { + if *layer_action == DEFAULT_ACTION { + *layer_action = match s.block_unmapped_keys && !is_a_button(osc as u16) { + true => Action::NoOp, + false => Action::Trans, + }; + } + } + + // Set fake keys on every layer. + for (y, action) in s.virtual_keys.values() { + let (x, y) = get_fake_key_coords(*y); + layers_cfg[layer_level][x as usize][y as usize] = **action; + } + + // If the user has configured delegation to the first (default) layer for transparent keys, + // (as opposed to delegation to defsrc), replace the defsrc actions with the actions from + // the first layer. + if layer_level == 0 && s.delegate_to_first_layer { + for (defsrc_ac, default_layer_ac) in defsrc_layer.iter_mut().zip(layers_cfg[0][0]) { + if default_layer_ac != Action::Trans { + *defsrc_ac = default_layer_ac; + } + } + } + + // Very last thing - ensure index 0 is always no-op. This shouldn't have any way to be + // physically activated. This enable other code to rely on there always being a no-op key. + layers_cfg[layer_level][0][0] = Action::NoOp; + } + Ok(layers_cfg) +} + +pub(crate) fn parse_layer_base( + ac_params: &[SExpr], + s: &ParserState, +) -> Result<&'static KanataAction> { + let idx = layer_idx(ac_params, &s.layer_idxs, s)?; + set_layer_change_lsp_hint(&ac_params[0], &mut s.lsp_hints.borrow_mut()); + Ok(s.a.sref(Action::DefaultLayer(idx))) +} + +pub(crate) fn parse_layer_toggle( + ac_params: &[SExpr], + s: &ParserState, +) -> Result<&'static KanataAction> { + let idx = layer_idx(ac_params, &s.layer_idxs, s)?; + set_layer_change_lsp_hint(&ac_params[0], &mut s.lsp_hints.borrow_mut()); + Ok(s.a.sref(Action::Layer(idx))) +} diff --git a/parser/src/cfg/deflocalkeys.rs b/parser/src/cfg/deflocalkeys.rs new file mode 100644 index 000000000..3bc7cc136 --- /dev/null +++ b/parser/src/cfg/deflocalkeys.rs @@ -0,0 +1,93 @@ +use super::*; + +use crate::anyhow_expr; +use crate::bail_expr; + +#[cfg(all( + not(feature = "interception_driver"), + any( + not(feature = "win_llhook_read_scancodes"), + not(feature = "win_sendinput_send_scancodes") + ), + target_os = "windows" +))] +pub(crate) const DEF_LOCAL_KEYS: &str = "deflocalkeys-win"; +#[cfg(all( + feature = "win_llhook_read_scancodes", + feature = "win_sendinput_send_scancodes", + not(feature = "interception_driver"), + target_os = "windows" +))] +pub(crate) const DEF_LOCAL_KEYS: &str = "deflocalkeys-winiov2"; +#[cfg(all(feature = "interception_driver", target_os = "windows"))] +pub(crate) const DEF_LOCAL_KEYS: &str = "deflocalkeys-wintercept"; +#[cfg(target_os = "macos")] +pub(crate) const DEF_LOCAL_KEYS: &str = "deflocalkeys-macos"; +#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))] +pub(crate) const DEF_LOCAL_KEYS: &str = "deflocalkeys-linux"; + +pub(crate) fn deflocalkeys_variant_applies_to_current_os(variant: &str) -> bool { + variant == DEF_LOCAL_KEYS +} + +pub(crate) const DEFLOCALKEYS_VARIANTS: &[&str] = &[ + "deflocalkeys-win", + "deflocalkeys-winiov2", + "deflocalkeys-wintercept", + "deflocalkeys-linux", + "deflocalkeys-macos", +]; + +/// Parse custom keys from an expression starting with deflocalkeys. +pub(crate) fn parse_deflocalkeys( + def_local_keys_variant: &str, + expr: &[SExpr], +) -> Result> { + let mut localkeys = HashMap::default(); + let mut exprs = check_first_expr(expr.iter(), def_local_keys_variant)?; + // Read k-v pairs from the configuration + while let Some(key_expr) = exprs.next() { + let key = key_expr.atom(None).ok_or_else(|| { + anyhow_expr!(key_expr, "No lists are allowed in {def_local_keys_variant}") + })?; + if localkeys.contains_key(key) { + bail_expr!( + key_expr, + "Duplicate {key} found in {def_local_keys_variant}" + ); + } + + // Bug: + // Trying to convert a number to OsCode is OS-dependent and is fallible. + // A valid number for Linux could throw an error on Windows. + // + // Fix: + // When the deflocalkeys variant does not apply to the current OS, + // use a dummy OsCode to keep the "same name" validation + // while avoiding the u16->OsCode conversion attempt. + if !deflocalkeys_variant_applies_to_current_os(def_local_keys_variant) { + localkeys.insert(key.to_owned(), OsCode::KEY_RESERVED); + continue; + } + + let osc = match exprs.next() { + Some(v) => v + .atom(None) + .ok_or_else(|| anyhow_expr!(v, "No lists are allowed in {def_local_keys_variant}")) + .and_then(|osc| { + osc.parse::().map_err(|_| { + anyhow_expr!(v, "Unknown number in {def_local_keys_variant}: {osc}") + }) + }) + .and_then(|osc| { + OsCode::from_u16(osc).ok_or_else(|| { + anyhow_expr!(v, "Unknown number in {def_local_keys_variant}: {osc}") + }) + })?, + None => bail_expr!(key_expr, "Key without a number in {def_local_keys_variant}"), + }; + log::debug!("custom mapping: {key} {}", osc.as_u16()); + localkeys.insert(key.to_owned(), osc); + } + Ok(localkeys) +} diff --git a/parser/src/cfg/defsrc.rs b/parser/src/cfg/defsrc.rs new file mode 100644 index 000000000..5f39127e1 --- /dev/null +++ b/parser/src/cfg/defsrc.rs @@ -0,0 +1,106 @@ +use super::*; + +use crate::anyhow_expr; +use crate::bail_expr; + +/// Parse mapped keys from an expression starting with defsrc. Returns the key mapping as well as +/// a vec of the indexes in order. The length of the returned vec should be matched by the length +/// of all layer declarations. +pub(crate) fn parse_defsrc( + expr: &[SExpr], + defcfg: &CfgOptions, +) -> Result<(MappedKeys, Vec, MouseInDefsrc)> { + let exprs = check_first_expr(expr.iter(), "defsrc")?; + let mut mkeys = MappedKeys::default(); + let mut ordered_codes = Vec::new(); + let mut is_mouse_used = MouseInDefsrc::NoMouse; + for expr in exprs { + let s = match expr { + SExpr::Atom(a) => &a.t, + _ => bail_expr!(expr, "No lists allowed in defsrc"), + }; + let oscode = str_to_oscode(s) + .ok_or_else(|| anyhow_expr!(expr, "Unknown key in defsrc: \"{}\"", s))?; + is_mouse_used = match (is_mouse_used, oscode) { + ( + MouseInDefsrc::NoMouse, + OsCode::BTN_LEFT + | OsCode::BTN_RIGHT + | OsCode::BTN_MIDDLE + | OsCode::BTN_SIDE + | OsCode::BTN_EXTRA + | OsCode::MouseWheelUp + | OsCode::MouseWheelDown + | OsCode::MouseWheelLeft + | OsCode::MouseWheelRight, + ) => MouseInDefsrc::MouseUsed, + _ => is_mouse_used, + }; + + if mkeys.contains(&oscode) { + bail_expr!(expr, "Repeat declaration of key in defsrc: \"{}\"", s) + } + mkeys.insert(oscode); + ordered_codes.push(oscode.into()); + } + + let mapped_exceptions = match &defcfg.process_unmapped_keys_exceptions { + Some(excluded_keys) => { + for excluded_key in excluded_keys.iter() { + log::debug!("process unmapped keys exception: {:?}", excluded_key); + if mkeys.contains(&excluded_key.0) { + bail_expr!( + &excluded_key.1, + "Keys cannot be included in defsrc and also excepted in process-unmapped-keys." + ); + } + } + + excluded_keys + .iter() + .map(|excluded_key| excluded_key.0) + .collect() + } + None => vec![], + }; + + log::info!("process unmapped keys: {}", defcfg.process_unmapped_keys); + if defcfg.process_unmapped_keys { + for osc in 0..KEYS_IN_ROW as u16 { + if let Some(osc) = OsCode::from_u16(osc) { + if osc.is_mouse_code() { + // Bugfix #1879: + // Auto-including mouse activity in mapped keys + // seems strictly incorrect to do, so never do it. + // Users can still choose to opt in if they want. + // Auto-including mouse activity breaks many scenarios. + continue; + } + match KeyCode::from(osc) { + KeyCode::No => {} + _ => { + if !mapped_exceptions.contains(&osc) { + mkeys.insert(osc); + } + } + } + } + } + } + + mkeys.shrink_to_fit(); + Ok((mkeys, ordered_codes, is_mouse_used)) +} + +pub(crate) fn create_defsrc_layer() -> [KanataAction; KEYS_IN_ROW] { + let mut layer = [KanataAction::NoOp; KEYS_IN_ROW]; + + for (i, ac) in layer.iter_mut().enumerate() { + *ac = OsCode::from_u16(i as u16) + .map(|osc| Action::KeyCode(osc.into())) + .unwrap_or(Action::NoOp); + } + // Ensure 0-index is no-op. + layer[0] = KanataAction::NoOp; + layer +} diff --git a/parser/src/cfg/deftemplate.rs b/parser/src/cfg/deftemplate.rs new file mode 100644 index 000000000..01afb501d --- /dev/null +++ b/parser/src/cfg/deftemplate.rs @@ -0,0 +1,563 @@ +//! This file is responsible for template expansion. +//! For simplicity of implementation, there is performance left off the table. +//! This code runs at parse time and not in runtime +//! so it is not performance critical. +//! +//! The known performance left off the table is: +//! +//! - Creating the expanded template recurses through all SExprs every time. +//! Instead the code could pre-compute the paths to access every variable +//! that needs substition. (perf_1) +//! +//! - Replacing the `template-expand|if-equal` items with the appropriate values +//! recreates the Vec for every replacement that happens at that recursion depth. +//! Instead the code could do recreate the vec only once +//! and insert SExprs at the proper places. (perf_2) + +use crate::anyhow_expr; +use crate::anyhow_span; +use crate::bail_expr; +use crate::bail_span; +use crate::err_expr; +use crate::err_span; + +use super::error::*; +use super::sexpr::*; +use super::*; + +#[derive(Debug)] +struct Template { + name: String, + vars: Vec, + // Same as vars above but all names are prefixed with '$'. + vars_substitute_names: Vec, + content: Vec, +} + +/// Parse `deftemplate`s and expand `template-expand`s. +/// +/// Syntax of `deftemplate` is: +/// +/// `(deftemplate