Repository: jtroo/kanata Branch: main Commit: 3bda1ec9035c Files: 272 Total size: 2.3 MB Directory structure: gitextract_lgrqbydp/ ├── .devcontainer/ │ └── devcontainer.json ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── build-everything.yml │ ├── linux-build.yml │ ├── macos-build.yml │ ├── rust.yml │ └── windows-build.yml ├── .gitignore ├── Cargo.toml ├── EnableUIAccess/ │ ├── EnableUIAccess_launch.ahk │ ├── Lib/ │ │ └── EnableUIAccess.ahk │ └── README.md ├── LICENSE ├── README.md ├── build.rs ├── cfg_samples/ │ ├── artsey.kbd │ ├── automousekeys-full-map.kbd │ ├── automousekeys-only.kbd │ ├── chords.tsv │ ├── colemak.kbd │ ├── deflayermap.kbd │ ├── f13_f24.kbd │ ├── fancy_symbols.kbd │ ├── home-row-mod-advanced.kbd │ ├── home-row-mod-basic.kbd │ ├── home-row-mod-prior-idle.kbd │ ├── included-file.kbd │ ├── japanese_mac_eisu_kana.kbd │ ├── jtroo.kbd │ ├── kanata.kbd │ ├── key-toggle_press-only_release-only.kbd │ ├── minimal.kbd │ ├── opposite-hand-hrm.kbd │ ├── push-msg.kbd │ ├── simple.kbd │ └── tray-icon/ │ ├── license_icons.txt │ └── tray-icon.kbd ├── docs/ │ ├── README.md │ ├── config-stylesheet.css │ ├── config.adoc │ ├── design.md │ ├── fancy_symbols.md │ ├── interception.md │ ├── kmonad_comparison.md │ ├── locales.adoc │ ├── platform-known-issues.adoc │ ├── release-template.md │ ├── sequence-adding-chords-ideas.md │ ├── setup-linux.md │ ├── simulated_output/ │ │ ├── sim.kbd │ │ ├── sim.txt │ │ └── sim_out.txt │ ├── simulated_passthru_ahk/ │ │ ├── [COPY HERE] kanata_passthru.dll _ │ │ ├── kanata_dll.kbd │ │ └── kanata_passthru.ahk │ └── switch-design ├── example_tcp_client/ │ ├── .gitignore │ ├── Cargo.toml │ └── src/ │ └── main.rs ├── interception/ │ ├── Cargo.toml │ └── src/ │ ├── lib.rs │ └── scancode.rs ├── justfile ├── key-sort-add/ │ ├── Cargo.toml │ ├── README.md │ ├── mapping.txt │ └── src/ │ └── main.rs ├── keyberon/ │ ├── .gitignore │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── KEYBOARDS.md │ ├── LICENSE │ ├── README.md │ ├── keyberon-macros/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ └── lib.rs │ └── src/ │ ├── action/ │ │ └── switch.rs │ ├── action.rs │ ├── chord.rs │ ├── key_code.rs │ ├── layout/ │ │ └── contextual_execution.rs │ ├── layout.rs │ ├── lib.rs │ ├── multikey_buffer.rs │ └── tap_hold_tracker.rs ├── parser/ │ ├── .gitignore │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── src/ │ │ ├── cfg/ │ │ │ ├── alloc.rs │ │ │ ├── arbitrary_code.rs │ │ │ ├── caps_word.rs │ │ │ ├── chord.rs │ │ │ ├── chord_v1.rs │ │ │ ├── clipboard.rs │ │ │ ├── cmd.rs │ │ │ ├── custom_tap_hold.rs │ │ │ ├── defcfg.rs │ │ │ ├── defhands.rs │ │ │ ├── deflayer.rs │ │ │ ├── deflocalkeys.rs │ │ │ ├── defsrc.rs │ │ │ ├── deftemplate.rs │ │ │ ├── error.rs │ │ │ ├── fake_key.rs │ │ │ ├── fork.rs │ │ │ ├── is_a_button.rs │ │ │ ├── key_outputs.rs │ │ │ ├── key_override.rs │ │ │ ├── layer_opts.rs │ │ │ ├── list_actions.rs │ │ │ ├── live_reload.rs │ │ │ ├── macro.rs │ │ │ ├── mod.rs │ │ │ ├── mouse.rs │ │ │ ├── multi.rs │ │ │ ├── oneshot.rs │ │ │ ├── override.rs │ │ │ ├── permutations.rs │ │ │ ├── platform.rs │ │ │ ├── push_msg.rs │ │ │ ├── releases.rs │ │ │ ├── sequence.rs │ │ │ ├── sexpr.rs │ │ │ ├── str_ext.rs │ │ │ ├── switch.rs │ │ │ ├── tap_dance.rs │ │ │ ├── tap_hold.rs │ │ │ ├── tests/ │ │ │ │ ├── ambiguous.rs │ │ │ │ ├── defcfg.rs │ │ │ │ ├── defhands.rs │ │ │ │ ├── device_detect.rs │ │ │ │ ├── environment.rs │ │ │ │ └── macros.rs │ │ │ ├── tests.rs │ │ │ ├── unicode.rs │ │ │ ├── unmod.rs │ │ │ ├── vars.rs │ │ │ └── zippychord.rs │ │ ├── custom_action.rs │ │ ├── keys/ │ │ │ ├── linux.rs │ │ │ ├── macos.rs │ │ │ ├── mappings.rs │ │ │ ├── mod.rs │ │ │ └── windows.rs │ │ ├── layers.rs │ │ ├── lib.rs │ │ ├── lsp_hints.rs │ │ ├── sequences.rs │ │ ├── subset.rs │ │ └── trie.rs │ └── test_cfgs/ │ ├── all_keys_in_defsrc.kbd │ ├── ancestor_seq.kbd │ ├── bad_multi.kbd │ ├── descendant_seq.kbd │ ├── icon_bad_dupe.kbd │ ├── icon_good.kbd │ ├── include-bad.kbd │ ├── include-bad2.kbd │ ├── include-good-optional-absent.kbd │ ├── include-good.kbd │ ├── included-bad.kbd │ ├── included-bad2.kbd │ ├── included-good.kbd │ ├── macro-chord-dont-panic.kbd │ ├── multiline_comment.kbd │ ├── nested_tap_hold.kbd │ ├── test.zch │ ├── testzch.kbd │ ├── unknown_defcfg_opt.kbd │ ├── utf8bom-included.kbd │ └── utf8bom.kbd ├── rustfmt.toml ├── scripts/ │ └── test_linux_list_devices.sh ├── simulated_input/ │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ └── src/ │ └── sim.rs ├── simulated_passthru/ │ ├── .gitignore │ ├── Cargo.toml │ ├── ReadMe.md │ └── src/ │ ├── key_in.rs │ ├── key_out.rs │ ├── lib_passthru.rs │ └── log_win.rs ├── src/ │ ├── gui/ │ │ ├── mod.rs │ │ ├── win.rs │ │ ├── win_dbg_logger/ │ │ │ ├── mod.rs │ │ │ └── win_dbg_logger.toml │ │ └── win_nwg_ext/ │ │ ├── license-MIT │ │ ├── license-nwg-MIT │ │ └── mod.rs │ ├── kanata/ │ │ ├── caps_word.rs │ │ ├── cfg_forced.rs │ │ ├── clipboard.rs │ │ ├── cmd.rs │ │ ├── dynamic_macro.rs │ │ ├── key_repeat.rs │ │ ├── linux.rs │ │ ├── macos.rs │ │ ├── millisecond_counting.rs │ │ ├── mod.rs │ │ ├── output_logic/ │ │ │ └── zippychord.rs │ │ ├── output_logic.rs │ │ ├── scroll.rs │ │ ├── sequences.rs │ │ ├── unknown.rs │ │ └── windows/ │ │ ├── exthook.rs │ │ ├── interception.rs │ │ ├── llhook.rs │ │ └── mod.rs │ ├── kanata.exe.manifest.rc │ ├── lib.rs │ ├── main.rs │ ├── main_lib/ │ │ ├── args.rs │ │ ├── mod.rs │ │ └── win_gui.rs │ ├── oskbd/ │ │ ├── linux.rs │ │ ├── macos.rs │ │ ├── mod.rs │ │ ├── sim_passthru.rs │ │ ├── simulated.rs │ │ └── windows/ │ │ ├── exthook_os.rs │ │ ├── interception.rs │ │ ├── interception_convert.rs │ │ ├── llhook/ │ │ │ └── mouse.rs │ │ ├── llhook.rs │ │ ├── mod.rs │ │ └── scancode_to_usvk.rs │ ├── tcp_server.rs │ ├── tests/ │ │ ├── passthru_macos_tests.rs │ │ └── sim_tests/ │ │ ├── block_keys_tests.rs │ │ ├── capsword_sim_tests.rs │ │ ├── chord_sim_tests.rs │ │ ├── delay_tests.rs │ │ ├── layer_sim_tests.rs │ │ ├── macro_sim_tests.rs │ │ ├── mod.rs │ │ ├── oneshot_tests.rs │ │ ├── output_chord_tests.rs │ │ ├── override_tests.rs │ │ ├── release_sim_tests.rs │ │ ├── repeat_sim_tests.rs │ │ ├── seq_sim_tests.rs │ │ ├── switch_sim_tests.rs │ │ ├── tap_dance_tests.rs │ │ ├── tap_hold_tests.rs │ │ ├── template_sim_tests.rs │ │ ├── timing_tests.rs │ │ ├── unicode_sim_tests.rs │ │ ├── unmod_sim_tests.rs │ │ ├── use_defsrc_sim_tests.rs │ │ ├── vkey_sim_tests.rs │ │ └── zippychord_sim_tests.rs │ └── tests.rs ├── tcp_protocol/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── wasm/ │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ └── src/ │ └── lib.rs └── windows_key_tester/ ├── .gitignore ├── Cargo.toml ├── README.md └── src/ ├── main.rs ├── windows/ │ ├── interception.rs │ └── llhook.rs └── windows.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "Rust", "image": "mcr.microsoft.com/devcontainers/rust:1-buster" // Features to add to the dev container. More info: https://containers.dev/implementors/features. // "features": {}, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "rustc --version", // Configure tool-specific properties. // "customizations": {}, // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" } ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ 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 ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Discussions url: https://github.com/jtroo/kanata/discussions about: Ask for help or interact with the community. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ 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 ================================================ FILE: .github/pull_request_template.md ================================================ ## 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 ================================================ FILE: .github/workflows/build-everything.yml ================================================ 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 ================================================ FILE: .github/workflows/linux-build.yml ================================================ 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@v3 - 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@v4 with: name: linux-binaries-x64 path: | artifacts-x64/kanata_linux_x64 artifacts-x64/kanata_linux_cmd_allowed_x64 ================================================ FILE: .github/workflows/macos-build.yml ================================================ 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@v3 - 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@v4 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@v3 - 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@v4 with: name: macos-binaries-x64 path: | artifacts/kanata_macos_x64 artifacts/kanata_macos_cmd_allowed_x64 ================================================ FILE: .github/workflows/rust.yml ================================================ name: cargo-checks on: push: branches: [ "main" ] paths: - Cargo.* - src/**/* - keyberon/**/* - cfg_samples/**/* - parser/**/* - tcp_protocol/**/* - wasm/**/* - .github/workflows/rust.yml pull_request: branches: [ "main" ] paths: - Cargo.* - src/**/* - keyberon/**/* - parser/**/* - cfg_samples/**/* - tcp_protocol/**/* - wasm/**/* - .github/workflows/rust.yml env: CARGO_TERM_COLOR: always RUSTFLAGS: "-Dwarnings" jobs: fmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Check fmt run: cargo fmt --all --check build-android: runs-on: ${{ matrix.os }} strategy: matrix: include: - build: linux os: ubuntu-latest target: aarch64-linux-android steps: - uses: actions/checkout@v3 - 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: ${{ matrix.os }} strategy: matrix: include: - build: linux os: ubuntu-latest target: x86_64-unknown-linux-musl steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 with: shared-key: "persist-cross-job" workspaces: ./ - run: rustup component add clippy - name: Run tests no features run: cargo test --all --no-default-features - name: Run clippy no features run: cargo clippy --all --no-default-features -- -D warnings - name: Run tests default features run: cargo test --all - name: Run clippy default features run: cargo clippy --all -- -D warnings - name: Run tests cmd run: cargo test --all --features=cmd - name: Run clippy cmd run: cargo clippy --all --features=cmd -- -D warnings - name: Run tests simulated output run: cargo test --features=simulated_output -- sim_tests - name: Run 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: ${{ matrix.os }} strategy: matrix: include: - build: windows os: windows-latest target: x86_64-pc-windows-msvc steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 with: shared-key: "persist-cross-job" workspaces: ./ - run: rustup component add clippy - name: Run tests no features run: cargo test --all --no-default-features - name: Run clippy no features run: cargo clippy --all --no-default-features -- -D warnings - name: Run tests default features run: cargo test --all - name: Run clippy default features run: cargo clippy --all -- -D warnings - name: Run tests winIOv2 run: cargo test --all --features=cmd,win_llhook_read_scancodes,win_sendinput_send_scancodes - name: Run clippy all winIOv2 run: cargo clippy --all --features=cmd,win_llhook_read_scancodes,win_sendinput_send_scancodes -- -D warnings - name: Run tests all features run: cargo test -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@v3 - uses: Swatinem/rust-cache@v2 with: shared-key: "persist-cross-job" workspaces: ./ - run: rustup component add clippy - name: Run tests default features run: cargo test --all - name: Run clippy default features run: cargo clippy --all -- -D warnings - name: Run tests cmd run: cargo test --all --features=cmd - name: Run clippy all features run: cargo clippy --all --features=cmd -- -D warnings ================================================ FILE: .github/workflows/windows-build.yml ================================================ 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@v3 - 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@v4 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@v3 - 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@v4 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 ================================================ FILE: .gitignore ================================================ **/target .vscode/ CLAUDE.md .DS_Store PLAN.md # Manual testing files test_*.kbd manual_test/ *.test.kbd ================================================ FILE: Cargo.toml ================================================ [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.11.0" authors = ["jtroo "] description = "Multi-layer keyboard customization" keywords = ["keyboard", "layout", "remapping"] categories = ["command-line-utilities"] homepage = "https://github.com/jtroo/kanata" repository = "https://github.com/jtroo/kanata" readme = "README.md" license = "LGPL-3.0-only" edition = "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] anyhow = "1" 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" parking_lot = "0.12" radix_trie = "0.2" rustc-hash = "1.1.0" simplelog = "0.12.0" serde_json = { version = "1", features = ["std"], default-features = false, optional = true } time = "0.3.47" web-time = "1.1.0" kanata-keyberon = { path = "keyberon", version = "0.1110.0" } kanata-parser = { path = "parser", version = "0.1110.0" } kanata-tcp-protocol = { path = "tcp_protocol", version = "0.1110.0" } [target.'cfg(not(any(target_arch = "wasm32", target_os = "android")))'.dependencies] arboard = "3.4" [target.'cfg(target_os = "macos")'.dependencies] karabiner-driverkit = "0.2.1" objc = "0.2.7" core-graphics = "0.24.0" open = { version = "5", optional = true } libc = "0.2" os_pipe = "1.2.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] encode_unicode = "0.3.6" winapi = { version = "0.3.9", features = [ "wincon", "timeapi", "mmsystem", "winuser", "windef", "minwindef", ] } 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] default = ["tcp_server","win_sendinput_send_scancodes", "zippychord"] perf_logging = [] 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" lto = "fat" panic = "abort" codegen-units = 1 ================================================ FILE: EnableUIAccess/EnableUIAccess_launch.ahk ================================================ #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, "*") } ================================================ FILE: EnableUIAccess/Lib/EnableUIAccess.ahk ================================================ #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') } ================================================ FILE: EnableUIAccess/README.md ================================================ # EnableUIAccess See [the guide documentation for context](https://github.com/jtroo/kanata/blob/main/docs/config.adoc#windows-only-work-elevated). ================================================ FILE: LICENSE ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ================================================ FILE: README.md ================================================

Kanata

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

Improve your keyboard comfort
## What does this do? This is a cross-platform software keyboard remapper for Linux, macOS and Windows. A short summary of the features: - multiple layers of key functionality - advanced key behaviour customization (e.g. tap-hold, macros, unicode) To see all of the features, see the [configuration guide](./docs/config.adoc). You can find pre-built binaries in the [releases page](https://github.com/jtroo/kanata/releases) or read on for build instructions. You can see a [list of known issues here](./docs/platform-known-issues.adoc). ### Demo #### Demo video [Showcase of multi-layer functionality (30s, 1.7 MB)](https://user-images.githubusercontent.com/6634136/183001314-f64a7e26-4129-4f20-bf26-7165a6e02c38.mp4). #### Online simulator You can check out the [online simulator](https://jtroo.github.io) to test configuration validity and test input simulation. ## Why is this useful? Imagine if, instead of pressing Shift to type uppercase letters, we had giant keyboards with separate keys for lowercase and uppercase letters. I hope we can all agree: that would be a terrible user experience! A way to think of how Shift keys work is that they switch your input to another layer of functionality where you now type uppercase letters and symbols instead of lowercase letters and numbers. What kanata allows you to do is take this alternate layer concept that Shift keys have and apply it to any key. You can then customize what those layers do to suit your exact needs and workflows. ## Usage Running kanata currently does not start it in a background process. You will need to keep the window that starts kanata running to keep kanata active. Some tips for running kanata in the background: - Windows: https://github.com/jtroo/kanata/discussions/193 - Linux: https://github.com/jtroo/kanata/discussions/130#discussioncomment-10227272 - Run from tray icon: [kanata-tray](https://github.com/rszyma/kanata-tray) ### Pre-built executables See the [releases page](https://github.com/jtroo/kanata/releases) for executables and instructions. ### Build it yourself This project uses the latest Rust stable toolchain. If you installed the Rust toolchain using `rustup`, e.g. by using the instructions from the [official website](https://www.rust-lang.org/learn/get-started), you can get the latest stable toolchain with `rustup update stable`.
Instructions Using `cargo install`: cargo install kanata # On Linux and macOS, this may not work without `sudo`, see below kanata --cfg Build and run yourself in Linux: git clone https://github.com/jtroo/kanata && cd kanata cargo build # --release optional, not really perf sensitive # sudo is used because kanata opens /dev/ files # # See below if you want to avoid needing sudo: # https://github.com/jtroo/kanata/wiki/Avoid-using-sudo-on-Linux sudo target/debug/kanata --cfg Build and run yourself in Windows. git clone https://github.com/jtroo/kanata; cd kanata cargo build # --release optional, not really perf sensitive target\debug\kanata --cfg Build and run yourself in macOS: First install the Karabiner driver by following the macOS documentation in the [releases page](https://github.com/jtroo/kanata/releases/). Then you can compile and run with the instructions below: git clone https://github.com/jtroo/kanata && cd kanata cargo build # --release optional, not really perf sensitive # sudo is needed to gain permission to intercept the keyboard sudo target/debug/kanata --cfg The full configuration guide is [found here](./docs/config.adoc). Sample configuration files are found in [cfg_samples](./cfg_samples). The [simple.kbd](./cfg_samples/simple.kbd) file contains a basic configuration file that is hopefully easy to understand but does not contain all features. The `kanata.kbd` contains an example of all features with documentation. The release assets also have a `kanata.kbd` file that is tested to work with that release. All key names can be found in the [keys module](./parser/src/keys/mod.rs), and you can also define your own key names.
### Feature flags When either building yourself or using `cargo install`, you can add feature flags that enable functionality that is turned off by default.
Instructions If you want to enable the `cmd` actions, add the flag `--features cmd`. For example: ``` cargo build --release --features cmd cargo install --features cmd ``` On Windows, if you want to compile a binary that uses the Interception driver, you should add the flag `--features interception_driver`. For example: ``` cargo build --release --features interception_driver cargo install --features interception_driver ``` To combine multiple flags, use a single `--features` flag and use a comma to separate the features. For example: ``` cargo build --release --features cmd,interception_driver cargo install --features cmd,interception_driver ```
## Other installation methods
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. - [Minimal example](./cfg_samples/minimal.kbd) - [Full guide](./docs/config.adoc) - [Simple example with explanations](./cfg_samples/simple.kbd) - [All features showcase](./cfg_samples/kanata.kbd) - Live reloading of the configuration for easy testing of your changes. - Multiple layers of key functionality - Advanced actions such as tap-hold, unicode output, dynamic and static macros - Vim-like leader sequences to execute other actions - Optionally run a TCP server to interact with other programs - Other programs can respond to [layer changes or trigger layer changes](https://github.com/jtroo/kanata/issues/47) - [Interception driver](https://web.archive.org/web/20240209172129/http://www.oblita.com/interception) support (use `kanata_wintercept.exe`) - Note that this issue exists, which is outside the control of this project: https://github.com/oblitum/Interception/issues/25 ## Contributing Contributions are welcome! Unless explicitly stated otherwise, your contributions to kanata will be made under the LGPL-3.0-only[*] license. Some directories are exceptions: - [keyberon](./keyberon): MIT License - [interception](./interception): MIT or Apache-2.0 Licenses [Here's a basic low-effort design doc of kanata](./docs/design.md) [*]: https://www.gnu.org/licenses/identify-licenses-clearly.html ## How you can help - Try it out and let me know what you think. Feel free to file an issue or start a discussion. - Usability issues and unhelpful error messages are considered bugs that should be fixed. If you encounter any, I would be thankful if you file an issue. - Browse the open issues and help out if you are able and/or would like to. If you want to try contributing, feel free to ping jtroo for some pointers. - If you know anything about writing a keyboard driver for Windows, starting an open-source alternative to the Interception driver would be lovely. ## Community projects related to kanata - [vscode-kanata](https://github.com/rszyma/vscode-kanata): Language support for kanata configuration files in VS Code - [komokana](https://github.com/LGUG2Z/komokana): Automatic application-aware layer switching for [`komorebi`](https://github.com/LGUG2Z/komorebi) (Windows) - [kanata-tray](https://github.com/rszyma/kanata-tray): Control kanata from a tray icon - [OverKeys](https://github.com/conventoangelo/overkeys): Visual layer display for kanata - see your active layers and keymaps in real-time (Windows) - Application-aware layer switching: - [qanata (Linux)](https://github.com/veyxov/qanata) - [kanawin (Windows)](https://github.com/Aqaao/kanawin) - [window_tools (Windows)](https://github.com/reidprichard/window_tools) - [nata (Linux)](https://github.com/mdSlash/nata) - [kanata-vk-agent (macOS)](https://github.com/devsunb/kanata-vk-agent) - [hyprkan (Linux)](https://github.com/mdSlash/hyprkan) - [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, kanata is an indigenous Iroquoian word meaning "village" or "settlement" and is the origin of Canada's name. There's also PPT✧. ## Motivation TLDR: QMK features but for any keyboard, not just fancy mechanical ones.
Long version I have a few keyboards that run [QMK](https://docs.qmk.fm/#/). QMK allows the user to customize the functionality of their keyboard to their heart's content. One great use case of QMK is its ability map keys so that they overlap with the home row keys but are accessible on another layer. I won't comment on productivity, but I find this greatly helps with my keyboard comfort. For example, these keys are on the right side of the keyboard: 7 8 9 u i o j k l m , . On one layer I have arrow keys in the same position, and on another layer I have a numpad. arrows: numpad: - - - 7 8 9 - ↑ - 4 5 6 ← ↓ → 1 2 3 - - - 0 * . One could add as many customizations as one likes to improve comfort, speed, etc. Personally my main motivator is comfort due to a repetitive strain injury in the past. However, QMK doesn't run everywhere. In fact, it doesn't run on **most** hardware you can get. You can't get it to run on a laptop keyboard or any mainstream office keyboard. I believe that the comfort and empowerment QMK provides should be available to anyone with a computer on their existing hardware, instead of having to purchase an enthusiast mechanical keyboard (which are admittedly very nice — I own a few — but can be costly). The best alternative solution that I found for keyboards that don't run QMK was [kmonad](https://github.com/kmonad/kmonad). This is an excellent project and I recommend it if you want to try something similar. The reason for this project's existence is that kmonad is written in Haskell and I have no idea how to begin contributing to a Haskell project. From an outsider's perspective I think Haskell is a great language but I really can't wrap my head around it. And there are a few [outstanding issues](./docs/kmonad_comparison.md) at the time of writing that make kmonad suboptimal for my personal workflows. This project is written in Rust because Rust is my favourite programming language and the prior work of the awesome [keyberon crate](https://github.com/TeXitoi/keyberon) exists.
## Similar Projects The most similar project is [kmonad](https://github.com/kmonad/kmonad), which served as the inspiration for kanata. [Here's a comparison document](./docs/kmonad_comparison.md). Other similar projects: - [QMK](https://docs.qmk.fm/#/): Open source keyboard firmware - [keyberon](https://github.com/TeXitoi/keyberon): Rust `#[no_std]` library intended for keyboard firmware - [ktrl](https://github.com/ItayGarin/ktrl): Linux-only keyboard customizer with layers, a TCP server, and audio support - [kbremap](https://github.com/timokroeger/kbremap): Windows-only keyboard customizer with layers and unicode - [xcape](https://github.com/alols/xcape): Linux-only tap-hold modifiers - [karabiner-elements](https://karabiner-elements.pqrs.org/): Mac-only keyboard customizer - [capsicain](https://github.com/cajhin/capsicain): Windows-only key remapper with driver-level key interception - [keyd](https://github.com/rvaiya/keyd): Linux-only key remapper very similar to QMK, kmonad, and kanata - [xremap](https://github.com/k0kubun/xremap): Linux-only application-aware key remapper inspired more by Emacs key sequences vs. QMK layers/Vim modes - [keymapper](https://github.com/houmain/keymapper): Context-aware cross-platform key remapper with a different transformation model (Linux, Windows, Mac) - [mouseless](https://github.com/jbensmann/mouseless): Linux-only mouse-focused key remapper that also has layers, key combo and tap-hold capabilities ### Why the list? While kanata is the best tool for some, it may not be the best tool for you. I'm happy to introduce you to tools that may better suit your needs. This list is also useful as reference/inspiration for functionality that could be added to kanata. ## Donations/Support? The author (jtroo) will not accept monetary donations for work on kanata. Please instead donate your time and/or money to charity. Some links are below. These links are provided for learning and as interesting reads. They are **not** an endorsement. - https://www.effectivealtruism.org/ - https://www.givewell.org/ ================================================ FILE: build.rs ================================================ 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(()) } } ================================================ FILE: cfg_samples/artsey.kbd ================================================ ;; ARTSEY MINI 0.2 https://github.com/artseyio/artsey/issues/7 ;; Exactly one defcfg entry is required. This is used for configuration key-pairs. (defcfg ;; Your keyboard device will likely differ from this. linux-dev /dev/input/event1 ;; Windows doesn't need any input/output configuration entries; however, there ;; must still be a defcfg entry. You can keep the linux-dev entry or delete ;; it and leave it empty. ) (defsrc q w e a s d ) (deflayer base (chord base A) (chord base R) (chord base T) (chord base S) (chord base E) (chord base Y) ) (deflayer meta (chord meta A) (chord meta R) (chord meta T) (chord meta S) (chord meta E) (chord meta Y) ) (defchords base 5000 (A R T S E Y) (layer-switch meta) (A R T ) (one-shot 2000 lsft) ( S E Y) spc (A ) a ( R T S ) b ( R S ) c (A E Y) d ( E ) e (A R ) f (A E ) g ( S Y) h ( R E ) i ( T S E ) j ( T E ) k ( S E ) l ( R T ) m ( E Y) n (A S ) o (A R Y) p ( T Y) q ( R ) r ( S ) s ( T ) t (A T ) u (A T E ) v ( T S ) w (A Y) x ( Y) y ( R S E ) z ) (defchords meta 5000 (A R T S E Y) (layer-switch base) ( S E Y) spc (A R T ) caps ;; should technically be shift lock, probably need to use fake keys for that (A R ) bspc ( R T ) del ( S E ) C-c ( E Y) C-v (A ) home ( R ) up ( T ) end ( S ) left ( E ) down ( Y) rght ) ================================================ FILE: cfg_samples/automousekeys-full-map.kbd ================================================ (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 ) ================================================ FILE: cfg_samples/automousekeys-only.kbd ================================================ (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 ___ @_ ) ================================================ FILE: cfg_samples/chords.tsv ================================================ 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 ================================================ FILE: cfg_samples/colemak.kbd ================================================ ;; ;; 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 () ) ================================================ FILE: cfg_samples/deflayermap.kbd ================================================ ;; 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 ) ================================================ FILE: cfg_samples/f13_f24.kbd ================================================ (defcfg linux-dev /dev/input/by-path/platform-i8042-serio-0-event-kbd ) (defsrc f13 f14 f15 f16 f17 f18 f19 f20 f21 f22 f23 f24 ) (deflayer test f13 f14 f15 f16 f17 f18 f19 f20 f21 f22 f23 f24 ) ================================================ FILE: cfg_samples/fancy_symbols.kbd ================================================ ;; 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 • • • • • • 🔣🔈⓪⓿₀ • 🔣🔈−➖₋⊖ 🔣🔈+➕₊⊕ • • 🔣⌨💡➕₊⊕ 🔣⌨💡➖₋⊖ • 🔣˜ • • • • • • • • • • - = 🔣⌫ • 🔣⤒↖ 🔣🔢 • • • • 🔣↹ • • • • • • • • • • 🔣「〔⎡ 🔣」〕⎣ 🔣\ 🔣⌦ 🔣⤓↘ • • • • • • • • • • • • • • • • • 🔣↩⌤␤ • • • • • • • • • • • • • / • • • • • 🔣🔢↩⌤␤ 🔣⌃ 🔣❖⌘ 🔣⌥ 🔣␣ 🔣▤𝌆 • • • • • • • ) ================================================ FILE: cfg_samples/home-row-mod-advanced.kbd ================================================ ;; Home row mods QWERTY example with more complexity. ;; Some of the changes from the basic example: ;; - when a home row mod activates tap, the home row mods are disabled ;; while continuing to type rapidly ;; - tap-hold-release helps make the hold action more responsive ;; - pressing another key on the same half of the keyboard ;; as the home row mod will activate an early tap action ;; ;; 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) ) ================================================ FILE: cfg_samples/home-row-mod-basic.kbd ================================================ ;; 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 @; ) ================================================ FILE: cfg_samples/home-row-mod-prior-idle.kbd ================================================ ;; 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) ) ================================================ FILE: cfg_samples/included-file.kbd ================================================ (defalias included-alias (macro i spc a m spc i n c l u d e d) ) ================================================ FILE: cfg_samples/japanese_mac_eisu_kana.kbd ================================================ #| 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) ) ================================================ FILE: cfg_samples/jtroo.kbd ================================================ (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) (defalias ;; toggle layer aliases num (layer-toggle numbers) chr (layer-toggle chords) arr (layer-toggle arrows) msc (layer-toggle misc) lay (layer-toggle layers) mse (layer-toggle mouse) ;; change the base layer between qwerty and dvorak dvk (layer-switch dvorak) qwr (layer-switch qwerty) ;; tap-hold keys with letters for tap and layer change for hold anm (tap-hold-release 200 200 a @num) oar (tap-hold-release 200 200 o @arr) ech (tap-hold-release 200 200 e @chr) umc (tap-hold-release 200 200 u @msc) grl (tap-hold-release 200 200 grv @lay) .ms (tap-hold-release 200 200 . @mse) ;; tap for capslk, hold for lctl cap (tap-hold-release 200 200 caps lctl) ;; chords csv C-S-v csc C-S-c ) (deflocalkeys-linux pho 445 ) (defalias ;; shifted keys { S-[ } S-] : S-; ral (tap-hold-release 200 200 sldr ralt) ) (defalias alp (multi a b c d e f g h i j k l m n o p q r s t u v w x y z) ls (macro l s spc min a l ret) ) (deflayer dvorak @grl 1 2 3 4 5 6 7 8 9 0 [ ] bspc tab ' , @.ms p y f g c r l / = \ @cap @anm @oar @ech @umc i d h t n s - ret lsft ; q j k x b m w v z rsft lctl lmet lalt spc @ral rmet rctl ) (deflayer qwerty @grl 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) (deflayer layers _ @qwr @dvk lrld _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) (deflayer numbers _ _ _ _ _ _ nlk kp7 kp8 kp9 _ _ _ _ _ _ C-S-tab C-tab _ XX _ kp4 kp5 kp6 min _ _ _ _ _ C-z C-y M-tab XX _ kp1 kp2 kp3 + _ _ _ C-z C-x C-c C-v XX _ kp0 kp0 . / _ _ _ _ _ _ _ _ ) (deflayer misc _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ins @{ @} [ ] _ _ _ _ _ _ _ C-u _ C-bspc bspc esc ret _ _ _ _ C-z C-x C-c C-v _ _ del _ _ _ _ _ _ _ _ _ _ _ ) (deflayer chords _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @csc _ @ls _ _ _ _ @alp _ _ C-u _ C-d _ S-; _ C-s _ _ _ _ _ _ _ _ C-b _ _ @csv _ _ _ _ _ _ _ _ _ ) (deflayer arrows _ f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 _ _ _ _ _ _ _ C-S-tab pgup up pgdn C-tab _ _ _ _ _ _ _ _ _ home left down rght end _ _ _ _ _ _ _ _ _ _ C-w _ _ _ _ _ _ _ _ _ _ ) (defalias mwu (mwheel-up 50 120) mwd (mwheel-down 50 120) mwl (mwheel-left 50 120) mwr (mwheel-right 50 120) ) (deflayer mouse _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @mwu bck _ fwd _ _ _ _ _ _ _ _ _ _ @mwd mlft _ mrgt mmid _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) (defseq "git status" (g s t) "git commit --amend --no-edit" (g c a) "git rebase -i HEAD~" (g r e b) "git log --pretty=oneline --abbrev-commit" (g l s) "git diff --ignore-space-change" (g d f) "git diff --cached --ignore-space-change" (g d c) "git push -f" (g p f) "git commit -m 'fix_this_commit_message'" (g c m) ) (deffakekeys "git status" (macro g i t spc s t a t u s) "git commit --amend --no-edit" (macro g i t spc c o m m i t spc min min a m e n d spc min min n o min e d i t) "git rebase -i HEAD~" (macro g i t spc r e b a s e spc min i spc S-h S-e S-a S-d S-grv) "git log --pretty=oneline --abbrev-commit" (macro g i t spc l o g spc min min p r e t t y = o n e l i n e spc min min a b b r e v min c o m m i t ) "git diff --ignore-space-change" (macro g i t spc d i f f spc min min i g n o r e min s p a c e min c h a n g e ) "git diff --cached --ignore-space-change" (macro g i t spc d i f f spc min min c a c h e d spc min min i g n o r e min s p a c e min c h a n g e ) "git push -f" (macro g i t spc p u s h spc min f) "git commit -m 'fix_this_commit_message'" (macro g i t spc c o m m i t spc min m spc ' f i x S-min t h i s S-min c o m m i t S-min m e s s a g e ' ) ) ================================================ FILE: cfg_samples/kanata.kbd ================================================ #| This is a sample configuration file that showcases every feature in kanata. A more detailed and less terse configuration guide is found here: https://github.com/jtroo/kanata/blob/main/docs/config.adoc Other configuration samples are found here: https://github.com/jtroo/kanata/tree/main/cfg_samples If anything is confusing or hard to discover, please file an issue or contribute a pull request to help improve the document. Since it may be important for you to know, pressing and holding all of the three following keys together at the same time will cause kanata to exit: Left Control, Space, Escape This is for the physical key input rather than after any remappings that are done by kanata. |# ;; Single-line comments are prefixed by double-semicolon. A single semicolon ;; is parsed as the keyboard key. Comments are ignored for the configuration ;; file. #| A multi-line comment block begin with an octothorphe followed by a pipe: `#|`. To end the multi-line comment block, have a pipe followed by an octothorpe, like the following sequence with the colon removed: `|:#`. The actual two character sequence is not used because it would end this multi-line comment block and cause a parsing error. This configuration language is Lisp-like and uses S-Expression syntax. If you're unfamiliar with Lisp, don't be alarmed. The maintainer jtroo is also unfamiliar with Lisp. You don't need to know Lisp in-depth to be able to configure kanata. If you follow along with the examples, you should be fine. Kanata should also hopefully have helpful error messages in case something goes wrong. If you need help, please feel welcome to ask in the GitHub discussions. |# ;; One defcfg entry may be added if desired. This is used for configuration ;; key-value pairs that change kanata's behaviour at a global level. ;; All defcfg options are optional. (defcfg ;; Your keyboard device will likely differ from this. I believe /dev/input/by-id/ ;; is preferable; I recall reading that it's less likely to change names on you, ;; but I didn't find any keyboard device in there in my VM. If you are on Linux ;; and omit this entry, kanata will try to attach to every device found on your ;; system that seems like a keyboard. ;; linux-dev /dev/input/by-path/platform-i8042-serio-0-event-kbd ;; If you want to read from multiple devices, separate them by `:`. ;; linux-dev /dev/input/:/dev/input/ ;; ;; If you have a colon in your device path, add a backslash before it so that ;; kanata does not parse it as multiple devices. ;; linux-dev /dev/input/path-to\:device ;; Alternatively, you can use list syntax, where both backslashes and colons ;; are parsed literally. List items are separated by spaces or newlines. ;; Using quotation marks for each item is optional, and only required if an ;; item contains spaces. ;; linux-dev ( ;; /dev/input/by-path/pci-0000:00:14.0-usb-0:1:1.0-event ;; /dev/input/by-id/usb-Dell_Dell_USB_Keyboard-event-kbd ;; ) ;; The linux-dev-names-include entry is parsed identically to linux-dev. It ;; defines a list of device names that should be included. This is only ;; used if linux-dev is omitted. ;; linux-dev-names-include device-1-name:device\:2\:name ;; The linux-dev-names-exclude entry is parsed identically to linux-dev. It ;; defines a list of device names that should be excluded. This is only ;; used if linux-dev is omitted. This and linux-dev-names-include are not ;; mutually exclusive but in practice it probably makes sense to only use ;; one of them. ;; linux-dev-names-exclude device-1-name:device\:2\:name ;; By default, kanata will crash if no input devices are found. You can change ;; this behaviour by setting `linux-continue-if-no-devs-found`. ;; ;; linux-continue-if-no-devs-found yes ;; Kanata on Linux automatically detects and grabs input devices ;; when none of the explicit device configurations are in use. ;; In case kanata is undesirably grabbing mouse-like devices, ;; you can use a configuration item to change detection behaviour. ;; ;; linux-device-detect-mode keyboard-only ;; On Linux, you can ask kanata to run `xset r rate ` on startup ;; and on live reload via the config below. The first number is the delay in ms ;; and the second number is the repeat rate in repeats/second. ;; ;; linux-x11-repeat-delay-rate 400,50 ;; On linux, you can ask kanata to label itself as a trackpoint. This has several ;; effects on libinput including enabling middle mouse button scrolling and using ;; a different acceleration curve. Otherwise, a trackpoint intercepted by kanata ;; may not behave as expected. ;; ;; If using this feature, it is recommended to filter out any non-trackpoint ;; pointing devices using linux-only-linux-dev-names-include, ;; linux-only-linux-dev-names-exclude or linux-only-linux-dev to avoid changing ;; their behavior as well. ;; ;; linux-use-trackpoint-property yes ;; Unicode on Linux works by pressing Ctrl+Shift+U, typing the unicode hex value, ;; then pressing Enter. However, if you do remapping in userspace, e.g. via ;; xmodmap/xkb, the keycode "U" that kanata outputs may not become a keysym "u" ;; after the userspace remapping. This will be likely if you use non-US, ;; non-European keyboards on top of kanata. For unicode to work, kanata needs to ;; use the keycode that outputs the keysym "u", which might not be the keycode ;; "U". ;; ;; You can use `evtest` or `kanata --debug`, set your userspace key remapping, ;; then press the key that outputs the keysym "u" to see which underlying keycode ;; is sent. Then you can use this configuration to change kanata's behaviour. ;; ;; Example: ;; ;; linux-unicode-u-code v ;; Unicode on Linux terminates with the Enter key by default. This may not work in ;; some applications. The termination is configurable with the following options: ;; ;; - `enter` ;; - `space` ;; - `enter-space` ;; - `space-enter` ;; ;; Example: ;; ;; linux-unicode-termination space ;; Kanata on Linux creates an evdev output device named "kanata". ;; This name can be changed with this linux-output-device-name. ;; ;; Examples: ;; ;; linux-output-device-name kanata_laptop ;; linux-output-device-name "kanata output device" ;; Kanata on Linux needs to declare a "bus type" for its evdev output device. ;; The options are USB and I8042. The default is I8042. ;; Using USB can break disable-touchpad-while-typing on Wayland. ;; But using I8042 appears to break some other scenarios. Thus it is configurable. ;; ;; Examples: ;; ;; linux-output-device-bus-type USB ;; linux-output-device-bus-type I8042 ;; There is an optional configuration entry for Windows to help mitigate strange ;; behaviour of AltGr if your layout uses that. Uncomment one of the items below ;; to change what kanata does with the key. ;; ;; For more context, see: https://github.com/jtroo/kanata/issues/55. ;; ;; windows-altgr cancel-lctl-press ;; remove the lctl press that comes as a combo with ralt ;; windows-altgr add-lctl-release ;; add an lctl release when ralt is released ;; ;; NOTE: even with these workarounds, putting lctl+ralt in your defsrc may ;; not work too well with other applications that use WH_KEYBOARD_LL. ;; Known applications with issues: GWSL/VcXsrv ;; Enable kanata to execute commands. ;; ;; I consider this feature a hazard so it is conditionally compiled out of ;; the default binary. ;; ;; This is dangerous because it allows kanata to execute arbitrary commands. ;; Using a binary compiled with the cmd feature enabled, uncomment below to ;; enable command execution: ;; ;; danger-enable-cmd yes ;; Enable processing of keys that are not in defsrc. ;; This is useful if you are only mapping a few keys in defsrc instead of ;; most of the keys on your keyboard. Without this, the tap-hold-release and ;; tap-hold-press actions will not activate for keys that are not in defsrc. ;; ;; The reason this is not enabled by default is because some keys may not ;; work correctly if they are intercepted. E.g. rctl/altgr on Windows; see the ;; windows-altgr configuration item above for context. ;; ;; process-unmapped-keys yes ;; We need to set it to yes in this kanata.kbd example config to allow the use of __ and ___ ;; in the deflayer-custom-map example below in the file ;; This also accepts a list parameter (all-except key1 ... keyN) ;; which behaves like "yes" but excludes the keys within the list. process-unmapped-keys (all-except f19) ;; Disable all keys not mapped in defsrc. ;; Only works if process-unmapped-keys is also yes. ;; block-unmapped-keys yes ;; Intercept mouse buttons for a specific mouse device. ;; The intended use case for this is for laptops such as a Thinkpad, which have ;; mouse buttons that may be useful to activate kanata actions with. This only ;; works with the Interception driver. ;; ;; To know what numbers to put into the string, you can run the ;; kanata-wintercept variant with this defcfg item defined with random numbers. ;; Then when a button is first pressed on the mouse device, kanata will print ;; its hwid in the log; you can then copy-paste that into this configuration ;; entry. If this defcfg item is not defined, the log will not print. ;; ;; windows-interception-mouse-hwid "70, 0, 90, 0, 20" ;; There is also a list version of windows-interception-mouse-hwid: ;; ;; windows-interception-mouse-hwids ( ;; "70, 0, 60, 0" ;; "71, 0, 62, 0" ;; ) ;; List configuration for kanata-wintercept variants ;; that allows intercepting only some connected keyboards. ;; Use similarly to mouse-hwid above. ;; ;; windows-interception-keyboard-hwids ( ;; "90, 80, 11, 34" ;; "99, 88, 77, 66" ;; ) ;; There are also exclude variants of the wintercept device configurations. ;; These cannot be defined at the same time as the non-exclude variants. ;; ;; windows-interception-keyboard-hwids-exclude ( ;; "90, 80, 11, 34" ;; "99, 88, 77, 66" ;; ) ;; ;; windows-interception-mouse-hwids-exclude ( ;; "70, 0, 60, 0" ;; "71, 0, 62, 0" ;; ) ;; Transparent keys on layers will delegate to the corresponding defsrc key ;; when found on a layer activated by `layer-switch`. This config entry ;; changes the behaviour to delegate to the action of the first layer, ;; which is the layer active upon startup, that is in the same position. ;; ;; delegate-to-first-layer yes ;; This config entry alters the behavior of movemouse-accel actions. ;; By default, this setting is disabled - vertical and horizontal ;; acceleration are independent. Enabling this setting will emulate QMK mouse ;; move acceleration behavior, i.e. the acceleration state of new mouse ;; movement actions are inherited if others are already being pressed. ;; ;; movemouse-inherit-accel-state yes ;; This config entry alters the behavior of movemouseaccel actions. ;; This makes diagonal movements simultaneous to mitigate choppiness in ;; drawing apps, if you're using kanata mouse movements to draw for ;; whatever reason. ;; ;; movemouse-smooth-diagonals yes ;; This configuration allows you to customize the length limit on dynamic macros. ;; The default limit is 128 keys. ;; ;; dynamic-macro-max-presses 1000 ;; This configuration makes multiple tap-hold actions that are activated near ;; in time expire their timeout quicker. Without this, the timeout for the 2nd ;; tap-hold onwards will start from 0ms after the previous tap-hold expires. ;; concurrent-tap-hold yes ;; This configuration makes the release of one-shot-press and of the tap in a tap-hold ;; by the defined number of milliseconds (approximate). ;; The default value is 5. ;; While the release is delayed, further processing of inputs is also paused. ;; This means that there will be a minor input latency impact in the mentioned scenarios. ;; The reason for this configuration existing is that some environments ;; do not process the scenarios correctly due to the rapidity of the release. ;; Kanata does send the events in the correct order, ;; so the fault is more in the environment, but kanata provides a workaround anyway. rapid-event-delay 5 ;; This setting defaults to yes but can be configured to no to save on ;; logging. However, if --log-layer-changes is passed as a command line ;; argument, a "no" in the configuration file will be overridden and layer ;; changes will be logged. ;; ;; log-layer-changes no ;; This configuration will press and then immediately release the non-modifier key ;; as soon as the override activates, meaning you are unlikely as a human to ever ;; release modifiers first, which can result in unintended behaviour. ;; ;; The downside of this configuration is that the non-modifier key ;; does not remain held which is important to consider for your use cases. override-release-on-activation yes ;; Accepts a single key name. ;; When configured, whenever a mouse cursor movement is received, ;; the configured key name will be "tapped" by Kanata, activating ;; the key's action. ;; ;; This enables reporting of every relative mouse movement, which ;; corresponds to standard mice, trackballs, trackpads and ;; trackpoints. Absolute movements, which can be generated by ;; touchscreens, drawing tablets and some mouse replacement or ;; accessibility software, are ignored. Scrolling events and mouse ;; buttons are also ignored. ;; ;; The intended use of these events is to provide a way to ;; automatically enable a mouse keys layer while mousing, which can ;; be disabled by a timeout or typing on other keys, rather than ;; explicit toggling. see cfg_examples/automousekeys-*.kbd for more. ;; ;; The `mvmt` key name is specially intended for this purpose. It ;; has no output key mapping and cannot be supplied as an action; ;; however, any key may be used. ;; ;; Supports live reload on Linux, but with Windows-interception, ;; this option must be present on startup to enable mouse movement ;; event collection, so restart is required to enable it. Changing ;; the key name is always supported, however. ;; ;; mouse-movement-key mvmt ) ;; deflocalkeys-* enables you to define and use key names that match your locale ;; by defining OS code number mappings for that character. ;; ;; There are five variants of deflocalkeys-*: ;; - deflocalkeys-win ;; - deflocalkeys-winiov2 ;; - deflocalkeys-wintercept ;; - deflocalkeys-linux ;; - deflocalkeys-macos ;; ;; Only one of each deflocalkeys-* variant is allowed. The variants that are ;; not applicable will be ignored, e.g. deflocalkeys-linux and deflocalkeys-wintercept ;; are both ignored when using the default Windows kanata binary. ;; ;; The configuration item is parsed similarly to defcfg; it is composed of ;; pairs of keys and values. ;; ;; You can check docs/locales.adoc for the mapping for your locale. If your ;; locale is not there, please ask for help if needed, and add the mapping for ;; your locale to the document. ;; ;; Web link for locales: https://github.com/jtroo/kanata/blob/main/docs/locales.adoc ;; ;; This example is for an Italian keyboard remapping in Linux. The numbers will ;; unfortunately differ between Linux, Windows-hooks, and Windows-interception. ;; ;; To see how you can discover key mappings for yourself, see the "Non-US keyboards" ;; section of docs/config.adoc. ;; ;; Web link or config: https://github.com/jtroo/kanata/blob/main/docs/config.adoc (deflocalkeys-win ì 187 ) (deflocalkeys-wintercept ì 187 ) (deflocalkeys-winiov2 ì 187 ) (deflocalkeys-linux ì 13 ) (deflocalkeys-macos ì 13 ) ;; Only one defsrc is allowed. ;; ;; defsrc defines the keys that will be intercepted by kanata. The order of the ;; keys matches the deflayer declarations and all deflayer declarations must ;; have the same number of keys as defsrc. ;; ;; The visual/spatial positioning is *not* mandatory; it is done by convention ;; for visual ease. These items are parsed as a long list with newlines being ;; ignored. ;; ;; If you are looking for other keys, the file src/keys/mod.rs should hopefully ;; provide some insight. (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) ;; The first layer defined is the layer that will be active by default when ;; kanata starts up. This layer is the standard QWERTY layout except for the ;; backtick/grave key (@grl) which is an alias for a tap-hold key. (deflayer qwerty @grl 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) ;; The dvorak layer remaps the keys to the dvorak layout. There are several ;; tap-hold aliases configured on the left side. (deflayer dvorak @grl 1 2 3 4 5 6 7 8 9 0 [ ] bspc tab ' , @.ms p y f g c r l / = \ @cap @anm @oar @ech @umc @ifk d h t n s - ret lsft ; q j k x b m w v z rsft lctl lmet lalt spc @ralt rmet @rcl ) ;; This is an alternative to deflayer and does not rely on defsrc. ;; It has the advantage of simpler config if only remapping a few keys. ;; You might still prefer the standard deflayer for its visual printing in ;; the log as you are learning a new configuration. (deflayermap (custom-map-example) caps esc esc caps ;; You can use _ , __ or ___ instead of specifying a key name to map all ;; keys that are not explicitly mapped in the layer. ;; E.g. esc and caps above will not be overwritten. ;; ;; _ maps only keys that are in defsrc. ;; __ excludes mapping keys that are in defsrc. ;; ___ maps both, keys that are in `defsrc`, and keys that are not. ;; ;; The two- and three-underscore variants require ;; "process-unmapped-keys yes" in defcfg to work. ;; ___ XX ;; maps all keys that are not mapped explicitly in the layer ;; ;; (i.e. esc and caps above) to "no-op" to disable the key. _ XX ;; maps all keys that are in defsrc and are not mapped in the layer __ XX ;; maps all keys that are NOT in defsrc and are not mapped in the layer ) ;; defvar can be used to declare commonly-used values (defvar tap-repress-timeout 100 hold-timeout 200 tt $tap-repress-timeout ht $hold-timeout ;; A list value in defvar that begins with concat behaves in a special manner ;; where strings will be joined together. ;; ;; Below results in 100200 a "hello" b "world" ct (concat $a " " $b) ) (defalias th1 (tap-hold $tt $ht caps lctl) th2 (tap-hold $tt $ht spc lsft) ) ;; defalias is used to declare a shortcut for a more complicated action to keep ;; the deflayer declarations clean and aligned. The alignment in deflayers is not ;; necessary, but is strongly recommended for ease of understanding visually. ;; ;; Aliases are referred to by `@`. Aliases can refer to each other, ;; e.g. in the `anm` alias. However, an alias can only refer to another alias ;; that has been defined before it in the file. (defalias ;; aliases to change the base layer to qwerty or dvorak dvk (layer-switch dvorak) qwr (layer-switch qwerty) ;; Aliases for layer "toggling". It's not quite toggling because the layer ;; will be active while the key is held and deactivated when the key is ;; released. An alternate action name you can use is layer-while-held. ;; However, the rest of this document will use The term "toggle" for brevity. num (layer-toggle numbers) chr (layer-toggle chords) arr (layer-toggle arrows) msc (layer-toggle misc) lay (layer-toggle layers) mse (layer-toggle mouse) fks (layer-while-held fakekeys) ;; tap-hold aliases with tap for dvorak key, and hold for toggle layers ;; WARNING(Linux only): key repeat with tap-hold can behave unexpectedly. ;; For full context, see https://github.com/jtroo/kanata/discussions/422 ;; ;; tap-hold parameter order: ;; 1. tap repress timeout ;; 2. hold timeout ;; 3. tap action ;; 4. hold action ;; ;; The hold timeout is the number of milliseconds after which the hold action ;; will activate. ;; ;; The tap repress timeout is best explained in a roundabout way. When you press and ;; hold a standard key on your keyboard (e.g. 'a'), your operating system will ;; read that and keep sending 'a' to the active application. To be able to ;; replicate this behaviour with a tap-hold key, you must press-release-press ;; the key within the tap repress timeout window (number is milliseconds). Simply ;; holding the key results in the hold action activating, which is why you ;; need to double-press for the tap action to stay pressed. ;; ;; There are two additional versions of tap-hold available: ;; 1. tap-hold-press: if there is a key press, the hold action is activated ;; 2. tap-hold-release: if there is a press and release of another key, the ;; hold action is activated ;; ;; These versions are useful if you don't want to wait for the whole hold ;; timeout to activate, but want to activate the hold action immediately ;; based on the next key press or press and release of another key. These ;; versions might be great to implement home row mods. ;; ;; If you come from kmonad, tap-hold-press and tap-hold-release are similar ;; to tap-hold-next and tap-hold-next-release, respectively. If you know ;; the underlying keyberon crate, tap-hold-press is the HoldOnOtherKeyPress ;; and tap-hold-release is the PermissiveHold configuration. anm (tap-hold 200 200 a @num) ;; tap: a hold: numbers layer oar (tap-hold 200 200 o @arr) ;; tap: o hold: arrows layer ech (tap-hold 200 200 e @chr) ;; tap: e hold: chords layer umc (tap-hold 200 200 u @msc) ;; tap: u hold: misc layer grl (tap-hold 200 200 grv @lay) ;; tap: grave hold: layers layer .ms (tap-hold 200 200 . @mse) ;; tap: . hold: mouse layer ifk (tap-hold 200 200 i @fks) ;; tap: i hold: fake keys layer ;; There are additional variants of tap-hold-press and tap-hold-release that ;; activate the timeout action (the 5th parameter) when the action times out ;; as opposed to the hold action being activated by other keys. ;; tap: o hold: arrows layer timeout: backspace oat (tap-hold-press-timeout 200 200 o @arr bspc) ;; tap: e hold: chords layer timeout: esc ect (tap-hold-release-timeout 200 200 e @chr esc) ;; 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 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-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) ;; Below is an alias for the `multi` action which executes multiple actions ;; in order but at the same time. ;; ;; It may result in some incorrect/unexpected behaviour if combining complex ;; actions, so be reasonable with it. One reasonable use case is this alias: ;; press right-alt while also toggling to the `ralted` layer. The utility of ;; this is better revealed if you go see `ralted` and its aliases. ralt (multi ralt (layer-toggle ralted)) ) ;; Wrapping a top-level configuration item in a list beginning with ;; (environment (env-var-name env-var-value) ...configuration...) ;; will make the configuration only active if the environment variable matches. (environment (LAPTOP lp1) (defalias met @lp1met) ) (environment (LAPTOP lp2) (defalias met @lp2met) ) ;; NOTE: the configuration below is an older and less general variant ;; of the environment configuration above. ;; ;; The defaliasenvcond variant of defalias is parsed similarly, but there must ;; be a list parameter first. The list must contain two strings. In order, ;; these strings are: an environment variable name, and the environment ;; variable value. When the environment variable defined by name has the ;; corresponding value when running kanata, the aliases within will be active. ;; Otherwise, the aliases will be skipped. (defaliasenvcond (LAPTOP lp1) met @lp1met ) (defaliasenvcond (LAPTOP lp2) met @lp2met ) (defalias ;; shifted keys { S-[ } S-] : S-; ;; alias numbers as themselves for use in macro 8 8 0 0 ) (defalias ;; For the multi action, all keys are pressed for the whole sequence ;; but still in the listed order which may be undesirable, particularly ;; for modifiers like shift. You probably want to use macro instead. ;; ;; Chording can be more succinctly described by the modifier prefixes ;; `C-`, `A-`, `S-`, and `M-` for lctrl, lalt, lshift, lmeta, but are possible ;; by using `multi` as well. The lmeta key is also known by some other ;; names: "Windows", "GUI", "Command", "Super". ;; ;; For ralt/altgr, you can use either of: `RA-` or `AG-`. They both work the ;; same and only one is allowed in a single chord. This chord can be useful for ;; international layouts. ;; ;; A special behaviour of output chords is that if another key is pressed, ;; all of the chord keys will be released. For the explanation about why ;; this is the case, see the configuration guide. ;; ;; This use case for multi is typing an all-caps string. alp (multi lsft a b c d e f g h i j k l m n o p q r s t u v w x y z) ;; Within multi you can also include reverse-release-order to release keys ;; from last-to-first order instead of first-to-last which is the default. S-a-reversed (multi lsft a reverse-release-order) ;; Chords using the shortcut syntax. These ones are used for copying/pasting ;; from some Linux terminals. csv C-S-v csc C-S-c ;; Windows shortcut for displaying all windows win M-tab ;; Accented e characters for France layout using altgr sequence. Showcases ;; both of the shortcuts. You can just use one version of shortcut at your ;; preference. é AG-2 è RA-7 testmacro (macro AG-2 RA-7) 🙃 (unicode 🙃) ;; macro accepts keys, chords, and numbers (a delay in ms). Note that numbers ;; will be parsed as delays, so they will need to be aliased to be used. lch (macro h t t p @: / / 100 l o c a l h o s t @: @8 @0 @8 @0) tbm (macro A-(tab 200 tab 200 tab) 200 S-A-(tab 200 tab 200 tab)) hpy (macro S-i spc a m spc S-(h a p p y) spc m y S-f r S-i e S-n d @🙃) rls (macro-release-cancel Digit1 500 bspc S-1 500 bspc S-2) cop (macro-cancel-on-press Digit1 500 bspc S-1 500 bspc S-2) rlpr (macro-release-cancel-and-cancel-on-press Digit1 500 bspc S-1 500 bspc S-2) ;; repeat variants will repeat while held, once ALL macros have ended, ;; including the held macro. mr1 (macro-repeat mltp) mr2 (macro-repeat-release-cancel mltp) mr3 (macro-repeat-cancel-on-press mltp) mr4 (macro-repeat-release-cancel-and-cancel-on-press mltp) ;; Kanata also supports dynamic macros. Dynamic macros can be nested, but ;; cannot recurse. dms dynamic-macro-record-stop dst (dynamic-macro-record-stop-truncate 3) dr0 (dynamic-macro-record 0) dr1 (dynamic-macro-record 1) dr2 (dynamic-macro-record 2) dp0 (dynamic-macro-play 0) dp1 (dynamic-macro-play 1) dp2 (dynamic-macro-play 2) ;; unmod will release all modifiers temporarily and send the . ;; So for example holding shift and tapping a @um1 key will still output 1. um1 (unmod 1) ;; dead keys é (as opposed to using AltGr) that outputs É when shifted dké (macro (unmod ') e) ;; unshift is like unmod but only releases shifts ;; In ISO German QWERTZ, force unshifted symbols even if shift is held de{ (unshift ralt 7) de[ (unshift ralt 8) ;; unmod can optionally take a list as the first parameter, ;; and then will only temporarily remove ;; the listed modifiers instead of all modifiers. unalt-a (unmod (lalt ralt) a) ;; unicode accepts a single unicode character. The unicode character will ;; not be automatically repeated by holding the key down. The alias name ;; is the unicode character itself and is referenced by @🙁 in deflayer. 🙁 (unicode 🙁) ;; You may output parentheses or double quotes using unicode ;; by quotes as well as special quoting syntax. lp1 (unicode r#"("#) rp1 (unicode r#")"#) dq (unicode r#"""#) lp2 (unicode "(") rp2 (unicode ")") ;; fork accepts two actions and a key list. The first (left) action will ;; activate by default. The second (right) action will activate if any of ;; the keys in the third parameter (right-trigger-keys) are currently active. frk (fork @🙃 @🙁 (lsft rsft)) ;; switch accepts triples of keys check, action, and fallthrough|break. ;; The default usage of keys check behaves similarly to fork. ;; However, it also accepts boolean operators and|or to allow more ;; complex use cases. ;; ;; The order of cases matters. If two different cases match the ;; currently pressed keys, the case listed earlier in the configuration ;; will activate first. If the early case uses break, the second case will ;; not activate at all. Otherwise if fallthrough is used, the second case ;; will also activate sequentially after the first case. swt (switch ;; translating this keys check to some other common languages ;; this might look like: ;; ;; (a && b && (c || d) && (e || f)) ((and a b (or c d) (or e f))) a break ;; this case behaves like fork, i.e. ;; ;; (or a b c) ;; ;; or for some other common languages: ;; ;; a || b || c (a b c) b fallthrough ;; key-history evaluates to true if the n'th most recent typed key, ;; {n | n ∈ [1, 8]}, matches the given key. ((key-history a 1) (key-history b 8)) c break ;; key-timing evaluates to true if the n'th most recent typed key, ;; {n | n ∈ [1, 8]}, was typed at a time less-than/greater-than the ;; given number of milliseconds. ((key-timing 1 lt 3000) (key-timing 2 gt 30000) ) c break ((key-timing 7 less-than 200) (key-timing 8 greater-than 500)) c break ;; not means "not any of the list constituents". ;; The example below behaves like: ;; ;; !(a || b || c) ;; ;; and is equivalent to: ;; ;; ((not (or a b c))) ((not a b c)) c break ;; input logic ((input real lctl)) d break ((input virtual sft)) e break ((input-history real lsft 2)) f break ((input-history virtual ctl 2)) g break ;; layer evaluates to `true` if the active layer matches the given name ((layer dvorak)) x break ((layer qwerty)) y break ;; base-layer evaluates to `true` if the base layer matches the given name ;; The base layer is the most recent target of layer-switch. ;; The base layer is not always the active layer. ((base-layer dvorak)) x break ((base-layer qwerty)) y break ;; default case, empty list always evaluates to true. ;; break vs. fallthrough doesn't matter here () c break ) ;; Having a cmd action in your configuration without explicitly enabling ;; `danger-enable-cmd` **and** using the cmd-enabled executable will make ;; kanata refuse to load your configuration. The aliases below are commented ;; out since commands aren't allowed by this configuration file. ;; ;; Note that the parameters to `cmd` are executed directly as opposed to ;; passed to a shell. So for example, `~` and `$HOME` would not refer ;; to your home directory on Linux. ;; ;; You can use: ;; `cmd bash -c "your_stuff_here"` to run your command inside of bash. ;; ;; cm1 (cmd bash -c "echo hello world") ;; cm2 (cmd rm -fr /tmp/testing) ;; One variant of `cmd` is `cmd-log`, which lets you control how ;; running command, stdout, stderr, and execution failure are logged. ;; ;; The command takes two extra arguments at the beginning ``, ;; and ``. `` controls where the name ;; of the command is logged, as well as the success message and command ;; stdout and stderr. ;; ;; `` is only used if there is a failure executing the initial ;; command. This can be if there is trouble spawning the command, or ;; the command is not found. This means if you use `bash -c "thisisntacommand"`, as ;; long as bash starts up correctly, nothing would be logged to this channel, but ;; something like `thisisntacommand` would be. ;; ;; The log level can be `debug`, `info`, `warn`, `error`, or `none`. ;; ;; cmd-log info error bash -c "echo these are the default levels" ;; cmd-log none none bash -c "echo nothing back in kanata logs" ;; cmd-log none error bash -c "only if command fails" ;; cmd-log debug debug bash -c "echo log, but require changing verbosity levels" ;; cmd-log warn warn bash -c "echo this probably isn't helpful" ;; Another variant of `cmd` is `cmd-output-keys`. This reads the output ;; of the command and treats it as an S-Expression, similarly to `macro`. ;; However, only delays, keys, chords, and chorded lists are supported. ;; Other actions are not. ;; ;; bash: type date-time as YYYY-MM-DD HH:MM ;; cmd-output-keys bash -c "date +'%F %R' | sed 's/./& /g' | sed 's/:/S-;/g' | sed 's/\(.\{20\}\)\(.*\)/\(\1 spc \2\)/'" ) ;; The underscore _ means transparent. The key on the base layer will be used ;; instead. XX means no-op. The key will do nothing. ;; ;; A similar concept to transparent, use-defsrc means the key will always ;; behave as the key as defined by defsrc. (defalias src use-defsrc) (deflayer numbers @src _ _ _ _ _ nlk kp7 kp8 kp9 _ _ _ _ _ _ _ _ _ XX _ kp4 kp5 kp6 - _ _ _ _ _ C-z _ _ XX _ kp1 kp2 kp3 + _ _ _ C-z C-x C-c C-v XX _ kp0 kp0 . / _ _ _ _ _ _ _ _ ) ;; The `lrld` action stands for "live reload". ;; ;; NOTE: live reload does not read changes to device-related configurations, ;; such as `linux-dev`, `macos-dev-names-include`, ;; or `windows-only-windows-interception-keyboard-hwids`. ;; ;; The variants `lrpv` and `lrnx` will cycle between multiple configuration files ;; if they are specified in the startup arguments. ;; The list action variant `lrld-num` takes a number parameter and ;; reloads the configuration file specified by the number, according to the ;; order passed into the arguments on kanata startup. ;; ;; Upon a successful reload, the kanata state will begin on the default base layer ;; in the configuration. E.g. in this example configuration, you would start on ;; the qwerty layer. (deflayer layers _ @qwr @dvk lrld lrpv lrnx (lrld-num 1) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) (defalias ;; Alias for one-shot which will activate an action until either the timeout ;; expires or a different key is pressed. The timeout is the first parameter ;; and the action is the second parameter. ;; ;; The intended use cases are pressing a modifier for exactly one key or ;; switching to another layer for exactly one key. ;; ;; If a one-shot key is held then it will act as a regular key. E.g. for os1 ;; below, holding an @os1 key will keep lsft held and holding an @os3 key ;; will keep the layer set to misc. os1 (one-shot 500 lsft) os2 (one-shot 500 C-S-lalt) os3 (one-shot 500 (layer-toggle misc)) ;; Another name for one-shot is one-shot-press, since it ends on the first ;; press of another key. ;; ;; There is another variant one-shot-release which ends on the first release ;; of another key. ;; ;; There are further variants of both of these: ;; - one-shot-press-pcancel ;; - one-shot-release-pcancel ;; ;; These will cancel the one-shot action and all other active one-shot actions ;; if a one-shot key is repressed while already active. osp (one-shot-press 500 lsft) osr (one-shot-release 500 lsft) opp (one-shot-press-pcancel 500 lsft) orp (one-shot-release-pcancel 500 lsft) ;; one-shot-pause-processing can be useful in some cases ;; to preserve an activated one-shot state when it otherwise ;; would get deactivated by some action that isn't intended ;; to consume the one-shot. ;; The unit is number of milliseconds. ops (one-shot-pause-processing 5) ;; Alias for tap-dance which will activate one of the actions in the action ;; list depending on how many taps were done. Tapping once will output the ;; first action and tapping N times will output the N'th action. ;; ;; The first parameter is a timeout. Tapping the same tap-dance key again ;; within the timeout will reset the timeout and advance the tap-dance to the ;; next key. ;; ;; The action activates either when any of the following happens: ;; - the timeout expires ;; - the tap sequence reaches the end ;; - a different key is pressed td (tap-dance 200 (a b c d spc)) ;; There is a variant of tap-dance — tap-dance-eager — that will activate ;; every action tapped in the sequence rather than a single one. The example ;; below is rather simple and behaves similarly to the original tap-dance. td2 (tap-dance-eager 500 ( (macro a) ;; use macro to prevent auto-repeat of the key (macro bspc b b) (macro bspc bspc c c c) )) ;; arbitrary-code allows sending an arbitrary number as an OS code. This is ;; not cross platform! This can be useful for testing keys that are not yet ;; named or mapped in kanata. Please contribute findings with names and/order ;; mappings, either in a GitHub issue or as a pull request! This is currently ;; not supported with Windows using the interception driver. ab1 (arbitrary-code 700) ) (defalias ;; caps-word will add an lsft to the active key list for all alphanumeric keys ;; a-z, and the US layout minus key; meaning it will be converted to an ;; underscore. ;; ;; The caps-word state will also be cleared if any key that doesn't get auto- ;; capitalized and also doesn't belong in this list is pressed: ;; - 0-9 ;; - kp0-kp9 ;; - bspc, del ;; - up, down, left, rght ;; ;; The single parameter is a timeout in milliseconds after which the caps-word ;; state will be cleared and lsft will not be added anymore. The timer is reset ;; any time a capitalizable or extra non-terminating key is active. cw (caps-word 2000) ;; Like caps-word, but you get to choose the key lists where lsft gets added. ;; This example is similar to the default caps-word behaviour but it moves the ;; 0-9 keys to capitalized key list from the extra non-terminating key list. cwc (caps-word-custom 2000 (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) ) ) ;; -toggle variants of caps-word will terminate caps-word on repress if it is ;; currently active, otherwise caps-word will be activated. (defalias cwt (caps-word-toggle 2000) cct (caps-word-custom-toggle 2000 (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) ) ) ;; Can see a new action `rpt` in this layer. This repeats the most recently ;; pressed key. Holding down the `rpt` key will not repeatedly send the key. ;; The intended use case is to be able to use a different finger to repeat a ;; double letter, as opposed to double-tapping a letter. ;; ;; The `rpt` action only repeats the last key output. For example, it won't ;; output a chord like `ctrl+c` if the previous key pressed was `C-c` - it ;; will only output `c`. There is a variant `rpt-any` which will repeat the ;; previous action and would work for that use case. (deflayer misc _ _ _ _ _ _ _ _ _ @é @è _ ì #|random custom key for testing|# _ _ _ @ab1 _ _ _ ins @{ @} [ ] _ _ + @cw _ _ _ C-u _ del bspc esc ret _ _ _ @cwc C-z C-x C-c C-v _ _ _ @td @os1 @os2 @os3 rpt rpt-any _ _ _ _ _ ) (deflayer chords ;; you can put list actions directly in deflayer but it's ugly, so prefer aliases. _ _ _ _ _ _ _ _ _ _ @🙁 (unicode 😀) _ _ _ _ _ _ _ _ _ _ @csc @hpy @lch @tbm _ _ _ @alp _ _ _ _ _ @ch1 @ch2 @ch4 @ch8 _ _ _ _ _ _ _ _ _ _ _ @csv _ _ _ _ _ _ _ _ _ ) (deflayer arrows _ f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 _ _ _ _ _ _ _ _ pgup up pgdn _ _ _ _ _ _ _ _ _ _ home left down rght end _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) ;; In Windows, using mouse buttons on the kanata window seems to cause it to hang. ;; Using the mouse on other windows seems to be fine though. ;; ;; The mouse buttons can be clicked using mlft, mrgt, mmid, mfwd and mbck, representing the ;; left, right, middle, forward and backward mouse buttons respectively. If the key is held, the ;; button press will also be held. ;; ;; If there are multiple mouse click actions within a single multi action, e.g. ;; (multi mrgt mlft), then all the buttons except the last will be clicked then ;; unclicked. The last button will remain held until key release. In the example ;; given, the button sequence will be: ;; press key->click right->unclick right->click left->release key->release left ;; ;; There are variants of the standard mouse buttons which "tap" the button. ;; These are mltp, mrtp, and mmtp. Rather than holding until key release, this ;; action will click and unclick the button once the key is pressed. Nothing ;; happens on key release. The action (multi lctl mltp) will result in the ;; sequence below: ;; press key->press lctl->click left->unclick left->release key->release lctl ;; ;; One can also see mouse movement actions at the lower right side, with the ;; arrow unicode characters. (deflayer mouse _ @mwu @mwd @mwl @mwr _ _ _ _ _ @ma↑ _ _ _ _ pgup bck _ fwd _ _ _ _ @ma← @ma↓ @ma→ _ _ _ pgdn mlft _ mrgt mmid _ mbck mfwd _ @ms↑ _ _ @fms _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ _ _ _ _ _ _ _ ) (defalias ;; Mouse wheel actions. The first number is the interval in milliseconds ;; between scroll actions. The second number is the distance in some arbitrary ;; unit. Play with the parameters to see what feels correct. Both numbers ;; must be in the range 1-65535 ;; ;; In both Windows and Linux, 120 distance units is equivalent to a single ;; notch movement on a physical wheel. In Linux, not all desktop environments ;; support the REL_WHEEL_HI_RES event so if you experience issues with `mwheel` ;; actions in Linux, using a distance value that is multiple of 120 may help. mwu (mwheel-up 50 120) mwd (mwheel-down 50 120) ;; Horizontal mouse wheel actions. Similar story to vertical mouse wheel. mwl (mwheel-left 50 120) mwr (mwheel-right 50 120) ;; 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. ms↑ (movemouse-up 1 1) ms← (movemouse-left 1 1) ms↓ (movemouse-down 1 1) ms→ (movemouse-right 1 1) ;; Mouse movement actions with linear acceleration. The first number is the ;; interval in milliseconds between mouse actions. The second number is the time ;; in milliseconds for the distance to linearly ramp up from the minimum distance ;; to the maximum distance. The third number is the minimum distance traveled ;; per interval in pixels. The fourth number is the maximum distance traveled ;; per interval in pixels. ma↑ (movemouse-accel-up 1 1000 1 5) ma← (movemouse-accel-left 1 1000 1 5) ma↓ (movemouse-accel-down 1 1000 1 5) ma→ (movemouse-accel-right 1 1000 1 5) ;; setmouse places the cursor at a specific pixel x-y position. This ;; example puts it in the middle of the screen. The coordinates go from 0,0 ;; which is the upper-left corner of the screen to 65535,65535 which is the ;; lower-right corner of the screen. If you have multiple monitors, they are ;; treated as one giant screen, which may make it a bit confusing for how to ;; set up the pixels. You will need to experiment. sm (setmouse 32228 32228) ;; movemouse-speed takes a percentage by which it then scales all of the ;; mouse movements while held. You can have as many of these active at a ;; given time as you would like, but be warned that some values, such as 33 ;; may not have correct pixel distance representations. fms (movemouse-speed 200) ) (defalias lft (multi (release-key ralt) left) ;; release ralt if held and also press left rgt (multi (release-key ralt) rght) ;; release ralt if held and also press rght rlr (release-layer ralted) ;; release layer-toggle of ralted ) ;; It's not clear what the practical use case is for the @rlr alias, but the ;; combination of @ralt on the dvorak layer and this layer with @lft and @rgt ;; results in the physical ralt key behaving mostly as ralt, **except** for ;; holding it **then** pressing specific keys. These specific keys release the ;; ralt because it would cause them to have undesired behaviour without the ;; release. ;; ;; E.g. ralt+@lft will result in only left being pressed instead of ralt+left, ;; while ralt(hold)+tab+tab+tab still works as intended. (deflayer ralted _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @lft @rlr @rgt _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) ;; Virtual key actions (defvirtualkeys ;; Define some virtual keys that perform modifier actions vkctl lctl vksft lsft vkmet lmet vkalt lalt ;; A virtual key that toggles all modifier virtual keys above vktal (multi (on-press toggle-virtualkey vkctl) (on-press toggle-virtualkey vksft) (on-press toggle-virtualkey vkmet) (on-press toggle-virtualkey vkalt) ) ;; Virtual key that activates a macro vkmacro (macro h e l l o spc w o r l d) ) (defalias psfvk (on-press press-virtualkey vksft) rsfvk (on-press release-virtualkey vksft) palvk (on-press tap-vkey vktal) macvk (on-press tap-vkey vkmacro) isfvk (on-idle 1000 tap-vkey vksft) 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 ;; - on-release-fakekey ;; - on-idle-fakekey ;; ;; One use case of fake keys is for holding modifier keys ;; for any number of keypresses and then releasing the modifiers when desired. ;; ;; The actions associated with fake keys in deffakekeys are parsed before ;; aliases, so you can't use aliases within deffakekeys. Other than the lack ;; of alias support, fake keys can do any action that a normal key can, ;; including doing operations on previously defined fake keys. ;; ;; Operations on fake keys can occur either on press (on-press-fakekey), ;; on release (on-release-fakekey), or on idle for a specified time ;; (on-idle-fakekey). ;; ;; Fake keys are flexible in usage but can be obscure to discover how they ;; can be useful to you. (deflayer fakekeys _ @fcp @fsp @fmp @pal _ _ _ _ _ _ _ _ _ _ @fcr @fsr @fap @ral _ _ _ _ _ _ _ _ _ _ @fct @fst @rma _ _ _ _ _ _ _ _ _ _ @t1 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) (deffakekeys ctl lctl sft lsft lsft lsft met lmet alt lalt mmid mmid pal (multi (on-press-fakekey ctl press) (on-press-fakekey sft press) (on-press-fakekey met press) (on-press-fakekey alt press) ) ral (multi (on-press-fakekey ctl release) (on-press-fakekey sft release) (on-press-fakekey met release) (on-press-fakekey alt release) ) ) (defalias fcp (on-press-fakekey ctl press) fcr (on-press-fakekey ctl release) fct (on-press-fakekey ctl tap) fsp (on-release-fakekey sft press) fsr (on-release-fakekey sft release) fst (on-release-fakekey sft tap) fsg (on-release-fakekey sft toggle) fmp (on-press-fakekey met press) fap (on-press-fakekey alt press) rma (multi (on-press-fakekey met release) (on-press-fakekey alt release) ) pal (on-press-fakekey pal tap) ral (on-press-fakekey ral tap) rdl (on-idle-fakekey ral tap 1000) hfd (hold-for-duration 1000 met) ;; Test of on-press-fakekey and on-release-fakekey in a macro t1 (macro-release-cancel @fsp 5 a b c @fsr 5 c b a) ;; If you find that an application isn't registering keypresses correctly ;; with multi, you can try out: ;; - on-press-fakekey-delay ;; - on-release-fakekey-delay ;; ;; Do note that processing a fakekey-delay and even a sequence of delays will ;; delay any other inputs from being processed until the fakekey-delays are ;; all complete, so use with care. stm (multi ;; Shift -> middle mouse with a delay (on-press-fakekey lsft press) (on-press-fakekey-delay 200) (on-press-fakekey mmid press) (on-release-fakekey mmid release) (on-release-fakekey-delay 200) (on-release-fakekey lsft release) ) ) ;; Vim-style leader-key sequences. Activate a fakekey-tap by pressing a "leader" ;; key and then a sequence of characters. ;; See: https://github.com/jtroo/kanata/issues/97 ;; ;; You can add an entry to defcfg to change the sequence timeout (default is 1000): ;; sequence-timeout ;; ;; If you want multiple timeouts with different leaders, you can also activate the ;; sequence action: ;; (sequence ) ;; This acts like `sldr` but uses a different timeout. ;; ;; There is also an option to customize the key sequence input mode. Its default ;; value when not configured is `hidden-suppressed`. ;; ;; The options are: ;; ;; - `visible-backspaced`: types sequence characters as they are inputted. The ;; typed characters will be erased with backspaces for a valid sequence termination. ;; - `hidden-suppressed`: hides sequence characters as they are typed. Does not ;; output the hidden characters for an invalid sequence termination. ;; - `hidden-delay-type`: hides sequence characters as they are typed. Outputs the ;; hidden characters for an invalid sequence termination either after either a ;; timeout or after a non-sequence key is typed. ;; ;; For `visible-backspaced` and `hidden-delay-type`, a sequence leader input will ;; be ignored if a sequence is already active. For historical reasons, and in case ;; it is desired behaviour, a sequence leader input using `hidden-suppressed` will ;; reset the key sequence. ;; ;; Example: ;; sequence-input-mode visible-backspaced (defseq git-status (g s t)) (deffakekeys git-status (macro g i t spc s t a t u s)) (defalias rcl (tap-hold-release 200 200 sldr rctl)) (defseq dotcom (. S-3) dotorg (. S-4) ) (deffakekeys dotcom (macro . c o m) dotorg (macro . o r g) ) ;; Enter sequence mode and input . (defalias dot-sequence (macro (sequence 250) 10 .)) (defalias dot-sequence-inputmode (macro (sequence 250 hidden-delay-type) 10 .)) ;; There are special keys that you can assign in your actions which will ;; never output events to your operating system, but which you can use ;; in sequences. They are named: nop0-nop9. (defseq dotcom (nop0 nop1) dotorg (nop8 nop9) ) ;; A key list within O-(...) signifies simultaneous presses. (defseq dotcom (O-(. c m)) dotorg (O-(. r g)) ) ;; 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 ;; defined above), these allow you to perform actions when a combination of ;; input keys (a "chord") are pressed all at once (order does not matter). ;; Each combination/chord can perform a different action, allowing you to bind ;; up to `2^n - 1` different actions to just `n` keys. ;; ;; Each `defchords` defines a named group of such chord-action pairs. ;; The 500 is a timeout after which a chord triggers if it isn't triggered by a ;; key release or press of a non-chord key before the timeout expires. ;; If a chord is not defined, no action will occur when it is triggered but the ;; keys used to input it will be consumed regardless. ;; ;; Each pair consists of the keys that make up a given chord in the parenthesis ;; followed by the action that should be executed when the given chord is ;; pressed. ;; Note that these keys do not directly correspond to real keys and are merely ;; arbitrary labels that make sense within the context of the chord. ;; They are mapped to real keys in layers by configuring the key in the layer to ;; map to a `(chord name key)` action (like those in the `defalias` below) where ;; `name` is the name of the chords group (here `binary`) and `key` is one of the ;; arbitrary labels of the keys in a chord (here `1`, `2`, `4` and `8`). ;; ;; Note that it is perfectly valid to nest these `chord` actions that enter ;; "chording mode" within other actions like `tap-dance` and that will work as ;; one would expect. ;; However, this only applies to the first key used to enter "chording mode". ;; Once "chording mode" is active, all other keys will be directly handled by ;; "chording mode" with no regard for wrapper actions; e.g. if a key is pressed ;; and it maps to a tap-hold with a chord as the hold action within, that chord ;; key will immediately activate instead of the key needing to be held for the ;; timeout period. ;; ;; The action executed by a chord (the right side of the chord-action pairs) may ;; be any regular or advanced action, including aliases. They currently cannot ;; however contain a `chord` action. (defchords binary 500 (1 ) 1 ( 2 ) 2 (1 2 ) 3 ( 4 ) 4 (1 4 ) 5 ( 2 4 ) 6 (1 2 4 ) 7 ( 8) 8 (1 8) 9 ( 2 8) (multi 1 0) (1 2 8) (multi 1 1) ( 4 8) (multi 1 2) (1 4 8) (multi 1 3) ( 2 4 8) (multi 1 4) (1 2 4 8) (multi 1 5) ) (defalias ch1 (chord binary 1) ch2 (chord binary 2) ch4 (chord binary 4) ch8 (chord binary 8) ) ;; The top-level action `include` will read a configuration from a new file. ;; At the time of writing, includes can only be placed at the top level. The ;; included files also cannot contain includes themselves. ;; ;; (include included-file.kbd) ;; The top-level item `deftemplate` declares a template ;; which can be expanded multiple times to reduce repetition. ;; ;; Expansion of a template is done via `expand-template`. ;; This template defines a chord group and aliases that use the chord group. ;; The purpose is to easily define the same chord position behaviour ;; for multiple layers that have different underlying keys. (deftemplate left-hand-chords (chordgroupname k1 k2 k3 k4 alias1 alias2 alias3 alias4) (defalias $alias1 (chord $chordgroupname $k1) $alias2 (chord $chordgroupname $k2) $alias3 (chord $chordgroupname $k3) $alias4 (chord $chordgroupname $k4) ) (defchords $chordgroupname $chord-timeout ($k1) $k1 ($k2) $k2 ($k3) $k3 ($k4) $k4 ($k1 $k2) lctl ($k3 $k4) lsft ) ) (defvar chord-timeout 200) (template-expand left-hand-chords qwerty a s d f qwa qws qwd qwf) ;; You can use t! as a short form of template-expand (t! left-hand-chords dvorak a o e u dva dvo dve dvu) (deflayer template-example _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @qwa @qws @qwd @qwf _ _ _ _ _ _ _ _ _ _ @dva @dvo @dve @dvu _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) ;; Within a deftemplate you can use if-equal to conditionally insert content ;; into the template. (deftemplate home-row (version) a s d f g h (if-equal $version v1 j) (if-equal $version v2 (tap-hold 200 200 j lctl)) k l ; ' ) (deftemplate common-overrides () (lctl 7) (lctl lsft tab) (lctl 9) (lctl tab) (lalt 7) (lalt lsft tab) (lalt 9) (lalt tab) ) ;; Wrapping a top-level configuration item in a list beginning with ;; (platform (applicable-platforms...) ...configuration...) ;; will make the configuration only active on a specific platform. (platform (macos) ;; Only on macos, use command arrows to jump/delete words ;; because command is used for so many other things ;; and it's weird that these cases use alt. (defoverrides (lmet bspc) (lalt bspc) (lmet left) (lalt left) (lmet right) (lalt right) (template-expand common-overrides) ) ) (platform (win winiov2 wintercept linux) (defoverrides (template-expand common-overrides) ) ) #| 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'") |# ) ================================================ FILE: cfg_samples/key-toggle_press-only_release-only.kbd ================================================ #| 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 ) ================================================ FILE: cfg_samples/minimal.kbd ================================================ #| This minimal config changes Caps Lock to act as Caps Lock on quick tap, but if held, it will act as Left Ctrl. It also changes the backtick/grave key to act as backtick/grave on quick tap, but change ijkl keys to arrow keys on hold. This text between the two pipe+octothorpe sequences is a multi-line comment. |# ;; Text after double-semicolons are single-line comments. #| One defcfg entry may be added, which is used for configuration key-pairs. These configurations change kanata's behaviour at a more global level than the other configuration entries. |# (defcfg #| This configuration will process all keys pressed inside of kanata, even if they are not mapped in defsrc. This is so that certain actions can activate at the right time for certain input sequences. By default, unmapped keys are not processed through kanata due to a Windows issue related to AltGr. If you use AltGr in your keyboard, you will likely want to follow the simple.kbd file while unmapping lctl and ralt from defsrc. |# process-unmapped-keys yes ) (defsrc caps grv i j k l lsft rsft ) (deflayer default @cap @grv _ _ _ _ _ _ ) (deflayer arrows _ _ up left down rght _ _ ) (defalias cap (tap-hold-press 200 200 caps lctl) grv (tap-hold-press 200 200 grv (layer-toggle arrows)) ) ================================================ FILE: cfg_samples/opposite-hand-hrm.kbd ================================================ ;; 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 ) ================================================ FILE: cfg_samples/push-msg.kbd ================================================ ;; 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 ;; ;; ========================================================================== ================================================ FILE: cfg_samples/simple.kbd ================================================ ;; Comments are prefixed by double-semicolon. A single semicolon is parsed as the ;; keyboard key. Comments are ignored for the configuration file. ;; ;; This configuration language is Lisp-like. If you're unfamiliar with Lisp, ;; don't be alarmed. The maintainer jtroo is also unfamiliar with Lisp. You ;; don't need to know Lisp in-depth to be able to configure kanata. ;; ;; If you follow along with the examples, you should be fine. Kanata should ;; also hopefully have helpful error messages in case something goes wrong. ;; If you need help, you are welcome to ask. ;; Only one defsrc is allowed. ;; ;; defsrc defines the keys that will be intercepted by kanata. The order of the ;; keys matches with deflayer declarations and all deflayer declarations must ;; have the same number of keys as defsrc. Any keys not listed in defsrc will ;; be passed straight to the operating system. (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) ;; The first layer defined is the layer that will be active by default when ;; kanata starts up. This layer is the standard QWERTY layout except for the ;; backtick/grave key (@grl) which is an alias for a tap-hold key. (deflayer qwerty @grl 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) ;; The dvorak layer remaps the keys to the dvorak layout. In addition there is ;; another tap-hold key: @cap. This key retains caps lock functionality when ;; quickly tapped but is read as left-control when held. (deflayer dvorak @grl 1 2 3 4 5 6 7 8 9 0 [ ] bspc tab ' , . p y f g c r l / = \ @cap a o e u i d h t n s - ret lsft ; q j k x b m w v z rsft lctl lmet lalt spc ralt rmet rctl ) ;; defalias is used to declare a shortcut for a more complicated action to keep ;; the deflayer declarations clean and aligned. The alignment in deflayers is not ;; necessary, but is strongly recommended for ease of understanding visually. ;; ;; Aliases are referred to by `@`. (defalias ;; tap: backtick (grave), hold: toggle layer-switching layer while held grl (tap-hold 200 200 grv (layer-toggle layers)) ;; layer-switch changes the base layer. dvk (layer-switch dvorak) qwr (layer-switch qwerty) ;; tap for capslk, hold for lctl cap (tap-hold 200 200 caps lctl) ) ;; The `lrld` action stands for "live reload". This will re-parse everything ;; except for linux-dev, meaning you cannot live reload and switch keyboard ;; devices. ;; ;; The keys 1 and 2 switch the base layer to qwerty and dvorak respectively. ;; ;; Apart from the layer switching and live reload, all other keys are the ;; underscore _ which means "transparent". Transparent means the base layer ;; behaviour is used when pressing that key. (deflayer layers _ @qwr @dvk lrld _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) ================================================ FILE: cfg_samples/tray-icon/license_icons.txt ================================================ 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. ================================================ FILE: cfg_samples/tray-icon/tray-icon.kbd ================================================ (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' ================================================ FILE: docs/README.md ================================================ ### 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) ================================================ FILE: docs/config-stylesheet.css ================================================ 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} } ================================================ FILE: docs/config.adoc ================================================ = 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. Useful terminology to learn early: [cols="1,5"] |=== | string | A sequence of characters. Optionally surrounded by quotes. Examples: `backspace`, `"string with spaces and 1 number"`. | list | A sequence of strings or nested lists within round brackets. List items are separated by any amount of whitespace characters, or by round brackets. Examples: `(lrld-num 1)`, `(tap-dance 200 (f1(unicode 😀)f2(unicode 🙂)))`. |=== If you have any questions, confusions, suggestions, etc., feel free to https://github.com/jtroo/kanata/discussions/new/choose[start a discussion] or https://github.com/jtroo/kanata/issues/new/choose[file an issue]. If you have ideas for how to improve this document or any other part of the project, please be welcome to make a pull request or file an issue. == Forcefully exit kanata [[force-exit]] Though this isn't configuration-related, it may be important for you to know that pressing and holding all of the three following keys together at the same time will cause kanata to exit: - Left Control - Space - Escape This mechanism works on the key input **before** any remappings done by kanata. [[comments]] == Comments You can add comments to your configuration file. Comments are prefixed with two semicolons. E.g: [source] ---- ;; This is a comment in a kanata configuration file. ;; Comments will be ignored and are intended for you to help understand your ;; own configuration when reading it later. ---- You can begin a multi-line comment block with `+#|+` and end it with `+|#+`: [source] ---- #| This is a multi-line comment block |# ---- [[required-configuration-entries]] == Required configuration entries [[defsrc]] === defsrc **Reference** Your configuration file must have exactly one `defsrc` list. This defines the order of keys that the `+deflayer+` entries will operate on. .Syntax: [source] ---- (defsrc $key1 $key2 ... $keyN) ---- [cols="1,6"] |=== | `$key` | The name of a key. This can be a default key name or one defined in <>. When physically pressing this input key, the action defined at the same order position on the active layer will activate. |=== **Description** The `defsrc` configuration entry defines which of your key inputs will be processed by kanata and how the keys map to defined layers. Keys excluded from `defsrc` will not be processed by Kanata unless you have `process-unmapped-keys yes` in <>. Keys not processed by Kanata has implications on various actions. For example: - Pressing an excluded key will type a letter while a prior `tap-hold` decision is still pending, resulting in potentially incorrect results. - Excluded keys do not trigger early activation in actions such as `tap-hold-press` or `tap-dance` - Excluded keys cannot be read by `fork` or `switch` logic. The `defsrc` entry is treated as a long sequence. The amount of whitespace (spaces, tabs, newlines) are not relevant. You may use spaces, tabs, or newlines however you like to visually format `defsrc` to your liking. The primary source of all key names are the `str_to_oscode` and `default_mappings` functions in https://github.com/jtroo/kanata/blob/main/parser/src/keys/mod.rs[the source]. Please feel welcome to file an issue if you're unable to find the key you're looking for. An example `defsrc` containing the US QWERTY keyboard keys as an approximately 60% keyboard layout: .Example: [source] ---- (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) ---- Note that some keyboards have a Menu key instead of a right Meta key. In this case you can use `menu` instead of `rmet`. For non-US keyboards, see <>. [[deflayer]] === deflayer **Reference** Your configuration file must have at least one `+deflayer+` entry. This defines how each physical key mapped in `+defsrc+` behaves when kanata runs. .Syntax: [source] ---- (deflayer $layer-name $action1 $action2 ... $actionN) ---- [cols="1,5"] |=== | `$layer-name` | A string representing the layer name. This name is used to reference this layer in other actions. | `$action` | The action that activates while this layer is active when the corresponding `defsrc` input key is pressed. |=== **Description** A `+deflayer+` configuration entry is followed by the layer name then a list of keys or actions. The usable key names are the same as in defsrc. Actions are explained further on in this document. The whitespace story is the same as with `+defsrc+`. The order of keys/actions in `+deflayer+` corresponds to the physical key in the same sequence position defined in `+defsrc+`. The first layer defined in your configuration file will be the starting layer when kanata runs. Other layers can be temporarily activated or switched to using actions. An example `defsrc` and `deflayer` that remaps QWERTY to the Dvorak layout would be: .Example: [source] ---- (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps a s d f g h j k l ; ' ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) (deflayer dvorak grv 1 2 3 4 5 6 7 8 9 0 [ ] bspc tab ' , . p y f g c r l / = \ caps a o e u i d h t n s - ret lsft ; q j k x b m w v z rsft lctl lmet lalt spc ralt rmet rctl ) ---- A <> also allows specifying layer icons in `+deflayer+` and `+deflayermap+` to show in the tray menu on layer activation, see https://github.com/jtroo/kanata/blob/main/cfg_samples/tray-icon/tray-icon.kbd[example config] ==== deflayermap **Reference** An alternative method for defining a layer exists: `deflayermap`. This method maps inputs to actions by defining input-output pairs, ignoring `defsrc` entirely. You will likely want to either enable <> or define most of your keyboard keys within <> when using `deflayermap`. Otherwise many actions do not behave as intended. See one of the links for more context. .Syntax: [source] ---- (deflayermap ($layer-name) $input1 $action1 $input2 $action2 ... $inputN $actionN) ---- [cols="1,5"] |=== | `$layer-name` | A string representing the layer name. This name is used to reference this layer in other actions. | `$input` | The input key mapped to the corresponding output. | `$action` | The action that activates while this layer is active when the corresponding input key is pressed. |=== **Description** The `deflayermap` variant has the advantage of terser configuration when only a few keys on a layer need to be mapped. When practicing a new configuration, the standard `deflayer` has an advantage of looking more like a physical keyboard layout, which may be helpful to some. Within `deflayermap`, the very first item must be the layer name. The layer name must be in parentheses unlike with `deflayer`. After the layer name, the layer is configured via pairs of items: * input key * output action An example complete configuration that maps Caps Lock to Escape is: [source] ---- ;; defsrc is still necessary (defsrc) (deflayermap (base-layer) caps esc) ---- The input key takes the same role as `defsrc` keys. The output action takes the role that items in the normal `deflayer` have. As special input names, you can use one of `_`, `__`, or `___` to map all the keys that are not explicitly mapped in the layer, e.g. in the example above, these affect keys other than `caps`. [cols="1,6"] |=== | `_` | Map all unmapped keys in this layer that are defined in `defsrc`. | `__` | Map all unmapped keys in this layer that are not defined in `defsrc`. | `___` | Map all unmapped keys in this layer. |=== If a key is not mapped explicitly or through these wildcards, it will be implicitly mapped to a <>. [[review-of-required-configuration-entries]] === Review of required configuration entries If you're reading in order, you have now seen all of the required entries: * `+defsrc+` * `+deflayer+` [[minimal-config]] An example minimal configuration is: [source] ---- (defsrc a b c) (deflayer start 1 2 3) ---- This will make kanata remap your `a b c` keys to `1 2 3`. This is almost certainly undesirable but is a valid configuration. NOTE: Please have a read through link:https://github.com/jtroo/kanata/blob/main/docs/platform-known-issues.adoc[the known platform issues] because they may have implications on what you should include/exclude in `defsrc`. The Windows LLHOOK I/O mechanism has the most issues by far. [[key-names]] == Key names for defsrc and deflayermap The source of truth for all default key names are the functions `str_to_oscode` and `add_default_str_osc_mappings` in the link:https://github.com/jtroo/kanata/blob/main/parser/src/keys/mod.rs[keys/mod.rs file]. https://www.toptal.com/developers/keycode[This online tool] will also work for most keys to tell you the key name. It will be shown as the `event.code` field in the web page after you press the key. [[non-us-keyboards]] == Non-US keyboards For non-US keyboard users, you may have some keys on your keyboard with characters that are not allowed in `defsrc` by default, at least according to the symbol shown on the physical keys. The two sections below can help you understand how to remap all your keys. === Browser event.code Ensure kanata and other key remapping programs are **not** running. Then you can use https://www.toptal.com/developers/keycode[this online tool] and press the key. The `event.code` field tells you the key name to use in Kanata. Alternatively, you can read through https://www.w3.org/TR/uievents-code/[this reference]. Due to the lengthy key names, you may want to use `deflayermap` if remapping using these key names. IMPORTANT: On Windows, you should use either `kanata_winIOv2.exe` or Interception when using key names according to the browser `event.code`. The default `kanata.exe` does not do mappings according to the browser `event.code` key names. [[deflocalkeys]] === deflocalkeys **Reference** You can use `deflocalkeys` to define additional key names that can be used in `defsrc`, `deflayer`, and anywhere else in the configuration. .Syntax: [source] ---- (deflocalkeys-$variant $key-name1 $key-number1 $key-name2 $key-number2 ... $key-nameN $key-numberN) ---- [cols="1,5"] |=== | `$variant` | One of: `win winiov2 wintercept linux macos` | `$key-name` | A key name of your choice that can be used in the rest of the configuration. | `$key-number` | A key number that varies based on the kanata variant you are using. |=== Only one of each deflocalkeys-* variant is allowed. The variants that are not applicable will be ignored, e.g. `deflocalkeys-linux` and `deflocalkeys-wintercept` are both ignored when using the default Windows `kanata.exe` binary. **Description** The `deflocalkeys` configurations are not strictly necessary. Their purpose is to help you match your physical keyboard's appearance to your kanata configuration, in the hopes it will be more readable and less confusing. In the underlying hardware, all keyboard positions send the same scan codes according to their position, regardless of what is printed on the key cap. The scan code names are typically referred to by the corresponding US layout name. It is the job of the operating system to translate the same scan code to the correct outputs according to the configured locale and layout. You can find configurations that others have made in https://github.com/jtroo/kanata/blob/main/docs/locales.adoc[this document]. If you do not see your keyboard there and are not confident in using the available tools, please feel welcome to ask for help in a discussion or issue. Please contribute to the document if you are able! There are five variants of deflocalkeys: - `deflocalkeys-win` - `deflocalkeys-winiov2` - `deflocalkeys-wintercept` - `deflocalkeys-linux` - `deflocalkeys-macos` .Example: [source] ---- (deflocalkeys-win ì 187 ) (deflocalkeys-winiov2 ì 187 ) (deflocalkeys-wintercept ì 187 ) (deflocalkeys-linux ì 13 ) (deflocalkeys-macos ì 13 ) (defsrc grv 1 2 3 4 5 6 7 8 9 0 - ì bspc ) ---- The number used for a custom key represents the converted value for an OsCode in base 10. This differs between Windows-hooks, Windows-interception, and Linux. Running kanata with the `--debug` flag lets you read the correct number, shown in parenthesis of `code` in the `KeyEvent` log lines. It also possible to use native tools, as described below. In Linux, `evtest` will give the correct number for the physical key you press. In Windows using the default hook mechanism, the non-interception version of the keyboard tester in the kanata repository will give the correct number in the `code: ` section. (https://github.com/jtroo/kanata/releases/tag/win-keycode-tester-v0.3.0[prebuilt binary]) In Windows uning `winIOv2`, the winIOv2 executable variant will give the correct number in the `code: ` section. In Windows using Interception, the interception version of the keyboard tester will give the correct number i the `num: ` section. Between the hook and interception versions, some keys may agree but others may not; do be aware that they are **not** compatible! However, Interception and winIOv2 should generally agree with each other. Ideas for improving the user-friendliness of this system are welcome! As mentioned before, please ask for help in an issue or discussion if needed, and help with https://github.com/jtroo/kanata/blob/main/docs/locales.adoc[this document] is very welcome so that future users can have an easier time 🙂. [[introduction-defcfg]] == Introduction to defcfg Your configuration file may include a single `defcfg` entry. The `defcfg` can be empty or omitted. There are options that change kanata's behaviour, but this introduction will introduce only the most prevalent entry: `process-unmapped-keys`. All other options can be found later in the <> section. .Example of an empty defcfg: [source] ---- (defcfg) ---- [[process-unmapped-keys]] === process-unmapped-keys The `process-unmapped-keys` option in `defcfg` is probably the most generally impactful option. Enabling this configuration makes kanata process keys that are not defined in `defsrc`. This might be useful if you are only mapping a few keys in defsrc instead of most of the keys on your keyboard. By default, keys excluded from `defsrc` will not work in various scenarios. Some examples: - The early hold for prior `+tap-hold-press+` actions will not - Prior `+one-shot+` actions will not be released - `fork` and `switch` logic will not see the key This option is disabled by default. The reason this is not enabled by default is because some keys may not work correctly if they are intercepted. A known issue being AltGr/ralt/Right Alt; see <>. .Example: [source] ---- (defcfg process-unmapped-keys yes) (defcfg process-unmapped-keys (all-except lctl ralt)) ---- == Aliases and variables[[aliases-and-vars]] Before learning about actions, it will be useful to first learn about aliases and variables. [[aliases]] === Aliases **Reference** Using the `defalias` configuration entry, you can introduce a shortcut label for an action. .Syntax: [source] ---- (defalias $alias-name1 $action1 $alias-name2 $action2 ... $alias-nameN $actionN) ---- [cols="1,5"] |=== | `$alias-name` | The chosen shortcut label for the action. This shortcut label can be used in the rest of the configuration by prefixing it with the `@` character. | `$action` | The ouput action used wherever the alias name is referenced. |=== **Description** The `defalias` entry reads pairs of items in a sequence where the first item in the pair is the alias name and the second item is the action it can be substituted for. A list is a sequence of strings or nested lists separated by whitespace, surrounded by parentheses. All of the configuration entries we've looked at so far are lists; `defalias` is where we'll first see nested lists in this guide. .Example: [source] ---- (defalias ;; tap for caps lock, hold for left control cap (tap-hold 200 200 caps lctl) ) ---- This alias can be used in `deflayer` as a substitute for the long action. The alias name is prefixed with `@` to signify that it's an alias as opposed to a normal key. [source] ---- (deflayer example @cap a s d f ) ---- You may have multiple `defalias` entries and multiple aliases within a single `defalias`. Aliases may also refer to other aliases that were defined earlier in the configuration file. .Example: [source] ---- (defalias one (tap-hold 200 200 caps lctl)) (defalias two (tap-hold 200 200 esc lctl)) (defalias three C-A-del ;; Ctrl+Alt+Del four (tap-hold 200 200 @three ralt) ) ---- You can choose to put actions without aliasing them right into `deflayer`. However, for long actions it is recommended not to do so to keep a nice visual alignment. Visually aligning your `deflayer` entries will hopefully make your configuration file easier to read. .Example: [source] ---- (deflayer example ;; this is equivalent to the previous deflayer example (tap-hold 200 200 caps lctl) a s d f ) ---- [[variables]] === Variables **Reference** Using the `defvar` configuration entry, you can introduce a shortcut label for an arbitrary string or list. .Syntax: [source] ---- (defvar $var-name1 $var-value1 $var-name2 $var-value2 ... $var-nameN $var-valueN) ---- [cols="1,5"] |=== | `$var-name` | The chosen shortcut label for the string or list. This shortcut label can be used in the rest of the configuration by prefixing it with `$`. | `$var-value` | An arbitrary string or list that will be substituted wherever the variable is used. |=== **Description** Unlike an alias, a variable does not need to be a valid standalone action. In other words, a variable can be used as components of actions. The most common use case is to define common number strings for actions such as `tap-hold`, `tap-dance`, and `one-shot`. Similar to how `defalias` works, `defvar` reads pairs of items in a sequence where the first item in the pair is the variable name and the second item is a string or list. Variables are allowed to refer to previously defined variables. Variables can be used to substitute most values. Some notable exceptions are: - variables cannot be used in `defcfg`, `defsrc`, or `deflocalkeys` - variables cannot be used to substitute an action name Variables are referred to by prefixing their name with `$`. .Example: [source] ---- (defvar tap-repress-timeout 100 hold-timeout 200 tt $tap-repress-timeout ht $hold-timeout ) (defalias th1 (tap-hold $tt $ht caps lctl) th2 (tap-hold $tt $ht spc lsft) ) ---- [[concat-in-defvar]] ==== concat in defvar Within the second item of `defvar`, a list that begins with the special keyword `concat` will concatenate all subsequent items in the list together into a single string value. Without using `concat`, lists are saved as-is. .Example: [source] ---- (defvar rootpath "/home/myuser/mysubdir" ;; $otherpath will be the string: /home/myuser/mysubdir/helloworld otherpath (concat $rootpath "/helloworld") ) ---- [[actions]] == Actions The actions kanata provides are what make it truly customizable. This section explains the available actions. [[live-reload]] === Live reload **Reference** Live reload variants: [cols="1,5"] |=== | `lrld` | String action that live-reloads the currently-used configuration file. | `lrld-next` | String action that live-reloads the configuration file specified consecutively later in the command line order. Cycles to the first-specified file if currently using the last file specified. | `lrld-prev` | String action that live-reloads the configuration file specified consecutively earlier in the command line order. Cycles to the last-specified file if currently using the first file specified. | `(lrld-num $n)` | List action that live-reloads the n'th file as specified in the command line order. The first file specified is `n=1`. |=== Live reload does not read or apply changes to device-related configurations. Examples of device-related configurations: `linux-dev`, `macos-dev-names-include`, `linux-use-trackpoint-property`, `windows-only-windows-interception-keyboard-hwids`. **Description** You can put the `+lrld+` action onto a key to live reload your configuration file. If kanata can't parse the file, the previous configuration will continue to be used. When live reload is activated, the active kanata layer will be the first `deflayer` defined in the configuration. .Example: [source] ---- (deflayer has-live-reload lrld a s d f ) ---- There are variants of `lrld`: `lrld-prev` and `lrld-next`. These will cycle through different configuration files that you specify on kanata's startup. The first configuration file specified will be the one loaded on startup. The prev/next variants can be used with shortened names of `lrpv` and `lrnx` as well. Another variant is the list action `lrld-num`. This reloads the configuration file specified by the number, according to the order that the configuration file arguments are passed into kanata's startup command. .Example: [source] ---- (deflayer has-live-reloads lrld lrpv lrnx (lrld-num 3) ) ---- Example specifying multiple config files in the command line: [source] ---- kanata -c startup.cfg -c 2nd.cfg -c 3rd.cfg ---- Given the above startup command, activating `(lrld-num 2)` would reload the `2nd.cfg` file. [[layer-switch]] === layer-switch **Reference** A list action that changes the active base layer. .Syntax: [source] ---- (layer-switch $layer-name) ---- [cols="1,5"] |=== | `$layer-name` | Layer name to switch to. |=== **Description** This action allows you to switch to another "base" layer. This is permanent until a `layer-switch` to another layer is activated. The concept of a base layer makes more sense when looking at the next action: `layer-while-held`. This action accepts a single subsequent string which must be a layer name defined in a `deflayer` entry. .Example: [source] ---- (defalias dvk (layer-switch dvorak)) ---- [[layer-while-held]] === layer-while-held **Reference** A list action that changes the active layer while the key is held. .Syntax: [source] ---- (layer-while-held $layer-name) ---- [cols="1,5"] |=== | `$layer-name` | Layer name to activate while key is held. |=== **Description** This action allows you to temporarily change to another layer while the key remains held. When the key is released, you go back to the currently active "base" layer. This action accepts a single subsequent string which must be a layer name defined in a `deflayer` entry. .Example: [source] ---- (defalias nav (layer-while-held navigation)) ---- You may also use `layer-toggle` in place of `layer-while-held`; they behave exactly the same. The `layer-toggle` name is slightly shorter but is a bit inaccurate with regards to its meaning. [[transparent-key]] === Transparent key **Reference** [cols="1,5"] |=== | `+_+` | String action that activates the action of the layer "underneath" the active one. |=== **Description** Kanata maintains a layer stack consisting in order of: * 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] ---- (defsrc a b c d) (defalias src use-defsrc) (deflayer remap-only-c-to-d _ _ d @src) ---- [[no-op]] === No-op **Reference** [cols="1,6"] |=== | `XX` | String action that will output nothing. |=== **Description** You may use the action `+XX+` as a "no operation" key, meaning pressing the key will do nothing. This might be desirable in place of a transparent key on a layer that is not fully mapped so that a key that is intentionally not mapped will do nothing as opposed to typing a letter. Alternatively you can use `+✗+` `+∅+` `+•+` to mean no-op. .Example: [source] ---- (deflayer contains-no-ops XX ✗ ∅ •) ---- [[unicode]] === Unicode **Reference** List action that outputs a single unicode codepoint. The unicode codepoint will not be repeatedly typed if you hold the key down. .Syntax: [source] ---- (unicode $unicode-codepoint) ---- [cols="1,4"] |=== | `$unicode-codepoint` | One unicode codepoint. Be warned that many emojis/glyphs/graphemes are composed of multiple codepoints. |=== **Description** NOTE: The <> may output unicode characters more consistently. The `+unicode+` (or `+🔣+`) action accepts a single unicode character (but not a composed character, so 🤲, but not 🤲🏿), or a single unicode number prefixed with `U+`. For example, both of these actions are the same: - `(unicode 🚆)` - `(unicode U+1F686)` If you want to output a glyph that is composed of multiple codepoints, you can use <> with multiple `unicode` actions. You may use a unicode character as an alias if desired or in its simplified form `+🔣😀+` (vs the usual `+(🔣 😀)+`). NOTE: The unicode action may not be correctly accepted by the active application. NOTE: If using Linux, make sure to look at the <> in defcfg. 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 sml (unicode 😀) 😀 (🔣 😀) 🙁 (unicode 🙁) ) (deflayer has-happy-sad @sml @🙁 @😀 🔣😀 d f ) ---- If you want output parentheses `+( )+` via unicode you can quote them. .Example with parentheses [source] ---- (defalias lp (unicode "(") rp (unicode ")") ) ---- If you want to output double quotes via unicode you need a special quoting syntax. .Example use of double-quote within a string [source] ---- (defalias dq (unicode r#"""#) ) ---- [[output-chordscombos]] === Output chords/combos **Reference** Prefixing a known key name with the following strings will output the key alongside the specified modifier. Multiple prefixes can be combined to add more modifiers to the same key output. Duplicate prefixes are not allowed. [cols="1,6"] |=== | `+C-+` | Left Control | `+RC-+` | Right Control | `+A-+` | Left Alt | `+RA-+` | Right Alt, also known as AltGr | `+AG-+` | Also means Right Alt/AltGr | `+S-+` | Left Shift | `+RS-+` | Right Shift | `+M-+` | Left Meta | `+RM-+` | Right Meta |=== A special behaviour of output chords is that if another key is pressed, all of the chord keys will be released before the newly pressed key action activates. The modifier keys are often not desired for subsequent actions and without this behaviour, rapid typing can result in undesired modified key presses. If you want keys to remain pressed, use <> instead. **Description** You may want to remap a key to automatically be pressed in combination with modifiers such as Control or Shift. Output chords are a way for you to achieve this. Output chords are typically used do one-off actions such as: - type a symbol, e.g. `S-1` to output `!` for the US layout. - type an accented character, e.g. `RA-a` to output `á` for the US international layout. - do a special action like `C-c` to send `SIGTERM` in the terminal It should be noted that output chords are not usable in all configuration items. If you get an unknown key error where you expected an output chord to be usable, you must split the output chord into its component keys. For example, `+(unmod C-l)+` is an error; instead you should use `+(unmod lctl l)+`. The output chord prefix strings are: * `+C-+`: Left Control (also `+‹⎈+` `+‹⌃+` or without the `+‹+` side indicator) * `+RC-+`: Right Control (also `+⎈›+` `+⌃›+`) * `+A-+`: Left Alt (also `+‹⎇+` `+‹⌥+` or without the `+‹+` side indicator)) * `+RA-+`: Right Alt, a.k.a. AltGr (also `+AG+` `+⎇›+` `+⌥›+`) * `+S-+`: Left Shift (also `+‹⇧+` or without the `+‹+` side indicator)) * `+RS-+`: Right Shift (also `+⇧›+`) * `+M-+`: Left Meta, a.k.a. Windows, GUI, Command, Super (also `+‹⌘+` `+‹❖+` `+‹◆+` or without the `+‹+` side indicator)) * `+RM-+`: Right Meta (also `+⌘›+` `+❖›+` `+◆›+`) .Example: [source] ---- (defalias ;; Type exclamation mark (US layout) ex! S-1 ;; Ctrl+C: send SIGINT to a Linux terminal program int C-c ;; Win+Tab: open Windows' Task View tsk M-tab ;; Ctrl+Shift+(C|V): copy or paste from certain terminal programs cpy C-S-c pst C-S-v ) ---- [[repeat-key]] === Repeat key **Reference** [cols="1,5"] |=== | `rpt` | String action that outputs the single most-recently typed key. | `rpt-any` | String action that outputs the most-recently outputted action. |=== **Description** The action `+rpt+` repeats the most recently typed key. Holding down this key will not repeatedly send the key. The intended use case is to be able to use a different finger or even thumb key to repeat a typed key, as opposed to double-tapping a key. .Example: [source] ---- (deflayer has-repeat rpt a s d f ) ---- The `rpt` action only repeats the last key output. For example, it won't output a chord like `ctrl+c` if the previous key pressed was `C-c`. The `rpt` action will only output `c` in this case. There is a variant `rpt-any` which will repeat any previous action and would output `ctrl+c` in the example case. ---- (deflayer has-repeat-any rpt-any a s d f ) ---- [[release-a-key-or-layer]] === Release a key or layer **Reference** [cols="1,2"] |=== | `(release-key $key)` | List action that releases the defined key from output actions. Notably this does not act on key inputs. | `(release-layer $layer-name)` | List action that releases `layer-while-held` activations for the given layer name. |=== **Description** You can release a held key or layer via these actions: * `release-key` or `key↑`: release a key, accepts `defsrc` compatible names * `release-layer` or `layer↑`: release a while-held layer A lower-level detail of these actions is that they operate on output states as opposed to virtually releasing an input key. This does have some practical significance. For example, if the action `(macro-repeat a 50)` were on the `a` key, activating `(release-key a)` will not stop the repeating macro. An example practical use case for `release-key` is seen in the `multi` section directly below. There is currently no known practical use case for `release-layer`, but it exists nonetheless. [[multi]] === multi **Reference** Activate multiple actions in sequence. .Syntax: [source] ---- (multi $action1 $action2 ... $actionN) ---- [cols="1,3"] |=== | `$action` | An output action. |=== **Description** The `+multi+` action executes multiple keys or actions in order but also simultaneously. It accepts one or more actions. An example use case is to press the "Alt" key while also activating another layer. In the example below, holding the physical "Alt" key will result in a held layer being activated while also holding "Alt" itself. The held layer operates nearly the same as the standard keyboard, so for example the sequence (hold Alt)+(Tab+Tab+Tab) will work as expected. This is in contrast to having a layer where `tab` is mapped to `A-tab`, which results in repeated press+release of the two keys and has different behaviour than expected. Some special keys will release the "Alt" key and do some other action that requires "Alt" to be released. In other words, the "Alt" key serves a dual purpose of still fulfilling the "Alt" key role for some button presses (e.g. Tab), but also as a new layer for keys that aren't typically used with "Alt" to have added useful functionality. [source] ---- (defalias atl (multi alt (layer-while-held alted-with-exceptions)) lft (multi (release-key alt) left) ;; release alt if held and also press left rgt (multi (release-key alt) rght) ;; release alt if held and also press rght ) (defsrc alt a s d f ) (deflayer base @atl _ _ _ _ ) (deflayer alted-with-exceptions _ _ _ @lft @rgt ) ---- WARNING: This action can sometimes behave in surprising ways with regards to simultaneity and order of actions. For example, an action like `(multi sldr ')` will not behave as expected. Due to implementation details, `sldr` will activate after the `'` even though it is listed before. This example could instead be written as `(macro sldr 10 ')`, and that would work as intended. It is recommended to avoid `multi` if it can be replaced with a different action like `macro` or an output chord. ==== reverse-release-order **Reference** String item that can be used inside of `(multi ...)` to reverse the release order of any keys that were pressed as part of `multi`. .Syntax: [source] ---- (multi ... reverse-release-order) ---- **Description** Within `multi` you can use include `reverse-release-order` to do what the action states: reverse the typical release order from if you have multiple keys in multi. For example, pressing then releasing a key with the action: `(multi a b c)` would press a b c in the stated order and then release a b c in the stated order. Changing it to `(multi a b c reverse-release-order)` would press a b c in the stated order and then release c b a in the stated order. .Example: [source] ---- (defalias S-a-reversed (multi lsft a reverse-release-order) ) ---- [[mouse-actions]] === Mouse actions You can click the left, middle, and right buttons using kanata actions, do vertical/horizontal scrolling, and move the mouse. [[mouse-buttons]] ==== Mouse buttons **Reference** You can activate mouse actions with the string actions below. [cols="1,5"] |=== | `mlft` | Hold left mouse button. | `mmid` | Hold middle mouse button. | `mrgt` | Hold right mouse button. | `mfwd` | Hold forward mouse button. | `mbck` | Hold backward mouse button. | `mltp` | Tap left mouse button. | `mmtp` | Tap middle mouse button. | `mrtp` | Tap right mouse button. | `mftp` | Tap forward mouse button. | `mbtp` | Tap backward mouse button. |=== In Linux and Windows, 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: * `mlft`: left mouse button * `mmid`: middle mouse button * `mrgt`: right mouse button * `mfwd`: forward mouse button * `mbck`: backward mouse button The mouse button will be held while the key mapped to it is held. Only on Linux and Windows, the above actions are also usable in `defsrc` to enable remapping specified mouse actions in your layers, like you would with keyboard keys. If there are multiple mouse click actions within a single multi action, e.g. `+(multi mrgt mlft)+` then all the buttons except the last will be clicked then unclicked. The last button will remain held until key release. In the example above, pressing then releasing the key mapped to this action will result in the following event sequence: . press key mapped to `+multi+` . click right mouse button . unclick right mouse button . click left mouse button . release key mapped to `+multi+` . release left mouse button There are variants of the standard mouse buttons which "tap" the button. Rather than holding the button while the key is held, a mouse click will be immediately followed by the release. Nothing happens when the key is released. The actions are as follows: * `mltp`: tap left mouse button * `mmtp`: tap middle mouse button * `mrtp`: tap right mouse button * `mftp`: tap forward mouse button * `mbtp`: tap bacward mouse button [[mouse-wheel]] ==== Mouse wheel **Reference** The `mwheel-*` actions allow you to emulate a mouse wheel. Holding the action will repeatedly scroll according to the action configuration. .Syntax: [source] ---- (mwheel-$variant $interval $distance) ---- [cols="1,4"] |=== | `$variant` | One of `up down left right` representing the scroll direction to use. | `$interval` | Number of milliseconds between scroll actions. | `$distance` | Distance to travel per activation. The number `120` represents a complete notch on standard resolution mice and in some environments, 120 or a multiple of it should be what is used. |=== You may use these key names within `defsrc` to remap scroll events as if they were keys, corresponding to up, down, left, right respectively: `mwu`, `mwd`, `mwl`, `mwr`. The remapping of mouse events is only effective on Linux and Windows. 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 wheel actions are: * `mwheel-up` or `🖱☸↑`: vertical scroll up * `mwheel-down` or `🖱☸↓`: vertical scroll down * `mwheel-left` or `🖱☸←`: horizontal scroll left * `mwheel-right` or `🖱☸→`: horizontal scroll right All of these actions accept two number strings. The first is the interval (unit: ms) between scroll actions. The second number is the distance (unit: arbitrary). In both 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]. NOTE: In Linux, not all desktop environments support the `REL_WHEEL_HI_RES` event. If this is the case for yours, it will likely be a better experience to use a distance value that is a multiple of 120. On Linux and 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` or `🖱↑` * `movemouse-down` or `🖱↓` * `movemouse-left` or `🖱←` * `movemouse-right` or `🖱→` Similar to the mouse wheel actions, all of these actions accept two number strings. The first is the interval (unit: ms) between movement actions and the second number is the distance (unit: pixels) of each movement. The following are variants of the above mouse movements that apply linear mouse acceleration from the minimum distance to the maximum distance as the mapped key is held. * `movemouse-accel-up` or `🖱accel↑` * `movemouse-accel-down` or `🖱accel↓` * `movemouse-accel-left` or `🖱accel←` * `movemouse-accel-right` or `🖱accel→` All these actions accept four number strings. The first number is the interval (unit: ms) between movement actions. The second number is the time it takes (unit: ms) to linearly ramp up from the minimum distance to the maximum distance. The third and fourth numbers are the minimum and maximum distances (unit: pixels) of each movement. There is a toggable defcfg option related to `movemouse-accel` - <>. You might want to enable it, especially if you're coming from QMK. [[set-mouse]] ==== Set absolute mouse position The action `setmouse` or `set🖱` sets the absolute mouse position. WARNING: This is 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 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] ---- (defalias mwu (mwheel-up 50 120) mwd (mwheel-down 50 120) mwl (mwheel-left 50 120) mwr (mwheel-right 50 120) ms↑ (movemouse-up 1 1) ms← (movemouse-left 1 1) ms↓ (movemouse-down 1 1) ms→ (movemouse-right 1 1) ma↑ (movemouse-accel-up 1 1000 1 5) ma← (movemouse-accel-left 1 1000 1 5) ma↓ (movemouse-accel-down 1 1000 1 5) ma→ (movemouse-accel-right 1 1000 1 5) sm (setmouse 32228 32228) fst (movemouse-speed 200) ) (deflayer mouse _ @mwu @mwd @mwl @mwr _ _ _ _ _ @ma↑ _ _ _ _ pgup bck _ fwd _ _ _ _ @ma← @ma↓ @ma→ _ _ _ pgdn mlft _ mrgt mmid _ mbck mfwd _ @ms↑ _ _ @fst _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ _ _ _ _ _ _ _ ) ---- [[tap-dance]] === tap-dance **Reference** The `tap-dance` action allows performing different actions based on number of consecutive taps of the same key. .Syntax: [source] ---- (tap-dance $timeout $action-list) ---- [cols="1,4"] |=== | `$timeout` | Number of milliseconds after which the tap-dance ends. | `$action-list` | A list of actions that can be selected, ordered by number of taps. |=== The `tap-dance-eager` variant will eagerly perform actions. Use of `macro` and `bspc` can help to backtrack for the 2nd tap onwards. .Syntax: [source] ---- (tap-dance-eager $timeout $action-list) ---- **Description** The `+tap-dance+` action allows repeated tapping of a key to result in different actions. It is followed by a timeout (unit: ms) and a list of keys or actions. Each time the key is pressed, its timeout will reset. The action will be chosen if one of the following events occur: * the timeout expires * a different key is pressed * the key is repeated up to the final action You may put normal keys or other actions in `+tap-dance+`. .Example: [source] ---- (defalias ;; 1 tap : "A" key ;; 2 taps: Control+C ;; 3 taps: Switch to another layer ;; 4 taps: Escape key td (tap-dance 200 (a C-c (layer-switch l2) esc)) ) ---- There is a variant of `tap-dance` with the name `tap-dance-eager`. The variant is parsed identically but the difference is that it will activate every action in the sequence as the taps progress. In the example below, repeated taps will, in order: 1. type `a` 2. erase the `a` and type `bb` 3. erase the `bb` and type `ccc` [source] ---- (defalias td2 (tap-dance-eager 500 ( (macro a) ;; use macro to prevent auto-repeat of the key (macro bspc b b) (macro bspc bspc c c c) )) ) ---- [[one-shot]] === one-shot **Reference** Activate keys or layers for a time without keeping the input key held, for one subsequent key. Activating other one-shot actions, while one or more are already active, will reset the timeout, and overlap the one-shot actions. .Syntax: [source] ---- ($one-shot-variant $timeout $action) ---- Values for `$variant`: [cols="1,3"] |=== | `one-shot-press` | End on the first press of another key. This is also the variant selected by the name `one-shot`. | `+one-shot-release+` | End on the first release of a newly pressed key. | `+one-shot-press-pcancel+` | End on the first press of another key or on re-press of 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 key is used. The `+one-shot+` action must be followed by a timeout (unit: ms) and another key or action. Some of the intended use cases are: * press a modifier for exactly one following key press * switch to another layer for exactly one following key press If a `+one-shot+` key is held then it will act as the regular key. E.g. holding a key assigned with `+@os2+` in the example below will keep Left Shift held for every key, not just one, as long as it's still physically pressed. Pressing multiple `+one-shot+` keys in a row within the timeout will combine the actions of those keys and reset the timeout to the value of the most recently pressed `+one-shot+` key. There are four variants of the `+one-shot+` action: - `+one-shot-press+` or `+one-shot↓+`: end on the first press of another key - `+one-shot-release+` or `+one-shot↑+`: end on the first release of another key - `+one-shot-press-pcancel+` or `+one-shot↓⤫+`: end on the first press of another key or on re-press of another active one-shot key - `+one-shot-release-pcancel+` or `+one-shot↑⤫+`: end on the first release of another key or on re-press of another active one-shot key It is important to note that the first activation of a one-shot key determines the behaviour with regards to the 4 variants for all subsequent one-shot key activations, even if a following one-shot key has a different configuration than the initial key pressed. The default name `+one-shot+` corresponds to `+one-shot-press+`. NOTE: When using one-shot with keys that will trigger defoverrides, you will likely want to adjust <> to yes in `defcfg`. .Example: [source] ---- (defalias os1 (one-shot 500 (layer-while-held another-layer)) os2 (one-shot-press 2000 lsft) os3 (one-shot-release 2000 lctl) os4 (one-shot-press-pcancel 2000 lalt) os5 (one-shot-release-pcancel 2000 lmet) ) ---- [[one-shot-pause-processing]] ==== one-shot-pause-processing **Reference** Pause `one-shot` processing of new input keypresses for a time, to allow actions that are not intended to consume `one-shot` to take place. .Syntax: [source] ---- (one-shot-pause-processing $time) ---- [cols="1,5"] |=== | `time` | Number of milliseconds to ignore processing. Something notable is that one virtual key press or releas (tap is a separate press and subsequent release) will take 1ms to process. If using virtual keys this number must be larger than the number of virtual key events that are taking place. |=== **Description** The `one-shot-pause-processing` list action allows you to pause the key press processing of one-shot activations. An example of when this is useful the following sequence: - Activate a layer-while-held - Activate a one-shot action on that layer - Release the layer-while-held key, which has an `(on-release ...)` action associated with it. - The on-release action is not intended to consume one-shot activations In the scenario above, by default the on-release activation would trigger deactivation of one-shot; thus the pause processing action must be used to stop this from happening. [[tap-hold]] === tap-hold WARNING: The `tap-hold` action and all variants can behave unexpectedly on Linux with respect to repeat of antecedent key presses. The full context is in https://github.com/jtroo/kanata/discussions/422[discussion #422]. In brief, the workaround is to use `tap-hold` inside of <>, combined with another key action that behaves as a no-op like `f24`. + Example: `(multi f24 (tap-hold ...))`. If multiple `tap-hold` actions may be pressed subsequently, all using the `f24` workaround, you may need to release the `f24` within the same `multi` to avoid repeats from one double-tapped `tap-hold` action followed by another, different `tap-hold` action. Example: `(defvirtualkeys relf24 (release-key f24)) ... (multi f24 (tap-hold ...) (macro 5 (on-press tap-vkey relf24)))` **Reference** The `tap-hold` action lets you activate different actions depending it a key is tapped or held. .Syntax: [source] ---- (tap-hold $tap-repress-timeout $hold-timeout $tap-action $hold-action) ---- [cols="1,4"] |=== | `$tap-repress-timeout` | Number of milliseconds for the window that a tap into re-press with hold results in the `$tap-action` being held. | `$hold-timeout` | Number of milliseconds after which the `$hold-action` activates. Releasing the key before this elapses results in `$tap-action` activating. | `$tap-action` | Action to activate when the input is determined to be a "tap". | `$hold-action` | Action to activate when the input is determined to be a "hold". |=== .Variants: ---- (tap-hold-press $tap-repress-timeout $hold-timeout $tap-action $hold-action) (tap-hold-release $tap-repress-timeout $hold-timeout $tap-action $hold-action) (tap-hold-press-timeout $tap-repress-timeout $hold-timeout $tap-action $hold-action $timeout-action) (tap-hold-release-timeout $tap-repress-timeout $hold-timeout $tap-action $hold-action $timeout-action [?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-opposite-hand $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-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-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 repress timeout (unit: ms) . hold timeout (unit: ms) . tap action . hold action The tap repress timeout is the number of milliseconds within which a rapid press+release+press of a key will result in the tap action being held instead of the hold action activating. .Tap repress timeout in more detail [%collapsible,indent=4] ==== The way a `tap-hold` action works with respect to the tap repress timeout is often unclear to newcomers. To make it concrete, the output event sequence of the `tap-hold` action `(tap-hold $tap-repress-timeout 200 a lctl)` for varying values of `$tap-repress-timeout` with a fixed input event sequence will be described. The input event sequence is: - press - 50 ms elapses - release - 50 ms elapses - press - 300 ms elapses - release With `(defvar $tap-repress-timeout 0)`, the output event sequence is: - 50 ms elapses - press `a` - release `a` - 250 ms elapses - press `lctl` - 100 ms elapses - release `lctl` The above output sequence is the same for all `$tap-repress-timeout` values between and including `0` and `99`. For a value of `100` or greater for `$tap-repress-timeout`, the output event sequence is instead: - 50 ms elapses - press `a` - release `a` - 50 ms elapses - press `a` - 300 ms elapses - release `a` ==== The hold timeout is the number of milliseconds after which the hold action will activate. There are two additional variants of `+tap-hold+`: * `+tap-hold-press+` or `+tap⬓↓+` ** If there is a press of a different key, the hold action is activated even if the hold timeout hasn't expired yet * `+tap-hold-release+` or `+tap⬓↑+` ** If there is a press+release of a different key, the hold action is activated even if the hold timeout hasn't expired yet These variants may be useful if you want more responsive tap-hold keys, but you should be wary of activating the hold action unintentionally. .Example: [source] ---- (defalias anm (tap-hold 200 200 a @num) ;; tap: a hold: numbers layer oar (tap-hold-press 200 200 o @arr) ;; tap: o hold: arrows layer ech (tap-hold-release 200 200 e @chr) ;; tap: e hold: chords layer ) ---- There are further additional variants of `tap-hold-press` and `tap-hold-release`: - `tap-hold-press-timeout` or `tap⬓↓timeout` - `tap-hold-release-timeout` or `tap⬓↑timeout` These variants take a 5th parameter, in addition to the same 4 as the other variants. The 5th parameter is another action, which will activate if the hold timeout expires as opposed to being triggered by other key actions, whereas the non `-timeout` variants will activate the hold action in both cases. 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`. 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 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. |=== 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, all keys are held, whereas in `+macro+`, keys are pressed then released. This means that with `+macro+` you can have some letters capitalized and others not. This is not possible with `+multi+`. The `+macro+` action accepts one or more keys, some actions, chords, and delays (unit: ms). It also accepts a list prefixed with <> modifiers where the list is subject to the aforementioned restrictions. IMPORTANT: The number keys `0-9` will be parsed as millisecond delays whereas in other contexts they would be parsed as key names. To use the numbered keys they must be aliased or otherwise use the key names `Digit0-Digit9`. Up to 4 macros can be active at the same time. The actions supported in `+macro+` are: * <> * <> * <> * <> * <> * <> * <> * <> * <> * <> NOTE: Some of these actions may need short delays between. For example, `(macro a (unmod b) 5 (unmod c) d))` needs the delay of `5` to work correctly. .Example: [source] ---- (defalias : S-; 8 8 0 0 🙃 (unicode 🙃) ;; Type "http://localhost:8080" lch (macro h t t p @: / / 100 l o c a l h o s t @: @8 @0 @8 @0) ;; Type "I am HAPPY my FrIeNd 🙃" hpy (macro S-i spc a m spc S-(h a p p y) spc m y S-f r S-i e S-n d spc @🙃) ;; alt-tab(x3) and alt-shift-tab(x3) with macro tfd (macro A-(tab 200 tab 200 tab)) tbk (macro A-S-(tab 200 tab 200 tab)) ) ---- [[macro-release-cancel]] ==== macro-release-cancel The `macro-release-cancel` variant of the `+macro+` action will cancel all active macros upon releasing the key. Shorter unicode variant: `+macro↑⤫+`. This variant is parsed identically to the non-cancelling version. An example use case for this action is holding down a key to get different outputs, similar to tap-dance but one can see which keys are being outputted. E.g. in the example below, when holding the key, first `1` is typed, then replaced by `!` after 500ms, and finally that is replaced by `@` after another 500ms. However, if the key is released, the last character typed will remain and the rest of the macro does not run. [source] ---- (defalias 1 1 ;; macro-release-cancel to output different characters with visual feedback ;; after holding for different amounts of time. 1!@ (macro-release-cancel @1 500 bspc S-1 500 bspc S-2) ) ---- [[macro-cancel-on-press]] ==== macro-cancel-on-press The `macro-cancel-on-press` variant of the `macro action` enables a cancellation trigger for all active macros including itself, which is activated when a physical press of any other key happens. The trigger is enabled while the macro is in progress. [source] ---- (defalias 1 1 1!@ (macro-cancel-on-press @1 500 bspc S-1 500 bspc S-2) ) ---- [[macro-release-cancel-and-cancel-on-press]] ==== macro-release-cancel-and-cancel-on-press The `macro-release-cancel-and-cancel-on-press` variant combines the cancel behaviours of both the release-cancel and cancel-on-press. [source] ---- (defalias 1 1 1!@ (macro-release-cancel-and-cancel-on-press @1 500 bspc S-1 500 bspc S-2) ) ---- [[macro-repeat]] ==== macro-repeat There are further `macro-repeat` variants of the three `macro` actions described previously. These variants repeat while held. The repeat will only occur once all macros have completed, including the held macro key. If multiple repeating macros are being held simulaneously, only the most recently pressed macro will be repeated. [source] ---- (defalias mr1 (macro-repeat mltp) mr2 (macro-repeat-release-cancel mltp) mr3 (macro-repeat-cancel-on-press mltp) mr4 (macro-repeat-release-cancel-and-cancel-on-press mltp) ) ---- [[dynamic-macro]] === dynamic-macro **Reference** Record and replay key inputs. .Syntax: [source] ---- (dynamic-macro-record $id) (dynamic-macro-play $id) (dynamic-macro-record-stop) (dynamic-macro-record-stop-truncate $count) ---- [cols="1,3"] |=== | `dynamic-macro-record` | Record a dynamicro macro which will be saved with the defined `$id`. | `dynamic-macro-play` | Play back a macro saved with the defined `$id`. | `dynamic-macro-record-stop` | Stop and save a macro recording. This can also be achieved by recording a new macro or re-pressing record with the same `$id`. | `dynamic-macro-record-stop-truncate` | Stop and save a macro recording while truncating `$count` events from the end of the recording. This can be useful if the record/stop button is on a different layer. |=== **Description** The dynamic-macro actions allow for recording and playing key presses. The dynamic macro records physical key presses, as opposed to kanata's outputs. This allows the dynamic macro to replicate any action, but it means that if the macro starts and ends on different layers, then the macro might not be properly repeatable. The action `dynamic-macro-record` accepts one number (0-65535), which represents the macro ID. Activating this action will begin recording physical key inputs. If `dynamic-macro-record` with the same ID is pressed again, the recording will end and be saved. If `dynamic-macro-record` with a different ID is pressed then the current recording will end and be saved, then a new recording with the new ID will begin. The action `dynamic-macro-record-stop` will stop and save any active recording. There is a variant of this: `dynamic-macro-record-stop-truncate` This is a list action that takes a single parameter: the number of key actions to remove at the end of a dynamic macro. This variant is useful if the macro stop button is on a different layer. The action `dynamic-macro-play` accepts one number (0-65535), which represents the macro ID. Activating this action will play the saved recording of physical keys from a previous `dynamic-macro-record` with the same macro ID, if it exists. One can nest dynamic macros within each other, e.g. activate `(dynamic-macro-play 1)` while recording with `(dynamic-macro-record 0)`. However, dynamic macros cannot recurse; e.g. activating `(dynamic-macro-play 0)` while recording with `(dynamic-macro-record 0)` will be ignored. .Example: [source] ---- (defalias dr0 (dynamic-macro-record 0) dr1 (dynamic-macro-record 1) dr2 (dynamic-macro-record 2) dp0 (dynamic-macro-play 0) dp1 (dynamic-macro-play 1) dp2 (dynamic-macro-play 2) dms dynamic-macro-record-stop dst (dynamic-macro-record-stop-truncate 1) ) ---- [[caps-word]] === caps-word **Reference** The `caps-word` action puts Kanata into a state where typed keys are automatically shifted by `lsft`. The state persists until terminated by timeout or by typing a key that ends the state. Typing a non-terminating key refreshes the timeout duration. The `-toggle` variants will end the caps-word state if pressed while caps-word is active, whereas the re-pressing the standard variants will keep the state active and refresh the timeout duration. .Syntax: [source] ---- (caps-word $timeout) (caps-word-toggle $timeout) (caps-word-custom $timeout $shifted-list $non-terminal-list) (caps-word-custom-toggle $timeout $shifted-list $non-terminal-list) ---- [cols="1,4"] |=== | `$timeout` | Number of milliseconds after which the caps-word state ends. The duration is refreshed upon typing a non-terminating character. | `$shifted-list` | List of keys that will be automatically shifted. | `$non-terminal-list` | List of keys that are not shifted but which do not terminate the caps-word state. |=== **Description** The `caps-word` or `word⇪` action triggers a state where the `lsft` key will be added to the active key list when a set of specific keys are active. The keys are: `a-z` and `-`, which will be outputted as `A-Z` and `_` respectively when using the US layout. Examples where this is helpful is capitalizing a single important word like in `IMPORTANT!` or defining a constant in code like `const P99_99_VALUE: ...`. This has an advantage over the regular caps lock because it automatically ends so it doesn't need to be toggled off manually, and it also shifts `-` to `_` which caps lock does not do. The `caps-word` state ends when the keyboard is idle for the duration of the defined timeout (1st parameter), or a terminating key is pressed. Every key is a terminating key except the keys which get capitalized and the extra keys in this list: - `0-9` - `kp0-kp9` - `bspc del` - `up down left rght` You can use `caps-word-custom` or `word⇪-custom` instead of `caps-word` if you want to manually define which keys are capitalized (2nd parameter) and what the extra non-terminal+non-capitalized keys should be (3rd parameter). .Example: [source] ---- (defalias cw (caps-word 2000) ;; This example is similar to the default caps-word behaviour but it moves the ;; 0-9 keys to the capitalized key list from the extra non-terminating key list. cwc (caps-word-custom 2000 (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) ) ) ---- ==== caps-word-toggle[[caps-word-toggle]] There are `-toggle` variants of the `caps-word` actions. By default re-pressing `caps-word` will keep `caps-word` active. The `-toggle` variants will end `caps-word` if it is currently active, otherwise `caps-word` will be activate as normal. .Example: [source] ---- (defalias cwt (caps-word-toggle 2000) cct (caps-word-custom-toggle 2000 (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) ) ) ---- === unmod[[unmod]] **Reference** The `unmod` action will deactivate modifier keys while outputting one or more defined keys. .Syntax: [source] ---- (unmod $key1 $key2 ... $keyN) (unmod ($mod1 $mod2 ... $modN) $key1 $key2 ... $keyN) ---- [cols="1,5"] |=== | `$key` | A key name to output while unmodded. | `$mod` | By default `unmod` will deactivate all modifier keys. An optional list as the first parameter allows specfying a subset of modifiers to deactivate during the action. |=== **Description** The `unmod` action will release all modifiers temporarily and send one or more keys. After the `unmod` key is released, the released modifiers are pressed again. The affected modifiers are: `lsft,rsft,lctl,rctl,lmet,rmet,lalt,ralt`. A variant of `unmod` is `unshift` or `un⇧`. This action only releases the `lsft,rsft` keys. This can be useful for forcing unshifted keys while AltGr is still held. NOTE: In case the modifiers to be undone are not part of `defsrc`, <> needs to be enabled in `defcfg` in order for their states to be tracked correctly. .Example: [source] ---- (defalias ;; holding shift and tapping a @um1 key will still output 1. um1 (unmod 1) ;; dead keys é (as opposed to using AltGr) that outputs É when shifted dké (macro (unmod ') e) ;; In ISO German QWERTZ, force unshifted symbols even if shift is held { (unshift ralt 7) [ (unshift ralt 8) ) ---- A list may optionally be used as the first parameter of `unmod`. The list must be non-empty and must contain only modifier keys, which are the keys in the affected modifiers list from earlier in this document section. When this list exists, the action will temporarily release only the keys listed rather than all modifiers. .Example: [source] ---- (defalias ;; only unshift the alt keys unalt-a (unmod (lalt ralt) a) ) ---- [[fork]] === fork **Reference** The `fork` action allows choosing between a default and an alternate action based on whether specific keys are active. `fork` is the equivalent of the basic key checks in `switch`, using none of the list logic items, i.e. virtual keys are not supported. .Syntax: [source] ---- (fork $left-action $right-action $right-trigger-keys) ---- [cols="1,3"] |=== | `$left-action` | Action to activate by default. | `$right-action` | Action to activate if any of `$right-trigger-keys` are active. | `$right-trigger-keys` | List of keys that, if active when fork activates, causes `$right-action` to happen in place of `$left-action`. |=== TIP: The keys `nop0-nop9` can be used as no-op outputs that can still be checked within `fork`, unlike what `XX` does. [[switch]] === switch **Reference** The `switch` action allows conditionally activating 0 or more actions, depending on conditional checks. .Syntax: [source] ---- (switch $logic-check1 $action1 $post-activate1 $logic-check2 $action2 $post-activate2 ... $logic-checkN $actionN $post-activateN) ---- [cols="1,4"] |=== | `$logic-check` | The condition, which if it evaluates to true, will trigger the corresponding action. | `$action` | Action to activate when logic evaluates to true. | `$post-activate` | Valid values are `fallthrough` and `break`. With `fallthrough`, when an action activates switch will continue evaluating further logic checks and potentially trigger more actions. With `break`, further actions will not activate. |=== The logic check is a list. The items within the list can either be key names or a special list check. A key name item evaluates to true if that key name is a currently active key output of Kanata upon activating `switch`. The outer-most list evaluates to true if any of the items evaluates to true. .Syntax of logic check: [source] ---- ($item1 $item2 ... $itemN) ---- .Syntax of special checks: [source] ---- (or $item1 $item2 ... $itemN) (and $item1 $item2 ... $itemN) (not $item1 $item2 ... $itemN) (key-history $key-name $key-recency) (key-timing $key-recency $comparator $time) (input $input-type $key-name) (input-history $input-type $key-name $input-recency) (layer $layer-name) (base-layer $layer-name) ---- [cols="1,4"] |=== | `or` | Evaluates to true if any `$item` is true. | `and` | Evaluates to true if all of `$item` are true. | `not` | Evaluates to true if all of `$item` are false. | `key-history` | Evaluates to true if the key in the recency slot matches `$key-name`. A `$key-recency` of 1 is the most recent key pressed according to Kanata processing. The max recency is 8. | `key-timing` | The valid values for `$comparator` are `less-than` and `greater-than`, with `lt` and `gt` as shorthand if desired. This item evaluates to true if the key with the corresponding recency was pressed — for `lt` more recently than, or for `gt` later than — the defined `$time` with unit milliseconds. | `input` | Evaluates to true if the `$key-name` is currently pressed. The `$input-type` must be either `real` or `virtual`. If using `real`, this will check against the defsrc inputs. If using `virtual`, this will check against virtual key activations. | `input-history` | Evaluates to true if the input in the `$input-recency` slot matches `$key-name`. Two input types use the same history with respect to recency slots. A recency of 1 is the most recent input i.e. the input activating `switch` itself. The max recency is 8. | `layer` | Evaluates to true if the active layer matches `$layer-name`. | `base-layer` | Evaluates to true if the most-recently-switched-to layer from a `layer-switch` action matches `$layer-name`. |=== **Description** Conceptually, the `switch` action is similar to <> but has more capabilities as well as more complexity. The `switch` action accepts multiple cases. One case is a triple of: - logic check - action: to activate if logic check evaluates to true - `fallthrough|break`: choose to continue vs. stop evaluating cases The default use of the logic check behaves similarly to fork. For example, the logic check `(a b c)` will activate the corresponding action if any of a, b, or c are currently pressed. TIP: the keys `nop0-nop9` can be used as no-op outputs that can still be checked within `switch`, unlike what `XX` does. The logic check also accepts the boolean operators `and|or|not` to allow more complex use cases. The order of cases matters. For example, if two different cases match the currently pressed keys, the case listed earlier in the configuration will activate first. If the early case uses break, the second case will not activate. Otherwise if fallthrough is used, the second case will activate sequentially after the first case. This idea generalizes to more than two cases, but the two case example is hopefully simple and effective enough. .Example: [source] ---- (defalias swt (switch ;; case 1 ((and a b (or c d) (or e f))) @ac1 break ;; case 2 (a b c) @ac2 fallthrough ;; case 3 () @ac3 break ) ) ---- Below is a description of how this example behaves. ==== Case 1 ---- ((and a b (or c d) (or e f))) a break ---- Translating case 1's logic check to some other common languages might look like: ---- (a && b && (c || d) && (e || f)) ---- If the logic check passes, the action `@ac1` will activate. No other action will activate since `break` is used. ==== Cases 2 and 3 ---- (a b c) c fallthrough () b break ---- Case 2's key check behaves like that of `fork`, i.e. (or a b c) or for some other common languages: a || b || c If this logic check passes and the case 1 does not pass, the action `@ac2` will activate first. Since the logic check of case 3 always passes, `@ac3` will activate next. If neither case 1 or case 2 pass their logic checks, case 3 will always activate with `@ac3`. [[key-history-and-key-timing]] ==== key-history and key-timing In addition to simple keys there are two list items that can be used within the case logic check that compare against your typed key history: * `key-history` * `key-timing` The `key-history` item compares the order that keys were typed. It accepts, in order: * a key * the key recency The key recency must be in the range 1-8, where 1 is the most recent key that was pressed and 8 is 8th most recent key pressed. .Example: [source] ---- (defalias swh (switch ((key-history a 1)) S-a break ((key-history b 1)) S-b break ((key-history c 1)) S-c break ((key-history d 8)) (macro d d d) break () XX break ) ) ---- The `key-timing` compares how long ago recent key typing events occurred. It accepts, in order, * the key recency * a comparison string, which is one of: `less-than|greater-than|lt|gt` * number of milliseconds to compare against The key recency must be in the range 1-8, where 1 is the most recent key that was pressed and 8 is 8th most recent key pressed. Most use cases are expected to use a value of 1 for this parameter, but perhaps you can find a creative use for the other values. The comparison string determines how the actual key event timing will be compared to the provided timing. The number of milliseconds must be 0-65535. WARNING: The maximum milliseconds value of this configuration item across your whole configuration will be a lower bound of how long it takes for kanata to become idle and stop processing its state machine every approximately 1ms. .Example: [source] ---- (defalias swh (switch ((key-timing 1 less-than 200)) S-a break ((key-timing 1 greater-than 500)) S-b break ((key-timing 2 lt 1000)) S-c break ((key-timing 8 gt 2000)) (macro d d d) break () XX break ) ) ---- ==== not The examples presented so far have not included the `not` boolean operator. This operator will now be discussed. Syntactically, the `not` operator is used similarly to `or|and`. Functionally, it means "not **any** of" the list elements. .Example: [source] ---- (defalias swn (switch ((not x y z)) S-a break ;; the above and below cases are equivalent in logic ((not (or x y z))) S-a break ) ) ---- In potentially more familiar notation, both cases have the logic: !(x || y || z) ==== input Until now, all `switch` logic has been associated to key code outputs. It is also possible to operate on inputs. Inputs can be either real keys or "virtual" (fake) keys. .Example: [source] ---- (defalias switch-input-example (switch ((input real lctl)) $ac1 break ((input virtual vk1)) $ac2 break () $ac3 break ) ) ---- Similar to `key-history` for regular active keys, `input-history` also exists. A perhaps surprising, but hopefully logical, behaviour of input-history when compared to key-history is that, at the time of switch activation, the history of `input-history` for recency `1` will be the just-pressed input. In other words recency `1` is the input activating the `switch` action itself. Whereas with `key-history` for example, the key that will be next outputted may be determined by the switch logic itself, so is not in the history. The consequence of this is that you should use a recency of `2` when referring to the previously pressed input because the current input is in the recency `1` slot. .Example: [source] ---- (defalias switch-input-history-example (switch ((input-history real lsft 2)) $var1 break ((input-history virtual vk2 2)) $var1 break () $ac3 break ) ) ---- ==== layer The `layer` list item can be used in `switch` logic to operate on the active layer. It accepts a single layer name and evaluates to true if the configured layer name is the active layer, otherwise it evaluates to false. .Example: [source] ---- (defalias switch-layer-example (switch ((layer base)) x break ((layer other)) y break () z break ) ) ---- ==== base-layer The `base-layer` list item evaluates to true if the configured layer name is the base layer. The base layer is the most recently switched-to layer from a `layer-switch` action, or the first layer defined in your configuration if `layer-switch` has never been activated. .Example: [source] ---- (defalias switch-layer-example (switch ((base-layer base)) x break ((base-layer other)) y break () z break ) ) ---- [[cmd]] === cmd WARNING: This action does not work unless you use the appropriate binary or - if compiling yourself - the appropriate feature flag. Additionally you must add the <> `defcfg` option. **Reference** The `cmd` action allows you to execute arbitrary binaries with arbitrary arguments. The `cmd-log` variant behaves similarly but allows customization of the stdout and stderr log levels within Kanata's output logging. The `cmd-output-keys` is like `cmd`, but stdout of the command will be parsed as a list of keys, output chords, and delays similar to <> and be typed as kanata outputs. .Syntax: [source] ---- (cmd $binary $arg1 $arg2 ... $argN) (cmd-log $stdout-log-level $stderr-log-level) (cmd-output-keys $binary $arg1 $arg2 ... $argN) ---- [cols="1,3"] |=== | `$binary` | Executable binary to run. | `$arg` | Argument passed into the binary. | `$stdout-log-level` | Log level for stdout of the command. Must be `+debug+`, `+info+`, `+warn+`, `+error+`, or `+none+`. | `$stderr-log-level` | Log level for stderr of the command. Must be `+debug+`, `+info+`, `+warn+`, `+error+`, or `+none+`. |=== **Description** The `+cmd+` action executes a program with arguments. It accepts one or more strings. The first string is the program that will be run and the following strings are arguments to that program. The arguments are provided to the program in the order written in the config file. Lists may also be used within `cmd` which you may desire to do for reuse via `defvar`. Lists will be flattened such that arguments are provided to the program in the order written in the config file, regardless of list nesting. To be technical, it would be a depth-first flattening (similar to DFS). Commands are executed directly and not via a shell, so you cannot make use of environment variables or symbols with special meaning. For example `+~+` or `+$HOME+` in Linux will not be substituted with your home directory. If you want to execute with a shell program use the shell as the first parameter, e.g. `bash` or `powershell.exe`. The user executing the command is the user that kanata was started with. For example, if kanata was started by root, the command will be run by the root user. If you need to execute as a different user, on Unix platforms you can use `sudo -u USER` before the rest of your command to achieve this. .Example: [source] ---- (defalias cm1 (cmd rm -fr /tmp/testing) ;; You can use bash -c and then a quoted string to execute arbitrary text in ;; bash. All text within double-quotes is treated as a single string. cm2 (cmd bash -c "echo hello world") ;; You can prefix commands with sudo -u USER ;; to execute commands as a different user. cm3 (cmd sudo -u other_user bash -c "echo goodbye") ) ---- By default, `+cmd+` logs start of command, completion of command, stdout, and stderr. Using the variant `+cmd-log+`, these log levels can be changed, and even disabled. It takes two arguments, `++` and `++`. `++` will be the level where the command to run, stdout, and stderr are logged. The error channel is logged only if there is a failure with running the command (typically if the command can't be found, or there is trouble spawning it). The valid levels are `+debug+`, `+info+`, `+warn+`, `+error+`, and `+none+`. .Example: [source] ---- (defalias ;; The first two arguments are the log levels, then just the normal command ;; This will only error if `bash` is not found or something else goes ;; wrong with the initial execution. Any logs produced by bash will not ;; be shown. noisy-cmd (cmd-log none error bash -c "echo hello this produces a log") ;; This will only log the output of the command, but it won't start ;; because the command doesn't exist. ignore-failure-cmd (cmd-log info none thiscmddoesnotexist) verbose-only-log (cmd-log verbose verbose bash -c "echo yo") ) ---- There is a variant of `cmd`: `cmd-output-keys`. This variant reads the output of the executed program and reads it as an S-expression, similarly to the <>. However — unlike macro — only delays, keys, chords, and chorded lists are supported. Other actions are not supported. [source] ---- (defalias ;; bash: type date-time as YYYY-MM-DD HH:MM pdb (cmd-output-keys bash -c "date +'%F %R' | sed 's/./& /g' | sed 's/:/S-;/g' | sed 's/\(.\{20\}\)\(.*\)/\(\1 spc \2\)/'") ;; powershell: type date-time as YYYY-MM-DD HH:MM pdp (cmd-output-keys powershell.exe "echo '(' (((Get-Date -Format 'yyyy-MM-dd HH:mm').toCharArray() -join ' ').insert(20, ' spc ') -replace ':','S-;') ')'") ) ---- [[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 condiditionally-used items within a template. It accepts a minimum of 2 parameters. The first two parameters must be strings and are compared against each other. If they match, the following parameters are inserted into the template in place of the `if-equal` list. Otherwise if the strings do not match then the whole `if-equal` list is removed from the template. .Example 4: ---- (deftemplate home-row (version) a s d f g h (if-equal $version v1 j) (if-equal $version v2 (tap-hold 200 200 j lctl)) k l ; ' ) (defsrc grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps (template-expand home-row v1) ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) (deflayer base grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p [ ] \ caps (template-expand home-row v2) ret lsft z x c v b n m , . / rsft lctl lmet lalt spc ralt rmet rctl ) ---- Similar to `if-equal` are three more conditional operators for templates: * `if-not-equal` ** the content is used if the first two string parameters are not equal * `if-in-list` ** the content is used if the first string parameter exists in the second list-of-strings parameter * `if-not-in-list` ** the content is used if the first string parameter does not exist in the second list-of-strings parameter .Example 5: ---- ;; defvar is parsed AFTER template expansion occurs. (defvar a hello) (deftemplate template1 (var1) a (if-equal hello $var1 b) c ) ;; Below will expand to: `a c` because the string ;; $a itself is compared against the string hello ;; and they are not equal. (template-expand template1 $a) (deftemplate template2 (var1) a (if-equal $a $var1 b) c ) ;; Below will expand to: `a b c` because the string ;; $a is compared against the string $a and they are equal. ;; But note that the variable $a is still not substituted ;; with its defvar value of: hello. (template-expand template2 $a) ---- [[concat-in-deftemplate]] === concat in deftemplate Like <>, a list beginning with `concat` within the content of `deftemplate` will be replaced with a single string that consists of all the subsequent items in the list concatenated to each other. == 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 () ) ---- [[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 or Windows-interception only: mouse-movement-key Accepts a single key name. When configured, whenever a mouse cursor movement is received, the configured key name will be "tapped" by Kanata, activating the key's action. This enables reporting of every relative mouse movement, which corresponds to standard mice, trackballs, trackpads and trackpoints. Absolute movements, which can be generated by touchscreens, drawing tablets and some mouse replacement or accessibility software, are ignored. Scrolling events and mouse buttons are also ignored. The intended use of these events is to provide a way to automatically enable a mouse keys layer while mousing, which can be disabled by a timeout or typing on other keys, rather than explicit toggling. see cfg_examples/automousekeys-*.kbd for more. The `mvmt` key name is specially intended for this purpose. It has no output key mapping and cannot be supplied as an action; however, any key may be used. Supports live reload on Linux, but with Windows-interception, this option must be present on startup to enable mouse movement event collection, so restart is required to enable it. Changing the key name is always supported, however. .Example: [source] ---- (defcfg process-unmapped-keys yes mouse-movement-key mvmt ) (defsrc k l ; mvmt ) (defvirtualkeys mouse (layer-while-held mouse-layer) ) (defalias mhld (hold-for-duration 750 mouse) ) (deflayer qwerty k l ; @mhld ) (deflayer mouse-layer mlft mmid mrgt @mhld ) ---- [[linux-only-linux-dev]] === Linux only: linux-dev By default, kanata will try to detect which input devices are keyboards and try to intercept them all. However, you may specify exact keyboard devices from the `/dev/input` directories using the `linux-dev` configuration. .Example: [source] ---- (defcfg linux-dev /dev/input/by-path/platform-i8042-serio-0-event-kbd ) ---- If you want to specify multiple keyboards, you can separate the paths with a colon `+:+`. .Example: [source] ---- (defcfg linux-dev /dev/input/dev1:/dev/input/dev2 ) ---- Due to using the colon to separate devices, if you have a device with colons in its file name, you must escape those colons with backslashes: [source] ---- (defcfg linux-dev /dev/input/path-to\:device ) ---- Alternatively, you can use list syntax, where both backslashes and colons are parsed literally. List items are separated by spaces or newlines. Using quotation marks for each item is optional, and only required if an item contains spaces. [source] ---- (defcfg linux-dev ( /dev/input/path:to:device "/dev/input/path to device" ) ) ---- For devices that do not have an easily identifiable device path like Bluetooth keyboards using the `linux-dev-names-include` option below is recommended. [[linux-only-linux-dev-names-include]] === Linux only: linux-dev-names-include In the case that `linux-dev` is omitted, this option defines a list of device names that should be included. Device names that do not exist in the list will be ignored. Device paths are not supported by this option; instead use the device names as output by Kanata during startup. Launch Kanata with a <> (any use of a `linux-dev*` option may hide devices) and look for lines beginning with "registering": ---- registering /dev/input/eventX: "Device name 1" registering /dev/input/eventY: "Device name 2" ---- The entire name within quotes must be used, partial matches and regex's are not supported. .Example: [source] ---- (defcfg linux-dev-names-include ( "Device name 1" "Device name 2" ) ) ---- [[linux-only-linux-dev-names-exclude]] === Linux only: linux-dev-names-exclude In the case that `linux-dev` is omitted, this option defines a list of device names that should be excluded. This option is parsed identically to `linux-dev-names-include`. The `linux-dev-names-include` and `linux-dev-names-exclude` options are not mutually exclusive but in practice it probably only makes sense to use one and not both. .Example: [source] ---- (defcfg linux-dev-names-exclude ( "Device Name 1" "Device Name 2" ) ) ---- [[linux-only-linux-continue-if-no-devs-found]] === Linux only: linux-continue-if-no-devs-found By default, kanata will crash if no input devices are found. You can change this behaviour by setting `linux-continue-if-no-devs-found`. .Example: [source] ---- (defcfg linux-continue-if-no-devs-found yes ) ---- [[linux-only-linux-device-detect-mode]] === Linux only: linux-device-detect-mode Kanata on Linux automatically detects and grabs input devices when none of the explicit device configurations are in use. In case kanata is undesirably grabbing mouse-like devices, you can use a configuration item to change detection behaviour. The configuration is `linux-device-detect-mode` and it has the options: [cols="1,4"] |=== | `keyboard-only` | Grab devices that seem to be a keyboard only. | `keyboard-mice` | Grab devices that seem to be a keyboard only and devices that declare **both** keyboard and mouse functionality. | `any` | Grab all keyboard-like and mouse-like devices. |=== The default behaviour is: [cols="1,4"] |=== | `keyboard-mice` | When no mouse events are in `defsrc`. | `any` | When any mouse buttons or mouse scroll events are in `defsrc`. |=== [[linux-only-linux-unicode-u-code]] === Linux only: linux-unicode-u-code Unicode on Linux works by pressing Ctrl+Shift+U, typing the unicode hex value, then pressing Enter. However, if you do remapping in userspace, e.g. via xmodmap/xkb, the keycode "U" that kanata outputs may not become a keysym "u" after the userspace remapping. This will be likely if you use non-US, non-European keyboards on top of kanata. For unicode to work, kanata needs to use the keycode that outputs the keysym "u", which might not be the keycode "U". You can use `evtest` or `kanata --debug`, set your userspace key remapping, then press the key that outputs the keysym "u" to see which underlying keycode is sent. Then you can use this configuration to change kanata's behaviour. .Example: [source] ---- (defcfg linux-unicode-u-code v ) ---- [[linux-only-linux-unicode-termination]] === Linux only: linux-unicode-termination Unicode on Linux terminates with the Enter key by default. This may not work in some applications. The termination is configurable with the following options: - `enter` - `space` - `enter-space` - `space-enter` .Example: [source] ---- (defcfg linux-unicode-termination space ) ---- === Linux only: linux-x11-repeat-delay-rate[[linux-only-x11-repeat-rate]] On Linux, you can tell kanata to run `xset r rate ` on startup and on live reload via the configuration item `linux-only-x11-repeat-rate`. This takes two numbers separated by a comma. The first number is the delay in ms and the second number is the repeat rate in repeats/second. This configuration item does not affect Wayland or no-desktop environments. .Example: [source] ---- (defcfg linux-x11-repeat-delay-rate 400,50 ) ---- [[linux-only-linux-use-trackpoint-property]] === Linux only: linux-use-trackpoint-property On linux, you can ask kanata to label itself as a trackpoint. This has several effects on libinput including enabling middle mouse button scrolling and using a different acceleration curve. Otherwise, a trackpoint intercepted by kanata may not behave as expected. If using this feature, it is recommended to filter out any non-trackpoint pointing devices using <>, <> or <> to avoid changing their behavior as well. .Example: [source] ---- (defcfg linux-use-trackpoint-property yes ) ---- [[linux-only-linux-output-device-name]] === Linux only: linux-output-device-name This option defines the name of the evdev output device. The default value is kanata. .Example: [source] ---- (defcfg linux-output-device-name "kanata output" ) ---- [[linux-only-linux-output-device-bus-type]] === Linux only: linux-output-device-bus-type Kanata on Linux needs to declare a "bus type" for its evdev output device. The options are `USB`, `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" ) ) ---- [[windows-only-windows-altgr]] === Windows only: windows-altgr There is an option for Windows to mitigate the strange behaviour of AltGr (ralt) if you're using `process-unmapped-keys yes` or have the key in your defsrc. This is applicable for many non-US layouts. You can use one of the listed values to change what kanata does with the key: * `cancel-lctl-press` ** This will remove the `lctl` press that is generated alonside `ralt` * `add-lctl-release` ** This adds an `lctl` release when `ralt` is released Without these workarounds, you should use `process-unmapped-keys (all-except lctl ralt))`, or use `process-unmapped-keys no` **and** also omit both `ralt` and `lctl` from `defsrc`. .Example: [source] ---- (defcfg windows-altgr add-lctl-release ) ---- For more context, see: https://github.com/jtroo/kanata/issues/55. NOTE: Even with these workarounds, putting `+lctl+` and `+ralt+` in your defsrc may not work properly with other applications that also use keyboard interception. Known application with issues: GWSL/VcXsrv === Windows only: windows-interception-mouse-hwid[[windows-only-windows-interception-mouse-hwid]] This defcfg item allows you to intercept mouse buttons for a specific mouse device. This only works with the Interception driver (the -wintercept variants of the release binaries). The original use case for this is for laptops such as a Thinkpad, which have mouse buttons that may be desirable to activate kanata actions with. To know what numbers to put into the string, you can run the variant with this defcfg item defined with any numbers. Then when a button is first pressed on the mouse device, kanata will print its hwid in the log; you can then copy-paste that into this configuration entry. If this defcfg item is not defined, the log will not print. Hwids in Kanata are byte array representations of a concatenation of the ASCII hardware ids, which can be seen in Device Manager on Windows. As such, they are an arbitrary length and can be very long. https://github.com/jtroo/kanata/issues/108[Relevant issue]. .Example: [source] ---- (defcfg windows-interception-mouse-hwid "70, 0, 60, 0" ) ---- === Windows only: windows-interception-mouse-hwids[[windows-only-windows-interception-mouse-hwids]] This item has a similar purpose as the singular version documented above, but is instead a list of strings that allows multiple mice to be intercepted. If both the singular and list items are used, the singular version will behave as if added to the list. .Example: [source] ---- (defcfg windows-interception-mouse-hwids ( "70, 0, 60, 0" "71, 0, 62, 0" ) ) ---- === Windows only: windows-interception-keyboard-hwids[[windows-only-windows-interception-keyboard-hwids]] This defcfg item allows you to intercept only specific keyboards. Its value must be a list of strings with each string representing one hardware ID. To know what numbers to put into the string, you can run the variant with this defcfg item empty. Then when a button is first pressed on the keyboard, kanata will print its hwid in the log. You can then copy-paste that into this configuration entry. If this defcfg item is not defined, the log will not print. Hwids in Kanata are byte array representations of a concatenation of the ASCII hardware ids, which can be seen in Device Manager on Windows. As such, they are an arbitrary length and can be very long. .Example: [source] ---- (defcfg windows-interception-keyboard-hwids ( "70, 0, 60, 0" "71, 72, 73, 74" ) ) ---- === Windows only: windows-interception-keyboard-hwids-exclude[[windows-only-windows-interception-keyboard-hwids-exclude]] This defcfg item allows you to exclude certain keyboards from being intercepted. You cannot define this alongside the inclusive keyboard configuration. It is parsed identically to the inclusive configuration. .Example: [source] ---- (defcfg windows-interception-keyboard-hwids-exclude ( "70, 0, 60, 0" "71, 72, 73, 74" ) ) ---- === Windows only: windows-interception-mouse-hwids-exclude[[windows-only-windows-interception-mouse-hwids-exclude]] This defcfg item allows you to exclude certain mice from being intercepted. You cannot define this alongside the inclusive mouse configuration. It is parsed identically to the inclusive configuration. .Example: [source] ---- (defcfg windows-interception-mouse-hwids-exclude ( "70, 0, 60, 0" "71, 0, 62, 0" ) ) ---- [[windows-only-tray-icon]] === Windows only: tray-icon Show a custom tray icon file for a <> gui-enabled build of kanata on Windows. Accepts either the full path (including the file name with an extension) to the icon file or just the file name, which is then searched in the following locations: * Default parent folders: ** config file's, executable's ** env vars: `XDG_CONFIG_HOME`, `APPDATA` (`C:\Users\\AppData\Roaming`), `USERPROFILE` `/.config` (`C:\Users\\.config`) * Default config subfolders: `kanata` `kanata-tray` * Default image subfolders (optional): `icon` `img` `icons` * Supported image file formats: `ico` `jpg` `jpeg` `png` `bmp` `dds` `tiff` If not specified, tries to load any icon file from the same locations with the name matching config name with extension replaced by one of the supported ones. See https://github.com/jtroo/kanata/blob/main/cfg_samples/tray-icon/tray-icon.kbd[example config] for more details. .Example: [source] ---- ;; in a config file C:\Users\\AppData\Roaming\kanata\kanata.kbd (defcfg tray-icon base.png ;; will load C:\Users\\AppData\Roaming\kanata\base.png ) ---- [[windows-only-icon-match-layer-name]] === Windows only: icon-match-layer-name When enabled, attempt to switch to a custom tray icon that matches the name of the active layer if the layer doesn't specify an explicit icon. If no icon file is found, the default icon will be used (see <>). File search rules are the same as in <>. Defaults to true. See https://github.com/jtroo/kanata/blob/main/cfg_samples/tray-icon/tray-icon.kbd[example config] for more details. [[windows-only-tooltip-layer-changes]] === Windows only: tooltip-layer-changes Show a custom layer icon near the mouse pointer position. Defaults to false. Requires <> gui-enabled build. [[windows-only-tooltip-show-blank]] === Windows only: tooltip-show-blank Show a blank square when instead of an icon if a layer isn't configured to have one. Defaults to false. Requires <> gui-enabled build. [[windows-only-tooltip-no-base]] === Windows only: tooltip-no-base Don't show a tooltip layer icon for the base layer (1st deflayer). Defaults to true. Requires <> gui-enabled build. [[windows-only-tooltip-duration]] === Windows only: tooltip-duration Set duration (in ms) for showing a custom layer icon near the mouse pointer position. 0 to never hide. Defaults to 500. Requires <> gui-enabled build. [[windows-only-tooltip-size]] === Windows only: tooltip-size Set the size (comma-separated Width,Height without spaces) for a custom layer icon near the mouse pointer position. Defaults to 24,24. Requires <> gui-enabled build. [[windows-only-notify-cfg-reload]] === Windows only: notify-cfg-reload Show system notification message on config reload. Defaults to true. Requires <> gui-enabled build. [[windows-only-notify-cfg-reload-silent]] === Windows only: notify-cfg-reload-silent Disable sound for the system notification message on config reload. Defaults to false. Requires <> gui-enabled build. [[windows-only-notify-error]] === Windows only: notify-error Show system notification message on kanata errors. Defaults to true. Requires <> gui-enabled build. [[using-multiple-defcfg-options]] === Using multiple defcfg options The `defcfg` entry is treated as a list with pairs of strings. For example: [source] ---- (defcfg a 1 b 2) ---- This will be treated as configuration `a` having value `1` and configuration `b` having value `2`. An example defcfg containing many of the options is shown below. It should be noted options that are Linux-only, Windows-only, or macOS-only will be ignored when used on a non-applicable operating system. [source] ---- ;; Don't actually use this exact configuration, ;; it's almost certainly not what you want. (defcfg process-unmapped-keys yes danger-enable-cmd yes sequence-timeout 2000 sequence-input-mode visible-backspaced sequence-backtrack-modcancel no log-layer-changes no delegate-to-first-layer yes movemouse-inherit-accel-state yes movemouse-smooth-diagonals yes dynamic-macro-max-presses 1000 linux-dev (/dev/input/dev1 /dev/input/dev2) linux-dev-names-include ("Name 1" "Name 2") linux-dev-names-exclude ("Name 3" "Name 4") linux-continue-if-no-devs-found yes linux-unicode-u-code v linux-unicode-termination space linux-x11-repeat-delay-rate 400,50 windows-altgr add-lctl-release windows-interception-mouse-hwid "70, 0, 60, 0" ) ---- == 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. [[args-nodelay]] === Remove startup delay: `-n`, `--nodelay` By default, Kanata adds a delay before reading keyboard inputs. This helps protect against stale states when started from a terminal. However, this may be undesirable when running as a background service. [[args-check]] === Only check configuration: `--check` Check the configuration file validity and then exit. [[args-log-layer-changes]] === Force log changes: `--log-layer-changes` 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. [[args-no-wait]] === Skip exit prompt: `--no-wait` By default, kanata displays "Press enter to exit" and waits for user input before exiting. This flag skips that prompt and exits immediately. 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. [[args-linux-dev-symlink]] === Linux only - Device symlink path: `-s`, `--symlink-path` If you want another program to consume the Kanata device output, you can use this flag to have a consistent device filesystem path. [[args-wait-device]] === Linux only - Wait before device grab attempt: `-w`, `--wait-device-ms` 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. [[args-macos-list-devices]] === macOS only - Only list keyboards: `-l`, `--list` List keyboard names that can be used within defcfg and then exit. == 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] ---- (defvirtualkeys ;; Define some virtual keys that perform modifier actions ctl lctl sft lsft met lmet alt lalt ;; A virtual key that toggles all modifier virtual keys above tal (multi (on-press toggle-virtualkey ctl) (on-press toggle-virtualkey sft) (on-press toggle-virtualkey met) (on-press toggle-virtualkey alt) ) ;; Virtual key that activates a macro vkmacro (macro h e l l o spc w o r l d) ) (defalias psf (on-press press-virtualkey sft) rsf (on-press release-virtualkey sft) tal (on-press tap-vkey tal) mac (on-press tap-vkey vkmacro) isf (on-idle 1000 tap-vkey sft) hfd (hold-for-duration 1000 met) ) (deflayer use-virtual-keys @psf @rsf @tal @mac a s d f @isf @hfd ) ---- .Older fake keys documentation [%collapsible] ==== The older configuration style of fake keys are still supported but the new style is preferred due to (hopefully) clearer naming. Fake keys can be defined inside of `deffakekeys`. The actions are: * `+(on-press-fakekey )+` or `on↓fakekey`: Activate a fake key action when pressing the key mapped to this action. * `+(on-release-fakekey )+` or `on↑fakekey`: Activate a fake key action when releasing the key mapped to this action. * `+(on-idle-fakekey )+`: Activate a fake key action when kanata has been idle for at least `idle time` milliseconds. The aforementioned `++` can be one of four values: * `+press+`: Press the fake key. It will not be released until another action triggers a release or tap. * `+release+`: Release the fake key. If it's not already pressed, this does nothing. * `+tap+`: Press and release the fake key. If it's already pressed, this only releases it. * `+toggle+`: Press the fake key if not already pressed, otherwise release it. .Example: [source] ---- (deffakekeys ctl lctl sft lsft met lmet alt lalt ;; Press all modifiers pal (multi (on-press fakekey ctl press) (on-press-fakekey sft press) (on-press-fakekey met press) (on-press-fakekey alt press) ) ;; Release all modifiers ral (multi (on-press-fakekey ctl release) (on-press-fakekey sft release) (on-press-fakekey met release) (on-press-fakekey alt release) ) ) (defalias psf (on-press-fakekey sft press) rsf (on-press-fakekey sft release) pal (on-press-fakekey pal tap) ral (on-press-fakekey ral tap) isf (on-idle-fakekey sft tap 1000) ) (deflayer use-virtual-keys @psf @rsf @pal @ral a s d f @isf ) ---- ==== For more context, you can read the https://github.com/jtroo/kanata/issues/80[issue that sparked the creation of virtual keys]. Something notable about virtual keys is that they don't always interrupt the state of an active `+tap-dance-eager+`. If a `macro` action is assigned to a virtual key, this won't interrupt a tap dance. However, most other action types, notably a "normal" key action like `+rsft+` will still interrupt a tap dance. [[sequences]] === Sequences The `+sldr+` action makes kanata go into "sequence" mode. The action name is short for "sequence leader". This comes from Vim which has the concept of a configurable sequence leader key. When in sequence mode, keys are not typed (<>) but are saved until one of the following happens: * A key is typed that does not match any sequence * `+sequence-timeout+` milliseconds elapses since the most recent key press Sequences are configured similarly to `+defvirtualkeys+`. The first parameter of a pair must be a defined virtual key name. The second parameter is a list of keys that will activate a virtual key tap when typed in the defined order. More precisely, the action triggered is: `+(on-press tap-vkey )+` .Example: [source] ---- (defseq git-status (g s t)) (defvirtualkeys git-status (macro g i t spc s t a t u s)) (defalias rcl (tap-hold-release 200 200 sldr rctl)) (defseq dotcom (. S-3) dotorg (. S-4) ;; The shifted letters in parentheses means a single press of lsft ;; must remain held while both h and then s are pressed. ;; This is not the same as S-h S-s, which means that the lsft key ;; must be released and repressed between the h and s presses. https (S-(h s)) ) (defvirtualkeys dotcom (macro . c o m) dotorg (macro . o r g) https (macro h t t p s S-; / /) ) ---- There are 10 special keys with names `nop0-nop9` which kanata treats specially. Kanata will never send OS events for these keys but they can still participate in sequences. See an example of using the nop keys alongside templates to define sequences below. .Example: [source] ---- (defsrc f7 f8 f9 f10) (deflayer base sldr nop0 nop1 nop2) (deftemplate seq (vk-name input-keys output-action) (defvirtualkeys $vk-name $output-action) (defseq $vk-name $input-keys) ) ;; template-expand has a shortened form: t! (t! seq dotcom (nop0 nop1) (macro . c o m)) (t! seq dotorg (nop0 nop2) (macro . o r g)) ---- If 10 special nop keys do not seem sufficient, you can get creative with your sequences and treat some as a prefix modifier. For example, you can get 24 "keys" by treating `nop0-nop5` as normal while treating `nop6-nop9` as prefixes that are always followed by a second nop key. .Example: [source] ---- (defalias nop0 nop0 ;; ... nop5 nop5 nop6 (macro nop6 nop0) ;; ... nop11 (macro nop6 nop5) ;; ... nop18 (macro nop9 nop0) ;; ... nop23 (macro nop9 nop5) ) ---- ==== Overlapping keys in any order Within the key list of `defseq` configuration items, the special `O-` list prefix can be used to denote a set of keys that must all be pressed before any are released in order to match the sequence. For an example, `O-(a b c)` is equivalent to `O-(c b a)`. .Example: [source] ---- (defvirtualkey hello (macro h (unshift e l) 5 (unshift l o))) (defseq hello (O-(h l o))) ---- WARNING: The way that sequences implements this functionality behind the scenes is by generating a sequence for every permutation of the overlapping keys. This can make kanata use up a lot of memory. Due to this, the maximum keys allowed in a given `O-(...)` list is 6, but you are still permitted to add more to the sequence, including more `O-(...)` lists. Doing the above can balloon kanata's memory consumption. .Sample of more advanced usage [%collapsible] ==== The configuration below showcases context-dependent chording with auto-space and auto-deleted spaces from typing punctuation. For example, chording `(d a y)` and then `(t u e)` will output `Tuesday`, while chording `(t u e)` by itself does nothing. .Example configuration: [source] ---- (defsrc f1) (deflayer base lrld) (defcfg process-unmapped-keys yes sequence-input-mode visible-backspaced concurrent-tap-hold true) (deftemplate seq (vk-name in out) (defvirtualkeys $vk-name $out) (defseq $vk-name $in)) (defvirtualkeys rls-sft (multi (release-key lsft)(release-key rsft))) (defvar rls-sft (on-press tap-vkey rls-sft)) (deftemplate rls-sft () $rls-sft 5) (defchordsv2 (d a y) (macro sldr d (t! rls-sft) a y spc nop0) 200 first-release () (h l o) (macro h (t! rls-sft) e l l o sldr spc nop0) 200 first-release () ) (t! seq Monday (d a y spc nop0 O-(m o n)) (macro S-m $rls-sft o n d a y nop9 sldr spc nop0)) (t! seq Tuesday (d a y spc nop0 O-(t u e)) (macro S-t $rls-sft u e s d a y nop9 sldr spc nop0)) (t! seq DelSpace_. (spc nop0 .) (macro .)) (t! seq DelSpace_; (spc nop0 ;) (macro ;)) ---- .Try using the above configuration to type the text: [source] ---- day; Day; Tuesday. day hello hello day Hello day. hello Tuesday Hello Monday; ---- ==== ==== Override the global timeout and input mode An alternative to using `sldr` is the `sequence` action. The syntax is `(sequence )`. This enters sequence mode with a sequence timeout different from the globally configured one. The `sequence` action can also be called with a second parameter. The second parameter is an override for `sequence-input-mode`: ---- (sequence ) ---- .Example: [source] ---- ;; Enter sequence mode and input . with a timeout of 250 (defalias dot-sequence (macro (sequence 250) 10 .)) ;; Enter sequence mode and input . with a timeout of 250 and using hidden-delay-type (defalias dot-sequence (macro (sequence 250 hidden-delay-type) 10 .)) ---- ==== 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 in sequences] to read about how chords in sequences behave. [[input-chords]] === Input chords Not to be confused with <>, `+chord+` actions allow you to perform various actions based on which specific combination of input keys are pressed together. Such an unordered combination of keys is called a "chord". Each chord can perform a different action, allowing you to bind up to `+2^n - 1+` different actions to just `+n+` keys. Input chords are configured similarly to `+defalias+` with two extra parameters at the beginning of each `+defchords+` group: the name of the group and a timeout value after which a chord triggers if it isn't triggered by a key release or press of a non-chord key before the timeout expires. [source] ---- (defsrc a b c) (deflayer default @cha @chb @chc ) (defalias cha (chord example a) chb (chord example b) chc (chord example c) ) (defchords example 500 (a ) a ( b ) b (a c) C-v (a b c) @three ) ---- The first item of each pair specifies the keys that make up a given chord. The second item of each pair is the action to be executed when the given chord is pressed and may be any regular or advanced action, including aliases. It currently cannot however contain another `+chord+` action. Note that unlike with `+defseq+`, these keys do not directly correspond to real keys and are merely arbitrary labels that make sense within the context of the chord. They are mapped to real keys in layers by configuring the key in the layer to map to a `+(chord name key)+` action where `+name+` is the name of the chords group (above `+example+`) and `+key+` is one of these arbitrary labels. It is perfectly valid to nest these `+chord+` actions that enter "chording mode" within other actions like `+tap-dance+` and that will work as one would expect. However, this only applies to the first key used to enter "chording mode". Once "chording mode" is active, all other keys will be directly handled by "chording mode" with no regard for wrapper actions; e.g. if a key is pressed and it maps to a tap-hold with a chord as the hold action within, that chord key will immediately activate instead of the key needing to be held for the timeout period. **Release behaviour** For single key actions and output chords — like `lctl` or `S-tab` — and for `layer-while-held`, an input chord will release the action only when all keys that are part of the input chord have been released. In other words, if even one key is held for the input chord then the output action will be continued to be held, but only for the mentioned action categories. The behaviour also applies to the actions mentioned above when used inside of `multi` but not within any other action. An exception to the behaviour described above for the action categories that would normally apply is if a chord decomposition occurs. A chord decomposition occurs when you input a chord that does not correspond to any action. When this happens, kanata splits up the key presses to activate other actions from the components of the input chord. In this scenario, the behaviour described in the next paragraph will occur. For chord decompositions and all other action categories, the release behaviour is more confusing: the output action will end when any key is released during the timeout, or if the timeout expires, the output action ends when the *first* key that was pressed in the chord gets released. This inconsistency is a limitation of the current implementation. In these scenarios it is recommended to hold down all keys if you want to keep holding and to release all keys if you want to do a release. This is because it will probably be difficult to know which key was pressed first. If you want to bypass the behaviour of keys being held for chord outputs, you could change the chord output actions to be <> instead. Using a macro will guarantee a rapid press+release for the output keys. [[defaliasenvcond]] === defaliasenvcond NOTE: this configuration item is older and instead you may want to use the newer and more generalized <> configuration. There is a variant of `defalias`: `defaliasenvcond`. This variant is parsed similarly, but there must be an extra list parameter that comes before all of the name-action pairs. The list must contain two strings. In order, these strings are: an environment variable name, and the environment variable value. When the environment variable defined by the name has the corresponding value when starting kanata, the aliases within will be active. Otherwise, the aliases will be skipped. A use case for `defaliasenvcond` is when one has multiple devices which vary in layout of keys, e.g. different special keys on the bottom row. Using environment variables, one can use the same kanata configuration across those multiple devices while changing key behaviours to keep consistent behaviour of specific key positions across the multiple devices, when the hardware keys at those physical key positions are not the same. .Example: [source] ---- (defaliasenvcond (LAPTOP lp1) met @lp1met ) (defaliasenvcond (LAPTOP lp2) met @lp2met ) ---- .Set environment variables in the current terminal process: [source] ---- # powershell $env:VAR_NAME = "var_value" # bash VAR_NAME=var_value ---- [[custom-tap-hold-behaviour]] === Custom tap-hold behaviour This is not currently configurable without modifying the source code, but if you're willing and/or capable, there is a tap-hold behaviour that is currently not exposed. Using this behaviour, one can be very particular about when and how tap vs. hold will activate by using extra information. The available information that can be used is exactly which keys have been pressed or released as well as the timing in milliseconds of those key presses. The action `+tap-hold-release-keys+` makes use of some of this capability, but doesn't make full use of the power of this functionality. For more context, you can read the https://github.com/jtroo/kanata/issues/128[motivation for custom tap-hold behaviour]. [[fancy-key-symbols]] === Fancy key symbols Instead of using the same `+a-z+` letters for special keys, e.g., `+lsft+` for `+LeftShift+` you can use much shorter, yet more visible, key symbols like `+‹⇧+`. For more details see https://github.com/jtroo/kanata/blob/main/docs/fancy_symbols.md[symbol list] and https://github.com/jtroo/kanata/blob/main/cfg_samples/fancy_symbols.kbd[example config], which not only uses these symbols in layer definitions, but also repurposes `+⎇›+` and `+⇧›+` `+⎇›+` keys into "symbol" keys that allow you to insert these fancy symbols by pressing the key, e.g., * hold `+⎇›+` and tap `+Delete+` would insert `+␡+` [[windows-only-work-elevated]] === Windows only: enable in elevated windows The default `kanata.exe` binary doesn't work in elevated windows (run with administrative privileges), e.g., `Control Panel`. However, you can use AutoHotkey's "EnableUIAccess" script to self-sign the binary, move it to "Program Files", then launching kanata from there will also work in these elevated windows. See https://github.com/jtroo/kanata/blob/main/EnableUIAccess[EnableUIAccess] folder with the script and its required libraries (needs https://www.autohotkey.com/download/[AutoHotkey v2] installed) If compiling yourself, you should add the feature flag `win_manifest` to enable the use of the `EnableUIAccess` script: ``` cargo build --win_manifest ``` [[windows-only-win-tray]] === Windows only: win-tray Kanata can be compiled as a Windows GUI tray app with the feature flag `gui`. This can simplify launching the app on user login by placing a `.lnk` at `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`, show custom icon indicator per config image:https://github.com/jtroo/kanata/blob/main/docs/win-tray/win-tray-screen.png[icon indicator per config,477,129] as well as dynamic icon indicator per layer (might need to click on the gif below to play) image:https://github.com/jtroo/kanata/blob/main/docs/win-tray/win-tray-layer-change.gif[icon indicator per layer,33,35,opts=autoplay] (see <>). It also supports (re)loading configs. Currently the only configuration supported is tray icon per profile, all other configuration should be done by passing cli flags in the `Target` field of `.lnk`, e.g., `"C:\Program Files\kanata\kanata.exe" -d -n` to launch kanata without a delay in a debug mode When launched from a command line, the app outputs log to the console, but otherwise the logs are currently only available via an app capable of viewing `OutputDebugString` debugs, e.g., https://github.com/smourier/TraceSpy[TraceSpy]. [[test-your-config]] === Test your config Kanata has a `+kanata_simulated_input+` tool to help test your configuration in a predictable manner. You can try it out on https://jtroo.github.io/[GitHub pages]. Code for the CLI tool can be found under link:https://github.com/jtroo/kanata/blob/main/simulated_input/[simulated_input]. Instead of physically typing to test something and wondering whether you didn't get the expected result because your config is wrong or because you mistyped something, you can write a sequence of key presses in a `+sim.txt+` file, run the tool with your config and get a "timeline" view of input/output events that can help understand how kanata translates your input into various key/mouse presses. WARNING: The format of this view may change. Emoji output may break vertical alignment. For more details download the files below and run `kanata_simulated_input -c sim.kbd -s sim.txt` + - https://github.com/jtroo/kanata/blob/main/docs/simulated_output/sim.kbd[example config] with simple home row mod bindings + - https://github.com/jtroo/kanata/blob/main/docs/simulated_output/sim.txt[example input sequence] + - https://github.com/jtroo/kanata/blob/main/docs/simulated_output/sim_out.txt[example output sequence] + Input sequence file format: whitespace insensitive list of `prefix:key` pairs where prefix is one of: + - `🕐`, `t`, or `tick` to add time between key events in `ms` + - `↓`, `d`, `down`, or `press` + - `↑`, `u`, `up`, or `release` + - `⟳`, `r`, or `repeat` + - `🎭`, `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. ================================================ FILE: docs/design.md ================================================ # Design doc ## Obligatory diagram ## main - read args - read config - start event loops ## event loop - read key events - send events to processing loop on channel ## processing loop - check for events on mpsc - if event: send event to layout - tick() the keyberon layout, send any events needed - if no event: sleep for 1ms - separate monotonic time checks, because can't rely on sleep to be fine-grained or accurate - send `ServerMessage`s to the TCP server ## TCP server - listen for `ClientMessage`s and act on them - recv `ServerMessage`s from processing loop and forward to all connected clients ## layout - uses keyberon - indices of `kanata_keyberon::layout::Event::{Press, Release}(x,y)`: x = 0 or 1 (0 is for physical key presses, 1 is for fake keys) y = OS code of key used as an index ## OS-specific code Most of the OS specific code is in `oskbd/` and `keys/`. There's a bit of it in `kanata/` since the event loops to receive OS events are different. ================================================ FILE: docs/fancy_symbols.md ================================================ ### 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 ================================================ FILE: docs/interception.md ================================================ # Windows Interception driver implementation notes - Interception handle is `!Send` and `!Sync` - means a single thread should own both input and output - `KbdOut` will need to send keyboard output events to that thread as opposed to Linux using `uinput` and the original Windows code using `SendInput` which are independent of the input devices. - Maybe save channel in kanata struct as part of new kanata - Interception can filter for only keyboard events - should use this filter feature; don't want to intercept mouse - Need to save previous device for sending to, in case wait/receive (with timeout) don't return anything so that sending stuff can be sent to some device. - Input `ScanCode` maps to the keyberon `KeyCode`; they both use the USB standard codes. - For ease of integration will probably need to unfortunately convert it to an `OsCode` even though the processing loop will soon after just convert it back to `KeyCode`. Oh well. ================================================ FILE: docs/kmonad_comparison.md ================================================ # Comparison with kmonad The kmonad project is the closest alternative for this project. ## Benefits of kmonad over kanata - ~MacOS support~ (this is implemented now) - Different features ## Why I built and use kanata - [Double-tapping a tap-hold key](https://github.com/kmonad/kmonad/issues/163) did not behave [how I want it to](https://docs.qmk.fm/#/tap_hold?id=tapping-force-hold) - Some key sequences with tap-hold keys [didn't behave how I want](https://github.com/kmonad/kmonad/issues/466): - `(press lsft) (press a) (release lsft) (release a)` (a is a tap-hold key) - The above outputs `a` in kmonad, but I want it to output `A` - kmonad was missing [mouse buttons](https://github.com/kmonad/kmonad/issues/150) The issues listed are all fixable in kmonad and I hope they are one day! For me though, I didn't and still don't know Haskell well enough to contribute to kmonad. That's why I instead built kanata based off of the excellent work that had already gone into the [keyberon](https://github.com/TeXitoi/keyberon), [ktrl](https://github.com/ItayGarin/ktrl), and [kbremap](https://github.com/timokroeger/kbremap) projects. If you want to see the features that kanata offers, the [configuration guide](./config.adoc) is a good starting point. I dogfood kanata myself and it works great for my use cases. Though kanata is a younger project than kmonad, it now has more features. If you give kanata a try, feel free to ask for help in an issue or discussion, or let me know how it went 🙂. ================================================ FILE: docs/locales.adoc ================================================ //// Commented out since it doesn't seem to add anything for now, but maybe in the future :sectlinks: :sectanchors: //// ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: :important-caption: :heavy_exclamation_mark: :caution-caption: :fire: :warning-caption: :warning: endif::[] = Keyboard locales //// Commented out since doc is short enough without a ToC for the time being. :toc: :toc-title: pass:[TABLE OF CONTENTS] :toclevels: 3 //// == ISO 100% Keyboard (event.code) NOTE: Tested on Linux only [%collapsible] ==== ---- (defsrc Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 PrintScreen ScrollLock Pause Backquote Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal Backspace Insert Home PageUp NumLock NumpadDivide NumpadMultiply NumpadSubtract Tab KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight Enter Delete End PageDown Numpad7 Numpad8 Numpad9 NumpadAdd CapsLock KeyA KeyS KeyD KeyF KeyG KeyH KeyJ KeyK KeyL Semicolon Quote Backslash Numpad4 Numpad5 Numpad6 ShiftLeft IntlBackslash KeyZ KeyX KeyC KeyV KeyB KeyN KeyM Comma Period Slash ShiftRight ArrowUp Numpad3 Numpad2 Numpad1 NumpadEnter ControlLeft MetaLeft AltLeft Space AltRight MetaRight ContextMenu ControlRight ArrowLeft ArrowDown ArrowRight Numpad0 NumpadDecimal ) ---- ==== == ISO German QWERTZ (Windows, non-interception)[[german]] === Using `deflocalkeys-win`:[[german-defwin]] [%collapsible] ==== ---- (defcustomkeys ü 186 + 187 # 191 ö 192 ß 219 ^ 220 ´ 221 ä 222 < 226 ) (defsrc ^ 1 2 3 4 5 6 7 8 9 0 ß ´ bspc tab q w e r t z u i o p ü + caps a s d f g h j k l ö ä # ret lsft < y x c v b n m , . - rsft lctl lmet lalt spc ralt rmet rctl ) ---- ==== === Without using `deflocalkeys`:[[german-nodeflocalkeys]] [%collapsible] ==== ---- (defsrc \ 1 2 3 4 5 6 7 8 9 0 [ ] bspc tab q w e r t z u i o p ; = caps a s d f g h j k l grv ' / ret lsft 102d y x c v b n m , . - rsft lctl lmet lalt spc ralt rmet rctl ) ---- ==== === Example aliases[[german-aliases]] [%collapsible] ==== ---- (defalias ;; shifted german keys ! S-1 ˝ S-2 ;; unicode 02DD ˝ look-a-like is used because @" is no valid alias, to be displayed correctly ;; in console requires a font that can - e.g. cascadia § S-3 $ S-4 % S-5 & S-6 / S-7 ﴾ S-8 ;; unicode FD3E ﴾ look-a-like is used because @( is no valid alias, to be displayed correctly... ﴿ S-9 ;; unicode FD3F ﴿ look-a-like is used because @) is no valid alias, to be displayed correctly ... = S-0 ? S-ß * S-+ ' S-# ; S-, : S-. _ S-- > S-< < < ;; not really needed but having @< and @> looks consistent ;; change dead keys in normal keys ´ (macro ´ spc ) ;; ´ ` (macro S-´ spc ) ;; ` ^ (macro ^ spc ) ;; ^ = \ - shifting @^ will produce an incorrect space now ° S-^ ;; AltGr german keys ~ A-C-+ \ A-C-ß ẞ A-C-S-ß | A-C-< } A-C-0 { A-C-7 ] A-C-9 [ A-C-8 € A-C-e @ A-C-q ² A-C-2 ³ A-C-3 µ A-C-m ) ---- ==== == ISO German QWERTZ (MacOS)[[german]] === Using `deflocalkeys-macos`:[[german-defmac]] [%collapsible] ==== ---- (deflocalkeys-macos ß 12 ´ 13 z 21 ü 26 + 27 ö 39 ä 40 < 41 # 43 y 44 - 53 ^ 86 ) (defsrc ⎋ f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 ^ 1 2 3 4 5 6 7 8 9 0 ß ´ ⌫ ↹ q w e r t z u i o p ü + ⇪ a s d f g h j k l ö ä # ↩ ‹⇧ < y x c v b n m , . - ▲ ⇧› fn ‹⌃ ‹⌥ ‹⌘ ␣ ⌘› ⌥› ◀ ▼ ▶ ) ---- ==== == ISO French Azerty (MacOS)[[french]] === Using `deflocalkeys-macos`:[[french-defmac]] [%collapsible] ==== ---- (deflocalkeys-macos @ 50 par 12 ;; Close parentheses - 13 ^ 73 $ 164 ù 85 ` 192 < 41 / 191 = 53 a 16 q 30 z 17 w 44 m 39 ) (defsrc ⎋ f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 @ 1 2 3 4 5 6 7 8 9 0 par - ⌫ ↹ a z e r t y u i o p ^ $ ⇪ q s d f g h j k l m ù ` ↩ ‹⇧ < w x c v b n , . / = ▲ ⇧› fn ‹⌃ ‹⌥ ‹⌘ ␣ ⌘› ⌥› ◀ ▼ ▶ ) ---- ==== == ISO French AZERTY (Windows, non-interception)[[french]] NOTE: This is for the https://kbdlayout.info/kbdfr?arrangement=ISO105[French AZERTY layout] (ISO105 arrangement). Tested on Windows only. [%collapsible] ==== ---- (deflocalkeys-win k252 223 ;; ref to the key [!] (VK_OEM_8) ) (defsrc ;; french ' 1 2 3 4 5 6 7 8 9 0 [ eql bspc tab a z e r t y u i o p ] ; caps q s d f g h j k l m ` bksl ret lsft nubs w x c v b n comm . / k252 rsft lctl lmet lalt spc ralt rctl ) ---- ==== == ISO Turkish QWERTY (Linux)[[turkish]] NOTE: This is for the https://kbdlayout.info/kbdtuq?arrangement=ISO105[Turkish QWERTY layout] (ISO105 arrangement). Tested on Linux only. [%collapsible] ==== ---- (deflocalkeys-linux * 12 - 13 ı 23 ğ 26 ü 27 ş 39 İ 40 , 43 < 86 ö 51 ç 52 . 53 ) (defsrc ;; turkish-iso105 grv 1 2 3 4 5 6 7 8 9 0 * - bspc tab q w e r t y u ı o p ğ ü caps a s d f g h j k l ş İ , ret lsft < z x c v b n m ö ç . rsft lctl lmet lalt spc ralt rmet rctl ) ;; We use İ instead of i because kanata doesn't allow using i in deflocalkeys, as it is a default key name. ---- ==== == ABNT2 Brazillian Portuguese QWERTY (Linux)[[portuguese]] NOTE: This is for the https://kbdlayout.info/kbdbr[ABNT2 QWERTY layout]. Tested on Linux only. [%collapsible] ==== ---- (deflocalkeys-linux ´ 26 [ 27 ç 39 ~ 40 ' 41 ] 43 ; 53 \ 86 / 89 ) (defsrc ;; brazillian-abnt2 esc f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 ' 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w e r t y u i o p ´ [ ret caps a s d f g h j k l ç ~ ] lsft \ z x c v b n m , . ; rsft lctl lmet lalt spc ralt / ) ---- ==== == ISO Swedish QWERTY (Linux)[[swedish]] [%collapsible] ==== ---- ;; Swedish ISO105 (deflocalkeys-linux § 41 + 12 ´ 13 ;; Acute accent. Opposite to the grave accent (grv). å 26 ¨ 27 ö 39 ä 40 ' 43 < 86 , 51 . 52 - 53 ) (defsrc ;; Swedish ISO105 § 1 2 3 4 5 6 7 8 9 0 + ´ bspc tab q w e r t y u i o p å ¨ caps a s d f g h j k l ö ä ' ret lsft < z x c v b n m , . - rsft lctl lmet lalt spc ralt rmet menu rctl ) ;; Empty layer that matches the Swedish layout (deflayer default _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ) ---- ==== == Swedish QWERTY Localkeys (Windows)[[swedish]] [%collapsible] ==== ---- (deflocalkeys-win § 220 + 187 ´ 219 å 221 ¨ 186 ö 192 ä 222 ' 191 < 226 , 188 . 190 - 189 ) ---- ==== ================================================ FILE: docs/platform-known-issues.adoc ================================================ = Hardware known issues At the electric circuit layer of many keyboards, cost-saving measures can lead to key presses not registering when pressing multiple keys simultaneously. Usually this happens with at least 3 key presses. Kanata cannot fix this issue. You can work around it by avoiding the problem key combination, or using a different keyboard. = Platform-dependent known issues == Preface This document contains a list of known issues which are unique to a given platform. The platform supported by the core maintainer (jtroo) are Windows 11 and Linux. Windows 10 is expected to work fine, but as Windows 10 end-of-support is approaching in 2025, jtroo no longer has any devices with it installed. On Windows, there are two backing mechanisms that can be used for keyboard input/output and they have different issues. These will be differentiated by the words "LLHOOK" and "Interception", which map to the binaries `kanata.exe` and `kanata_wintercept.exe` respectively. == Windows 11 LLHOOK * Some input key combinations (e.g. Win+L) cannot be intercepted before running their default action ** https://github.com/jtroo/kanata/issues/192 ** https://github.com/jtroo/kanata/discussions/428 * OS-level key remapping behaves differently vs. Linux or Interception ** Does not affect winiov2 variant ** https://github.com/jtroo/kanata/issues/152 * Certain applications that also use the LLHOOK mechanism may not behave correctly ** https://github.com/jtroo/kanata/issues/55 ** https://github.com/jtroo/kanata/issues/250 ** https://github.com/jtroo/kanata/issues/430 ** https://github.com/espanso/espanso/issues/1488 * AltGr / ralt / Right Alt can misbehave ** https://github.com/jtroo/kanata/blob/main/docs/config.adoc#windows-only-windows-altgr * NumLock state can mess with arrow keys in unexpected ways ** Does not affect winiov2 variant ** https://github.com/jtroo/kanata/issues/78 ** https://github.com/jtroo/kanata/issues/667 ** Workaround: use the correct https://github.com/jtroo/kanata/discussions/354[numlock state] * Without `process-unmapped-keys yes`, using arrow keys without also having the shift keys in `defsrc` will break shift highlighting ** Does not affect winiov2 variant ** https://github.com/jtroo/kanata/issues/858 ** Workaround: add shift keys to `defsrc` or use `process-unmapped-keys yes` in `defcfg` == Windows 11 Interception * Sleeping your system or unplugging/replugging devices enough times causes inputs to stop working ** https://github.com/oblitum/Interception/issues/25 * Some less-frequently used keys are not supported or handled correctly ** https://github.com/jtroo/kanata/issues/127 ** https://github.com/jtroo/kanata/issues/164 ** https://github.com/jtroo/kanata/issues/425 ** https://github.com/jtroo/kanata/issues/532 == Linux * Key repeats can occur when they normally wouldn't in some cases ** https://github.com/jtroo/kanata/discussions/422 ** https://github.com/jtroo/kanata/issues/450 ** https://github.com/jtroo/kanata/issues/1441 * Unicode support has 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 * Only left, right, and middle mouse buttons are implemented for clicking * Mouse input processing is not implemented, e.g. putting `mlft` into `defsrc` does nothing ================================================ FILE: docs/release-template.md ================================================ ## Configuration guide Link to the appropriate configuration guide version: [guide link TODO: FIX LINK](https://github.com/jtroo/kanata/blob/FIXME/docs/config.adoc). ## Changelog (since )
Change log * TODO: fill this out
## Sample configuration file The attached `kanata.kbd` file is tested to work with the current version. The one in the `main` branch of the repository may have extra features that are not supported in this release. ## Windows
Instructions 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 ` ### Binary variants 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.
## Linux
Instructions 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:** ``` chmod +x kanata # may be downloaded without executable permissions 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
## macOS
Instructions The supported Karabiner driver version in this release is `v6.2.0`. **WARNING**: macOS does not support mouse as input. The `mbck` and `mfwd` mouse button actions are also not operational. ### Binary variants Explanation of items in the binary variant: - 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 ### Instructions for macOS 11 and newer You must use the Karabiner driver version `v6.2.0`. Please read through this issue comment: https://github.com/jtroo/kanata/issues/1264#issuecomment-2763085239 Also have a read through this discussion: https://github.com/jtroo/kanata/discussions/1537 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. ### Install Karabiner driver for macOS 10 and older: - Install the [Karabiner kernel extension](https://github.com/pqrs-org/Karabiner-VirtualHIDDevice). ### After installing the appropriate driver for your OS (both macOS <=10 and >=11) Download 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:** ``` 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
Sums ``` TODO: fill this out ```
================================================ FILE: docs/sequence-adding-chords-ideas.md ================================================ # Sequence improvement: sequence chords ## Preface This document is a record of designing/braindumping for the improvement to the sequences feature to add chord support. It is left in an informal and disorganized state — as opposed to a being presentable design doc — out of laziness. Apologies ahead of time if you read this and it's hard to follow, feel free to contribute a PR to create a new and more polished doc. ## Motivation The desire is to be able to add output chords to a sequence. The effect of this is that: `(S-(a b))` can be differentiated from `(S-a b)`. Today, chords are not supported at all. The two sequences above could be written as `(lsft a b)`; however, the code today has no way to decide the difference between `lsft` being applied to only `a` or to both `a` and `b`. The feature will codenamed "seqchords" for brevity in this document. ## An exploration of an idea: track releases? Today, the sequence `(lsft a b c)` doesn't care when the `lsft`, or even `a` or `b` are released relative to when the subsequent keys are pressed. However, with seqchords, the code could be potentially changed to make sequences release-aware. It seems a little difficult to integrate this into the trie structure used to track sequences though. With an implementation that is release-aware, it seems like the code would need to figure out how to conditionally add release events to the trie, depending if the seq was `(lsft a)` or `(S-a)` For now, I think a different approach would be better. ## A different idea: modify presses held with mod keys. The current sequence type is `Vec` since keys don't fit into `u8`. However, there are fewer than 1024 (2^10) keys total. That means there are 6 bits to play with. 6 bits are enough for the types of modifiers (of which there are 4), but differentiating both sides (increases to 8). Perhaps one only cares to use both left and right shifts though, and maybe both left and right alts. One could also use a `u32` instead, but that seems unnecessary for now. I see no backwards compatibility issues if one desired that change in the future. With this in mind, while modifiers are held, set the upper unused bits of the stored `u16` values. ### Backwards compatibility? This does mean that `(lsft a b)` behaves differently with vs. without seqchords. Unless maybe the code automatically generates the various permutations of this type of sequence, but that seems complicated. Or maybe have a `u16` with a special bit pattern that could be used to differentiate between `(S-(a b))` and `(lsft a b)`. For now, let's say that the bit pattern is `0xFFFF`. If a modifier is pressed and the sequence `[..., , 0xFFFF]` exists in the trie: continue processing the sequence in mod-aware mode. OR for simplicity, just say "screw backwards compatibility" and force users to be clear about what they mean and define the extra permutations, if they want them. I prefer this. ### Data format examples Let's begin the description of the new data format. Since shifted keys seem like they will be the main use case for seqchords, only that will be described in this document for now. Here are the numerical key values relevant to the examples. - `a: 0x001E` - `b: 0x0030`. - `lsft: 0x002A`. This differs by OS, but that's not important. The transformation of `(lsft a b)` to a sequence in the trie today looks like: - `[0x002A, 0x001E, 0x0030]` This will remain unchanged with seqchords. Let's say that chorded keys using `lsft` will have the otherwise-unused MSB (bit 15) set. The transformation of some sequences using chords will be: 1. `(S-(a b)) => [0x802A, 0x801E, 0x8030]` 2. ` (S-a b) => [0x802A, 0x801E, 0x0030]` 3. `(S-a S-b) => [0x802A, 0x801E, 0x802A, 0x8030]` Notably, `lsft` is modifying its own upper bits. This should simplify the implementation logic so that the code does not need to add a special-case check that the newly-pressed key is itself a modifier. One may need to define different sequences if one wishes to use both left and right shifts to be able to trigger these shifted sequences. The syntax does not exist today, but maybe `(S-(a b))` and `(RS-(a b))` as an example for left and right shifts. The reason different sequences would be required is because the sequence->trie check operates on the integers that correspond to the keycodes. Consideration: maybe there could be transformations for the right modifier keys to ensure they get translated to the left modifier keys. This seems like it could be a sensible default to start with. If a change is desired in the future to **not** do this transformation, it doesn't seem too difficult to add a configuration item to do so. For now that will be left out, deferring to the YAGNI principle. ### Backwards compatibility revisited Thinking back on the topic of backwards compatibility, I'm scrapping that idea of special bit patterns. I thought of a probably-better way: backtracking with modifier cancellation. By default when seqchords gets added, the modified bit patterns will be used to check in the trie for valid sequences. However, with a `defcfg` item `sequence-backtrack-modcancel` — which should be `yes` by default for back-compat reasons — if the code encounters an invalid sequence with the modded bit pattern, it will try again with the unmodded bit pattern, and only if that does not match will sequence-mode end with an invalid termination. This backtracking can be turned off if desired, e.g. if it behaves badly in some future seqchords use cases. ================================================ FILE: docs/setup-linux.md ================================================ # 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="--config=/home//.config/kanata/kanata.kbd" command_background=true pidfile="/run/${RC_SVCNAME}.pid" command_user="" ``` Then run: ``` sudo chmod +x /etc/init.d/kanata # script must be executable sudo rc-service kanata start rc-status # check that kanata isn't listed as [ crashed ] sudo rc-update add kanata default # start the service automatically at boot ``` # Credits The original text was taken and adapted from: https://github.com/kmonad/kmonad/blob/master/doc/faq.md#linux ================================================ FILE: docs/simulated_output/sim.kbd ================================================ (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) ‗ 🔣🤲🏿 ‗ @⌂‹◆ @⌂‹⎇ @⌂‹⎈ @⌂‹⇧ @⌂⇧› @⌂⎈› @⌂⎇› @⌂◆›) ================================================ FILE: docs/simulated_output/sim.txt ================================================ ↓j 🕐1600 ↓l 🕐5000 ↓1 🕐50 ↑1 🕐50 ↓1 🕐50 ↑1 🕐50 ↑j 🕐50 ↑l 🕐50 ================================================ FILE: docs/simulated_output/sim_out.txt ================================================ 🕐Δ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 │ ↓⇧› ↓⎇› 🤲 🤲 ↑⇧› ↑⎇› ================================================ FILE: docs/simulated_passthru_ahk/[COPY HERE] kanata_passthru.dll _ ================================================ ================================================ FILE: docs/simulated_passthru_ahk/kanata_dll.kbd ================================================ ;;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⌂⇧›) ================================================ FILE: docs/simulated_passthru_ahk/kanata_passthru.ahk ================================================ #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 } } ================================================ FILE: docs/switch-design ================================================ # 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 ---- ================================================ FILE: example_tcp_client/.gitignore ================================================ target ================================================ FILE: example_tcp_client/Cargo.toml ================================================ [package] name = "kanata_example_tcp_client" description = "Example kanata TCP client" version = "1.1.0" edition = "2021" license = "LGPL-3.0" authors = ["jtroo "] [dependencies] anyhow = "1" clap = { version = "4", features = [ "derive" ] } kanata-tcp-protocol = { path = "../tcp_protocol" } log = "0.4.8" simplelog = "0.12" serde_json = "1" ================================================ FILE: example_tcp_client/src/main.rs ================================================ use clap::Parser; use kanata_tcp_protocol::*; use simplelog::*; use std::io::{BufRead, BufReader, Write, stdin}; use std::net::{SocketAddr, TcpStream}; use std::process::exit; use std::time::Duration; #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] struct Args { /// Port that kanata's TCP server is listening on #[clap(short, long)] port: Option, /// Enable debug logging #[clap(short, long)] debug: bool, /// Enable trace logging (implies --debug as well) #[clap(short, long)] trace: bool, } 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], port)), Duration::from_secs(5), ) .expect("connect to kanata"); log::info!("successfully connected"); 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, (true, false) => LevelFilter::Debug, (false, false) => LevelFilter::Info, }; let mut log_cfg = ConfigBuilder::new(); if let Err(e) = log_cfg.set_time_offset_to_local() { eprintln!("WARNING: could not set log TZ to local: {e:?}"); }; CombinedLogger::init(vec![TermLogger::new( log_lvl, log_cfg.build(), TerminalMode::Mixed, ColorChoice::AlwaysAnsi, )]) .expect("init logger"); log::info!( "kanata_example_tcp_client v{} starting", env!("CARGO_PKG_VERSION") ); } fn write_to_kanata(mut s: TcpStream) { log::info!("writer starting"); 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 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(s: TcpStream) { log::info!("reader starting"); let mut reader = BufReader::new(s); let mut msg = String::new(); loop { 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:?}"); } } } } ================================================ FILE: interception/Cargo.toml ================================================ [workspace] members = ["."] [package] name = "kanata-interception" description = "Safe wrapper for Interception. Forked for use with kanata." version = "0.3.0" authors = ["Joe Kaushal "] edition = "2018" repository = "https://github.com/jtroo/kanata" license = "MIT OR Apache-2.0" [dependencies] interception-sys = "0.1.3" bitflags = "1.2.1" num_enum = "0.6.0" serde = { version = "1.0.114", features = ["derive"] } ================================================ FILE: interception/src/lib.rs ================================================ pub extern crate interception_sys; #[macro_use] extern crate bitflags; pub use interception_sys as raw; pub mod scancode; pub use scancode::ScanCode; use std::convert::{TryFrom, TryInto}; use std::default::Default; use std::time::Duration; use std::vec::Vec; pub type Device = i32; pub type Precedence = i32; pub enum Filter { MouseFilter(MouseFilter), KeyFilter(KeyFilter), } pub type Predicate = extern "C" fn(device: Device) -> bool; bitflags! { pub struct MouseState: u16 { const LEFT_BUTTON_DOWN = 1; const LEFT_BUTTON_UP = 2; const RIGHT_BUTTON_DOWN = 4; const RIGHT_BUTTON_UP = 8; const MIDDLE_BUTTON_DOWN = 16; const MIDDLE_BUTTON_UP = 32; const BUTTON_4_DOWN = 64; const BUTTON_4_UP = 128; const BUTTON_5_DOWN = 256; const BUTTON_5_UP = 512; const WHEEL = 1024; const HWHEEL = 2048; // MouseFilter only const MOVE = 4096; } } pub type MouseFilter = MouseState; bitflags! { pub struct MouseFlags: u16 { const MOVE_RELATIVE = 0; const MOVE_ABSOLUTE = 1; const VIRTUAL_DESKTOP = 2; const ATTRIBUTES_CHANGED = 4; const MOVE_NO_COALESCE = 8; const TERMSRV_SRC_SHADOW = 256; } } bitflags! { pub struct KeyState: u16 { const DOWN = 0; const UP = 1; const E0 = 2; const E1 = 3; const TERMSRV_SET_LED = 8; const TERMSRV_SHADOW = 16; const TERMSRV_VKPACKET = 32; } } bitflags! { pub struct KeyFilter: u16 { const DOWN = 1; const UP = 2; const E0 = 4; const E1 = 8; const TERMSRV_SET_LED = 16; const TERMSRV_SHADOW = 32; const TERMSRV_VKPACKET = 64; } } #[derive(Debug, Copy, Clone)] pub enum Stroke { Mouse { state: MouseState, flags: MouseFlags, rolling: i16, x: i32, y: i32, information: u32, }, Keyboard { code: ScanCode, state: KeyState, information: u32, }, } impl TryFrom for Stroke { type Error = &'static str; fn try_from(raw_stroke: raw::InterceptionMouseStroke) -> Result { let state = match MouseState::from_bits(raw_stroke.state) { Some(state) => state, None => return Err("Extra bits in raw mouse state"), }; let flags = match MouseFlags::from_bits(raw_stroke.flags) { Some(flags) => flags, None => return Err("Extra bits in raw mouse flags"), }; Ok(Stroke::Mouse { state: state, flags: flags, rolling: raw_stroke.rolling, x: raw_stroke.x, y: raw_stroke.y, information: raw_stroke.information, }) } } impl TryFrom for Stroke { type Error = &'static str; fn try_from(raw_stroke: raw::InterceptionKeyStroke) -> Result { let state = match KeyState::from_bits(raw_stroke.state) { Some(state) => state, None => return Err("Extra bits in raw keyboard state"), }; let code = match ScanCode::try_from(raw_stroke.code) { Ok(code) => code, Err(_) => ScanCode::Esc, }; Ok(Stroke::Keyboard { code: code, state: state, information: raw_stroke.information, }) } } impl TryFrom for raw::InterceptionMouseStroke { type Error = &'static str; fn try_from(stroke: Stroke) -> Result { if let Stroke::Mouse { state, flags, rolling, x, y, information, } = stroke { Ok(raw::InterceptionMouseStroke { state: state.bits(), flags: flags.bits(), rolling: rolling, x: x, y: y, information: information, }) } else { Err("Stroke must be a mouse stroke") } } } impl TryFrom for raw::InterceptionKeyStroke { type Error = &'static str; fn try_from(stroke: Stroke) -> Result { if let Stroke::Keyboard { code, state, information, } = stroke { Ok(raw::InterceptionKeyStroke { code: code as u16, state: state.bits(), information: information, }) } else { Err("Stroke must be a keyboard stroke") } } } pub struct Interception { ctx: raw::InterceptionContext, } impl Interception { pub fn new() -> Option { let ctx = unsafe { raw::interception_create_context() }; if ctx == std::ptr::null_mut() { return None; } Some(Interception { ctx: ctx }) } pub fn get_precedence(&self, device: Device) -> Precedence { unsafe { raw::interception_get_precedence(self.ctx, device) } } pub fn set_precedence(&self, device: Device, precedence: Precedence) { unsafe { raw::interception_set_precedence(self.ctx, device, precedence) } } pub fn get_filter(&self, device: Device) -> Filter { if is_invalid(device) { return Filter::KeyFilter(KeyFilter::empty()); } let raw_filter = unsafe { raw::interception_get_filter(self.ctx, device) }; if is_mouse(device) { let filter = match MouseFilter::from_bits(raw_filter) { Some(filter) => filter, None => MouseFilter::empty(), }; Filter::MouseFilter(filter) } else { let filter = match KeyFilter::from_bits(raw_filter) { Some(filter) => filter, None => KeyFilter::empty(), }; Filter::KeyFilter(filter) } } pub fn set_filter(&self, predicate: Predicate, filter: Filter) { let filter = match filter { Filter::MouseFilter(filter) => filter.bits(), Filter::KeyFilter(filter) => filter.bits(), }; unsafe { let predicate = std::mem::transmute(Some(predicate)); raw::interception_set_filter(self.ctx, predicate, filter) } } pub fn wait(&self) -> Device { unsafe { raw::interception_wait(self.ctx) } } pub fn wait_with_timeout(&self, duration: Duration) -> Device { let millis = match u32::try_from(duration.as_millis()) { Ok(m) => m, Err(_) => u32::MAX, }; unsafe { raw::interception_wait_with_timeout(self.ctx, millis) } } pub fn send(&self, device: Device, strokes: &[Stroke]) -> i32 { if is_mouse(device) { self.send_internal::(device, strokes) } else if is_keyboard(device) { self.send_internal::(device, strokes) } else { 0 } } fn send_internal>(&self, device: Device, strokes: &[Stroke]) -> i32 { let mut raw_strokes = Vec::new(); for stroke in strokes { if let Ok(raw_stroke) = T::try_from(*stroke) { raw_strokes.push(raw_stroke) } } let ptr = raw_strokes.as_ptr(); let len = match u32::try_from(raw_strokes.len()) { Ok(l) => l, Err(_) => u32::MAX, }; unsafe { raw::interception_send(self.ctx, device, std::mem::transmute(ptr), len) } } pub fn receive(&self, device: Device, strokes: &mut [Stroke]) -> i32 { if is_mouse(device) { self.receive_internal::(device, strokes) } else if is_keyboard(device) { self.receive_internal::(device, strokes) } else { 0 } } fn receive_internal + Default + Copy>( &self, device: Device, strokes: &mut [Stroke], ) -> i32 { let mut raw_strokes: Vec = Vec::with_capacity(strokes.len()); raw_strokes.resize_with(strokes.len(), Default::default); let ptr = raw_strokes.as_ptr(); let len = match u32::try_from(raw_strokes.len()) { Ok(l) => l, Err(_) => u32::MAX, }; let num_read = unsafe { raw::interception_receive(self.ctx, device, std::mem::transmute(ptr), len) }; let mut num_valid: i32 = 0; for i in 0..num_read { if let Ok(stroke) = raw_strokes[i as usize].try_into() { strokes[num_valid as usize] = stroke; num_valid += 1; } } num_valid } pub fn get_hardware_id(&self, device: Device, buffer: &mut [u8]) -> u32 { let ptr = buffer.as_mut_ptr(); let len = match u32::try_from(buffer.len()) { Ok(l) => l, Err(_) => u32::MAX, }; unsafe { raw::interception_get_hardware_id(self.ctx, device, std::mem::transmute(ptr), len) } } } impl Drop for Interception { fn drop(&mut self) { unsafe { raw::interception_destroy_context(self.ctx) } } } pub extern "C" fn is_invalid(device: Device) -> bool { unsafe { raw::interception_is_invalid(device) != 0 } } pub extern "C" fn is_keyboard(device: Device) -> bool { unsafe { raw::interception_is_keyboard(device) != 0 } } pub extern "C" fn is_mouse(device: Device) -> bool { unsafe { raw::interception_is_mouse(device) != 0 } } ================================================ FILE: interception/src/scancode.rs ================================================ use num_enum::TryFromPrimitive; use serde::{Deserialize, Serialize}; // ref: https://handmade.network/wiki/2823-keyboard_inputs_-_scancodes,_raw_input,_text_input,_key_names #[derive(Serialize, Deserialize, Hash, Debug, Eq, PartialEq, Copy, Clone, TryFromPrimitive)] #[repr(u16)] pub enum ScanCode { Esc = 0x01, Num1 = 0x02, Num2 = 0x03, Num3 = 0x04, Num4 = 0x05, Num5 = 0x06, Num6 = 0x07, Num7 = 0x08, Num8 = 0x09, Num9 = 0x0A, Num0 = 0x0B, Minus = 0x0C, Equals = 0x0D, Backspace = 0x0E, Tab = 0x0F, Q = 0x10, W = 0x11, E = 0x12, R = 0x13, T = 0x14, Y = 0x15, U = 0x16, I = 0x17, O = 0x18, P = 0x19, LeftBracket = 0x1A, RightBracket = 0x1B, Enter = 0x1C, LeftControl = 0x1D, A = 0x1E, S = 0x1F, D = 0x20, F = 0x21, G = 0x22, H = 0x23, J = 0x24, K = 0x25, L = 0x26, SemiColon = 0x27, Apostrophe = 0x28, Grave = 0x29, LeftShift = 0x2A, BackSlash = 0x2B, Z = 0x2C, X = 0x2D, C = 0x2E, V = 0x2F, B = 0x30, N = 0x31, M = 0x32, Comma = 0x33, Period = 0x34, Slash = 0x35, RightShift = 0x36, NumpadMultiply = 0x37, LeftAlt = 0x38, Space = 0x39, CapsLock = 0x3A, F1 = 0x3B, F2 = 0x3C, F3 = 0x3D, F4 = 0x3E, F5 = 0x3F, F6 = 0x40, F7 = 0x41, 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, SC_55 = 0x55, Int1 = 0x56, F11 = 0x57, F12 = 0x58, SC_59 = 0x59, Oem1 = 0x5A, Oem2 = 0x5B, Oem3 = 0x5C, EraseEOF = 0x5D, Oem4 = 0x5E, Oem5 = 0x5F, SC_60 = 0x60, SC_61 = 0x61, Zoom = 0x62, Help = 0x63, F13 = 0x64, F14 = 0x65, F15 = 0x66, F16 = 0x67, F17 = 0x68, F18 = 0x69, F19 = 0x6A, F20 = 0x6B, F21 = 0x6C, F22 = 0x6D, F23 = 0x6E, Oem6 = 0x6F, Katakana = 0x70, Oem7 = 0x71, SC_72 = 0x72, SC_73 = 0x73, SC_74 = 0x74, SC_75 = 0x75, F24 = 0x76, SBCSChar = 0x77, SC_78 = 0x78, Convert = 0x79, 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, } ================================================ FILE: justfile ================================================ 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" strip "{{output_dir}}/kanata" cargo build --release --features cmd cp target/release/kanata "{{output_dir}}/kanata_cmd_allowed" strip "{{output_dir}}/kanata_cmd_allowed" cp cfg_samples/kanata.kbd "{{output_dir}}" # Build the release binaries for Windows and put the binaries+cfg in the output directory. build_release_windows output_dir: cargo build --release --no-default-features --features tcp_server,win_manifest; cp target/release/kanata.exe "{{output_dir}}\kanata_legacy_output.exe" cargo build --release --features win_manifest,interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_wintercept.exe" cargo build --release --features win_manifest,win_sendinput_send_scancodes; cp target/release/kanata.exe "{{output_dir}}\kanata.exe" cargo build --release --features win_manifest,win_sendinput_send_scancodes,win_llhook_read_scancodes; cp target/release/kanata.exe "{{output_dir}}\kanata_winIOv2.exe" cargo build --release --features win_manifest,cmd,win_sendinput_send_scancodes; cp target/release/kanata.exe "{{output_dir}}\kanata_cmd_allowed.exe" cargo build --release --features win_manifest,cmd,interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_wintercept_cmd_allowed.exe" cargo build --release --features passthru_ahk --package=simulated_passthru; cp target/release/kanata_passthru.dll "{{output_dir}}\kanata_passthru.dll" cargo build --release --features win_manifest,gui ; cp target/release/kanata.exe "{{output_dir}}\kanata_gui.exe" cargo build --release --features win_manifest,gui,cmd; cp target/release/kanata.exe "{{output_dir}}\kanata_gui_cmd_allowed.exe" cargo build --release --features win_manifest,gui,interception_driver ; cp target/release/kanata.exe "{{output_dir}}\kanata_gui_wintercept.exe" cargo build --release --features win_manifest,gui,cmd,interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_gui_wintercept_cmd_allowed.exe" cp cfg_samples/kanata.kbd "{{output_dir}}" # Generate the sha256sums for all files in the output directory sha256sums output_dir: rm -f {{output_dir}}/sha256sums cd {{output_dir}}; sha256sum * > sha256sums test: cargo test -p kanata -p kanata-parser -p kanata-keyberon -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}}" ================================================ FILE: key-sort-add/Cargo.toml ================================================ [workspace] members = ["."] [package] name = "key-sort-add" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] ================================================ FILE: key-sort-add/README.md ================================================ # key-sort-add A small program that was used to sort and fill in OsCode mappings. ================================================ FILE: key-sort-add/mapping.txt ================================================ === 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, ================================================ FILE: key-sort-add/src/main.rs ================================================ //! one: //! //! Takes a file formatted as: //! //! KEY_RESERVED = 0, //! KEY_ESC = 1, //! KEY_1 = 2, //! KEY_2 = 3, //! KEY_3 = 4, //! KEY_4 = 5, //! ... //! //! 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() { 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"); let mut keys = s .lines() .map(|l| { let mut segments = l.trim_end_matches(',').trim().split(" = "); let key = segments.next().expect("a string"); 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::>(); keys.sort_by_key(|k| k.1); let mut keys_to_add = vec![]; let mut cur_key = keys.iter(); let mut prev_key = keys.iter(); cur_key.next(); 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!("K{missing}"), missing)); } } keys.append(&mut keys_to_add); keys.sort_by_key(|k| k.1); for key in keys { 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)); } } ================================================ FILE: keyberon/.gitignore ================================================ # Ignore Cargo.lock since this is a library crate Cargo.lock ================================================ FILE: keyberon/CHANGELOG.md ================================================ # v0.2.0 * New Keyboard::leds_mut function for getting underlying leds object. * Made Layout::current_layer public for getting current active layer. * Added a procedural macro for defining layouts (`keyberon::layout::layout`) * Corrected HID report descriptor * Add max_packet_size() to HidDevice to allow differing report sizes * Allows default layer to be set on a Layout externally * Add Chording for multiple keys pressed at the same time to equal another key Breaking changes: * Row and Column pins are now a simple array. For the STM32 MCU, you should now use `.downgrade()` to have an homogenous array. * `Action::HoldTap` now takes a configuration for different behaviors. * `Action::HoldTap` now takes the `tap_hold_interval` field. Not implemented yet. * `Action` is now generic, for the `Action::Custom(T)` variant, allowing custom actions to be handled outside of keyberon. This functionality can be used to drive non keyboard actions, such as resetting the microcontroller, driving leds (for backlight or underglow for example), managing a mouse emulation, or any other ideas you can have. As there is a default value for the type parameter, the update should be transparent. * Layers don't sum anymore, the last pressed layer action set the layer. * Rename MeidaCoffee in MediaCoffee to fix typo. # v0.1.1 * HidClass::control_xxx: check interface number [#26](https://github.com/TeXitoi/keyberon/pull/26) # v0.1.0 First published version. ================================================ FILE: keyberon/Cargo.toml ================================================ [package] name = "kanata-keyberon" version = "0.1110.0" authors = ["Guillaume Pinot ", "Robin Krahl ", "jtroo "] edition = "2021" description = "Pure Rust keyboard firmware. Fork intended for use with kanata." documentation = "https://docs.rs/keyberon" repository = "https://github.com/TeXitoi/keyberon" keywords = ["keyboard", "kanata"] categories = ["no-std"] license = "MIT" readme = "README.md" [features] tap_hold_tracker = [] [dependencies] kanata-keyberon-macros = { version = "0.2.0" } heapless = "0.7.16" rustc-hash = "1.1.0" arraydeque = { version = "0.5.1", default-features = false } ================================================ FILE: keyberon/KEYBOARDS.md ================================================ | Keyboard | PCB or Handwired | MCU | Feature Status | | - | - | - | - | | [KeySeeBee](https://github.com/TeXitoi/keyseebee) | PCB | STM32F072 |
  • [x] Matrix
  • [x] Split
| | [Keyberon-f4](https://github.com/TeXitoi/keyberon-f4) | Handwired | STM32F401 |
  • [x] Matrix
| | [Arisu](https://github.com/help-14/arisu-handwired) | Handwired | STM32F401 |
  • [x] Matrix
| | [ortho60-keyberon](https://github.com/TeXitoi/ortho60-keyberon) | PCB | STM32F103 |
  • [x] Matrix
| | [keyberon-grid](https://github.com/TeXitoi/keyberon-grid) | Handwired | STM32F103 |
  • [x] Matrix
| | [Clueboard 66% LP](https://github.com/wezm/clueboard-rust-firmware) | PCB | STM32F303 |
  • [x] Matrix
  • [ ] LEDs
  • [ ] Speakers
| | [anne-keyberon](https://github.com/hdhoang/anne-keyberon) | PCB | STM32L151 |
  • [ ] Matrix
  • [ ] BT proto
  • [ ] LED proto
  • [ ] LED MCU
| | [corne-xiao](https://github.com/lehmanju/corne-xiao) | PCB | ATSAMD21 |
  • [x] Matrix
| | [pinci](https://github.com/camrbuss/pinci) | PCB | RP2040 |
  • [x] Matrix
  • [x] Split
| | [nibble-rp2040-rs](https://github.com/DrewTChrist/nibble-rp2040-rs) | PCB | RP2040 |
  • [x] Matrix
  • [ ] Rotary Encoder
  • [ ] RGB LEDs
  • [ ] OLED
| | [keyboard-labs](https://github.com/rgoulter/keyboard-labs) | PCB | STM32F401 |
  • [x] Matrix
  • [x] Split
| | [makerdiary M60](https://github.com/jamesmunns/m60-keyboard/) | PCB | nRF52840 |
  • [x] Matrix
  • [x] RGB LEDs
  • | | [PouetPouet](https://github.com/dkm/pouetpouet-board) | PCB | STM32F072 |
    • [x] Matrix
    | | [corne](https://github.com/simmsb/keyboard) | PCB | nRF52840 |
    • [x] Matrix
    • [x] RGB LEDs
    • [x] OLED
    • [x] split
    | | [Cantor](https://github.com/dariogoetz/cantor-firmware-keyberon) | PCB | STM32F401 |
    • [x] Matrix
    • [ ] LEDs
    • [x] Split
    • [x] Diodeless
    | ================================================ FILE: keyberon/LICENSE ================================================ MIT License Copyright (c) 2019 Guillaume P. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: keyberon/README.md ================================================ # kanata-keyberon ## Note 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. ================================================ FILE: keyberon/keyberon-macros/Cargo.toml ================================================ [package] name = "kanata-keyberon-macros" version = "0.2.0" authors = ["Antoni Simka "] edition = "2018" description = "Macros for keyberon. Fork for kanata project" license = "MIT" [lib] proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" ================================================ FILE: keyberon/keyberon-macros/README.md ================================================ # kanata keyberon macros ## Note This is a fork intended for use by the [kanata keyboard remapper software](https://github.com/jtroo/keyberon). Please make contributions to the [original project](https://github.com/TeXitoi/keyberon) where applicable. ================================================ FILE: keyberon/keyberon-macros/src/lib.rs ================================================ extern crate proc_macro; use proc_macro2::{Delimiter, Group, Literal, Punct, Spacing, TokenStream, TokenTree}; use quote::quote; #[proc_macro] pub fn layout(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input: TokenStream = input.into(); let mut out = TokenStream::new(); let mut inside = TokenStream::new(); for t in input { match t { TokenTree::Group(g) if g.delimiter() == Delimiter::Brace => { let layer = parse_layer(g.stream()); inside.extend(quote! { [#layer], }); } _ => panic!("{}", "Invalid token, expected layer: {{ ... }}"), } } let all: TokenStream = quote! { [#inside] }; out.extend(all); out.into() } fn parse_layer(input: TokenStream) -> TokenStream { let mut out = TokenStream::new(); for t in input { match t { TokenTree::Group(g) if g.delimiter() == Delimiter::Bracket => { let row = parse_row(g.stream()); out.extend(quote! { [#row], }); } TokenTree::Punct(p) if p.as_char() == ',' => (), _ => panic!("Invalid token, expected row: [ ... ]"), } } out } fn parse_row(input: TokenStream) -> TokenStream { let mut out = TokenStream::new(); for t in input { match t { TokenTree::Ident(i) => match i.to_string().as_str() { "n" => out.extend(quote! { keyberon::action::Action::NoOp, }), "t" => out.extend(quote! { keyberon::action::Action::Trans, }), _ => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::#i), }), }, TokenTree::Punct(p) => punctuation_to_keycode(&p, &mut out), TokenTree::Literal(l) => literal_to_keycode(&l, &mut out), TokenTree::Group(g) => parse_group(&g, &mut out), } } out } fn parse_group(g: &Group, out: &mut TokenStream) { match g.delimiter() { // Handle empty groups Delimiter::Parenthesis if g.stream().is_empty() => { eprintln!("Expected a layer number in layer switch"); } Delimiter::Brace if g.stream().is_empty() => { eprintln!("Expected an action - group cannot be empty"); } Delimiter::Bracket if g.stream().is_empty() => { eprintln!("Expected keycodes - keycode group cannot be empty"); } // Momentary layer switch (Action::Layer) Delimiter::Parenthesis => { let tokens = g.stream(); out.extend(quote! { keyberon::action::Action::Layer(#tokens), }); } // Pass the expression unchanged (adding a comma after it) Delimiter::Brace => out.extend(g.stream().into_iter().chain(TokenStream::from( TokenTree::Punct(Punct::new(',', Spacing::Alone)), ))), // Multiple keycodes (Action::MultipleKeyCodes) Delimiter::Bracket => parse_keycode_group(g.stream(), out), // Is this reachable? Delimiter::None => eprintln!("Unexpected group"), } } fn parse_keycode_group(input: TokenStream, out: &mut TokenStream) { let mut inner = TokenStream::new(); for t in input { match t { TokenTree::Ident(i) => inner.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::#i), }), TokenTree::Punct(p) => punctuation_to_keycode(&p, &mut inner), TokenTree::Literal(l) => literal_to_keycode(&l, &mut inner), TokenTree::Group(g) => parse_group(&g, &mut inner), } } out.extend(quote! { keyberon::action::Action::MultipleActions(&[#inner].as_slice()), }); } fn punctuation_to_keycode(p: &Punct, out: &mut TokenStream) { match p.as_char() { // Normal punctuation '-' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Minus), }), '=' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Equal), }), ';' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::SColon), }), ',' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Comma), }), '.' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Dot), }), '/' => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Slash), }), // Shifted punctuation '!' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb1].as_slice()), }), '@' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb2].as_slice()), }), '#' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb3].as_slice()), }), '$' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb4].as_slice()), }), '%' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb5].as_slice()), }), '^' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb6].as_slice()), }), '&' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb7].as_slice()), }), '*' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb8].as_slice()), }), '_' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Minus].as_slice()), }), '+' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Equal].as_slice()), }), '|' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Bslash].as_slice()), }), '~' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Grave].as_slice()), }), '<' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Comma].as_slice()), }), '>' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Dot].as_slice()), }), '?' => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Slash].as_slice()), }), // Is this reachable? _ => eprintln!("Punctuation could not be parsed as a keycode") } } fn literal_to_keycode(l: &Literal, out: &mut TokenStream) { //let repr = l.to_string(); match l.to_string().as_str() { "1" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb1), }), "2" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb2), }), "3" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb3), }), "4" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb4), }), "5" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb5), }), "6" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb6), }), "7" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb7), }), "8" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb8), }), "9" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb9), }), "0" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Kb0), }), // Char literals; mostly punctuation which can't be properly tokenized alone r#"'\''"# => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Quote), }), r#"'\\'"# => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Bslash), }), // Shifted characters "'['" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::LBracket), }), "']'" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::RBracket), }), "'`'" => out.extend(quote! { keyberon::action::Action::KeyCode(keyberon::key_code::KeyCode::Grave), }), "'\"'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Quote].as_slice()), }), "'('" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb9].as_slice()), }), "')'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Kb0].as_slice()), }), "'{'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::LBracket].as_slice()), }), "'}'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::RBracket].as_slice()), }), "'_'" => out.extend(quote! { keyberon::action::Action::MultipleKeyCodes(&[keyberon::key_code::KeyCode::LShift, keyberon::key_code::KeyCode::Minus].as_slice()), }), s if s.starts_with('\'') => eprintln!("Literal could not be parsed as a keycode"), s if s.starts_with('\"') => { eprintln!("Typing strings on key press is not yet supported") } _ => eprintln!("Literal could not be parsed as a keycode") } } ================================================ FILE: keyberon/src/action/switch.rs ================================================ //! 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 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(Debug, Clone, Copy, PartialEq)] /// 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> { pub cases: &'a [Case<'a, T>], } // 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; // 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), } #[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 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. pub fn actions( &self, active_keys: A1, active_positions: A2, historical_keys: H1, historical_positions: H2, layers: L, default_layer: u16, ) -> SwitchActions<'a, T, A1, A2, H1, H2, L> where A1: Iterator + Clone, A2: Iterator + Clone, H1: Iterator> + Clone, H2: Iterator> + Clone, L: Iterator + Clone, { SwitchActions { cases: self.cases, active_keys, active_positions, historical_keys, historical_positions, layers, default_layer, case_index: 0, } } } #[derive(Debug, Clone)] /// Iterator returned by `Switch::actions`. pub struct SwitchActions<'a, T, A1, A2, H1, H2, L> where A1: Iterator + Clone, A2: Iterator + Clone, H1: Iterator> + Clone, H2: Iterator> + Clone, L: 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, case_index: usize, } impl<'a, T, A1, A2, H1, H2, L> Iterator for SwitchActions<'a, T, A1, A2, H1, H2, L> where A1: Iterator + Clone, A2: Iterator + Clone, H1: Iterator> + Clone, H2: Iterator> + Clone, L: 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, ) { 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 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), _ => 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. 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, ) -> 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 = false; } 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; } }; 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)] fn evaluate_bool_test(opcodes: &[OpCode], keycodes: impl Iterator + Clone) -> bool { evaluate_boolean( opcodes, keycodes, [].iter().copied(), [].iter().copied(), [].iter().copied(), [].iter().copied(), 0, ) } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[test] fn bool_evaluation_test_4() { let opcodes = []; let keycodes = []; assert!(evaluate_bool_test( opcodes.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[test] fn switch_fallthrough() { let sw = Switch { cases: &[ (&[], &Action::<()>::KeyCode(KeyCode::A), Fallthrough), (&[], &Action::<()>::KeyCode(KeyCode::B), Fallthrough), ], }; let mut actions = sw.actions( [].iter().copied(), [].iter().copied(), [].iter().copied(), [].iter().copied(), [].iter().copied(), 0, ); 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), ], }; let mut actions = sw.actions( [].iter().copied(), [].iter().copied(), [].iter().copied(), [].iter().copied(), [].iter().copied(), 0, ); 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, ), ], }; let mut actions = sw.actions( [].iter().copied(), [].iter().copied(), [].iter().copied(), [].iter().copied(), [].iter().copied(), 0, ); assert_eq!(actions.next(), None); } #[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, }, ]; assert!(evaluate_boolean( opcode_true.as_slice(), [].iter().copied(), [].iter().copied(), hist_keycodes.iter().copied(), [].iter().copied(), [].iter().copied(), 0, )); assert!(evaluate_boolean( opcode_true2.as_slice(), [].iter().copied(), [].iter().copied(), hist_keycodes.iter().copied(), [].iter().copied(), [].iter().copied(), 0, )); assert!(!evaluate_boolean( opcode_false.as_slice(), [].iter().copied(), [].iter().copied(), hist_keycodes.iter().copied(), [].iter().copied(), [].iter().copied(), 0, )); assert!(!evaluate_boolean( opcode_false2.as_slice(), [].iter().copied(), [].iter().copied(), hist_keycodes.iter().copied(), [].iter().copied(), [].iter().copied(), 0, )); } #[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| { assert_eq!( evaluate_boolean( opcodes, [].iter().copied(), [].iter().copied(), hist_keycodes.iter().copied(), [].iter().copied(), [].iter().copied(), 0, ), expectation ); }; 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| { assert_eq!( evaluate_boolean( opcodes, [].iter().copied(), [].iter().copied(), hist_keycodes.iter().copied(), [].iter().copied(), [].iter().copied(), 0, ), expectation ); }; 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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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.as_slice(), keycodes.iter().copied(), )); } #[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| { assert_eq!( evaluate_boolean( opcodes, [].iter().copied(), active_inputs.iter().copied(), [].iter().copied(), [].iter().copied(), [].iter().copied(), 0, ), expectation ); }; 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| { assert_eq!( evaluate_boolean( opcodes, [].iter().copied(), [].iter().copied(), [].iter().copied(), historical_inputs.iter().copied(), [].iter().copied(), 0, ), expectation ); }; 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); } ================================================ FILE: keyberon/src/action.rs ================================================ //! The different actions that can be executed via any given key. use crate::key_code::KeyCode; 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)] pub enum SequenceEvent<'a, T: 'a> { /// No operation action: just do nothing (a placeholder). NoOp, /// A keypress/keydown Press(KeyCode), /// Key release/keyup Release(KeyCode), /// A shortcut for `Press(KeyCode), Release(KeyCode)` Tap(KeyCode), /// For sequences that need to wait a bit before continuing Delay { /// How long (in ticks) this Delay will last duration: u32, // NOTE: This isn't a u16 because that's only max ~65 seconds (assuming 1000 ticks/sec) }, /// Custom event in sequence. Custom(&'a T), /// Cancels the running sequence and can be used to mark the end of a sequence /// instead of using a number of Release() events Complete, } impl Debug for SequenceEvent<'_, T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::NoOp => write!(f, "NoOp"), Self::Press(arg0) => f.debug_tuple("Press").field(arg0).finish(), Self::Release(arg0) => f.debug_tuple("Release").field(arg0).finish(), Self::Tap(arg0) => f.debug_tuple("Tap").field(arg0).finish(), Self::Delay { duration } => { f.debug_struct("Delay").field("duration", duration).finish() } Self::Custom(_) => write!(f, "Custom"), Self::Complete => write!(f, "Complete"), } } } /// Behavior configuration of HoldTap. #[non_exhaustive] #[derive(Clone, Copy)] pub enum HoldTapConfig<'a> { /// Only the timeout will determine between hold and tap action. /// /// This is a sane default. Default, /// If there is a key press, the hold action is activated. /// /// This behavior is interesting for a key which the tap action is /// 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. /// /// This behavior is interesting for fast typist: the different /// between hold and tap would more be based on the sequence of /// events than on timing. Be aware that doing the good succession /// of key might require some training. PermissiveHold, /// A custom configuration. Allows the behavior to be controlled by a caller /// supplied handler function. /// /// 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 /// [WaitingAction::NoOp] to drop handling of the key press. A [None] /// 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. /// 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 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 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, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] /// A state that that can be released from the active states via the ReleaseState action. pub enum ReleasableState { /// Release an active keycode KeyCode(KeyCode), /// Release an active layer Layer(usize), } /// Perform different actions on key hold/tap. /// /// If the key is held more than `timeout` ticks (usually /// milliseconds), performs the `hold` action, else performs the /// `tap` action. Mostly used with a modifier for the hold action /// and a normal key on the tap action. Any action can be /// performed, but using a `HoldTap` in a `HoldTap` is not /// specified (but guaranteed to not crash). /// /// Different behaviors can be configured using the config field, /// but whatever the configuration is, if the key is pressed more /// than `timeout`, the hold action is activated (if no other /// action was determined before). #[derive(Debug, Clone, Copy, PartialEq)] pub struct HoldTapAction<'a, T> where T: 'a, { /// The duration, in ticks (usually milliseconds) giving the /// difference between a hold and a tap. pub timeout: u16, /// The hold action. pub hold: Action<'a, T>, /// The tap action. pub tap: Action<'a, T>, /// The timeout action pub timeout_action: Action<'a, T>, /// Behavior configuration. pub config: HoldTapConfig<'a>, /// Configuration of the tap and hold holds the tap action. /// /// If you press and release the key in such a way that the tap /// action is performed, and then press it again in less than /// `tap_hold_interval` ticks, the tap action will /// be held. This allows the tap action to be held by /// pressing, releasing and holding the key, allowing the computer /// to auto repeat the tap behavior. The timeout starts on the /// first press of the key, NOT on the release. /// /// Pressing a different key in between will not result in the /// behaviour described above; the HoldTap key must be pressed twice /// in a row. /// /// 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. #[derive(Debug, Clone, Copy, PartialEq)] pub struct OneShot<'a, T = core::convert::Infallible> where T: 'a, { /// Action to activate until timeout expires or exactly one non-one-shot key is activated. pub action: &'a Action<'a, T>, /// Timeout after which one shot will expire. Note: timeout will be overwritten if another /// one shot key is pressed. pub timeout: u16, /// Configuration of one shot end behaviour. Note: this will be overwritten if another one shot /// key is pressed. Consider keeping this consistent between all your one shot keys to prevent /// surprising behaviour. pub end_config: OneShotEndConfig, } /// Determine the ending behaviour of the one shot key. #[non_exhaustive] #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum OneShotEndConfig { /// End one shot activation on first non-one-shot key press. EndOnFirstPress, /// End one shot activation on first non-one-shot key press or a repress of an already-pressed /// one-shot key. EndOnFirstPressOrRepress, /// End one shot activation on first non-one-shot key release. EndOnFirstRelease, /// End one shot activation on first non-one-shot key release or a repress of an already-pressed /// one-shot key. EndOnFirstReleaseOrRepress, } /// Defines the maximum number of one shot keys that can be combined. pub const ONE_SHOT_MAX_ACTIVE: usize = 16; /// Define tap dance behaviour. #[derive(Debug, Clone, Copy, PartialEq)] pub struct TapDance<'a, T = core::convert::Infallible> where T: 'a, { /// List of actions that activate based on number of taps. Only one of the actions will /// activate. Tapping the tap-dance key once will activate the action in index 0, three /// times will activate the action in index 2. pub actions: &'a [&'a Action<'a, T>], /// Timeout after which a tap will expire and become an action. A new tap for the same /// tap-dance key will reset this timeout. pub timeout: u16, /// Determine behaviour of tap dance. Eager evaluation will activate every action in the /// sequence as keys are pressed. Lazy will activate only a single action, decided by the /// number of taps in the sequence. pub config: TapDanceConfig, } /// Determines the behaviour for a `TapDance`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TapDanceConfig { Lazy, Eager, } /// A group of chords (actions mapped to a combination of multiple physical keys pressed together). #[derive(Debug, Clone, Copy, PartialEq)] pub struct ChordsGroup<'a, T = core::convert::Infallible> where T: 'a, { /// List of key coordinates participating in this chord group, each with the corresponding [ChordKeys] they map to. pub coords: &'a [((u8, u16), ChordKeys)], /// Map of chords to actions they execute. pub chords: &'a [(ChordKeys, &'a Action<'a, T>)], /// Timeout after which a chord will expire and either trigger its action or be discarded if there is no corresponding action. /// A chord may trigger its action even before this timeout expires, if a chord key is released, a non-chord key is pressed or the pressed chord is already uniquely identifyable. pub timeout: u16, } impl<'a, T> ChordsGroup<'a, T> { /// Gets the chord keys corresponding to the given key coordinates. pub fn get_keys(&self, coord: (u8, u16)) -> Option { self.coords.iter().find(|c| c.0 == coord).map(|c| c.1) } /// Gets the chord action assigned to the given chord keys. pub fn get_chord(&self, keys: ChordKeys) -> Option<&'a Action<'a, T>> { self.chords .iter() .find(|(chord_keys, _)| *chord_keys == keys) .map(|(_, action)| *action) } /// Gets the chord action assigned to the given chord keys if they are already unambigous (i.e. there is no key that could still be pressed that would result in a different chord). pub fn get_chord_if_unambiguous(&self, keys: ChordKeys) -> Option<&'a Action<'a, T>> { self.chords .iter() .try_fold(None, |res, &(chord_keys, action)| { if chord_keys == keys { Ok(Some(action)) } else if chord_keys | keys == chord_keys { // The given keys are a subset of this chord but not an exact match // -> ambiguity Err(()) } else { Ok(res) } }) .unwrap_or_default() } } /// 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 = 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; /// An action that can do one of two actions. The `left` action is the default. The `right` action /// will trigger if any of the key codes in `right_triggers` are active in the current layout /// state. #[derive(Debug, Clone, Copy, PartialEq)] pub struct ForkConfig<'a, T> { pub left: Action<'a, T>, pub right: Action<'a, T>, pub right_triggers: &'a [KeyCode], } /// The different actions that can be done. #[derive(Clone, Copy, PartialEq, Debug)] pub enum Action<'a, T = core::convert::Infallible> where T: 'a, { /// No operation action: just do nothing. NoOp, /// Transparent, i.e. get the action from the default layer. On /// the default layer, it is equivalent to `NoOp`. Trans, /// A key code, i.e. a classic key. KeyCode(KeyCode), /// Multiple key codes sent at the same time, as if these keys /// were pressed at the same time. Useful to send a shifted key, /// or complex shortcuts like Ctrl+Alt+Del in a single key press. MultipleKeyCodes(&'a &'a [KeyCode]), /// Multiple actions sent at the same time. MultipleActions(&'a &'a [Action<'a, T>]), /// While pressed, change the current layer. That's the classic /// Fn key. If several layer actions are hold at the same time, /// the last pressed defines the current layer. Layer(usize), /// Change the default layer. DefaultLayer(usize), /// A sequence of SequenceEvents Sequence { /// An array of SequenceEvents that will be triggered (in order) events: &'a &'a [SequenceEvent<'a, T>], }, /// A sequence of SequenceEvents, which will be repeated so long as the key is held. RepeatableSequence { /// An array of SequenceEvents that will be triggered (in order) events: &'a &'a [SequenceEvent<'a, T>], }, /// Cancels any running sequences CancelSequences, /// Action to release either a keycode state or a layer state. ReleaseState(ReleasableState), /// Perform different actions on key hold/tap (see [`HoldTapAction`]). HoldTap(&'a HoldTapAction<'a, T>), /// Custom action. /// /// Define a user defined action. This enum can be anything you /// want, as long as it has the `'a` lifetime. It can be used /// to drive any non keyboard related actions that you might /// manage with key events. Custom(T), /// One shot key. Also known as "sticky key". See `struct OneShot` for configuration info. /// Activates `action` until a single other key that is not also a one shot key is used. For /// example, a one shot key can be used to activate shift for exactly one keypress or switch to /// another layer for exactly one keypress. Holding a one shot key will be treated as a normal /// held keypress. /// /// If you use one shot outside of its intended use cases (modifier key action or layer /// action) then you will likely have undesired behaviour. E.g. one shot with the space /// 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: /// /// - a different key is pressed /// - `timeout` ticks elapse since the last tap of the same tap-dance key /// - the number of taps is equal to the length of `actions`. TapDance(&'a TapDance<'a, T>), /// Chord key. Enters chording mode where multiple keys may be pressed together to active /// different actions depending on the specific combination ("chord") pressed. /// See `struct ChordGroup` for configuration info. /// /// Keys participating in chording mode are listed in `coords`. /// Chording mode ends when a non-participating key is pressed, a participating key is released, /// the timeout expires, or when the pressed chord uniquely identifies an action (i.e. there are /// no more keys you could press to change the result). Chords(&'a ChordsGroup<'a, T>), /// Repeat the previous action. Repeat, /// 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 Action<'_, T> { /// Gets the layer number if the action is the `Layer` action. pub fn layer(self) -> Option { match self { Action::Layer(l) => Some(l), _ => None, } } /// Returns an iterator on the `KeyCode` corresponding to the action. pub fn key_codes(&self) -> impl Iterator + '_ { match self { Action::KeyCode(kc) => core::slice::from_ref(kc).iter().cloned(), Action::MultipleKeyCodes(kcs) => kcs.iter().cloned(), _ => [].iter().cloned(), } } } /// A shortcut to create a `Action::KeyCode`, useful to create compact /// layout. pub const fn k(kc: KeyCode) -> Action<'static, T> { Action::KeyCode(kc) } /// A shortcut to create a `Action::Layer`, useful to create compact /// layout. pub const fn l(layer: usize) -> Action<'static, T> { Action::Layer(layer) } /// A shortcut to create a `Action::DefaultLayer`, useful to create compact /// layout. pub const fn d(layer: usize) -> Action<'static, T> { Action::DefaultLayer(layer) } ================================================ FILE: keyberon/src/chord.rs ================================================ //! 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) => { // 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, } } }); if presses.is_empty() { drainq.push_back(*qd); false } else { true } } }) } fn process_presses(&mut self, active_layer: u16) { let mut presses = 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"); } 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.is_empty()); let since = self.queue.iter().next().unwrap().since; for press in presses.iter().copied() { min_timeout = u16::MAX; accumulated_presses .push(press) .expect("accpresses same len as presses"); 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)); 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)) { // 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.contains(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)) && pch .participating_keys .iter() .all(|pk| accumulated_presses.contains(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)) && pch .participating_keys .iter() .all(|pk| accumulated_presses.contains(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)) && pch .participating_keys .iter() .all(|pk| accumulated_presses.contains(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.contains(&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, } } ================================================ FILE: keyberon/src/key_code.rs ================================================ //! 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 { 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 { pub fn is_mod(self) -> bool { use KeyCode::*; matches!( self, LShift | RShift | LCtrl | RCtrl | LAlt | RAlt | LGui | RGui ) } } 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:?}"), } } } ================================================ FILE: keyberon/src/layout/contextual_execution.rs ================================================ //! 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); } } } ================================================ FILE: keyberon/src/layout.rs ================================================ //! Layout management. /// A procedural macro to generate [Layers](type.Layers.html) /// ## Syntax /// Items inside the macro are converted to Actions as such: /// - [`Action::KeyCode`]: Idents are automatically understood as keycodes: `A`, `RCtrl`, `Space` /// - Punctuation, numbers and other literals that aren't special to the rust parser are converted /// to KeyCodes as well: `,` becomes `KeyCode::Commma`, `2` becomes `KeyCode::Kb2`, `/` becomes `KeyCode::Slash` /// - Characters which require shifted keys are converted to `Action::MultipleKeyCodes(&[LShift, ])`: /// `!` becomes `Action::MultipleKeyCodes(&[LShift, Kb1])` etc /// - Characters special to the rust parser (parentheses, brackets, braces, quotes, apostrophes, underscores, backslashes and backticks) /// left alone cause parsing errors and as such have to be enclosed by apostrophes: `'['` becomes `KeyCode::LBracket`, /// `'\''` becomes `KeyCode::Quote`, `'\\'` becomes `KeyCode::BSlash` /// - [`Action::NoOp`]: Lowercase `n` /// - [`Action::Trans`]: Lowercase `t` /// - [`Action::Layer`]: A number in parentheses: `(1)`, `(4 - 2)`, `(0x4u8 as usize)` /// - [`Action::MultipleActions`]: Actions in brackets: `[LCtrl S]`, `[LAlt LCtrl C]`, `[(2) B {Action::NoOp}]` /// - Other `Action`s: anything in braces (`{}`) is copied unchanged to the final layout - `{ Action::Custom(42) }` /// simply becomes `Action::Custom(42)` /// /// **Important note**: comma (`,`) is a keycode on its own, and can't be used to separate keycodes as one would have /// to do when not using a macro. pub use kanata_keyberon_macros::*; 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. /// /// `Layers` type is an array of layers which contain the description /// of actions on the switch matrix. For example `layers[1][2][3]` /// 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, 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]. 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; /// 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; /// 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, ACTION_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, T = core::convert::Infallible> where T: 'a + std::fmt::Debug, { /// 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, 4, arraydeque::behavior::Wrapping>, pub action_queue: ActionQueue<'a, T>, pub rpt_action: Option<&'a Action<'a, T>>, pub historical_keys: History, pub historical_inputs: 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>, 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}; pub struct History { events: ArrayDeque, ticks_since_occurrences: ArrayDeque, } #[derive(Copy, Clone)] 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. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Event { /// Press event with coordinates (i, j). Press(u8, u16), /// Release event with coordinates (i, j). Release(u8, u16), } impl Event { /// Returns the coordinates (i, j) of the event. pub fn coord(self) -> KCoord { match self { Event::Press(i, j) => (i, j), Event::Release(i, j) => (i, j), } } /// Transforms the coordinates of the event. /// /// # Example /// /// ``` /// # use kanata_keyberon::layout::Event; /// assert_eq!( /// Event::Press(3, 10), /// Event::Press(3, 1).transform(|i, j| (i, 11 - j)), /// ); /// ``` pub fn transform(self, f: impl FnOnce(u8, u16) -> KCoord) -> Self { match self { Event::Press(i, j) => { let (i, j) = f(i, j); Event::Press(i, j) } Event::Release(i, j) => { let (i, j) = f(i, j); Event::Release(i, j) } } } /// Returns `true` if the event is a key press. pub fn is_press(self) -> bool { match self { Event::Press(..) => true, Event::Release(..) => false, } } /// Returns `true` if the event is a key release. pub fn is_release(self) -> bool { match self { Event::Release(..) => true, Event::Press(..) => false, } } } /// Event from custom action. #[derive(Debug, Default, PartialEq, Eq)] pub enum CustomEvent<'a, T: 'a> { /// No custom action. #[default] NoEvent, /// The given custom action key is pressed. Press(&'a T), /// The given custom action key is released. Release(&'a T), } impl CustomEvent<'_, T> { /// Update an event according to a new event. /// ///The event can only be modified in the order `NoEvent < Press < /// Release` fn update(&mut self, e: Self) { use CustomEvent::*; match (&e, &self) { (Release(_), NoEvent) | (Release(_), Press(_)) => *self = e, (Press(_), NoEvent) => *self = e, _ => (), } } } /// 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, coord: KCoord, }, Custom { value: &'a T, coord: KCoord, }, FakeKey { keycode: KeyCode, }, // Fake key event for sequences RepeatingSequence { sequence: &'a &'a [SequenceEvent<'a, T>], coord: KCoord, }, SeqCustomPending(&'a T), SeqCustomActive(&'a T), Tombstone, NoOpInput { coord: KCoord, }, } impl Copy for State<'_, T> {} impl Clone for State<'_, T> { fn clone(&self) -> Self { *self } } impl<'a, T: 'a> State<'a, T> { pub fn keycode(&self) -> Option { match self { NormalKey { keycode, .. } => Some(*keycode), FakeKey { keycode } => Some(*keycode), _ => None, } } 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 { 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)); None } _ => Some(*self), } } pub fn release_state(&self, s: ReleasableState) -> Option { match (*self, s) { ( NormalKey { keycode: k1, .. } | FakeKey { keycode: k1 }, ReleasableState::KeyCode(k2), ) => { if k1 == k2 { None } else { Some(*self) } } (LayerModifier { value: l1, .. }, ReleasableState::Layer(l2)) => { if l1 == l2 { None } else { Some(*self) } } _ => Some(*self), } } fn seq_release(&self, kc: KeyCode) -> Option { match *self { FakeKey { keycode, .. } if keycode == kc => None, _ => Some(*self), } } fn get_layer(&self) -> Option { match self { LayerModifier { value, .. } => Some(*value), _ => 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)] pub(crate) struct TapDanceState<'a, T: 'a> { actions: &'a [&'a Action<'a, T>], timeout: u16, num_taps: u16, } #[derive(Copy, Clone, Debug)] pub struct TapDanceEagerState<'a, T: 'a> { coord: KCoord, actions: &'a [&'a Action<'a, T>], timeout: u16, orig_timeout: u16, num_taps: u16, } impl TapDanceEagerState<'_, T> { fn tick_tde(&mut self) { self.timeout = self.timeout.saturating_sub(1); } fn is_expired(&self) -> bool { self.timeout == 0 || usize::from(self.num_taps) >= self.actions.len() } fn set_expired(&mut self) { self.timeout = 0; } fn incr_taps(&mut self) { self.num_taps += 1; self.timeout = self.orig_timeout; } } #[derive(Debug)] pub(crate) enum WaitingConfig<'a, T: 'a + std::fmt::Debug> { HoldTap(HoldTapConfig<'a>), TapDance(TapDanceState<'a, T>), Chord(&'a ChordsGroup<'a, T>), } #[derive(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. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum WaitingAction { /// Trigger the holding event. Hold, /// Trigger the tapping event. Tap, /// Trigger the timeout event. Timeout, /// Drop this event. It will act as if no key was pressed. NoOp, } impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { fn tick_wt( &mut self, queued: &mut Queue, action_queue: &mut ActionQueue<'a, T>, ) -> Option<(WaitingAction, Option)> { self.timeout = self.timeout.saturating_sub(1); self.ticks = self.ticks.saturating_add(1); let mut pq = None; let (ret, cfg_change) = match self.config { WaitingConfig::HoldTap(htc) => (self.handle_hold_tap(htc, queued), None), 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() { let idx = core::cmp::min(num_taps.into(), tds.actions.len()).saturating_sub(1); self.tap = tds.actions[idx]; } if num_taps > tds.num_taps { self.timeout = tds.timeout; } ( ret, Some(WaitingConfig::TapDance(TapDanceState { num_taps, ..*tds })), ) } WaitingConfig::Chord(config) => { if let Some((ret, action, cpq)) = self.handle_chord(config, queued, action_queue) { self.tap = action; pq = Some(cpq); (Some(ret), None) } else { (None, None) } } }; if let Some(cfg) = cfg_change { self.config = cfg; } ret.map(|v| (v, pq)) } 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 => { if queued.iter().any(|s| s.event.is_press()) { 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() { if q.event.is_press() { 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::Custom(func) => { 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: since_release, .. }) = queued .iter() .find(|s| self.is_corresponding_release(&s.event)) { if self.timeout >= self.delay.saturating_sub(since_release) { Some(WaitingAction::Tap) } else { Some(WaitingAction::Timeout) } } else if self.timeout == 0 && (!skip_timeout) { Some(WaitingAction::Timeout) } else { None } } fn handle_tap_dance( &self, num_taps: u16, 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 // single release regardless of how many taps were actually done. let evict_same_coord_events = |num_taps: u16, queued: &mut Queue| { let mut releases_to_remove = num_taps.saturating_sub(1); queued.retain(|s| { let mut do_retain = true; if self.is_corresponding_release(&s.event) { if releases_to_remove > 0 { do_retain = false; releases_to_remove = releases_to_remove.saturating_sub(1) } } else if self.is_corresponding_press(&s.event) { do_retain = false; } do_retain }); }; if self.timeout == 0 { evict_same_coord_events(num_taps, queued); return (Some(WaitingAction::Tap), num_taps); } // Get the number of sequential taps for this tap-dance key. If a different key was // pressed, activate a tap-dance action. match queued.iter().try_fold(1, |same_tap_count, s| { if self.is_corresponding_press(&s.event) { Ok(same_tap_count + 1) } else if matches!(s.event, Event::Press(..)) { Err((same_tap_count, ())) } else { 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); (Some(WaitingAction::Tap), num_taps) } } } fn handle_chord( &mut self, config: &'a ChordsGroup<'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 // `Ok` when chording mode may continue // `Err` when it should end for various reasons let active = queued .iter() .try_fold(config.get_keys(self.coord).unwrap_or(0), |active, s| { if self.delay.saturating_sub(s.since) > self.timeout { Ok(active) } else if let Some(chord_keys) = config.get_keys(s.event.coord()) { match s.event { Event::Press(_, _) => { handled_press_events += 1; Ok(active | chord_keys) } Event::Release(i, j) => { // release chord quickly by changing the coordinate to the released // key, to be consistent with chord decomposition behaviour. released_coord = Some((i, j)); Err(active) } } } else if matches!(s.event, Event::Press(..)) { Err(active) // pressed a non-chord key, abort } else { Ok(active) } }) .and_then(|active| { if self.timeout.saturating_sub(self.delay) == 0 { Err(active) // timeout expired, abort } else { Ok(active) } }); let res = match active { Ok(active) => { // Chording mode still active, only trigger action if it's unambiguous if let Some(action) = config.get_chord_if_unambiguous(active) { if let Some(coord) = released_coord { self.coord = coord; } (WaitingAction::Tap, action) } else { return None; // nothing to do yet, we'll check back later } } Err(active) => { // Abort chording mode. Trigger a chord action if there is one. if let Some(action) = config.get_chord(active) { if let Some(coord) = released_coord { self.coord = coord; } (WaitingAction::Tap, action) } else { self.decompose_chord_into_action_queue(config, queued, action_queue); (WaitingAction::NoOp, &Action::NoOp) } } }; 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| { if self.delay.saturating_sub(s.since) > self.timeout { true } else if matches!(s.event, Event::Press(i, j) if config.get_keys((i, j)).is_some()) && handled_press_events > 0 { handled_press_events -= 1; let _ = pq.push_back(s.event().coord()); false } else { true } }); Some((res.0, res.1, pq)) } fn decompose_chord_into_action_queue( &mut self, config: &'a ChordsGroup<'a, T>, queued: &Queue, action_queue: &mut ActionQueue<'a, T>, ) { 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 default_associated_coord = self.coord; let starting_mask = config.get_keys(self.coord).unwrap_or(0); let mut mask_bits_set = 1; chord_key_order[0] = starting_mask; let _ = queued.iter().try_fold(starting_mask, |active, s| { if self.delay.saturating_sub(s.since) > self.timeout { Ok(active) } else if let Some(chord_keys) = config.get_keys(s.event.coord()) { match s.event { Event::Press(..) => { if active | chord_keys != active { chord_key_order[mask_bits_set] = chord_keys; mask_bits_set += 1; } Ok(active | chord_keys) } Event::Release(i, j) => { default_associated_coord = (i, j); Err(active) // released a chord key, abort } } } else if matches!(s.event, Event::Press(..)) { Err(active) // pressed a non-chord key, abort } else { Ok(active) } }); 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 // defined with an action, but the user has pressed all of h, j, k, l in the listed order, // so now kanata needs to break down the combo. How should it work? // // Figuratively "release" keys in reverse-temporal order until a valid chord is found. So // first, l is figuratively released, and if (h j k) is a valid chord, that action will // activate. If (l) by itself is valid that then activates after (h j k) is finished. // // In the case that (h j k) is not a chord, instead activate (h j). If that is a valid // chord, then try to activate (k l) together, and if not, evaluate (k), then (l). // // If (h j) is not a valid chord, try to activate (h). Then try to activate (j k l). If // that is invalid, try (j k), then (j). If (j k) is valid, try (l). If (j) is valid, try // (k l). If (k l) is invalid, try (k) then (l). // // The possible executions, listed in descending order of priority (first listed has // highest execution priority) are: // (h j k l) // (h j k) (l) // (h j) (k l) // (h j) (k) (l) // (h) (j k l) // (h) (j k) (l) // (h) (j) (k l) // (h) (j) (k) (l) 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 .iter() .copied() .reduce(|acc, e| acc | e) .unwrap_or(0); if let Some(action) = config.get_chord(chord_mask) { 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 // the individual start key. while end > start { let sub_chord = &chord_keys[start..end]; let chord_mask = sub_chord .iter() .copied() .reduce(|acc, e| acc | e) .unwrap_or(0); if let Some(action) = config.get_chord(chord_mask) { let coord = get_coord_for_chord(chord_mask); let _ = action_queue.push_back(Some(( coord, delay, action, Default::default(), ))); break; } end -= 1; } } start = if end <= start { start + 1 } else { end }; end = len; } } fn is_corresponding_release(&self, event: &Event) -> bool { matches!(event, Event::Release(i, j) if (*i, *j) == self.coord) } fn is_corresponding_press(&self, event: &Event) -> bool { matches!(event, Event::Press(i, j) if (*i, *j) == self.coord) } } type OneShotCoords = ArrayDeque; #[derive(Debug, Copy, Clone)] pub struct SequenceState<'a, T: 'a> { cur_event: Option>, delay: u32, // Keeps track of SequenceEvent::Delay time remaining tapped: Option, // Keycode of a key that should be released at the next tick remaining_events: &'a [SequenceEvent<'a, T>], } 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, /// KCoordinates of one shot keys that have been released 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, /// 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)] enum OneShotHandlePressKey { OneShotKey(KCoord), Other(KCoord), } impl OneShotState { 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 } } /// 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) => { if matches!( self.end_config, OneShotEndConfig::EndOnFirstReleaseOrRepress | OneShotEndConfig::EndOnFirstPressOrRepress ) && 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) => { if matches!( self.end_config, OneShotEndConfig::EndOnFirstPress | OneShotEndConfig::EndOnFirstPressOrRepress ) { 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. /// The second value in the tuple represents an overflow of released one shot keys and should /// be released is it is `Some`. fn handle_release(&mut self, (i, j): KCoord) -> (bool, Option) { if self.keys.is_empty() { return (true, None); } if !self.keys.contains(&(i, j)) { if matches!( self.end_config, OneShotEndConfig::EndOnFirstRelease | OneShotEndConfig::EndOnFirstReleaseOrRepress ) && self.other_pressed_keys.contains(&(i, j)) { self.release_on_next_tick = true; } (true, None) } else { // delay release for one shot keys (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. /// /// Events can be retrieved by iterating over this struct and calling [Queued::event]. #[derive(Clone)] pub struct QueuedIter<'a>(arraydeque::Iter<'a, Queued>); impl<'a> Iterator for QueuedIter<'a> { type Item = &'a Queued; fn next(&mut self) -> Option { self.0.next() } fn size_hint(&self) -> (usize, Option) { self.0.size_hint() } } /// An event, waiting in a queue to be processed. #[derive(Debug, Copy, Clone)] pub struct Queued { pub(crate) event: Event, pub(crate) since: u16, } impl From for Queued { fn from(event: Event) -> Self { Queued { event, since: 0 } } } impl Queued { 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); } /// Get the [Event] from this object. pub fn event(&self) -> Event { self.event } } #[derive(Default)] pub struct LastPressTracker { pub coord: KCoord, pub tap_hold_timeout: u16, } impl LastPressTracker { 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, T: 'a + Copy + std::fmt::Debug> Layout<'a, C, R, T> { /// Creates a new `Layout` object. 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 { timeout: 0, 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(), rpt_action: None, historical_keys: History::new(), historical_inputs: 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, 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 + 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 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; let delay = match w.config { WaitingConfig::HoldTap(..) | WaitingConfig::Chord(_) => w.delay + w.ticks, WaitingConfig::TapDance(_) => 0, }; 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; } // 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; self.do_action(hold, coord, delay, false, &mut layer_stack.into_iter()) } else { CustomEvent::NoEvent } } 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, }; 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 ret = self.do_action( tap, coord, delay, false, &mut layer_stack.clone().into_iter(), ); if let Some(pq) = pq { 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, &mut layer_stack.clone().into_iter(), ); } } 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(), ); } } } } _ => {} } 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, 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; let delay = match w.config { WaitingConfig::HoldTap(..) | WaitingConfig::Chord(_) => w.delay + w.ticks, WaitingConfig::TapDance(_) => 0, }; 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, &mut layer_stack.into_iter(), ) } else { CustomEvent::NoEvent } } fn drop_waiting(&mut self) -> CustomEvent<'a, T> { self.waiting = None; CustomEvent::NoEvent } /// A time event. /// /// This method must be called regularly, typically every millisecond. /// /// Returns the corresponding `CustomEvent`, allowing to manage /// custom actions thanks to the `Action::Custom` variant. pub fn tick(&mut self) -> CustomEvent<'a, T> { 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, delay, false, &mut layer_stack.into_iter()); } 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(); if tde.is_expired() { self.tap_dance_eager = None; } } self.process_sequences(); self.historical_keys.tick_hist(); self.historical_inputs.tick_hist(); let mut custom = CustomEvent::NoEvent; 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), since: 0, })); } } custom.update(match &mut self.waiting { 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 => { 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, /// giving us sequences (aka macros) of nearly limitless length! fn process_sequences(&mut self) { // Iterate over all active sequence events for _ in 0..self.active_sequences.len() { if let Some(mut seq) = self.active_sequences.pop_front() { // If we've encountered a SequenceEvent::Delay we must count // that down completely before doing anything else... if seq.delay > 0 { seq.delay = seq.delay.saturating_sub(1); } else if let Some(keycode) = seq.tapped { // Clear out the Press() matching this Tap()'s keycode self.states.retain(|s| s.seq_release(keycode).is_some()); seq.tapped = None; } else { // Pull the next SequenceEvent 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) => { 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 .handle_press(OneShotHandlePressKey::Other((0, 0))); } 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); } Some(SequenceEvent::Release(keycode)) => { // Nothing valid should be at (0, 0). It's fine to fake this. self.oneshot.handle_release((0, 0)); self.states.retain(|s| s.seq_release(keycode).is_some()); } Some(SequenceEvent::Delay { duration }) => { // 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; } } Some(SequenceEvent::Custom(custom)) => { let _ = self.states.push(State::SeqCustomPending(custom)); } _ => {} // We'll never get here } } if !seq.remaining_events.is_empty() { // Put it back self.active_sequences.push_back(seq); } } } if self.active_sequences.is_empty() { // Push only the latest pressed repeating macro. if let Some(State::RepeatingSequence { sequence, .. }) = self .states .iter() .rev() .find(|s| matches!(s, State::RepeatingSequence { .. })) { self.active_sequences.push_back(SequenceState { cur_event: None, delay: 0, tapped: None, remaining_events: sequence, }); } } } 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>, ) -> CustomEvent<'a, T> { 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 { State::SeqCustomPending(custom) => { current_custom.update(CustomEvent::Press(custom)); *state = State::SeqCustomActive(custom); break; } State::SeqCustomActive(custom) => { current_custom.update(CustomEvent::Release(custom)); *state = State::Tombstone; break; } _ => continue, }; } current_custom } fn dequeue(&mut self, queue: Queued) -> CustomEvent<'a, T> { use Event::*; match queue.event { Release(i, j) => { let mut custom = CustomEvent::NoEvent; let (do_release, overflow_key) = self.oneshot.handle_release((i, j)); if do_release { self.states.retain(|s| { !s.clear_on_next_release() && s.release((i, j), &mut custom).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).is_some() } } }); } if let Some((i2, j2)) = overflow_key { self.states .retain(|s| s.release((i2, j2), &mut custom).is_some()); } custom } Press(i, j) => { let mut layer_stack = self.trans_resolution_layer_order().into_iter(); 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_action, (i, j), queue.since, false, &mut layer_stack.skip(1), ); custom } else { // 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) } } else { self.do_action(&Action::Trans, (i, j), queue.since, false, &mut layer_stack) } } } } /// Register a key event. pub fn event(&mut self, event: Event) { if let Event::Press(x, y) = event { self.historical_inputs.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)); } 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); } } /// 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 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, } } if x == 0 { &self.src_keys[y] } else { &NoOp } } fn do_action( &mut self, action: &'a Action<'a, T>, coord: KCoord, delay: u16, is_oneshot: bool, layer_stack: &mut (impl Iterator + Clone), // used to resolve Trans action ) -> CustomEvent<'a, T> { 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::*; 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 => { // 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()); } 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 => { // 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? E.g. // fork and switch are inconsistent with each other even though the actions are // conceptually very similar. This is because switch can potentially activate // multiple actions (but not always), so uses the action queue, while fork does // not. As another example, 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()); } } HoldTap(HoldTapAction { timeout, hold, tap, 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 .iter_hevents() .find(|prior| prior.event.0 == REAL_KEY_ROW && prior.event != coord) .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); 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.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, }; 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, layer_stack)); } // Need to set tap_hold_tracker coord AFTER the checks. self.last_press_tracker.update_coord(coord); return custom; } &OneShot(oneshot) => { self.last_press_tracker.update_coord(coord); let custom = self.do_action(oneshot.action, coord, delay, true, &mut std::iter::empty()); // 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; self.oneshot.end_config = oneshot.end_config; if let Some(overflow) = self.oneshot.keys.push_back((coord.0, coord.1)) { self.event(Event::Release(overflow.0, overflow.1)); } 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.update_coord(coord); match td.config { TapDanceConfig::Lazy => { self.waiting = Some(WaitingState { coord, timeout: td.timeout, delay, ticks: 0, 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 => { match self.tap_dance_eager { None => { self.tap_dance_eager = Some(TapDanceEagerState { coord, actions: td.actions, timeout: td.timeout, orig_timeout: td.timeout, num_taps: 1, }) } Some(tde) => { if tde.coord != coord { self.tap_dance_eager = Some(TapDanceEagerState { coord, actions: td.actions, timeout: td.timeout, orig_timeout: td.timeout, num_taps: 1, }); } } }; return self.do_action(td.actions[0], coord, delay, false, layer_stack); } } } &Chords(chords) => { self.last_press_tracker.update_coord(coord); self.waiting = Some(WaitingState { coord, timeout: chords.timeout, delay, ticks: 0, 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.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 { 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.update_coord(coord); for &keycode in *v { // 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 { 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.update_coord(coord); let mut custom = CustomEvent::NoEvent; for action in *v { custom.update(self.do_action( action, coord, delay, is_oneshot, &mut layer_stack.clone(), )); } // 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 } => { self.active_sequences.push_back(SequenceState { cur_event: None, delay: 0, tapped: None, remaining_events: events, }); if !is_oneshot { self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } self.rpt_action = Some(action); } RepeatableSequence { events } => { self.active_sequences.push_back(SequenceState { cur_event: None, delay: 0, tapped: None, remaining_events: events, }); let _ = self.states.push(RepeatingSequence { sequence: events, coord, }); if !is_oneshot { 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 self.active_sequences.clear(); for fake_key in self.states.clone().iter() { if let FakeKey { keycode } = *fake_key { self.states.retain(|s| s.seq_release(keycode).is_some()); } } if !is_oneshot { self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } self.rpt_action = Some(action); } &Layer(value) => { 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.update_coord(coord); self.set_default_layer(*value); if !is_oneshot { self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } } Custom(value) => { self.last_press_tracker.update_coord(coord); if !is_oneshot { self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } self.rpt_action = Some(action); if self.states.push(State::Custom { value, coord }).is_ok() { return CustomEvent::Press(value); } } ReleaseState(rs) => { self.states.retain(|s| s.release_state(*rs).is_some()); if !is_oneshot { self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } self.rpt_action = Some(action); } Fork(fcfg) => { 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, &mut layer_stack.clone()) } true => { self.do_action(&fcfg.right, coord, delay, false, &mut layer_stack.clone()) } }; // 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 action_queue = &mut self.action_queue; 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, ) { action_queue.push_back(Some((coord, 0, ac, layer_stack.collect()))); } // Switch is not properly repeatable. This has to use the action queue for the // purpose of proper Custom action handling, because a single switch action can // activate multiple inner actions. But because of the use of the action queue, // switch has no way to set `rpt_action` after the queue is depleted. I suppose // that can be fixable, but for now will keep it as-is. } } CustomEvent::NoEvent } /// Obtain the index of the current active layer pub fn current_layer(&self) -> usize { self.states .iter() .rev() .find_map(State::get_layer) .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() { self.default_layer = value } } } #[cfg(test)] mod test { extern crate std; use super::{Event::*, Layout, *}; use crate::action::Action::*; use crate::action::HoldTapConfig; use crate::action::{k, l}; use crate::key_code::KeyCode; use crate::key_code::KeyCode::*; use std::collections::BTreeSet; #[track_caller] fn assert_keys(expected: &[KeyCode], iter: impl Iterator) { let expected: BTreeSet<_> = expected.iter().copied().collect(); let tested = iter.collect(); assert_eq!(expected, tested); } #[test] fn basic_hold_tap() { 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), timeout_action: k(RShift), 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(LShift), tap: k(Enter), config: HoldTapConfig::Default, tap_hold_interval: 0, }), ]], [[Trans, MultipleKeyCodes(&[LCtrl, Enter].as_slice())]], ]; let mut layout = Layout::new(LAYERS); 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, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Release(0, 0)); for _ in 0..197 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, Space], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] 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), timeout_action: l(1), 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(Enter), config: HoldTapConfig::Default, tap_hold_interval: 0, }), ]], [[Trans, MultipleKeyCodes(&[LCtrl, Enter].as_slice())]], ]; let mut layout = Layout::new(LAYERS); 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, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Release(0, 0)); for _ in 0..197 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LCtrl], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LCtrl], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LCtrl, Space], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LCtrl], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn hold_tap_interleaved_timeout() { 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), tap: k(Space), config: HoldTapConfig::Default, 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), tap: k(Enter), config: HoldTapConfig::Default, tap_hold_interval: 0, }), ]]]; let mut layout = Layout::new(LAYERS); 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)); for _ in 0..15 { 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()); for _ in 0..10 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Space], layout.keycodes()); } layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Space, LCtrl], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LCtrl], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn hold_on_press() { 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), tap: k(Space), config: HoldTapConfig::HoldOnOtherKeyPress, tap_hold_interval: 0, }), k(Enter), ]]]; let mut layout = Layout::new(LAYERS); // Press 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(&[LAlt], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt, Enter], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Enter], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Press another key after timeout assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 0)); for _ in 0..200 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt], layout.keycodes()); layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt, Enter], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Enter], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); 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> = &[[[ 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::PermissiveHold, tap_hold_interval: 0, }), k(Enter), ]]]; let mut layout = Layout::new(LAYERS); // 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(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()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt], 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(&[], 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> = &[ [[MultipleActions(&[l(1), k(LShift)].as_slice()), k(F)]], [[Trans, k(E)]], ]; let mut layout = Layout::new(LAYERS); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, E], layout.keycodes()); layout.event(Release(0, 1)); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn custom() { 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()); // Custom event layout.event(Press(0, 0)); assert_eq!(CustomEvent::Press(&42), layout.tick()); assert_keys(&[], layout.keycodes()); // nothing more assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // release custom layout.event(Release(0, 0)); assert_eq!(CustomEvent::Release(&42), layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn multiple_layers() { 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); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_eq!(0, layout.current_layer()); assert_keys(&[], layout.keycodes()); // press L1 layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_eq!(1, layout.current_layer()); assert_keys(&[], layout.keycodes()); // press L3 on L1 layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_eq!(3, layout.current_layer()); assert_keys(&[], layout.keycodes()); // release L1, still on l3 layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_eq!(3, layout.current_layer()); assert_keys(&[], layout.keycodes()); // press and release C on L3 layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[C], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // release L3, back to L0 layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_eq!(0, layout.current_layer()); assert_keys(&[], layout.keycodes()); // back to empty, going to L2 layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_eq!(2, layout.current_layer()); assert_keys(&[], layout.keycodes()); // and press the L0 key on L2 layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_eq!(0, layout.current_layer()); assert_keys(&[], layout.keycodes()); // release the L0, back to L2 layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_eq!(2, layout.current_layer()); assert_keys(&[], layout.keycodes()); // release the L2, back to L0 layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_eq!(0, layout.current_layer()); assert_keys(&[], layout.keycodes()); } #[test] fn custom_handler() { fn always_tap(_: QueuedIter, _: KCoord) -> (Option, bool) { (Some(WaitingAction::Tap), false) } fn always_hold(_: QueuedIter, _: KCoord) -> (Option, bool) { (Some(WaitingAction::Hold), false) } fn always_nop(_: QueuedIter, _: KCoord) -> (Option, bool) { (Some(WaitingAction::NoOp), false) } fn always_none(_: QueuedIter, _: KCoord) -> (Option, bool) { (None, false) } 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), tap: k(Kb0), config: HoldTapConfig::Custom(&always_tap), 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), tap: k(Kb2), config: HoldTapConfig::Custom(&always_hold), 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), tap: k(Kb4), config: HoldTapConfig::Custom(&always_nop), 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), tap: k(Kb6), config: HoldTapConfig::Custom(&always_none), tap_hold_interval: 0, }), ]]]; let mut layout = Layout::new(LAYERS); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Custom handler always taps layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb0], layout.keycodes()); // nothing more layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Custom handler always holds layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb3], layout.keycodes()); // nothing more layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Custom handler always prevents any event layout.event(Press(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // even timeout does not trigger for _ in 0..200 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // nothing more layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Custom handler timeout fallback layout.event(Press(0, 3)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); for _ in 0..199 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb7], layout.keycodes()); } #[test] fn tap_hold_interval() { 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), tap: k(Space), config: HoldTapConfig::Default, tap_hold_interval: 200, }), k(Enter), ]]]; let mut layout = Layout::new(LAYERS); // press and release the HT key, expect tap action 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(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Space], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // press again within tap_hold_interval, tap action should be in keycode immediately layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Space], layout.keycodes()); // tap action should continue to be in keycodes even after timeout for _ in 0..300 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Space], layout.keycodes()); } layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Press again. This is outside the tap_hold_interval window, so should result in hold // action. layout.event(Press(0, 0)); for _ in 0..200 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn tap_hold_interval_interleave() { 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: 200, }), k(Enter), HoldTap(&HoldTapAction { on_press_reset_timeout_to: None, require_prior_idle: None, timeout: 200, hold: k(LAlt), timeout_action: k(LAlt), tap: k(Enter), config: HoldTapConfig::Default, tap_hold_interval: 200, }), ]]]; let mut layout = Layout::new(LAYERS); // press and release the HT key, expect tap action 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(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Space], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // press a different key in between layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Enter], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // press HT key again, should result in hold action layout.event(Press(0, 0)); for _ in 0..200 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // press HT key, press+release diff key, release HT key 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(Release(0, 1)); 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(&[Enter, Space], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Space], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // press HT key again, should result in hold action layout.event(Press(0, 0)); for _ in 0..200 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // press HT key, press+release diff (HT) key, release HT key 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, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Release(0, 2)); 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(&[Space], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Enter, Space], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Space], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // press HT key again, should result in hold action layout.event(Press(0, 0)); for _ in 0..200 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt], layout.keycodes()); } #[test] fn tap_hold_interval_short_hold() { 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), tap: k(Space), config: HoldTapConfig::Default, tap_hold_interval: 200, })]]]; let mut layout = Layout::new(LAYERS); // press and hold the HT key, expect hold action assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 0)); for _ in 0..50 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // press and hold the HT key, expect hold action, even though it's within the // tap_hold_interval assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 0)); for _ in 0..50 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn tap_hold_interval_different_hold() { 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), tap: k(Space), config: HoldTapConfig::Default, 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), tap: k(Enter), config: HoldTapConfig::Default, tap_hold_interval: 200, }), ]]]; let mut layout = Layout::new(LAYERS); // press HT1, press HT2, release HT1 after hold timeout, release HT2, press HT2 layout.event(Press(0, 0)); layout.event(Press(0, 1)); for _ in 0..50 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt, Enter], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Enter], layout.keycodes()); // press HT2 again, should result in tap action layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); for _ in 0..300 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Enter], layout.keycodes()); } } #[test] fn one_shot() { static LAYERS: Layers<3, 1> = &[[[ OneShot(&crate::action::OneShot { timeout: 100, action: &k(LShift), end_config: OneShotEndConfig::EndOnFirstPress, }), k(A), k(B), ]]]; let mut layout = Layout::new(LAYERS); layout.oneshot.pause_input_processing_delay = 1; // Test: // 1. press one-shot // 2. release one-shot // 3. press A within timeout // 4. press B within timeout // 5. release A, B layout.event(Press(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Release(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Press(0, 1)); layout.event(Press(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A, LShift], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A, B], layout.keycodes()); layout.event(Release(0, 1)); layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[B], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: // 1. press one-shot // 2. release one-shot // 3. press A after timeout // 4. release A layout.event(Press(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Release(0, 0)); for _ in 0..75 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: // 1. press one-shot // 2. press A // 3. release A // 4. release one-shot layout.event(Press(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, A], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: // 1. press one-shot // 2. press A after timeout // 3. release A // 4. release one-shot layout.event(Press(0, 0)); for _ in 0..200 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, A], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn one_shot_end_press_or_repress() { static LAYERS: Layers<3, 1> = &[[[ OneShot(&crate::action::OneShot { timeout: 100, action: &k(LShift), end_config: OneShotEndConfig::EndOnFirstPressOrRepress, }), k(A), k(B), ]]]; let mut layout = Layout::new(LAYERS); layout.oneshot.pause_input_processing_delay = 1; // Test: // 1. press one-shot // 2. release one-shot // 3. press A within timeout // 4. press B within timeout // 5. release A, B layout.event(Press(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Release(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Press(0, 1)); layout.event(Press(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A, LShift], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A, B], layout.keycodes()); layout.event(Release(0, 1)); layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[B], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: // 1. press one-shot // 2. release one-shot // 3. press A after timeout // 4. release A layout.event(Press(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Release(0, 0)); for _ in 0..75 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: // 1. press one-shot // 2. press A // 3. release A // 4. release one-shot layout.event(Press(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, A], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: // 1. press one-shot // 2. press A after timeout // 3. release A // 4. release one-shot layout.event(Press(0, 0)); for _ in 0..200 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, A], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: // 1. press one-shot // 2. release one-shot // 3. press one-shot within timeout // 4. release one-shot quickly - should end layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: // 1. press one-shot // 2. release one-shot // 3. press one-shot within timeout // 4. release one-shot after timeout layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); for _ in 0..200 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn one_shot_end_on_release() { static LAYERS: Layers<3, 1> = &[[[ OneShot(&crate::action::OneShot { timeout: 100, action: &k(LShift), end_config: OneShotEndConfig::EndOnFirstRelease, }), k(A), k(B), ]]]; let mut layout = Layout::new(LAYERS); // Test: // 1. press one-shot // 2. release one-shot // 3. press A within timeout // 4. press B within timeout // 5. release A, B layout.event(Press(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Release(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Press(0, 1)); layout.event(Press(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A, LShift], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A, B, LShift], layout.keycodes()); layout.event(Release(0, 1)); layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[B, LShift], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: // 1. press one-shot // 2. release one-shot // 3. press A after timeout // 4. release A layout.event(Press(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Release(0, 0)); for _ in 0..75 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: // 1. press one-shot // 2. press A // 3. release A // 4. release one-shot layout.event(Press(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, A], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: // 1. press one-shot // 2. press A after timeout // 3. release A // 4. release one-shot layout.event(Press(0, 0)); for _ in 0..200 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, A], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: // 3. press A // 1. press one-shot // 2. release one-shot // 3. release A // 4. press B within timeout // 5. release B layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A], layout.keycodes()); layout.event(Press(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A, LShift], layout.keycodes()); } layout.event(Release(0, 0)); for _ in 0..25 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A, LShift], layout.keycodes()); } layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Press(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[B, LShift], layout.keycodes()); layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn one_shot_multi() { static LAYERS: Layers<4, 1> = &[ [[ OneShot(&crate::action::OneShot { timeout: 100, action: &k(LShift), end_config: OneShotEndConfig::EndOnFirstPress, }), OneShot(&crate::action::OneShot { timeout: 100, action: &k(LCtrl), end_config: OneShotEndConfig::EndOnFirstPress, }), OneShot(&crate::action::OneShot { timeout: 100, action: &Layer(1), end_config: OneShotEndConfig::EndOnFirstPress, }), NoOp, ]], [[k(A), k(B), k(C), k(D)]], ]; let mut layout = Layout::new(LAYERS); layout.oneshot.pause_input_processing_delay = 1; layout.event(Press(0, 0)); layout.event(Release(0, 0)); for _ in 0..90 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Press(0, 1)); for _ in 0..90 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, LCtrl], layout.keycodes()); } assert_eq!(layout.current_layer(), 0); layout.event(Press(0, 2)); layout.event(Release(0, 2)); for _ in 0..90 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, LCtrl], layout.keycodes()); assert_eq!(layout.current_layer(), 1); } layout.event(Press(0, 3)); layout.event(Release(0, 3)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, LCtrl, D], layout.keycodes()); assert_eq!(layout.current_layer(), 1); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LCtrl], layout.keycodes()); assert_eq!(layout.current_layer(), 0); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn one_shot_tap_hold() { static LAYERS: Layers<3, 1> = &[ [[ OneShot(&crate::action::OneShot { timeout: 200, action: &k(LShift), 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), tap: k(Space), config: HoldTapConfig::Default, tap_hold_interval: 0, }), NoOp, ]], [[k(A), k(B), k(C)]], ]; let mut layout = Layout::new(LAYERS); layout.oneshot.pause_input_processing_delay = 1; layout.event(Press(0, 0)); layout.event(Release(0, 0)); for _ in 0..90 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Press(0, 1)); for _ in 0..90 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, Space], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 0)); layout.event(Release(0, 0)); for _ in 0..90 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } layout.event(Press(0, 1)); for _ in 0..100 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, LAlt], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn tap_dance_uneager() { static LAYERS: Layers<2, 2> = &[[ [ TapDance(&crate::action::TapDance { timeout: 100, actions: &[ &k(LShift), &OneShot(&crate::action::OneShot { timeout: 100, action: &k(LCtrl), 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), tap: k(Space), config: HoldTapConfig::Default, tap_hold_interval: 0, }), ], config: TapDanceConfig::Lazy, }), k(A), ], [k(B), k(C)], ]]; let mut layout = Layout::new(LAYERS); // Test: tap-dance first key, timeout layout.event(Press(0, 0)); for _ in 0..100 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: tap-dance first key, press another key 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(&[LShift], layout.keycodes()); layout.event(Release(0, 0)); assert_keys(&[LShift], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, A], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: tap-dance second key, timeout layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Release(0, 0)); for _ in 0..50 { 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(Release(0, 0)); for _ in 0..99 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } for _ in 0..100 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LCtrl], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: tap-dance third key, timeout, tap layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Release(0, 0)); for _ in 0..50 { 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(Release(0, 0)); for _ in 0..50 { 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(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: tap-dance third key, timeout, hold layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Release(0, 0)); for _ in 0..50 { 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(Release(0, 0)); for _ in 0..50 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } layout.event(Press(0, 0)); for _ in 0..100 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } for _ in 0..200 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt], layout.keycodes()); } layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn tap_dance_eager() { static LAYERS: Layers<2, 2> = &[[ [ TapDance(&crate::action::TapDance { timeout: 100, actions: &[&k(Kb1), &k(Kb2), &k(Kb3)], config: TapDanceConfig::Eager, }), k(A), ], [k(B), k(C)], ]]; let mut layout = Layout::new(LAYERS); // Test: tap-dance-eager first key layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb1], layout.keycodes()); for _ in 0..200 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb1], layout.keycodes()); } layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: tap-dance-eager first key, press another key layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb1], layout.keycodes()); layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb1, A], layout.keycodes()); layout.event(Release(0, 0)); assert_keys(&[Kb1, A], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: tap-dance second key layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb1], layout.keycodes()); layout.event(Release(0, 0)); for _ in 0..50 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb2], layout.keycodes()); layout.event(Release(0, 0)); for _ in 0..99 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Test: tap-dance third key layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb1], layout.keycodes()); layout.event(Release(0, 0)); for _ in 0..50 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb2], layout.keycodes()); layout.event(Release(0, 0)); for _ in 0..50 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb3], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn release_state() { static LAYERS: Layers<2, 1> = &[ [[ MultipleActions(&(&[KeyCode(LCtrl), Layer(1)] as _)), MultipleActions(&(&[KeyCode(LAlt), Layer(1)] as _)), ]], [[ MultipleActions( &(&[ReleaseState(ReleasableState::KeyCode(LAlt)), KeyCode(Space)] as _), ), ReleaseState(ReleasableState::Layer(1)), ]], ]; let mut layout = Layout::new(LAYERS); layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LAlt], layout.keycodes()); assert_eq!(1, layout.current_layer()); layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Space], layout.keycodes()); layout.event(Release(0, 0)); 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()); layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LCtrl], layout.keycodes()); assert_eq!(1, layout.current_layer()); layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_eq!(0, layout.current_layer()); assert_keys(&[LCtrl], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LCtrl], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn test_chord() { const GROUP: ChordsGroup = ChordsGroup { coords: &[((0, 2), 1), ((0, 3), 2), ((0, 4), 4), ((0, 5), 8)], chords: &[ (1, &KeyCode(Kb1)), (2, &KeyCode(Kb2)), (4, &KeyCode(Kb3)), (8, &KeyCode(Kb4)), (3, &KeyCode(Kb5)), (11, &KeyCode(Kb6)), ], timeout: 100, }; static LAYERS: Layers<6, 1> = &[[[ NoOp, NoOp, Chords(&GROUP), Chords(&GROUP), Chords(&GROUP), Chords(&GROUP), ]]]; let mut layout = Layout::new(LAYERS); layout.event(Press(0, 2)); // timeout on non-terminal chord for _ in 0..50 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } layout.event(Press(0, 3)); for _ in 0..49 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb5], layout.keycodes()); layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb5], layout.keycodes()); layout.event(Release(0, 3)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // timeout on terminal chord with no action associated // combo like (h j k) -> (h j) (k) layout.event(Press(0, 2)); for _ in 0..50 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } layout.event(Press(0, 3)); for _ in 0..30 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } layout.event(Press(0, 4)); for _ in 0..20 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb5], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb5, Kb3], layout.keycodes()); layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); 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)); for _ in 0..50 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } layout.event(Press(0, 3)); for _ in 0..30 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } layout.event(Press(0, 4)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Release(0, 4)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb5], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb5, Kb3], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); 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()); // release terminal chord with no action associated // Test combo like (h j k l) -> (h) (j k l) layout.event(Press(0, 4)); layout.event(Press(0, 2)); layout.event(Press(0, 3)); layout.event(Press(0, 5)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); for _ in 0..30 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb3], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb3, Kb6], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb3], layout.keycodes()); } #[test] fn test_chord_normalkey_order() { const GROUP: ChordsGroup = ChordsGroup { coords: &[((0, 2), 1), ((0, 3), 2), ((0, 4), 4), ((0, 5), 8)], chords: &[ (1, &KeyCode(Kb1)), (2, &KeyCode(Kb2)), (4, &KeyCode(Kb3)), (8, &KeyCode(Kb4)), (3, &KeyCode(Kb5)), (11, &KeyCode(Kb6)), ], timeout: 100, }; static LAYERS: Layers<6, 1> = &[[[ NoOp, k(A), Chords(&GROUP), Chords(&GROUP), Chords(&GROUP), Chords(&GROUP), ]]]; let mut layout = Layout::new(LAYERS); layout.event(Press(0, 2)); // timeout on non-terminal chord for _ in 0..50 { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb1], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb1, A], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb1], layout.keycodes()); layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); 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> = &[[[ Fork(&ForkConfig { left: k(Kb1), right: k(Kb2), right_triggers: &[Space], }), k(Space), ]]]; let mut layout = Layout::new(LAYERS); layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb1], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 1)); layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Space], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Space, Kb2], layout.keycodes()); layout.event(Release(0, 1)); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[Kb2], layout.keycodes()); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } #[test] fn test_repeat() { static LAYERS: Layers<5, 1> = &[ [[ k(A), MultipleKeyCodes(&[LShift, B].as_slice()), Repeat, MultipleActions(&[k(C), k(D)].as_slice()), Layer(1), ]], [[ k(E), MultipleKeyCodes(&[LShift, F].as_slice()), Repeat, MultipleActions(&[k(G), k(H)].as_slice()), Layer(1), ]], ]; let mut layout = Layout::new(LAYERS); // Press a key layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Repeat it, should be the same layout.event(Press(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[A], layout.keycodes()); layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Press a chord layout.event(Press(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, B], layout.keycodes()); layout.event(Release(0, 1)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Repeat it, should be the same layout.event(Press(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[LShift, B], layout.keycodes()); layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Press a multiple action layout.event(Press(0, 3)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[C, D], layout.keycodes()); layout.event(Release(0, 3)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Repeat it, should be the same layout.event(Press(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[C, D], layout.keycodes()); layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Go to a different layer and press a key layout.event(Press(0, 4)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[E], layout.keycodes()); layout.event(Release(0, 4)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[E], layout.keycodes()); layout.event(Release(0, 0)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Repeat, should be the same as the other layer layout.event(Press(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[E], layout.keycodes()); layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); // Activate the layer action and press repeat there, should still be the same action layout.event(Press(0, 4)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Press(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[E], layout.keycodes()); layout.event(Release(0, 2)); assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); layout.event(Release(0, 4)); 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)], }), ]], ]; 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)); // No idea why we have to wait 2 ticks here. Is this a bug in switch? assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); 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()); } } ================================================ FILE: keyberon/src/lib.rs ================================================ //! 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. pub mod action; pub mod chord; pub mod key_code; pub mod layout; mod multikey_buffer; pub mod tap_hold_tracker; ================================================ FILE: keyberon/src/multikey_buffer.rs ================================================ //! 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)); } } } ================================================ FILE: keyberon/src/tap_hold_tracker.rs ================================================ //! 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::*; ================================================ FILE: parser/.gitignore ================================================ # Ignore Cargo.lock since this is a library crate Cargo.lock target ================================================ FILE: parser/Cargo.toml ================================================ [package] name = "kanata-parser" version = "0.1110.0" 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-only" edition = "2021" [dependencies] anyhow = "1" bitflags = "2.5.0" bytemuck = "1.15.0" itertools = "0.12" log = { version = "0.4.8", default-features = false } miette = { version = "5.7.0", features = ["fancy"] } once_cell = "1" ordered-float = "5.1.0" parking_lot = "0.12" patricia_tree = "0.8" rustc-hash = "1.1.0" thiserror = "1.0.38" kanata-keyberon = { path = "../keyberon", version = "0.1110.0" } [dev-dependencies] simplelog = "0.12.0" [features] cmd = [] interception_driver = [] gui = [] lsp = [] win_llhook_read_scancodes = [] win_sendinput_send_scancodes = [] zippychord = [] ================================================ FILE: parser/LICENSE ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ================================================ FILE: parser/README.md ================================================ # 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. ================================================ FILE: parser/src/cfg/alloc.rs ================================================ //! This module contains a helper struct for generating 'static lifetime allocations while still //! keeping track of them so that they can be freed later. use parking_lot::Mutex; use std::sync::Arc; /// This struct tracks the allocations that are leaked by its provided methods and frees them when /// dropped. The `new` function is unsafe because dropping the struct can create dangling /// references. Care must be taken to ensure that all allocations made by this struct's methods are /// no longer referenced when the struct gets dropped. /// /// 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>, } #[derive(Debug, Copy, Clone)] pub(crate) struct Allocation { ptr: usize, len: usize, } impl std::fmt::Debug for Allocations { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Allocations").finish() } } impl Drop for Allocations { fn drop(&mut self) { log::debug!( "freeing allocations of length {}", self.allocations.lock().len() ); 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, ))) }; } } } impl Allocations { /// Create a new allocations group. /// /// # Safety /// /// Ensure that all associated allocations are no longer referenced before dropping all /// clones of the `Arc`. pub(crate) unsafe fn new() -> Arc { Arc::new(Self { allocations: Mutex::new(vec![]), }) } /// Returns a `&'static T` by leaking a newly created Box of `v`. 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"); } 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) }) } 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() { 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(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(crate) fn sref_slice(&self, v: T) -> &'static [&'static T] { log::debug!("sref_slice {}", std::any::type_name::()); 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 } } } ================================================ FILE: parser/src/cfg/arbitrary_code.rs ================================================ 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]))?; Ok(s.a.sref(Action::Custom( s.a.sref(s.a.sref_slice(CustomAction::SendArbitraryCode(code))), ))) } ================================================ FILE: parser/src/cfg/caps_word.rs ================================================ 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")?; Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice( 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, }), ))))) } 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")?; Ok(s.a.sref(Action::Custom( s.a.sref( s.a.sref_slice(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, })), ), ))) } ================================================ FILE: parser/src/cfg/chord.rs ================================================ use itertools::Itertools; 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_vec()) } _ => 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_vec(); 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, &&[&CustomAction]>], ) -> 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_vec(); // 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 } } ================================================ FILE: parser/src/cfg/chord_v1.rs ================================================ 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<&&[&CustomAction]>], 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 }) => { 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), }))) } } } ================================================ FILE: parser/src/cfg/clipboard.rs ================================================ 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(); Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice( CustomAction::ClipboardSet(s.a.sref_str(clip_string.to_string())), ))))) } 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")?; Ok(s.a.sref(Action::Custom( s.a.sref(s.a.sref_slice(CustomAction::ClipboardSave(id))), ))) } 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")?; Ok(s.a.sref(Action::Custom( s.a.sref(s.a.sref_slice(CustomAction::ClipboardRestore(id))), ))) } 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")?; Ok(s.a.sref(Action::Custom( s.a.sref(s.a.sref_slice(CustomAction::ClipboardSaveSwap(id1, id2))), ))) } 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"))?; Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice( CustomAction::ClipboardSaveSet(id, s.a.sref_str(save_content.into())), ))))) } ================================================ FILE: parser/src/cfg/cmd.rs ================================================ 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(); Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice( CustomAction::CmdLog(log_level, error_log_level, s.a.sref_vec(cmds)), ))))) } #[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 Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice( CustomAction::ClipboardSaveCmdSet(save_id, s.a.sref_vec(cmds)), ))))); } 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); Ok(s.a .sref(Action::Custom(s.a.sref(s.a.sref_slice(match cmd_type { CmdType::Standard => CustomAction::Cmd(cmds), CmdType::OutputKeys => CustomAction::CmdOutputKeys(cmds), CmdType::ClipboardSet => CustomAction::ClipboardCmdSet(cmds), CmdType::ClipboardSaveSet => unreachable!(), }))))) } } /// 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" ] ); } ================================================ FILE: parser/src/cfg/custom_tap_hold.rs ================================================ 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 CustomTapHoldFn { let keys = a.sref_vec(Vec::from_iter(keys.iter().copied())); 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) }, ) } 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) }, ) } ================================================ FILE: parser/src/cfg/defcfg.rs ================================================ 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>, } #[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 = "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 = "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); } } "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 = "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, } ================================================ FILE: parser/src/cfg/defhands.rs ================================================ 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 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, )?); } _ => bail_expr!( &option[0], "unknown option '{}' for tap-hold-opposite-hand. \ Valid options: timeout, same-hand, neutral, unknown-hand, neutral-keys, require-prior-idle", 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: 0, 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), } } ================================================ FILE: parser/src/cfg/deflayer.rs ================================================ 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))) } ================================================ FILE: parser/src/cfg/deflocalkeys.rs ================================================ 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) } ================================================ FILE: parser/src/cfg/defsrc.rs ================================================ 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 } ================================================ FILE: parser/src/cfg/deftemplate.rs ================================================ //! 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