├── .devcontainer └── devcontainer.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── pull_request_template.md └── workflows │ ├── macos-build.yml │ ├── rust.yml │ └── windows-build.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── EnableUIAccess ├── EnableUIAccess_launch.ahk ├── Lib │ └── EnableUIAccess.ahk └── README.md ├── LICENSE ├── README.md ├── assets ├── Interception.zip ├── kanata-icon.svg ├── kanata.ico └── reload_32px.png ├── 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 ├── included-file.kbd ├── japanese_mac_eisu_kana.kbd ├── jtroo.kbd ├── kanata.kbd ├── key-toggle_press-only_release-only.kbd ├── minimal.kbd ├── simple.kbd └── tray-icon │ ├── 3trans.parent.png │ ├── 6name-match.png │ ├── _custom-icons │ └── s.png │ ├── icons │ └── 1symbols.png │ ├── img │ └── 2Nav Num.png │ ├── license_icons.txt │ ├── tray-icon.kbd │ └── tray-icon.png ├── docs ├── README.md ├── config-stylesheet.css ├── config.adoc ├── design.md ├── fancy_symbols.md ├── interception.md ├── kanata-basic-diagram.svg ├── 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 └── win-tray │ ├── win-tray-layer-change.gif │ └── win-tray-screen.png ├── example_tcp_client ├── .gitignore ├── Cargo.toml └── src │ └── main.rs ├── interception ├── Cargo.lock ├── Cargo.toml └── src │ ├── lib.rs │ └── scancode.rs ├── justfile ├── key-sort-add ├── Cargo.lock ├── Cargo.toml ├── README.md ├── mapping.txt └── src │ └── main.rs ├── keyberon ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── KEYBOARDS.md ├── LICENSE ├── README.md ├── images │ └── keyberon.jpg ├── keyberon-macros │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs └── src │ ├── action.rs │ ├── action │ └── switch.rs │ ├── chord.rs │ ├── key_code.rs │ ├── layout.rs │ ├── lib.rs │ └── multikey_buffer.rs ├── parser ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── src │ ├── cfg │ │ ├── alloc.rs │ │ ├── chord.rs │ │ ├── custom_tap_hold.rs │ │ ├── defcfg.rs │ │ ├── deftemplate.rs │ │ ├── error.rs │ │ ├── fake_key.rs │ │ ├── is_a_button.rs │ │ ├── key_outputs.rs │ │ ├── key_override.rs │ │ ├── layer_opts.rs │ │ ├── list_actions.rs │ │ ├── mod.rs │ │ ├── permutations.rs │ │ ├── platform.rs │ │ ├── sexpr.rs │ │ ├── str_ext.rs │ │ ├── switch.rs │ │ ├── tests.rs │ │ ├── tests │ │ │ ├── ambiguous.rs │ │ │ ├── defcfg.rs │ │ │ ├── device_detect.rs │ │ │ ├── environment.rs │ │ │ └── macros.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 ├── simulated_input ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ └── sim.rs ├── simulated_passthru ├── Cargo.lock ├── 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.exe.manifest.rc ├── 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.rs │ ├── output_logic │ │ └── zippychord.rs │ ├── sequences.rs │ ├── unknown.rs │ └── windows │ │ ├── exthook.rs │ │ ├── interception.rs │ │ ├── llhook.rs │ │ └── mod.rs ├── lib.rs ├── main.rs ├── main_lib │ ├── 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.rs │ │ ├── mod.rs │ │ └── scancode_to_usvk.rs ├── tcp_server.rs ├── tests.rs └── tests │ └── 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 │ ├── override_tests.rs │ ├── release_sim_tests.rs │ ├── repeat_sim_tests.rs │ ├── seq_sim_tests.rs │ ├── switch_sim_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 ├── tcp_protocol ├── Cargo.toml └── src │ └── lib.rs ├── wasm ├── .gitignore ├── Cargo.toml ├── README.md └── src │ └── lib.rs └── windows_key_tester ├── Cargo.lock ├── Cargo.toml ├── README.md └── src ├── main.rs ├── windows.rs └── windows ├── interception.rs └── llhook.rs /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rust", 3 | "image": "mcr.microsoft.com/devcontainers/rust:1-buster" 4 | 5 | // Features to add to the dev container. More info: https://containers.dev/implementors/features. 6 | // "features": {}, 7 | 8 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 9 | // "forwardPorts": [], 10 | 11 | // Use 'postCreateCommand' to run commands after the container is created. 12 | // "postCreateCommand": "rustc --version", 13 | 14 | // Configure tool-specific properties. 15 | // "customizations": {}, 16 | 17 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 18 | // "remoteUser": "root" 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "Bug report" 2 | description: Create a report to help us improve. 3 | labels: ["bug"] 4 | assignees: ["jtroo"] 5 | title: "Bug: title_goes_here" 6 | body: 7 | - type: checkboxes 8 | attributes: 9 | label: Requirements 10 | description: Before you create a bug report, please check the following 11 | options: 12 | - 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. 13 | required: true 14 | - 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). 15 | required: true 16 | - type: textarea 17 | id: summary 18 | attributes: 19 | label: Describe the bug 20 | description: A clear and concise description of what the bug is. 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: config 25 | attributes: 26 | label: Relevant kanata config 27 | description: E.g. defcfg, defsrc, deflayer, defalias items. If in doubt, feel free to include your entire config. Please ensure to use code formatting, e.g. surround with triple backticks to avoid pinging users with the @ character. 28 | validations: 29 | required: false 30 | - type: textarea 31 | id: reproduce 32 | attributes: 33 | label: To Reproduce 34 | description: | 35 | Walk us through the steps needed to reproduce the bug. 36 | value: | 37 | 1. 38 | 2. 39 | 3. 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: expected 44 | attributes: 45 | label: Expected behavior 46 | description: A clear and concise description of what you expected to happen. 47 | validations: 48 | required: true 49 | - type: input 50 | id: version 51 | attributes: 52 | label: Kanata version 53 | 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. 54 | placeholder: e.g. kanata 1.3.0 55 | validations: 56 | required: true 57 | - type: textarea 58 | id: logs 59 | attributes: 60 | label: Debug logs 61 | 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. 62 | validations: 63 | required: false 64 | - type: input 65 | id: os 66 | attributes: 67 | label: Operating system 68 | description: Linux or Windows? 69 | placeholder: e.g. Windows 11 70 | validations: 71 | required: true 72 | - type: textarea 73 | id: additional 74 | attributes: 75 | label: Additional context 76 | description: Add any other context about the problem here. 77 | validations: 78 | required: false 79 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Discussions 4 | url: https://github.com/jtroo/kanata/discussions 5 | about: Ask for help or interact with the community. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "Feature request" 2 | description: Suggest an idea for this project 3 | title: 'Feature request: feature_summary_goes_here' 4 | labels: ["enhancement"] 5 | assignees: [] 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Is your feature request related to a problem? Please describe. 10 | description: | 11 | A clear and concise description of what the problem is. 12 | placeholder: Ex. I'm always frustrated when [...] 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: Describe the solution you'd like. 18 | description: | 19 | A clear and concise description of what you want to happen. 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: Describe alternatives you've considered. 25 | description: | 26 | A clear and concise description of any alternative solutions or features you've considered. 27 | validations: 28 | required: true 29 | - type: textarea 30 | attributes: 31 | label: Additional context 32 | description: | 33 | Add any other context or screenshots about the feature request here. 34 | validations: 35 | required: false 36 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes. Use imperative present tense. 2 | 3 | ## Checklist 4 | 5 | - Add documentation to docs/config.adoc 6 | - [ ] Yes or N/A 7 | - Add example and basic docs to cfg_samples/kanata.kbd 8 | - [ ] Yes or N/A 9 | - Update error messages 10 | - [ ] Yes or N/A 11 | - Added tests, or did manual testing 12 | - [ ] Yes 13 | -------------------------------------------------------------------------------- /.github/workflows/macos-build.yml: -------------------------------------------------------------------------------- 1 | name: macos-build 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: [ "main" ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | RUSTFLAGS: "-Dwarnings" 10 | 11 | jobs: 12 | build-macos-aarch: 13 | runs-on: macos-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: stable 20 | override: true 21 | target: aarch64-apple-darwin 22 | - uses: Swatinem/rust-cache@v2 23 | - name: Do the stuff on arm64 24 | shell: bash 25 | run: | 26 | mkdir -p artifacts-arm64 27 | cargo build --release --target aarch64-apple-darwin 28 | mv target/aarch64-apple-darwin/release/kanata artifacts-arm64/kanata_macos_arm64 29 | cargo build --release --features cmd --target aarch64-apple-darwin 30 | mv target/aarch64-apple-darwin/release/kanata artifacts-arm64/kanata_macos_cmd_allowed_arm64 31 | - uses: actions/upload-artifact@v4 32 | with: 33 | name: macos-binaries-arm64 34 | path: | 35 | artifacts-arm64/kanata_macos_arm64 36 | artifacts-arm64/kanata_macos_cmd_allowed_arm64 37 | 38 | build-macos: 39 | runs-on: macos-13 40 | 41 | steps: 42 | - uses: actions/checkout@v3 43 | - uses: Swatinem/rust-cache@v2 44 | with: 45 | shared-key: "persist-cross-job" 46 | - name: Do the stuff on x86-64 47 | shell: bash 48 | run: | 49 | mkdir -p artifacts 50 | cargo build --release 51 | mv target/release/kanata artifacts/kanata_macos_x86_64 52 | cargo build --release --features cmd 53 | mv target/release/kanata artifacts/kanata_macos_cmd_allowed_x86_64 54 | - uses: actions/upload-artifact@v4 55 | with: 56 | name: macos-binaries-x86-64 57 | path: | 58 | artifacts/kanata_macos_x86_64 59 | artifacts/kanata_macos_cmd_allowed_x86_64 60 | 61 | -------------------------------------------------------------------------------- /.github/workflows/windows-build.yml: -------------------------------------------------------------------------------- 1 | name: windows-build 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: [ "main" ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | RUSTFLAGS: "-Dwarnings" 10 | 11 | jobs: 12 | build-windows: 13 | runs-on: windows-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: Swatinem/rust-cache@v2 18 | with: 19 | shared-key: "persist-cross-job" 20 | - name: Build x64 21 | shell: bash 22 | run: | 23 | mkdir -p artifacts 24 | cargo build --release --target x86_64-pc-windows-msvc 25 | move target\release\kanata.exe artifacts\kanata_windows_x64.exe 26 | cargo build --release --features cmd --target x86_64-pc-windows-msvc 27 | move target\release\kanata.exe artifacts\kanata_windows_cmd_allowed_x64.exe 28 | - uses: actions/upload-artifact@v4 29 | with: 30 | name: windows-binaries-x64 31 | path: | 32 | artifacts/kanata_windows_x64.exe 33 | artifacts/kanata_windows_cmd_allowed_x64.exe 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | .vscode/ 3 | -------------------------------------------------------------------------------- /EnableUIAccess/README.md: -------------------------------------------------------------------------------- 1 | # EnableUIAccess 2 | 3 | See [the guide documentation for context](https://github.com/jtroo/kanata/blob/main/docs/config.adoc#windows-only-work-elevated). 4 | -------------------------------------------------------------------------------- /assets/Interception.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/kanata/dd02c3e6bd6305d4e4593de492ad2dec5abe3684/assets/Interception.zip -------------------------------------------------------------------------------- /assets/kanata.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/kanata/dd02c3e6bd6305d4e4593de492ad2dec5abe3684/assets/kanata.ico -------------------------------------------------------------------------------- /assets/reload_32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/kanata/dd02c3e6bd6305d4e4593de492ad2dec5abe3684/assets/reload_32px.png -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() -> std::io::Result<()> { 2 | #[cfg(all(target_os = "windows", any(feature = "win_manifest", feature = "gui")))] 3 | { 4 | windows::build()?; 5 | } 6 | Ok(()) 7 | } 8 | 9 | #[cfg(all(target_os = "windows", any(feature = "win_manifest", feature = "gui")))] 10 | mod windows { 11 | use indoc::formatdoc; 12 | use regex::Regex; 13 | use std::fs::File; 14 | use std::io::Write; 15 | extern crate embed_resource; 16 | 17 | // println! during build 18 | macro_rules! pb { 19 | ($($tokens:tt)*) => {println!("cargo:warning={}", format!($($tokens)*))}} 20 | 21 | pub(super) fn build() -> std::io::Result<()> { 22 | let manifest_path: &str = "./target/kanata.exe.manifest"; 23 | 24 | // Note about expected version format: 25 | // MS says "Use the four-part version format: mmmmm.nnnnn.ooooo.ppppp" 26 | // https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests 27 | 28 | let re_ver_build = Regex::new(r"^(?<vpre>(\d+\.){2}\d+)[-a-zA-Z]+(?<vpos>\d+)quot;).unwrap(); 29 | let re_ver_build2 = Regex::new(r"^(?<vpre>(\d+\.){2}\d+)[-a-zA-Z]+quot;).unwrap(); 30 | let re_version3 = Regex::new(r"^(\d+\.){2}\d+quot;).unwrap(); 31 | let mut version: String = env!("CARGO_PKG_VERSION").to_string(); 32 | 33 | if re_version3.find(&version).is_some() { 34 | version = format!("{}.0", version); 35 | } else if re_ver_build.find(&version).is_some() { 36 | version = re_ver_build 37 | .replace_all(&version, r"$vpre.$vpos") 38 | .to_string(); 39 | } else if re_ver_build2.find(&version).is_some() { 40 | version = re_ver_build2.replace_all(&version, r"$vpre.0").to_string(); 41 | } else { 42 | pb!("unknown version format '{}', using '0.0.0.0'", version); 43 | version = "0.0.0.0".to_string(); 44 | } 45 | 46 | let manifest_str = formatdoc!( 47 | r#"<?xml version="1.0" encoding="utf-8" standalone="yes"?> 48 | <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:v3="urn:schemas-microsoft-com:asm.v3"> 49 | <assemblyIdentity name="kanata.exe" version="{}" type="win32"></assemblyIdentity> 50 | <v3:trustInfo><v3:security> 51 | <v3:requestedPrivileges><v3:requestedExecutionLevel level="asInvoker" uiAccess="false"></v3:requestedExecutionLevel></v3:requestedPrivileges> 52 | </v3:security></v3:trustInfo> 53 | <v3:application> 54 | <v3:windowsSettings> 55 | <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware> 56 | <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness> 57 | </v3:windowsSettings> 58 | </v3:application> 59 | <dependency><dependentAssembly> 60 | <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" 61 | version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"></assemblyIdentity></dependentAssembly> 62 | </dependency> 63 | </assembly> 64 | "#, 65 | version 66 | ); 67 | let mut manifest_f = File::create(manifest_path)?; 68 | write!(manifest_f, "{}", manifest_str)?; 69 | embed_resource::compile("./src/kanata.exe.manifest.rc", embed_resource::NONE); 70 | Ok(()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /cfg_samples/artsey.kbd: -------------------------------------------------------------------------------- 1 | ;; ARTSEY MINI 0.2 https://github.com/artseyio/artsey/issues/7 2 | 3 | ;; Exactly one defcfg entry is required. This is used for configuration key-pairs. 4 | (defcfg 5 | ;; Your keyboard device will likely differ from this. 6 | linux-dev /dev/input/event1 7 | 8 | ;; Windows doesn't need any input/output configuration entries; however, there 9 | ;; must still be a defcfg entry. You can keep the linux-dev entry or delete 10 | ;; it and leave it empty. 11 | ) 12 | 13 | (defsrc 14 | q w e 15 | a s d 16 | ) 17 | 18 | (deflayer base 19 | (chord base A) (chord base R) (chord base T) 20 | (chord base S) (chord base E) (chord base Y) 21 | ) 22 | 23 | (deflayer meta 24 | (chord meta A) (chord meta R) (chord meta T) 25 | (chord meta S) (chord meta E) (chord meta Y) 26 | ) 27 | 28 | (defchords base 5000 29 | (A R T S E Y) (layer-switch meta) 30 | (A R T ) (one-shot 2000 lsft) 31 | ( S E Y) spc 32 | (A ) a 33 | ( R T S ) b 34 | ( R S ) c 35 | (A E Y) d 36 | ( E ) e 37 | (A R ) f 38 | (A E ) g 39 | ( S Y) h 40 | ( R E ) i 41 | ( T S E ) j 42 | ( T E ) k 43 | ( S E ) l 44 | ( R T ) m 45 | ( E Y) n 46 | (A S ) o 47 | (A R Y) p 48 | ( T Y) q 49 | ( R ) r 50 | ( S ) s 51 | ( T ) t 52 | (A T ) u 53 | (A T E ) v 54 | ( T S ) w 55 | (A Y) x 56 | ( Y) y 57 | ( R S E ) z 58 | ) 59 | 60 | (defchords meta 5000 61 | (A R T S E Y) (layer-switch base) 62 | ( S E Y) spc 63 | (A R T ) caps ;; should technically be shift lock, probably need to use fake keys for that 64 | (A R ) bspc 65 | ( R T ) del 66 | ( S E ) C-c 67 | ( E Y) C-v 68 | (A ) home 69 | ( R ) up 70 | ( T ) end 71 | ( S ) left 72 | ( E ) down 73 | ( Y) rght 74 | ) 75 | -------------------------------------------------------------------------------- /cfg_samples/automousekeys-full-map.kbd: -------------------------------------------------------------------------------- 1 | (defcfg 2 | ;; F* keys and arrow keys are left unmapped 3 | process-unmapped-keys yes 4 | 5 | ;; you may wish to only capture a trackpoint and keyboard 6 | ;; but not e.g. a trackpad or external mouse 7 | ;;linux-dev-names-include ( 8 | ;; "AT Translated Set 2 keyboard" 9 | ;; "TPPS/2 Elan TrackPoint" 10 | ;;) 11 | ;; optional, but useful with the trackpoint 12 | ;;linux-use-trackpoint-property yes 13 | 14 | mouse-movement-key mvmt 15 | ) 16 | 17 | ;; ANSI layout for eg thinkpad internal or external keyboard 18 | (defsrc 19 | grv 1 2 3 4 5 6 7 8 9 0 - = bspc 20 | tab q w e r t y u i o p [ ] \ 21 | caps a s d f g h j k l ; ' ret 22 | lsft z x c v b n m , . / rsft 23 | lctl lmet lalt spc ralt menu rctl 24 | mvmt 25 | ) 26 | 27 | (defvirtualkeys 28 | mouse (layer-while-held mouse-layer) 29 | ) 30 | 31 | (defalias 32 | mhld (hold-for-duration 750 mouse) 33 | 34 | moff (on-press release-vkey mouse) 35 | 36 | _ (multi 37 | @moff 38 | _ 39 | ) 40 | 41 | ;; mouse click extended time out for double tap 42 | mdbt (hold-for-duration 500 mouse) 43 | mbl (multi 44 | mlft 45 | @mdbt 46 | ) 47 | mbm (multi 48 | mmid 49 | @mdbt 50 | ) 51 | mbr (multi 52 | mrgt 53 | @mdbt 54 | ) 55 | ) 56 | 57 | ;; no mappings 58 | (deflayer qwerty 59 | grv 1 2 3 4 5 6 7 8 9 0 - = bspc 60 | tab q w e r t y u i o p [ ] \ 61 | caps a s d f g h j k l ; ' ret 62 | lsft z x c v b n m , . / rsft 63 | lctl lmet lalt spc ralt menu rctl 64 | @mhld 65 | ) 66 | 67 | ;; places mouse keys on the row above the home row. 68 | ;; pressing any other keys exits the mouse layer until mouse movement stops and restarts again. 69 | (deflayer mouse-layer 70 | @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ 71 | @_ @_ mrgt mmid @mbl @_ @_ @mbl mmid mrgt @_ @_ @_ @_ 72 | @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ 73 | @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ @_ 74 | @_ @_ @_ @_ @_ @_ @_ 75 | @mhld 76 | ) 77 | -------------------------------------------------------------------------------- /cfg_samples/automousekeys-only.kbd: -------------------------------------------------------------------------------- 1 | (defcfg 2 | ;; we are only mapping the keys we want to use for mouse keys 3 | process-unmapped-keys yes 4 | 5 | ;; you may wish to only capture a trackpoint and keyboard 6 | ;; but not e.g. a trackpad or external mouse 7 | ;;linux-dev-names-include ( 8 | ;; "Lenovo TrackPoint Keyboard II" 9 | ;;) 10 | ;; optional, but useful with the trackpoint 11 | ;;linux-use-trackpoint-property yes 12 | 13 | mouse-movement-key mvmt 14 | ) 15 | 16 | (defsrc) 17 | 18 | (defvirtualkeys 19 | mouse (layer-while-held mouse-layer) 20 | ) 21 | 22 | (defalias 23 | mhld (hold-for-duration 750 mouse) 24 | 25 | moff (on-press release-vkey mouse) 26 | 27 | _ (multi 28 | @moff 29 | _ 30 | ) 31 | 32 | ;; mouse click extended time out for double tap 33 | mdbt (hold-for-duration 500 mouse) 34 | mbl (multi 35 | mlft 36 | @mdbt 37 | ) 38 | mbm (multi 39 | mmid 40 | @mdbt 41 | ) 42 | mbr (multi 43 | mrgt 44 | @mdbt 45 | ) 46 | ) 47 | 48 | ;; no key mappings 49 | (deflayermap (base) 50 | mvmt @mhld 51 | ) 52 | 53 | ;; places mouse keys on the row above the home row. 54 | ;; pressing any other keys exits the mouse layer until mouse movement stops and restarts again. 55 | (deflayermap (mouse-layer) 56 | w mrgt 57 | e mmid 58 | r @mbl 59 | 60 | u @mbl 61 | i mmid 62 | o mrgt 63 | 64 | mvmt @mhld 65 | ___ @_ 66 | ) 67 | -------------------------------------------------------------------------------- /cfg_samples/chords.tsv: -------------------------------------------------------------------------------- 1 | rus rust 2 | col cool 3 | nice nice 4 | you you 5 | th the 6 | a a 7 | an an 8 | man man 9 | name name 10 | an and 11 | as as 12 | or or 13 | bu but 14 | if if 15 | so so 16 | dn then 17 | bc because 18 | 19 | to to 20 | of of 21 | in in 22 | f for 23 | w with 24 | on on 25 | at at 26 | fm from 27 | by by 28 | abt about 29 | up up 30 | io into 31 | ov over 32 | af after 33 | wo without 34 | i I 35 | me me 36 | my my 37 | ou you 38 | ur your 39 | he he 40 | hm him 41 | his his 42 | sh she 43 | hr her 44 | it it 45 | ts its 46 | we we 47 | us us 48 | our our 49 | dz they 50 | dr their 51 | dm them 52 | wc which 53 | wn when 54 | wt what 55 | wr where 56 | ho who 57 | hw how 58 | wz why 59 | is is 60 | ar are 61 | wa was 62 | er were 63 | be be 64 | hv have 65 | hs has 66 | hd had 67 | nt not 68 | cn can 69 | do do 70 | wl will 71 | cd could 72 | wd would 73 | sd should 74 | li like 75 | bn been 76 | ge get 77 | maz may 78 | mad made 79 | mk make 80 | ai said 81 | wk work 82 | uz use 83 | sz say 84 | g go 85 | kn know 86 | tk take 87 | se see 88 | lk look 89 | cm come 90 | thk think 91 | wnt want 92 | gi give 93 | ct cannot 94 | de does 95 | di did 96 | sem seem 97 | cl call 98 | tha thank 99 | 100 | im I'm 101 | id I'd 102 | dt that 103 | dis this 104 | des these 105 | tes test 106 | al all 107 | o one 108 | mo more 109 | the there 110 | out out 111 | ao also 112 | tm time 113 | sm some 114 | js just 115 | ne new 116 | odr other 117 | pl people 118 | n no 119 | dan than 120 | oz only 121 | m most 122 | ay any 123 | may many 124 | el well 125 | fs first 126 | vy very 127 | much much 128 | now now 129 | ev even 130 | go good 131 | grt great 132 | way way 133 | t two 134 | yr year 135 | bk back 136 | day day 137 | qn question 138 | sc second 139 | dg thing 140 | y yes 141 | cn' can't 142 | dif different 143 | dgh though 144 | tru through 145 | sr sorry 146 | mv move 147 | dir dir 148 | stop stop 149 | tye type 150 | nx next 151 | sam same 152 | tp top 153 | cod code 154 | git git 155 | to TODO 156 | cls class 157 | clus cluster 158 | sure sure 159 | lets let's 160 | sup super 161 | such such 162 | thig thing 163 | yet yet 164 | don done 165 | sem seem 166 | ran ran 167 | job job 168 | bot bot 169 | fx effect 170 | nce once 171 | rad read 172 | ltr later 173 | lot lot 174 | brw brew 175 | unst uninstall 176 | rmv remove 177 | ad add 178 | poe problem 179 | buld build 180 | tol tool 181 | got got 182 | les less 183 | 0 zero 184 | 1 one 185 | 2 two 186 | 3 three 187 | 4 four 188 | 5 five 189 | 6 six 190 | 7 seven 191 | 8 eight 192 | 9 nine 193 | -------------------------------------------------------------------------------- /cfg_samples/colemak.kbd: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; Learn Colemak, a few keys at a time. 3 | ;; 4 | ;; The "j" key moves around the keyboard each step, 5 | ;; until you reach the full Colemak layout. 6 | ;; 7 | ;; To select the layout for your current step, press the 8 | ;; letter "m" and the number of your current step, as a chord. 9 | ;; 10 | ;; Check out: https://dreymar.colemak.org/tarmak-intro.html 11 | ;; and: https://colemak.com 12 | ;; 13 | 14 | (defsrc 15 | q w e r t y u i o p 16 | a s d f g h j k l ; 17 | z x c v b n m 18 | ) 19 | 20 | (deflayer colemak_j1 21 | _ _ j _ _ _ _ _ _ _ 22 | _ _ _ _ _ _ n e _ _ 23 | _ _ _ _ _ k _ 24 | ) 25 | 26 | (deflayer colemak_j2 27 | _ _ f _ g _ _ _ _ _ 28 | _ _ _ t j _ n e _ _ 29 | _ _ _ _ _ k _ 30 | ) 31 | 32 | (deflayer colemak_j3 33 | _ _ f j g _ _ _ _ _ 34 | _ r s t d _ n e _ _ 35 | _ _ _ _ _ k _ 36 | ) 37 | 38 | (deflayer colemak_j4 39 | _ _ f p g j _ _ y ; 40 | _ r s t d _ n e _ o 41 | _ _ _ _ _ k _ 42 | ) 43 | 44 | (deflayer colemak 45 | _ _ f p g j l u y ; 46 | _ r s t d _ n e i o 47 | _ _ _ _ _ k _ 48 | ) 49 | 50 | (defcfg 51 | process-unmapped-keys yes 52 | concurrent-tap-hold yes 53 | allow-hardware-repeat no 54 | ) 55 | 56 | (defchordsv2 57 | (m 1) (layer-switch colemak_j1) 300 all-released () 58 | (m 2) (layer-switch colemak_j2) 300 all-released () 59 | (m 3) (layer-switch colemak_j3) 300 all-released () 60 | (m 4) (layer-switch colemak_j4) 300 all-released () 61 | (m 5) (layer-switch colemak) 300 all-released () 62 | ) 63 | 64 | -------------------------------------------------------------------------------- /cfg_samples/deflayermap.kbd: -------------------------------------------------------------------------------- 1 | ;; A configuration showcasing deflayermap. 2 | ;; 3 | ;; The process-unmapped-keys defcfg item is not used 4 | ;; and the lctl and ralt keys are unmapped 5 | ;; because mapping them can cause problems on Windows 6 | ;; with non-US layouts. 7 | 8 | (defsrc 9 | grv 1 2 3 4 5 6 7 8 9 0 - = bspc 10 | tab q w e r t y u i o p [ ] \ 11 | caps a s d f g h j k l ; ' ret 12 | lsft z x c v b n m , . / rsft 13 | lmet lalt spc rmet rctl 14 | ) 15 | 16 | (deflayermap (base) 17 | caps (tap-hold 200 200 (caps-word 2000) lctl) 18 | spc (tap-hold 200 200 spc (layer-while-held nav)) 19 | ) 20 | 21 | (deflayermap (nav) 22 | i up 23 | j left 24 | k down 25 | l right 26 | ) 27 | -------------------------------------------------------------------------------- /cfg_samples/f13_f24.kbd: -------------------------------------------------------------------------------- 1 | (defcfg 2 | linux-dev /dev/input/by-path/platform-i8042-serio-0-event-kbd 3 | ) 4 | 5 | (defsrc 6 | f13 f14 f15 f16 f17 f18 f19 f20 f21 f22 f23 f24 7 | ) 8 | 9 | (deflayer test 10 | f13 f14 f15 f16 f17 f18 f19 f20 f21 f22 f23 f24 11 | ) 12 | 13 | -------------------------------------------------------------------------------- /cfg_samples/fancy_symbols.kbd: -------------------------------------------------------------------------------- 1 | ;; Turns ⎇› RightAlt into a symbol key to insert valid kanata unicode symbols for the pressed key 2 | ;; Turns ⇧›⎇› RightShift+RightAlt into a symbol key to insert extra symbols for the same keys 3 | ;; e.g., ⎇›Delete will print ␡ 4 | (defcfg) 5 | (defalias 6 | 🔣 (layer-while-held fancy-symbol) 7 | ⇧🔣 (layer-while-held ⇧fancy-symbol)) 8 | (defsrc 9 | ‹🖰 🖰› 🖰3 🖰4 🖰5 10 | ▶⏸ ◀◀ ▶▶ 🔇 🔉 🔊 🔅 🔆 🎛 ⌨💡+ ⌨💡− 11 | ⎋ 12 | ˋ 1 2 3 4 5 6 7 8 9 0 - = ␈ ⎀ ⇤ ⇞ ⇭ 🔢⁄ 🔢∗ 🔢₋ 13 | ⭾ q w e r t y u i o p [ ] \ ␡ ⇥ ⇟ 🔢₇ 🔢₈ 🔢₉ 🔢₊ 14 | ⇪ a s d f g h j k l ; ' ⏎ 🔢₄ 🔢₅ 🔢₆ 15 | ‹⇧ z x c v b n m , . / ⇧› ▲ 🔢₁ 🔢₂ 🔢₃ 🔢⏎ 16 | ‹⎈ ‹◆ ‹⎇ ␠ ⎇› ☰ ⎈› ◀ ▼ ▶ 🔢₀ 🔢⸴ ) 17 | (deflayer qwerty ;; =base with ⎇› as a fancy symbol key 18 | ‗ ‗ ‗ ‗ ‗ 19 | ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ 20 | ‗ 21 | ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ 22 | ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ 23 | ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ 24 | ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ ‗ 25 | ‗ ‗ ‗ ‗ @🔣 ‗ ‗ ‗ ‗ ‗ ‗ ‗ ) 26 | (deflayer fancy-symbol ;; •block all other keys 27 | 🔣‹🖰 🔣🖰› 🔣🖰3 🔣🖰4 🔣🖰5 28 | 🔣▶⏸ 🔣◀◀ 🔣▶▶ 🔣🔇 🔣🔉 🔣🔊 🔣🔅 🔣🔆 🔣🎛 🔣⌨💡+ 🔣⌨💡− 29 | 🔣⎋ 30 | 🔣ˋ • • • • • • • • • • 🔣‐ 🔣₌ 🔣␈ 🔣⎀ 🔣⇤ 🔣⇞ 🔣⇭ 🔣🔢⁄ 🔣🔢∗ 🔣🔢₋ 31 | 🔣⭾ • • • • • • • • • • 🔣【 🔣】 🔣⧵ 🔣␡ 🔣⇥ 🔣⇟ 🔣🔢₇ 🔣🔢₈ 🔣🔢₉ 🔣🔢₊ 32 | 🔣⇪ • • • • • • • • • 🔣︔ ' 🔣⏎ 🔣🔢₄ 🔣🔢₅ 🔣🔢₆ 33 | 🔣⇧ • • • • • • • 🔣⸴ 🔣. 🔣⁄ @⇧🔣 🔣▲ 🔣🔢₁ 🔣🔢₂ 🔣🔢₃ 🔣🔢⏎ 34 | 🔣⎈ 🔣◆ 🔣⎇ 🔣␠ • 🔣☰ • 🔣◀ 🔣▼ 🔣▶ 🔣🔢₀ 🔣🔢⸴ ) 35 | (deflayer ⇧fancy-symbol ;; •block all other keys 36 | 🔣🖰1 🔣🖰2 • • • 37 | • • • 🔣🔈⓪⓿₀ • 🔣🔈−➖₋⊖ 🔣🔈+➕₊⊕ • • 🔣⌨💡➕₊⊕ 🔣⌨💡➖₋⊖ 38 | • 39 | 🔣˜ • • • • • • • • • • - = 🔣⌫ • 🔣⤒↖ 🔣🔢 • • • • 40 | 🔣↹ • • • • • • • • • • 🔣「〔⎡ 🔣」〕⎣ 🔣\ 🔣⌦ 🔣⤓↘ • • • • • 41 | • • • • • • • • • • • • 🔣↩⌤ • • • 42 | • • • • • • • • • • / • • • • • 🔣🔢↩⌤ 43 | 🔣⌃ 🔣❖⌘ 🔣⌥ 🔣␣ 🔣▤𝌆 • • • • • • • ) 44 | -------------------------------------------------------------------------------- /cfg_samples/home-row-mod-advanced.kbd: -------------------------------------------------------------------------------- 1 | ;; Home row mods QWERTY example with more complexity. 2 | ;; Some of the changes from the basic example: 3 | ;; - when a home row mod activates tap, the home row mods are disabled 4 | ;; while continuing to type rapidly 5 | ;; - tap-hold-release helps make the hold action more responsive 6 | ;; - pressing another key on the same half of the keyboard 7 | ;; as the home row mod will activate an early tap action 8 | 9 | (defcfg 10 | process-unmapped-keys yes 11 | ) 12 | (defsrc 13 | a s d f j k l ; 14 | ) 15 | (defvar 16 | ;; Note: consider using different time values for your different fingers. 17 | ;; For example, your pinkies might be slower to release keys and index 18 | ;; fingers faster. 19 | tap-time 200 20 | hold-time 150 21 | 22 | left-hand-keys ( 23 | q w e r t 24 | a s d f g 25 | z x c v b 26 | ) 27 | right-hand-keys ( 28 | y u i o p 29 | h j k l ; 30 | n m , . / 31 | ) 32 | ) 33 | (deflayer base 34 | @a @s @d @f @j @k @l @; 35 | ) 36 | 37 | (deflayer nomods 38 | a s d f j k l ; 39 | ) 40 | (deffakekeys 41 | to-base (layer-switch base) 42 | ) 43 | (defalias 44 | tap (multi 45 | (layer-switch nomods) 46 | (on-idle-fakekey to-base tap 20) 47 | ) 48 | 49 | a (tap-hold-release-keys $tap-time $hold-time (multi a @tap) lmet $left-hand-keys) 50 | s (tap-hold-release-keys $tap-time $hold-time (multi s @tap) lalt $left-hand-keys) 51 | d (tap-hold-release-keys $tap-time $hold-time (multi d @tap) lctl $left-hand-keys) 52 | f (tap-hold-release-keys $tap-time $hold-time (multi f @tap) lsft $left-hand-keys) 53 | j (tap-hold-release-keys $tap-time $hold-time (multi j @tap) rsft $right-hand-keys) 54 | k (tap-hold-release-keys $tap-time $hold-time (multi k @tap) rctl $right-hand-keys) 55 | l (tap-hold-release-keys $tap-time $hold-time (multi l @tap) ralt $right-hand-keys) 56 | ; (tap-hold-release-keys $tap-time $hold-time (multi ; @tap) rmet $right-hand-keys) 57 | ) -------------------------------------------------------------------------------- /cfg_samples/home-row-mod-basic.kbd: -------------------------------------------------------------------------------- 1 | ;; Basic home row mods example using QWERTY 2 | ;; For a more complex but perhaps usable configuration, 3 | ;; see home-row-mod-advanced.kbd 4 | 5 | (defcfg 6 | process-unmapped-keys yes 7 | ) 8 | (defsrc 9 | a s d f j k l ; 10 | ) 11 | (defvar 12 | ;; Note: consider using different time values for your different fingers. 13 | ;; For example, your pinkies might be slower to release keys and index 14 | ;; fingers faster. 15 | tap-time 200 16 | hold-time 150 17 | ) 18 | (defalias 19 | a (tap-hold $tap-time $hold-time a lmet) 20 | s (tap-hold $tap-time $hold-time s lalt) 21 | d (tap-hold $tap-time $hold-time d lctl) 22 | f (tap-hold $tap-time $hold-time f lsft) 23 | j (tap-hold $tap-time $hold-time j rsft) 24 | k (tap-hold $tap-time $hold-time k rctl) 25 | l (tap-hold $tap-time $hold-time l ralt) 26 | ; (tap-hold $tap-time $hold-time ; rmet) 27 | ) 28 | (deflayer base 29 | @a @s @d @f @j @k @l @; 30 | ) -------------------------------------------------------------------------------- /cfg_samples/included-file.kbd: -------------------------------------------------------------------------------- 1 | (defalias 2 | included-alias (macro i spc a m spc i n c l u d e d) 3 | ) 4 | -------------------------------------------------------------------------------- /cfg_samples/japanese_mac_eisu_kana.kbd: -------------------------------------------------------------------------------- 1 | #| 2 | Using meta keys as japanese eisu and kana on Mac with US keyboard. 3 | 4 | | Source | Tap | Hold | 5 | | ------- | ------------ | ---- | 6 | | lmet | lang2 (eisu) | lmet | 7 | | rmet | lang1 (kana) | rmet | 8 | 9 | |# 10 | 11 | (defcfg 12 | process-unmapped-keys yes 13 | ) 14 | 15 | (defsrc 16 | lmet rmet 17 | ) 18 | 19 | (deflayer default 20 | @lmet @rmet 21 | ) 22 | 23 | (defalias 24 | lmet (tap-hold-press 200 200 eisu lmet) 25 | rmet (tap-hold-press 200 200 kana rmet) 26 | ) 27 | 28 | -------------------------------------------------------------------------------- /cfg_samples/key-toggle_press-only_release-only.kbd: -------------------------------------------------------------------------------- 1 | #| 2 | 3 | This configuration showcases all of: 4 | - key toggle 5 | - press-only 6 | - release-only 7 | 8 | |# 9 | 10 | (deftemplate toggle-key (vkey-name output-key alias) 11 | (defvirtualkeys $vkey-name $output-key) 12 | (defalias $alias (on-press toggle-vkey $vkey-name)) 13 | ) 14 | 15 | (deftemplate press-only-release-only-pair 16 | (vkey-name output-key press-alias release-alias) 17 | (defvirtualkeys $vkey-name $output-key) 18 | (defalias $press-alias (on-press press-vkey $vkey-name)) 19 | (defalias $release-alias (on-press release-vkey $vkey-name)) 20 | ) 21 | 22 | (template-expand toggle-key v-lctl lctl lcl) 23 | (template-expand toggle-key v-rctl rctl rcl) 24 | 25 | ;; t! is a short form of template-expand 26 | (t! press-only-release-only-pair v-lalt lalt p-a r-a) 27 | 28 | (defsrc 29 | lctl rctl lalt ralt 30 | ) 31 | 32 | (deflayer base 33 | @lcl @rcl @p-a @r-a 34 | ) 35 | -------------------------------------------------------------------------------- /cfg_samples/minimal.kbd: -------------------------------------------------------------------------------- 1 | #| 2 | This minimal config changes Caps Lock to act as Caps Lock on quick tap, but 3 | if held, it will act as Left Ctrl. It also changes the backtick/grave key to 4 | act as backtick/grave on quick tap, but change ijkl keys to arrow keys on hold. 5 | 6 | This text between the two pipe+octothorpe sequences is a multi-line comment. 7 | |# 8 | 9 | ;; Text after double-semicolons are single-line comments. 10 | 11 | #| 12 | One defcfg entry may be added, which is used for configuration key-pairs. These 13 | configurations change kanata's behaviour at a more global level than the other 14 | configuration entries. 15 | |# 16 | 17 | (defcfg 18 | #| 19 | This configuration will process all keys pressed inside of kanata, even if 20 | they are not mapped in defsrc. This is so that certain actions can activate 21 | at the right time for certain input sequences. By default, unmapped keys are 22 | not processed through kanata due to a Windows issue related to AltGr. If you 23 | use AltGr in your keyboard, you will likely want to follow the simple.kbd 24 | file while unmapping lctl and ralt from defsrc. 25 | |# 26 | process-unmapped-keys yes 27 | ) 28 | 29 | (defsrc 30 | caps grv i 31 | j k l 32 | lsft rsft 33 | ) 34 | 35 | (deflayer default 36 | @cap @grv _ 37 | _ _ _ 38 | _ _ 39 | ) 40 | 41 | (deflayer arrows 42 | _ _ up 43 | left down rght 44 | _ _ 45 | ) 46 | 47 | (defalias 48 | cap (tap-hold-press 200 200 caps lctl) 49 | grv (tap-hold-press 200 200 grv (layer-toggle arrows)) 50 | ) 51 | -------------------------------------------------------------------------------- /cfg_samples/simple.kbd: -------------------------------------------------------------------------------- 1 | ;; Comments are prefixed by double-semicolon. A single semicolon is parsed as the 2 | ;; keyboard key. Comments are ignored for the configuration file. 3 | ;; 4 | ;; This configuration language is Lisp-like. If you're unfamiliar with Lisp, 5 | ;; don't be alarmed. The maintainer jtroo is also unfamiliar with Lisp. You 6 | ;; don't need to know Lisp in-depth to be able to configure kanata. 7 | ;; 8 | ;; If you follow along with the examples, you should be fine. Kanata should 9 | ;; also hopefully have helpful error messages in case something goes wrong. 10 | ;; If you need help, you are welcome to ask. 11 | 12 | ;; Only one defsrc is allowed. 13 | ;; 14 | ;; defsrc defines the keys that will be intercepted by kanata. The order of the 15 | ;; keys matches with deflayer declarations and all deflayer declarations must 16 | ;; have the same number of keys as defsrc. Any keys not listed in defsrc will 17 | ;; be passed straight to the operating system. 18 | (defsrc 19 | grv 1 2 3 4 5 6 7 8 9 0 - = bspc 20 | tab q w e r t y u i o p [ ] \ 21 | caps a s d f g h j k l ; ' ret 22 | lsft z x c v b n m , . / rsft 23 | lctl lmet lalt spc ralt rmet rctl 24 | ) 25 | 26 | ;; The first layer defined is the layer that will be active by default when 27 | ;; kanata starts up. This layer is the standard QWERTY layout except for the 28 | ;; backtick/grave key (@grl) which is an alias for a tap-hold key. 29 | (deflayer qwerty 30 | @grl 1 2 3 4 5 6 7 8 9 0 - = bspc 31 | tab q w e r t y u i o p [ ] \ 32 | caps a s d f g h j k l ; ' ret 33 | lsft z x c v b n m , . / rsft 34 | lctl lmet lalt spc ralt rmet rctl 35 | ) 36 | 37 | ;; The dvorak layer remaps the keys to the dvorak layout. In addition there is 38 | ;; another tap-hold key: @cap. This key retains caps lock functionality when 39 | ;; quickly tapped but is read as left-control when held. 40 | (deflayer dvorak 41 | @grl 1 2 3 4 5 6 7 8 9 0 [ ] bspc 42 | tab ' , . p y f g c r l / = \ 43 | @cap a o e u i d h t n s - ret 44 | lsft ; q j k x b m w v z rsft 45 | lctl lmet lalt spc ralt rmet rctl 46 | ) 47 | 48 | ;; defalias is used to declare a shortcut for a more complicated action to keep 49 | ;; the deflayer declarations clean and aligned. The alignment in deflayers is not 50 | ;; necessary, but is strongly recommended for ease of understanding visually. 51 | ;; 52 | ;; Aliases are referred to by `@<alias_name>`. 53 | (defalias 54 | ;; tap: backtick (grave), hold: toggle layer-switching layer while held 55 | grl (tap-hold 200 200 grv (layer-toggle layers)) 56 | 57 | ;; layer-switch changes the base layer. 58 | dvk (layer-switch dvorak) 59 | qwr (layer-switch qwerty) 60 | 61 | ;; tap for capslk, hold for lctl 62 | cap (tap-hold 200 200 caps lctl) 63 | ) 64 | 65 | ;; The `lrld` action stands for "live reload". This will re-parse everything 66 | ;; except for linux-dev, meaning you cannot live reload and switch keyboard 67 | ;; devices. 68 | ;; 69 | ;; The keys 1 and 2 switch the base layer to qwerty and dvorak respectively. 70 | ;; 71 | ;; Apart from the layer switching and live reload, all other keys are the 72 | ;; underscore _ which means "transparent". Transparent means the base layer 73 | ;; behaviour is used when pressing that key. 74 | (deflayer layers 75 | _ @qwr @dvk lrld _ _ _ _ _ _ _ _ _ _ 76 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ 77 | _ _ _ _ _ _ _ _ _ _ _ _ _ 78 | _ _ _ _ _ _ _ _ _ _ _ _ 79 | _ _ _ _ _ _ _ 80 | ) 81 | -------------------------------------------------------------------------------- /cfg_samples/tray-icon/3trans.parent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/kanata/dd02c3e6bd6305d4e4593de492ad2dec5abe3684/cfg_samples/tray-icon/3trans.parent.png -------------------------------------------------------------------------------- /cfg_samples/tray-icon/6name-match.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/kanata/dd02c3e6bd6305d4e4593de492ad2dec5abe3684/cfg_samples/tray-icon/6name-match.png -------------------------------------------------------------------------------- /cfg_samples/tray-icon/_custom-icons/s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/kanata/dd02c3e6bd6305d4e4593de492ad2dec5abe3684/cfg_samples/tray-icon/_custom-icons/s.png -------------------------------------------------------------------------------- /cfg_samples/tray-icon/icons/1symbols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/kanata/dd02c3e6bd6305d4e4593de492ad2dec5abe3684/cfg_samples/tray-icon/icons/1symbols.png -------------------------------------------------------------------------------- /cfg_samples/tray-icon/img/2Nav Num.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/kanata/dd02c3e6bd6305d4e4593de492ad2dec5abe3684/cfg_samples/tray-icon/img/2Nav Num.png -------------------------------------------------------------------------------- /cfg_samples/tray-icon/license_icons.txt: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2024, Fred Vatin 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /cfg_samples/tray-icon/tray-icon.kbd: -------------------------------------------------------------------------------- 1 | (defcfg 2 | 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. 3 | log-layer-changes yes ;;|no| overhead 4 | tray-icon "./_custom-icons/s.png" ;; should activate for layers without icons like '5no-icn' 5 | ;;opt val |≝| 6 | icon-match-layer-name yes ;;|yes| match layer name to icon files even without an explicit (icon name.ico) config 7 | tooltip-layer-changes yes ;;|false| 8 | tooltip-show-blank yes ;;|no| 9 | tooltip-duration 500 ;;|500| 10 | tooltip-size 24,24 ;;|24 24| 11 | notify-cfg-reload yes ;;|yes| 12 | notify-cfg-reload-silent no ;;|no| 13 | notify-error yes ;;|yes| 14 | ) 15 | (defalias l1 (layer-while-held 1emoji)) 16 | (defalias l2 (layer-while-held 2icon-quote)) 17 | (defalias l3 (layer-while-held 3emoji_alt)) 18 | (defalias l4 (layer-while-held 4my-lmap)) 19 | (defalias l5 (layer-while-held 5no-icn)) 20 | (defalias l6 (layer-while-held 6name-match)) 21 | 22 | (defsrc 1 2 3 4 5 6) 23 | (deflayer (⌂ icon base.png) @l1 @l2 @l3 @l4 @l5 @l6) ;; find in the 'icon' subfolder 24 | (deflayer (1emoji 🖻 1symbols.png) q q q q q q) ;; find in the 'icons' subfolder 25 | (deflayer (2icon-quote 🖻 "2Nav Num.png") w w w w w w) ;; find in the 'img' subfolder 26 | (deflayer (3emoji_alt 🖼 3trans.parent) e e e e e e) ;; find '.png' 27 | (deflayermap (4my-lmap 🖻 "..\..\assets\kanata.ico") 1 r 2 r 3 r 4 r 5 r 6 r) ;; find in relative path 28 | (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 29 | (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' 30 | -------------------------------------------------------------------------------- /cfg_samples/tray-icon/tray-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/kanata/dd02c3e6bd6305d4e4593de492ad2dec5abe3684/cfg_samples/tray-icon/tray-icon.png -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 2 | ### Converting ".adoc" to html 3 | 4 | To generate html from the these documentation files, use ["asciidoctor"](https://asciidoctor.org) 5 | (they are not fully compatible with the separate "asciidoc" project) 6 | 7 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # Design doc 2 | 3 | ## Obligatory diagram 4 | 5 | <img src="./kanata-basic-diagram.svg"> 6 | 7 | ## main 8 | 9 | - read args 10 | - read config 11 | - start event loops 12 | 13 | ## event loop 14 | 15 | - read key events 16 | - send events to processing loop on channel 17 | 18 | ## processing loop 19 | 20 | - check for events on mpsc 21 | - if event: send event to layout 22 | - tick() the keyberon layout, send any events needed 23 | - if no event: sleep for 1ms 24 | - separate monotonic time checks, because can't rely on sleep to be 25 | fine-grained or accurate 26 | - send `ServerMessage`s to the TCP server 27 | 28 | ## TCP server 29 | 30 | - listen for `ClientMessage`s and act on them 31 | - recv `ServerMessage`s from processing loop and forward to all connected 32 | clients 33 | 34 | ## layout 35 | 36 | - uses keyberon 37 | - indices of `kanata_keyberon::layout::Event::{Press, Release}(x,y)`: 38 | 39 | x = 0 or 1 (0 is for physical key presses, 1 is for fake keys) 40 | y = OS code of key used as an index 41 | 42 | ## OS-specific code 43 | 44 | Most of the OS specific code is in `oskbd/` and `keys/`. There's a bit of it in 45 | `kanata/` since the event loops to receive OS events are different. 46 | -------------------------------------------------------------------------------- /docs/fancy_symbols.md: -------------------------------------------------------------------------------- 1 | ### Supported key symbols 2 | 3 | |Symbol(s)[^1] |Key `name` | 4 | |--------- |-------- | 5 | |‹x x› | Left/Right modifiers (e.g., ‹⎈ LCtrl) | 6 | |⇧ | Shift | 7 | |⎈ ⌃ | Control | 8 | |⌘ ◆ ❖ | Windows/Command | 9 | |⎇ ⌥ | Alt | 10 | |⇪ | capslock | 11 | |⎋ |`escape` | 12 | |⭾ ↹ |`tab` | 13 | |␠ ␣ | `spc` spacebar | 14 | |␈ ⌫ |`bspc` backspace (delete backward) | 15 | |␡ ⌦ |`del` delete forward | 16 | |⏎ ↩ ⌤  |`ret` return or enter | 17 | |︔ ⸴ .⁄ |semicolon `;` / comma `,` / period `.` / slash `/` | 18 | |⧵ \ | backslash `\` | 19 | |﹨ < |`non_us_backslash` | 20 | |【 「 〔 ⎡ |`open_bracket` | 21 | |】 」 〕 ⎣ |`close_bracket` | 22 | |ˋ ˜ |`grave_accent_and_tilde` | 23 | |‐ ₌ |`hyphen` `equal_sign` | 24 | |▲ ▼ ◀ ▶ |`up`/`down`/`left`/`right` (arrows) | 25 | |⇞ ⇟ |`pgup`/`pgdn` (page up, page down) | 26 | |⎀ |`insert` | 27 | |⇤ ⤒ ↖ |`home` | 28 | |⇥ ⤓ ↘ |`end` | 29 | |⇭ |`numlock` | 30 | |🔢₁ 🔢₂ 🔢₃ 🔢₄ 🔢₅ |`keypad_` `1`–`5` | 31 | |🔢₆ 🔢₇ 🔢₈ 🔢₉ 🔢₀ |`keypad_` `6`–`0` | 32 | |🔢₋ 🔢₌ 🔢₊ |`keypad_` `hyphen`/`equal_sign`/`plus` | 33 | |🔢⁄ 🔢.🔢∗ |`keypad_` `slash`/`period`/`asterisk` | 34 | |◀◀ ▶⏸ ▶▶ |`vk_consumer_` `previous`/`play`/`next` | 35 | |🔊 🔈+ or ➕₊⊕ |`volume_up` | 36 | |🔉 🔈− or ➖₋⊖ |`volume_down` | 37 | |🔇 🔈⓪ or ⓿ ₀ |`mute` | 38 | |🔆 🔅 |`vk_consumer_brightness_` `up`/`down` | 39 | |⌨💡+ or ➕₊⊕ |`vk_consumer_illumination_up` | 40 | |⌨💡− or ➖₋⊖ |`vk_consumer_illumination_down` | 41 | |🎛 |`vk_dashboard` | 42 | |▤ ☰ 𝌆 |`application` | 43 | |🖰1 🖰2 ... 🖰5 |`button` `1`–`5` | 44 | |‹🖰 🖰› |`button` `1` `2` | 45 | 46 | [^1]: space-separated list of keys; `or` means only last symbol in a pair changes 47 | -------------------------------------------------------------------------------- /docs/interception.md: -------------------------------------------------------------------------------- 1 | # Windows Interception driver implementation notes 2 | 3 | - Interception handle is `!Send` and `!Sync` 4 | - means a single thread should own both input and output 5 | - `KbdOut` will need to send keyboard output events to that thread as opposed 6 | to Linux using `uinput` and the original Windows code using `SendInput` 7 | which are independent of the input devices. 8 | - Maybe save channel in kanata struct as part of new kanata 9 | - Interception can filter for only keyboard events 10 | - should use this filter feature; don't want to intercept mouse 11 | - Need to save previous device for sending to, in case wait/receive (with 12 | timeout) don't return anything so that sending stuff can be sent to some 13 | device. 14 | - Input `ScanCode` maps to the keyberon `KeyCode`; they both use the USB 15 | standard codes. 16 | - For ease of integration will probably need to unfortunately convert it to 17 | an `OsCode` even though the processing loop will soon after just convert it 18 | back to `KeyCode`. Oh well. 19 | -------------------------------------------------------------------------------- /docs/kmonad_comparison.md: -------------------------------------------------------------------------------- 1 | # Comparison with kmonad 2 | 3 | The kmonad project is the closest alternative for this project. 4 | 5 | ## Benefits of kmonad over kanata 6 | 7 | - ~MacOS support~ (this is implemented now) 8 | - Different features 9 | 10 | ## Why I built and use kanata 11 | 12 | - [Double-tapping a tap-hold key](https://github.com/kmonad/kmonad/issues/163) did not behave 13 | [how I want it to](https://docs.qmk.fm/#/tap_hold?id=tapping-force-hold) 14 | - Some key sequences with tap-hold keys [didn't behave how I want](https://github.com/kmonad/kmonad/issues/466): 15 | - `(press lsft) (press a) (release lsft) (release a)` (a is a tap-hold key) 16 | - The above outputs `a` in kmonad, but I want it to output `A` 17 | - kmonad was missing [mouse buttons](https://github.com/kmonad/kmonad/issues/150) 18 | 19 | The issues listed are all fixable in kmonad and I hope they are one day! For me 20 | though, I didn't and still don't know Haskell well enough to contribute to 21 | kmonad. That's why I instead built kanata based off of the excellent work that 22 | had already gone into the 23 | [keyberon](https://github.com/TeXitoi/keyberon), 24 | [ktrl](https://github.com/ItayGarin/ktrl), and 25 | [kbremap](https://github.com/timokroeger/kbremap) projects. 26 | 27 | If you want to see the features that kanata offers, the 28 | [configuration guide](./config.adoc) is a good starting point. 29 | 30 | I dogfood kanata myself and it works great for my use cases. Though kanata is a 31 | younger project than kmonad, it now has more features. If you give kanata a 32 | try, feel free to ask for help in an issue or discussion, or let me know how it 33 | went 🙂. 34 | -------------------------------------------------------------------------------- /docs/platform-known-issues.adoc: -------------------------------------------------------------------------------- 1 | = Hardware known issues 2 | 3 | At the electric circuit layer of many keyboards, 4 | cost-saving measures can lead to key presses not registering 5 | when pressing multiple keys simultaneously. 6 | Usually this happens with at least 3 key presses. 7 | Kanata cannot fix this issue. 8 | You can work around it by avoiding 9 | the problem key combination, 10 | or using a different keyboard. 11 | 12 | = Platform-dependent known issues 13 | 14 | == Preface 15 | 16 | This document contains a list of known issues 17 | which are unique to a given platform. 18 | The platform supported by the core maintainer (jtroo) 19 | are Windows 11 and Linux. 20 | Windows 10 is expected to work fine, 21 | but as Windows 10 end-of-support is approaching in 2025, 22 | jtroo no longer has any devices with it installed. 23 | 24 | On Windows, there are two backing mechanisms that can be used 25 | for keyboard input/output and they have different issues. 26 | These will be differentiated by the words "LLHOOK" and "Interception", 27 | which map to the binaries 28 | `kanata.exe` and `kanata_wintercept.exe` respectively. 29 | 30 | == Windows 11 LLHOOK 31 | 32 | * Mouse inputs cannot be used for processing or remapping 33 | ** https://github.com/jtroo/kanata/issues/108 34 | ** https://github.com/jtroo/kanata/issues/170 35 | * Some input key combinations (e.g. Win+L) cannot be intercepted before 36 | running their default action 37 | ** https://github.com/jtroo/kanata/issues/192 38 | ** https://github.com/jtroo/kanata/discussions/428 39 | * OS-level key remapping behaves differently vs. Linux or Interception 40 | ** Does not affect winiov2 variant 41 | ** https://github.com/jtroo/kanata/issues/152 42 | * Certain applications that also use the LLHOOK mechanism may not behave correctly 43 | ** https://github.com/jtroo/kanata/issues/55 44 | ** https://github.com/jtroo/kanata/issues/250 45 | ** https://github.com/jtroo/kanata/issues/430 46 | ** https://github.com/espanso/espanso/issues/1488 47 | * AltGr / ralt / Right Alt can misbehave 48 | ** https://github.com/jtroo/kanata/blob/main/docs/config.adoc#windows-only-windows-altgr 49 | * NumLock state can mess with arrow keys in unexpected ways 50 | ** Does not affect winiov2 variant 51 | ** https://github.com/jtroo/kanata/issues/78 52 | ** https://github.com/jtroo/kanata/issues/667 53 | ** Workaround: use the correct https://github.com/jtroo/kanata/discussions/354[numlock state] 54 | * Without `process-unmapped-keys yes`, using arrow keys 55 | without also having the shift keys in `defsrc` will break shift highlighting 56 | ** Does not affect winiov2 variant 57 | ** https://github.com/jtroo/kanata/issues/858 58 | ** Workaround: add shift keys to `defsrc` or use `process-unmapped-keys yes` in `defcfg` 59 | 60 | == Windows 11 Interception 61 | 62 | * Sleeping your system or unplugging/replugging devices enough times causes 63 | inputs to stop working 64 | ** https://github.com/oblitum/Interception/issues/25 65 | * Some less-frequently used keys are not supported or handled correctly 66 | ** https://github.com/jtroo/kanata/issues/127 67 | ** https://github.com/jtroo/kanata/issues/164 68 | ** https://github.com/jtroo/kanata/issues/425 69 | ** https://github.com/jtroo/kanata/issues/532 70 | 71 | == Linux 72 | 73 | * Key repeats can occur when they normally wouldn't in some cases 74 | ** https://github.com/jtroo/kanata/discussions/422 75 | ** https://github.com/jtroo/kanata/issues/450 76 | ** https://github.com/jtroo/kanata/issues/1441 77 | * Unicode support has limitations, using xkb is a more consistent solution 78 | ** https://github.com/jtroo/kanata/discussions/703 79 | * Key actions can behave incorrectly due to the rapidity of key events 80 | ** https://github.com/jtroo/kanata/discussions/733 81 | ** https://github.com/jtroo/kanata/issues/740 82 | ** adjusting https://github.com/jtroo/kanata/blob/main/docs/config.adoc#rapid-event-delay[rapid-event-delay] can potentially be a workaround 83 | * Macro keys on certain gaming keyboards might stop being processed 84 | ** Context: search for `POTENTIAL PROBLEM - G-keys` in 85 | link:../src/kanata/mod.rs[the code]. 86 | ** Workaround: leave `process-unmapped-keys` disabled 87 | and explicitly map keys in `defsrc` instead 88 | 89 | == MacOS 90 | 91 | * Only left, right, and middle mouse buttons are implemented for clicking 92 | * Mouse input processing is not implemented, e.g. putting `mlft` into `defsrc` does nothing 93 | -------------------------------------------------------------------------------- /docs/setup-linux.md: -------------------------------------------------------------------------------- 1 | # Instructions 2 | 3 | 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. 4 | 5 | ### 1. If the uinput group does not exist, create a new group 6 | 7 | ```bash 8 | sudo groupadd uinput 9 | ``` 10 | 11 | ### 2. Add your user to the input and the uinput group 12 | 13 | ```bash 14 | sudo usermod -aG input $USER 15 | sudo usermod -aG uinput $USER 16 | ``` 17 | 18 | Make sure that it's effective by running `groups`. You might have to logout and login. 19 | 20 | ### 3. Make sure the uinput device file has the right permissions. 21 | 22 | #### Create a new file: 23 | `/etc/udev/rules.d/99-input.rules` 24 | 25 | #### Insert the following in the code 26 | ```bash 27 | KERNEL=="uinput", MODE="0660", GROUP="uinput", OPTIONS+="static_node=uinput" 28 | ``` 29 | 30 | #### Machine reboot or run this to reload 31 | ```bash 32 | sudo udevadm control --reload-rules && sudo udevadm trigger 33 | ``` 34 | 35 | #### Verify settings by following command: 36 | ```bash 37 | ls -l /dev/uinput 38 | ``` 39 | 40 | #### Output: 41 | ```bash 42 | crw-rw---- 1 root date uinput /dev/uinput 43 | ``` 44 | 45 | ### 4. Make sure the uinput drivers are loaded 46 | 47 | You may need to run this command whenever you start kanata for the first time: 48 | 49 | ``` 50 | sudo modprobe uinput 51 | ``` 52 | ### 5a. To create and enable a systemd daemon service 53 | 54 | Run this command first: 55 | ```bash 56 | mkdir -p ~/.config/systemd/user 57 | ``` 58 | 59 | Then add this to: `~/.config/systemd/user/kanata.service`: 60 | ```bash 61 | [Unit] 62 | Description=Kanata keyboard remapper 63 | Documentation=https://github.com/jtroo/kanata 64 | 65 | [Service] 66 | Environment=PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/bin 67 | # Uncomment the 4 lines beneath this to increase process priority 68 | # of Kanata in case you encounter lagginess when resource constrained. 69 | # WARNING: doing so will require the service to run as an elevated user such as root. 70 | # Implementing least privilege access is an exercise left to the reader. 71 | # 72 | # CPUSchedulingPolicy=rr 73 | # CPUSchedulingPriority=99 74 | # IOSchedulingClass=realtime 75 | # Nice=-20 76 | Type=simple 77 | ExecStart=/usr/bin/sh -c 'exec $(which kanata) --cfg ${HOME}/.config/kanata/config.kbd' 78 | Restart=no 79 | 80 | [Install] 81 | WantedBy=default.target 82 | ``` 83 | 84 | Make sure to update the executable location for sh in the snippet above. 85 | This would be the line starting with `ExecStart=/usr/bin/sh -c`. 86 | You can check the executable path with: 87 | ```bash 88 | which sh 89 | ``` 90 | 91 | Also, verify if the path to kanata is included in the line `Environment=PATH=[...]`. 92 | 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`. 93 | `%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 94 | 95 | Then run: 96 | ```bash 97 | systemctl --user daemon-reload 98 | systemctl --user enable kanata.service 99 | systemctl --user start kanata.service 100 | systemctl --user status kanata.service # check whether the service is running 101 | ``` 102 | ### 5b. To create and enable an OpenRC daemon service 103 | Edit new file `/etc/init.d/kanata` as root, replacing \<username\> as appropriate: 104 | ```bash 105 | #!/sbin/openrc-run 106 | 107 | command="/home/<username>/.cargo/bin/kanata" 108 | #command_args="--config=/home/<username>/.config/kanata/kanata.kbd" 109 | 110 | command_background=true 111 | pidfile="/run/${RC_SVCNAME}.pid" 112 | 113 | command_user="<username>" 114 | ``` 115 | 116 | Then run: 117 | ``` 118 | sudo chmod +x /etc/init.d/kanata # script must be executable 119 | sudo rc-service kanata start 120 | rc-status # check that kanata isn't listed as [ crashed ] 121 | sudo rc-update add kanata default # start the service automatically at boot 122 | ``` 123 | 124 | # Credits 125 | 126 | The original text was taken and adapted from: https://github.com/kmonad/kmonad/blob/master/doc/faq.md#linux 127 | -------------------------------------------------------------------------------- /docs/simulated_output/sim.kbd: -------------------------------------------------------------------------------- 1 | (defcfg 2 | 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. 3 | log-layer-changes yes ;;|no| overhead 4 | ) 5 | (defvar ;; declare commonly-used values. prefix with $ to call them. They are refered with `lt;var name>` 6 | tap-repress-timeout 1000 ;;|500| 7 | hold-timeout 1500 ;;|500| 8 | 🕐↕ $tap-repress-timeout 9 | 🕐🠿 $hold-timeout 10 | ) 11 | (defalias 12 | ;; home row mods ↕tap 🠿hold 13 | ;; pinky ring middle index | index middle ring pinky 14 | ;; timeout ↕tap 🠿hold¦↕tap 🠿hold action 15 | ⌂‹◆ (tap-hold-release $🕐↕ $🕐🠿 a ‹◆) ;; 16 | ⌂‹⎇ (tap-hold-release $🕐↕ $🕐🠿 s ‹⎇) ;; 17 | ⌂‹⎈ (tap-hold-release $🕐↕ $🕐🠿 d ‹⎈) ;; 18 | ⌂‹⇧ (tap-hold-release $🕐↕ $🕐🠿 f ‹⇧) ;; 19 | ⌂⇧› (tap-hold-release $🕐↕ $🕐🠿 j ⇧›) ;; same actions for the right side 20 | ⌂⎈› (tap-hold-release $🕐↕ $🕐🠿 k ⎈›) ;; 21 | ⌂⎇› (tap-hold-release $🕐↕ $🕐🠿 l ⎇›) ;; 22 | ⌂◆› (tap-hold-release $🕐↕ $🕐🠿 ; ◆›) ;; 23 | ) 24 | 25 | (defsrc 26 | ` 1 2 27 | a s d f j k l ;) 28 | (deflayer ⌂ ;; modtap layer for home row mods and 1 printing a 🤲🏿 char (will appear as 🤲 until kanata's unicode feature is extended) 29 | ‗ 🔣🤲🏿 ‗ 30 | @⌂‹◆ @⌂‹⎇ @⌂‹⎈ @⌂‹⇧ @⌂⇧› @⌂⎈› @⌂⎇› @⌂◆›) 31 | -------------------------------------------------------------------------------- /docs/simulated_output/sim.txt: -------------------------------------------------------------------------------- 1 | ↓j 🕐1600 ↓l 🕐5000 ↓1 🕐50 ↑1 🕐50 ↓1 🕐50 ↑1 🕐50 ↑j 🕐50 ↑l 🕐50 2 | -------------------------------------------------------------------------------- /docs/simulated_output/sim_out.txt: -------------------------------------------------------------------------------- 1 | 🕐Δms│ 1500 100 1500 3500 50 50 50 50 50 2 | In───┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 3 | k↑ │ 1 1 J L 4 | k↓ │ J L 1 1 5 | k⟳ │ 6 | Σin │ ↓J 🕐1600 ↓L 🕐5000 ↓1 🕐50 ↑1 🕐50 ↓1 🕐50 ↑1 🕐50 ↑J 🕐50 ↑L 🕐50 7 | Out──┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 8 | k↑ │ ⇧› ⎇› 9 | k↓ │ ⇧› ⎇› 10 | 🖰↑ │ 11 | 🖰↓ │ 12 | 🖰 │ 13 | 🔣 │ 🤲 🤲 14 | code│ 15 | raw↑│ 16 | raw↓│ 17 | Σout │ ↓⇧› ↓⎇› 🤲 🤲 ↑⇧› ↑⎇› 18 | -------------------------------------------------------------------------------- /docs/simulated_passthru_ahk/[COPY HERE] kanata_passthru.dll _: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/kanata/dd02c3e6bd6305d4e4593de492ad2dec5abe3684/docs/simulated_passthru_ahk/[COPY HERE] kanata_passthru.dll _ -------------------------------------------------------------------------------- /docs/simulated_passthru_ahk/kanata_dll.kbd: -------------------------------------------------------------------------------- 1 | ;;Test config for kanata.dll use by AutoHotkey, only maps two keys (f,j) to left/right modtap home row mod Shifts 2 | (defcfg 3 | process-unmapped-keys yes ;;|no| enable processing of keys that are not in defsrc 4 | log-layer-changes no ;;|no| overhead 5 | ) 6 | 7 | (defvar 8 | 🕐↕ 1000 ;;|500| tap-repress-timeout 9 | 🕐🠿 1500 ;;|500| hold-timeout 10 | ) 11 | (defalias ;; timeout→ tap hold ¦ tap hold ←action 12 | f⌂‹⇧ (tap-hold-release $🕐↕ $🕐🠿 f ‹⇧) 13 | j⌂⇧› (tap-hold-release $🕐↕ $🕐🠿 j ⇧›) 14 | ) 15 | 16 | (defsrc f j ) 17 | (deflayer ⌂ @f⌂‹⇧ @j⌂⇧›) 18 | -------------------------------------------------------------------------------- /docs/switch-design: -------------------------------------------------------------------------------- 1 | # Preface: 2 | 3 | This document is a scratch space for the design of the switch action. 4 | It may be out of date and is kept around for posterity. 5 | 6 | .syntax: 7 | ---- 8 | (switch 9 | (or a b c) (cmd <cmd1>) break 10 | (and a b (or c d)) (cmd <cmd2>) fallthrough 11 | (and a b (or c d) (or e f)) fallthrough 12 | () 13 | ) 14 | ---- 15 | 16 | 17 | .opcode format examples: 18 | ---- 19 | (or a b c) 20 | OR-4 a b c 21 | 22 | (and a b (or c d)) 23 | AND-6 a b OR-6 c d 24 | 25 | (and a b (or c d) (or e f)) 26 | AND-9 a b OR-6 c d OR-9 e f 27 | ---- 28 | 29 | .opcodes: 30 | ---- 31 | key: all values < 1024 32 | OR/AND: OP & 0xF000 33 | OR : 0x1000 34 | AND: 0x2000 35 | length: OP & 0x0FFF 36 | ---- 37 | 38 | .Rough algorithm for opcodes: 39 | ---- 40 | value=true 41 | push first opcode 42 | WHILE stack is not empty 43 | WHILE index <= ending_index 44 | switch 45 | opcode: 46 | push, continue 47 | key(OR): 48 | value=true: skip to index, pop 49 | value=false: continue 50 | key(AND): 51 | value=true: continue 52 | value=false: skip to index, pop 53 | pop 54 | switch 55 | current_value(OR): 56 | value=true: skip to index, pop 57 | value=false: continue 58 | current_value(AND): 59 | value=true: continue 60 | value=false: skip to index, pop 61 | return value 62 | ---- 63 | 64 | .statestruct: 65 | ---- 66 | value 67 | current_index 68 | current_end_index 69 | current_op 70 | stack (op, ending_index) 71 | ---- 72 | 73 | .rough sequence 1: 74 | ---- 75 | pressed: y y y y y y 76 | opcodes: AND-9 a b OR-6 c d OR-9 e f 77 | 78 | index: 0 79 | push: AND-9 80 | stack: AND-9 81 | 82 | index: 1 83 | val: true 84 | 85 | index: 2 86 | val: true 87 | 88 | index: 3 89 | push: OR-6 90 | stack: AND-9 OR-6 91 | 92 | index: 4 93 | val: true 94 | skip to 6 95 | pop 96 | stack: AND-9 97 | 98 | index: 6 99 | push: OR-9 100 | stack: AND-9 OR-9 101 | 102 | index: 7 103 | val: true 104 | skip to 9 105 | pop 106 | stack: AND-9-true 107 | 108 | index 9: 109 | pop 110 | stack: empty 111 | return val: true 112 | ---- 113 | 114 | .rough sequence 2: 115 | ---- 116 | pressed: y y n n y y 117 | opcodes: AND-9 a b OR-6 c d OR-9 e f 118 | 119 | index: 0 120 | push: AND-9 121 | stack: AND-9 122 | 123 | index: 1 124 | val: true 125 | 126 | index: 2 127 | val: true 128 | 129 | index: 3 130 | push: OR-6 131 | stack: AND-9 OR-6 132 | val: true 133 | 134 | index: 4 135 | val: false 136 | 137 | index: 5 138 | val: false 139 | 140 | index: 6 141 | val: false 142 | pop 143 | stack: AND-9 144 | skip to 9 145 | pop 146 | stack: empty 147 | return val: false 148 | ---- 149 | 150 | .rough sequence 3: 151 | ---- 152 | pressed: n y n n y y 153 | opcodes: AND-9 a b OR-6 c d OR-9 e f 154 | 155 | index: 0 156 | push: AND-9 157 | stack: AND-9 158 | 159 | index: 1 160 | val: false 161 | skip to 9 162 | pop 163 | stack: empty 164 | return val: false 165 | ---- 166 | 167 | 168 | .pseudo code again: 169 | ---- 170 | let mut value = true 171 | let mut current_index = 1 172 | let mut current_end_index = first_opcode - end_index 173 | let mut current_op = OR 174 | while current_index < slice_length { 175 | if index >= current_end_index: 176 | if stack is empty: 177 | break 178 | else: 179 | pop stack to current_op and current_end_index 180 | switch 181 | current_value(OR): 182 | value=true: skip to current_end_index; continue 183 | current_value(AND): 184 | value=false: skip to current_end_index; continue 185 | switch 186 | opcode: 187 | push (current_end_index,current_op) 188 | update (current_end_index,current_op) with opcode 189 | key(OR): 190 | value=true: skip to current_end_index; continue 191 | value=false 192 | key(AND): 193 | value=true 194 | value=false: skip to current_end_index; continue 195 | current_index++; 196 | } 197 | return value 198 | ---- 199 | -------------------------------------------------------------------------------- /docs/win-tray/win-tray-layer-change.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/kanata/dd02c3e6bd6305d4e4593de492ad2dec5abe3684/docs/win-tray/win-tray-layer-change.gif -------------------------------------------------------------------------------- /docs/win-tray/win-tray-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/kanata/dd02c3e6bd6305d4e4593de492ad2dec5abe3684/docs/win-tray/win-tray-screen.png -------------------------------------------------------------------------------- /example_tcp_client/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /example_tcp_client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kanata_example_tcp_client" 3 | description = "Example kanata TCP client" 4 | version = "1.1.0" 5 | edition = "2021" 6 | license = "LGPL-3.0" 7 | authors = ["jtroo <j.andreitabs@gmail.com>"] 8 | 9 | [dependencies] 10 | anyhow = "1" 11 | clap = { version = "4", features = [ "derive" ] } 12 | kanata-tcp-protocol = { path = "../tcp_protocol" } 13 | log = "0.4.8" 14 | simplelog = "0.12" 15 | serde_json = "1" -------------------------------------------------------------------------------- /interception/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["."] 3 | 4 | [package] 5 | name = "kanata-interception" 6 | description = "Safe wrapper for Interception. Forked for use with kanata." 7 | version = "0.3.0" 8 | authors = ["Joe Kaushal <joe.kaushal@gmail.com>"] 9 | edition = "2018" 10 | repository = "https://github.com/jtroo/kanata" 11 | license = "MIT OR Apache-2.0" 12 | 13 | [dependencies] 14 | interception-sys = "0.1.3" 15 | bitflags = "1.2.1" 16 | num_enum = "0.6.0" 17 | serde = { version = "1.0.114", features = ["derive"] } 18 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] 2 | 3 | # Build the release binaries for Linux and put the binaries+cfg in the output directory 4 | build_release_linux output_dir: 5 | cargo build --release 6 | cp target/release/kanata "{{output_dir}}/kanata" 7 | strip "{{output_dir}}/kanata" 8 | cargo build --release --features cmd 9 | cp target/release/kanata "{{output_dir}}/kanata_cmd_allowed" 10 | strip "{{output_dir}}/kanata_cmd_allowed" 11 | cp cfg_samples/kanata.kbd "{{output_dir}}" 12 | 13 | # Build the release binaries for Windows and put the binaries+cfg in the output directory. 14 | build_release_windows output_dir: 15 | cargo build --release --no-default-features --features tcp_server,win_manifest; cp target/release/kanata.exe "{{output_dir}}\kanata_legacy_output.exe" 16 | cargo build --release --features win_manifest,interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_wintercept.exe" 17 | cargo build --release --features win_manifest,win_sendinput_send_scancodes; cp target/release/kanata.exe "{{output_dir}}\kanata.exe" 18 | cargo build --release --features win_manifest,win_sendinput_send_scancodes,win_llhook_read_scancodes; cp target/release/kanata.exe "{{output_dir}}\kanata_winIOv2.exe" 19 | cargo build --release --features win_manifest,cmd,win_sendinput_send_scancodes; cp target/release/kanata.exe "{{output_dir}}\kanata_cmd_allowed.exe" 20 | cargo build --release --features win_manifest,cmd,interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_wintercept_cmd_allowed.exe" 21 | cargo build --release --features passthru_ahk --package=simulated_passthru; cp target/release/kanata_passthru.dll "{{output_dir}}\kanata_passthru.dll" 22 | cargo build --release --features win_manifest,gui ; cp target/release/kanata.exe "{{output_dir}}\kanata_gui.exe" 23 | cargo build --release --features win_manifest,gui,cmd; cp target/release/kanata.exe "{{output_dir}}\kanata_gui_cmd_allowed.exe" 24 | cargo build --release --features win_manifest,gui,interception_driver ; cp target/release/kanata.exe "{{output_dir}}\kanata_gui_wintercept.exe" 25 | cargo build --release --features win_manifest,gui,cmd,interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_gui_wintercept_cmd_allowed.exe" 26 | cp cfg_samples/kanata.kbd "{{output_dir}}" 27 | 28 | # Generate the sha256sums for all files in the output directory 29 | sha256sums output_dir: 30 | rm -f {{output_dir}}/sha256sums 31 | cd {{output_dir}}; sha256sum * > sha256sums 32 | 33 | test: 34 | cargo test -p kanata -p kanata-parser -p kanata-keyberon -- --nocapture 35 | cargo test --features=simulated_output sim_tests 36 | cargo clippy --all 37 | 38 | fmt: 39 | cargo fmt --all 40 | 41 | guic: 42 | cargo check --features=gui 43 | guif: 44 | cargo fmt --all 45 | cargo clippy --all --fix --features=gui -- -D warnings 46 | 47 | ahkc: 48 | cargo check --features=passthru_ahk 49 | ahkf: 50 | cargo fmt --all 51 | cargo clippy --all --fix --features=passthru_ahk -- -D warnings 52 | 53 | change_subcrate_versions version: 54 | sed -i 's/^version = ".*"$/version = "{{version}}"/' parser/Cargo.toml tcp_protocol/Cargo.toml keyberon/Cargo.toml 55 | sed -i 's/^\(#\? \?kanata-\(keyberon\|parser\|tcp-protocol\).*version\) = "[0-9.]*"/\1 = "{{version}}"/' Cargo.toml parser/Cargo.toml 56 | 57 | cov: 58 | cargo llvm-cov clean --workspace 59 | cargo llvm-cov --no-report --workspace --no-default-features 60 | cargo llvm-cov --no-report --workspace 61 | cargo llvm-cov --no-report --workspace --features=cmd,win_llhook_read_scancodes,win_sendinput_send_scancodes 62 | cargo llvm-cov --no-report --workspace --features=cmd,interception_driver,win_sendinput_send_scancodes 63 | cargo llvm-cov --no-report --features=simulated_output -- sim_tests 64 | cargo llvm-cov report --html 65 | 66 | publish: 67 | cd keyberon && cargo publish 68 | cd tcp_protocol && cargo publish 69 | cd parser && cargo publish 70 | cargo publish 71 | 72 | # Include the trailing `\` or `/` in the output_dir parameter. The parameter should be an absolute path. 73 | cfg_to_html output_dir: 74 | cd docs ; asciidoctor config.adoc 75 | cd docs ; cp config.html "{{output_dir}}config.html"; rm config.html 76 | 77 | # Include the trailing `\` or `/` in the output_dir parameter. The parameter should be an absolute path. 78 | wasm_pack output_dir: 79 | cd wasm; wasm-pack build --target web; cd pkg; cp kanata_wasm_bg.wasm "{{output_dir}}"; cp kanata_wasm.js "{{output_dir}}" 80 | -------------------------------------------------------------------------------- /key-sort-add/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "key-sort-add" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /key-sort-add/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["."] 3 | 4 | [package] 5 | name = "key-sort-add" 6 | version = "0.1.0" 7 | edition = "2021" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | -------------------------------------------------------------------------------- /key-sort-add/README.md: -------------------------------------------------------------------------------- 1 | # key-sort-add 2 | 3 | A small program that was used to sort and fill in OsCode mappings. 4 | -------------------------------------------------------------------------------- /key-sort-add/src/main.rs: -------------------------------------------------------------------------------- 1 | //! one: 2 | //! 3 | //! Takes a file formatted as: 4 | //! 5 | //! KEY_RESERVED = 0, 6 | //! KEY_ESC = 1, 7 | //! KEY_1 = 2, 8 | //! KEY_2 = 3, 9 | //! KEY_3 = 4, 10 | //! KEY_4 = 5, 11 | //! ... 12 | //! 13 | //! Outputs to stdout a sorted version of the file with numeric gaps filled in with: 14 | //! 15 | //! KEY_X = X, 16 | //! 17 | //! two: mapping.txt to ensure KeyCode and OsCode can simply be transmuted into each other. 18 | 19 | use std::io::Read; 20 | 21 | fn main() { 22 | match std::env::args().nth(1).expect("function parameter").as_str() { 23 | "one" => one(), 24 | "two" => two(), 25 | _ => panic!("unknown capabality"), 26 | } 27 | } 28 | 29 | fn one() { 30 | let mut f = std::fs::File::open(std::env::args().nth(2).expect("filename parameter")) 31 | .expect("file open"); 32 | let mut s = String::new(); 33 | f.read_to_string(&mut s).expect("read file"); 34 | let mut keys = s 35 | .lines() 36 | .map(|l| { 37 | let mut segments = l.trim_end_matches(',').trim().split(" = "); 38 | let key = segments.next().expect("a string"); 39 | let num: u16 = u16::from_str_radix( 40 | segments 41 | .next() 42 | .map(|s| s.trim_start_matches("0x")) 43 | .expect("string after ="), 44 | 10, 45 | ) 46 | .expect("u16"); 47 | (key.to_owned(), num) 48 | }) 49 | .collect::<Vec<_>>(); 50 | keys.sort_by_key(|k| k.1); 51 | let mut keys_to_add = vec![]; 52 | let mut cur_key = keys.iter(); 53 | let mut prev_key = keys.iter(); 54 | cur_key.next(); 55 | for cur in cur_key { 56 | let prev = prev_key.next().expect("lagging iterator is valid"); 57 | for missing in prev.1 + 1..cur.1 { 58 | keys_to_add.push((format!("K{missing}"), missing)); 59 | } 60 | } 61 | keys.append(&mut keys_to_add); 62 | keys.sort_by_key(|k| k.1); 63 | for key in keys { 64 | println!("{} = {},", key.0, key.1); 65 | } 66 | } 67 | 68 | fn two() { 69 | use std::collections::HashMap; 70 | 71 | let mut f = std::fs::File::open(std::env::args().nth(2).expect("filename parameter")) 72 | .expect("file open"); 73 | let mut s = String::new(); 74 | f.read_to_string(&mut s).expect("read file"); 75 | let mut lines = s.lines(); 76 | 77 | // filter out useless lines 78 | while let Some(line) = lines.next() { 79 | if line == "=== kc to osc" { 80 | break; 81 | } 82 | } 83 | 84 | // parse kc to osc 85 | let mut kc_to_osc: HashMap<&str, &str> = HashMap::new(); 86 | while let Some(line) = lines.next() { 87 | if line.trim().is_empty() { 88 | continue; 89 | } 90 | if line == "=== osc to u16" { 91 | break; 92 | } 93 | let (kc, osc) = line.split_once(" => ").expect("arrow separator"); 94 | let kc = kc.trim_start_matches("KeyCode::"); 95 | let osc = osc.trim_end_matches(',') 96 | .trim_start_matches("OsCode::"); 97 | kc_to_osc.insert(kc, osc); 98 | } 99 | 100 | // parse osc to u16 101 | let mut osc_vals: HashMap<&str, u16> = HashMap::new(); 102 | while let Some(line) = lines.next() { 103 | if line.trim().is_empty() { 104 | continue; 105 | } 106 | if line == "=== all kcs" { 107 | break; 108 | } 109 | let (kc, num) = line.split_once(" = ").expect("equal separator"); 110 | let num = num.trim_end_matches(',').parse::<u16>().expect("u16"); 111 | osc_vals.insert(kc, num); 112 | } 113 | 114 | // parse kcs 115 | let mut kc_vals: Vec<(&str, Option<u16>)> = vec![]; 116 | while let Some(line) = lines.next() { 117 | if line.trim().is_empty() { 118 | continue; 119 | } 120 | let kc = line.trim_end_matches(','); 121 | let val: Option<u16> = kc_to_osc.get(&kc) 122 | .and_then(|osc| osc_vals.get(osc)) 123 | .copied(); 124 | kc_vals.push((kc, val)); 125 | } 126 | 127 | for (kc, val) in kc_vals.iter() { 128 | println!("{kc} = {},", val.unwrap_or(65535)); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /keyberon/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Cargo.lock since this is a library crate 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /keyberon/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.2.0 2 | 3 | * New Keyboard::leds_mut function for getting underlying leds object. 4 | * Made Layout::current_layer public for getting current active layer. 5 | * Added a procedural macro for defining layouts (`keyberon::layout::layout`) 6 | * Corrected HID report descriptor 7 | * Add max_packet_size() to HidDevice to allow differing report sizes 8 | * Allows default layer to be set on a Layout externally 9 | * Add Chording for multiple keys pressed at the same time to equal another key 10 | 11 | Breaking changes: 12 | * Row and Column pins are now a simple array. For the STM32 MCU, you 13 | should now use `.downgrade()` to have an homogenous array. 14 | * `Action::HoldTap` now takes a configuration for different behaviors. 15 | * `Action::HoldTap` now takes the `tap_hold_interval` field. Not 16 | implemented yet. 17 | * `Action` is now generic, for the `Action::Custom(T)` variant, 18 | allowing custom actions to be handled outside of keyberon. This 19 | functionality can be used to drive non keyboard actions, such as resetting 20 | the microcontroller, driving leds (for backlight or underglow for 21 | example), managing a mouse emulation, or any other ideas you can 22 | have. As there is a default value for the type parameter, the update 23 | should be transparent. 24 | * Layers don't sum anymore, the last pressed layer action set the layer. 25 | * Rename MeidaCoffee in MediaCoffee to fix typo. 26 | 27 | # v0.1.1 28 | 29 | * HidClass::control_xxx: check interface number [#26](https://github.com/TeXitoi/keyberon/pull/26) 30 | 31 | # v0.1.0 32 | 33 | First published version. 34 | -------------------------------------------------------------------------------- /keyberon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kanata-keyberon" 3 | version = "0.190.0" 4 | authors = ["Guillaume Pinot <texitoi@texitoi.eu>", "Robin Krahl <robin.krahl@ireas.org>", "jtroo <j.andreitabs@gmail.com>"] 5 | edition = "2021" 6 | description = "Pure Rust keyboard firmware. Fork intended for use with kanata." 7 | documentation = "https://docs.rs/keyberon" 8 | repository = "https://github.com/TeXitoi/keyberon" 9 | keywords = ["keyboard", "kanata"] 10 | categories = ["no-std"] 11 | license = "MIT" 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | kanata-keyberon-macros = { version = "0.2.0" } 16 | heapless = "0.7.16" 17 | rustc-hash = "1.1.0" 18 | arraydeque = { version = "0.5.1", default-features = false } 19 | -------------------------------------------------------------------------------- /keyberon/KEYBOARDS.md: -------------------------------------------------------------------------------- 1 | | Keyboard | PCB or Handwired | MCU | Feature Status | 2 | | - | - | - | - | 3 | | [KeySeeBee](https://github.com/TeXitoi/keyseebee) | PCB | STM32F072 | <ul><li>[x] Matrix </li><li>[x] Split</li></ul> | 4 | | [Keyberon-f4](https://github.com/TeXitoi/keyberon-f4) | Handwired | STM32F401 | <ul><li>[x] Matrix </li></ul> | 5 | | [Arisu](https://github.com/help-14/arisu-handwired) | Handwired | STM32F401 | <ul><li>[x] Matrix </li></ul> | 6 | | [ortho60-keyberon](https://github.com/TeXitoi/ortho60-keyberon) | PCB | STM32F103 | <ul><li>[x] Matrix </li></ul> | 7 | | [keyberon-grid](https://github.com/TeXitoi/keyberon-grid) | Handwired | STM32F103 | <ul><li>[x] Matrix </li></ul> | 8 | | [Clueboard 66% LP](https://github.com/wezm/clueboard-rust-firmware) | PCB | STM32F303 | <ul><li>[x] Matrix </li><li>[ ] LEDs</li><li>[ ] Speakers</li></ul> | 9 | | [anne-keyberon](https://github.com/hdhoang/anne-keyberon) | PCB | STM32L151 | <ul><li>[ ] Matrix </li><li>[ ] BT proto </li><li>[ ] LED proto </li><li>[ ] LED MCU </li></ul> | 10 | | [corne-xiao](https://github.com/lehmanju/corne-xiao) | PCB | ATSAMD21 | <ul><li>[x] Matrix </li></ul> | 11 | | [pinci](https://github.com/camrbuss/pinci) | PCB | RP2040 | <ul><li>[x] Matrix </li><li>[x] Split</li></ul> | 12 | | [nibble-rp2040-rs](https://github.com/DrewTChrist/nibble-rp2040-rs) | PCB | RP2040 | <ul><li>[x] Matrix </li><li>[ ] Rotary Encoder</li><li>[ ] RGB LEDs</li><li>[ ] OLED</li></ul> | 13 | | [keyboard-labs](https://github.com/rgoulter/keyboard-labs) | PCB | STM32F401 | <ul><li>[x] Matrix </li><li>[x] Split</li></ul> | 14 | | [makerdiary M60](https://github.com/jamesmunns/m60-keyboard/) | PCB | nRF52840 | <ul><li>[x] Matrix </li><li>[x] RGB LEDs</li> | 15 | | [PouetPouet](https://github.com/dkm/pouetpouet-board) | PCB | STM32F072 | <ul><li>[x] Matrix </li></ul> | 16 | | [corne](https://github.com/simmsb/keyboard) | PCB | nRF52840 | <ul><li>[x] Matrix </li><li>[x] RGB LEDs</li><li>[x] OLED</li><li>[x] split</li></ul> | 17 | | [Cantor](https://github.com/dariogoetz/cantor-firmware-keyberon) | PCB | STM32F401 | <ul><li>[x] Matrix </li><li>[ ] LEDs</li><li>[x] Split</li><li>[x] Diodeless</li></ul> | 18 | -------------------------------------------------------------------------------- /keyberon/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Guillaume P. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /keyberon/README.md: -------------------------------------------------------------------------------- 1 | # kanata-keyberon 2 | 3 | ## Note 4 | 5 | This is a fork intended for use by the [kanata keyboard remapper software](https://github.com/jtroo/kanata). 6 | Please make contributions to the [original project](https://github.com/TeXitoi/keyberon) where applicable. 7 | 8 | This crate does not follow semver. It tracks the version of kanata. 9 | -------------------------------------------------------------------------------- /keyberon/images/keyberon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtroo/kanata/dd02c3e6bd6305d4e4593de492ad2dec5abe3684/keyberon/images/keyberon.jpg -------------------------------------------------------------------------------- /keyberon/keyberon-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kanata-keyberon-macros" 3 | version = "0.2.0" 4 | authors = ["Antoni Simka <antonisimka.8@gmail.com>"] 5 | edition = "2018" 6 | description = "Macros for keyberon. Fork for kanata project" 7 | license = "MIT" 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | proc-macro2 = "1.0" 14 | quote = "1.0" 15 | -------------------------------------------------------------------------------- /keyberon/keyberon-macros/README.md: -------------------------------------------------------------------------------- 1 | # kanata keyberon macros 2 | 3 | ## Note 4 | 5 | This is a fork intended for use by the [kanata keyboard remapper software](https://github.com/jtroo/keyberon). 6 | Please make contributions to the [original project](https://github.com/TeXitoi/keyberon) where applicable. 7 | -------------------------------------------------------------------------------- /keyberon/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This is a fork intended for use by the [kanata keyboard remapper software](https://github.com/jtroo/kanata). 2 | //! Please make contributions to the original project. 3 | 4 | pub mod action; 5 | pub mod chord; 6 | pub mod key_code; 7 | pub mod layout; 8 | mod multikey_buffer; 9 | -------------------------------------------------------------------------------- /keyberon/src/multikey_buffer.rs: -------------------------------------------------------------------------------- 1 | //! Module for `MultiKeyBuffer`. 2 | 3 | use std::{array, slice}; 4 | 5 | use crate::action::{Action, ONE_SHOT_MAX_ACTIVE}; 6 | use crate::key_code::KeyCode; 7 | 8 | // Presumably this should be plenty. 9 | // ONE_SHOT_MAX_ACTIVE is already likely unreasonably large enough. 10 | // This buffer capacity adds more onto that, 11 | // just in case somebody finds a way to use all of the one-shot capacity. 12 | const BUFCAP: usize = ONE_SHOT_MAX_ACTIVE + 4; 13 | 14 | /// This is an unsafe container that enables a mutable Action::MultipleKeyCodes. 15 | pub(crate) struct MultiKeyBuffer<'a, T> { 16 | buf: [KeyCode; BUFCAP], 17 | size: usize, 18 | ptr: *mut &'static [KeyCode], 19 | ac: *mut Action<'a, T>, 20 | } 21 | 22 | unsafe impl<T> Send for MultiKeyBuffer<'_, T> {} 23 | 24 | impl<'a, T> MultiKeyBuffer<'a, T> { 25 | /// Create a new instance of `MultiKeyBuffer`. 26 | /// 27 | /// # Safety 28 | /// 29 | /// The program should not have any references to the inner buffer when the struct is dropped. 30 | pub(crate) unsafe fn new() -> Self { 31 | Self { 32 | buf: array::from_fn(|_| KeyCode::Escape), 33 | size: 0, 34 | ptr: Box::leak(Box::new(slice::from_raw_parts( 35 | core::ptr::NonNull::dangling().as_ptr(), 36 | 0, 37 | ))), 38 | ac: Box::leak(Box::new(Action::NoOp)), 39 | } 40 | } 41 | 42 | /// Set the current size of the buffer to zero. 43 | /// 44 | /// # Safety 45 | /// 46 | /// The program should not have any references to the inner buffer. 47 | pub(crate) unsafe fn clear(&mut self) { 48 | self.size = 0; 49 | } 50 | 51 | /// Push to the end of the buffer. If the buffer is full, this silently fails. 52 | /// 53 | /// # Safety 54 | /// 55 | /// The program should not have any references to the inner buffer. 56 | pub(crate) unsafe fn push(&mut self, kc: KeyCode) { 57 | if self.size < BUFCAP { 58 | self.buf[self.size] = kc; 59 | self.size += 1; 60 | } 61 | } 62 | 63 | /// Get a reference to the inner buffer in the form of an `Action`. 64 | /// The `Action` will be the variant `MultipleKeyCodes`, 65 | /// containing all keys that have been pushed. 66 | /// 67 | /// # Safety 68 | /// 69 | /// The program should not have any references to the inner buffer before calling. 70 | /// The program should not mutate the buffer after calling this function until after the 71 | /// returned reference is dropped. 72 | pub(crate) unsafe fn get_ref(&self) -> &'a Action<'a, T> { 73 | *self.ac = Action::NoOp; 74 | *self.ptr = slice::from_raw_parts(self.buf.as_ptr(), self.size); 75 | *self.ac = Action::MultipleKeyCodes(&*self.ptr); 76 | &*self.ac 77 | } 78 | } 79 | 80 | impl<T> Drop for MultiKeyBuffer<'_, T> { 81 | fn drop(&mut self) { 82 | unsafe { 83 | drop(Box::from_raw(self.ac)); 84 | drop(Box::from_raw(self.ptr)); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /parser/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Cargo.lock since this is a library crate 2 | Cargo.lock 3 | target -------------------------------------------------------------------------------- /parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kanata-parser" 3 | version = "0.190.0" 4 | authors = ["jtroo <j.andreitabs@gmail.com>"] 5 | description = "A parser for configuration language of kanata, a keyboard remapper." 6 | keywords = ["kanata", "parser"] 7 | homepage = "https://github.com/jtroo/kanata" 8 | repository = "https://github.com/jtroo/kanata" 9 | readme = "README.md" 10 | license = "LGPL-3.0-only" 11 | edition = "2021" 12 | 13 | [dependencies] 14 | anyhow = "1" 15 | bitflags = "2.5.0" 16 | bytemuck = "1.15.0" 17 | itertools = "0.12" 18 | log = { version = "0.4.8", default-features = false } 19 | miette = { version = "5.7.0", features = ["fancy"] } 20 | once_cell = "1" 21 | parking_lot = "0.12" 22 | patricia_tree = "0.8" 23 | rustc-hash = "1.1.0" 24 | thiserror = "1.0.38" 25 | 26 | kanata-keyberon = { path = "../keyberon", version = "0.190.0" } 27 | 28 | [dev-dependencies] 29 | simplelog = "0.12.0" 30 | 31 | [features] 32 | cmd = [] 33 | interception_driver = [] 34 | gui = [] 35 | lsp = [] 36 | win_llhook_read_scancodes = [] 37 | win_sendinput_send_scancodes = [] 38 | zippychord = [] 39 | -------------------------------------------------------------------------------- /parser/README.md: -------------------------------------------------------------------------------- 1 | # kanata-parser 2 | 3 | A parser for configuration language of [kanata](https://github.com/jtroo/kanata). 4 | 5 | This crate does not follow semver. It tracks the version of kanata. 6 | -------------------------------------------------------------------------------- /parser/src/cfg/alloc.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a helper struct for generating 'static lifetime allocations while still 2 | //! keeping track of them so that they can be freed later. 3 | 4 | use parking_lot::Mutex; 5 | use std::sync::Arc; 6 | 7 | /// This struct tracks the allocations that are leaked by its provided methods and frees them when 8 | /// dropped. The `new` function is unsafe because dropping the struct can create dangling 9 | /// references. Care must be taken to ensure that all allocations made by this struct's methods are 10 | /// no longer referenced when the struct gets dropped. 11 | /// 12 | /// In practice, this is not difficult to do in the `cfg` module which only exposes a single public 13 | /// method. 14 | pub(crate) struct Allocations { 15 | allocations: Mutex<Vec<usize>>, 16 | } 17 | 18 | impl std::fmt::Debug for Allocations { 19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 20 | f.debug_struct("Allocations").finish() 21 | } 22 | } 23 | 24 | impl Drop for Allocations { 25 | fn drop(&mut self) { 26 | log::debug!( 27 | "freeing allocations of length {}", 28 | self.allocations.lock().len() 29 | ); 30 | for p in self.allocations.lock().iter().rev().copied() { 31 | unsafe { drop(Box::from_raw(p as *mut usize)) }; 32 | } 33 | } 34 | } 35 | 36 | impl Allocations { 37 | /// Create a new allocations group. 38 | /// 39 | /// # Safety 40 | /// 41 | /// Ensure that all associated allocations are no longer referenced before dropping all 42 | /// clones of the `Arc`. 43 | pub(crate) unsafe fn new() -> Arc<Self> { 44 | Arc::new(Self { 45 | allocations: Mutex::new(vec![]), 46 | }) 47 | } 48 | 49 | /// Returns a `&'static T` by leaking a newly created Box of `v`. 50 | pub(crate) fn sref<T>(&self, v: T) -> &'static T { 51 | let p = Box::into_raw(Box::new(v)); 52 | if (p as usize) < 16 { 53 | panic!("sref bad ptr"); 54 | } 55 | self.allocations.lock().push(p as usize); 56 | Box::leak(unsafe { Box::from_raw(p) }) 57 | } 58 | 59 | pub(crate) fn bref_slice<T>(&self, v: Box<[T]>) -> &'static [T] { 60 | // An empty slice has no backing allocation. `Box<[T]>` is a fat pointer so the leaked return 61 | // will contain a length of 0 and an invalid pointer. 62 | if !v.is_empty() { 63 | self.allocations.lock().push(v.as_ptr() as usize); 64 | } 65 | Box::leak(v) 66 | } 67 | 68 | /// Returns a &'static [&'static T] from a `Vec<T>` by converting to a boxed slice and leaking it. 69 | pub(crate) fn sref_vec<T>(&self, v: Vec<T>) -> &'static [T] { 70 | self.bref_slice(v.into_boxed_slice()) 71 | } 72 | 73 | /// Returns a `&'static [&'static T]` by leaking a newly created box and boxed slice of `v`. 74 | pub(crate) fn sref_slice<T>(&self, v: T) -> &'static [&'static T] { 75 | self.bref_slice(vec![self.sref(v)].into_boxed_slice()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /parser/src/cfg/custom_tap_hold.rs: -------------------------------------------------------------------------------- 1 | use kanata_keyberon::layout::{Event, QueuedIter, WaitingAction}; 2 | 3 | use crate::keys::OsCode; 4 | 5 | use super::alloc::Allocations; 6 | 7 | /// Returns a closure that can be used in `HoldTapConfig::Custom`, which will return early with a 8 | /// Tap action in the case that any of `keys` are pressed. Otherwise it behaves as 9 | /// `HoldTapConfig::PermissiveHold` would. 10 | pub(crate) fn custom_tap_hold_release( 11 | keys: &[OsCode], 12 | a: &Allocations, 13 | ) -> &'static (dyn Fn(QueuedIter) -> (Option<WaitingAction>, bool) + Send + Sync) { 14 | let keys = a.sref_vec(Vec::from_iter(keys.iter().copied())); 15 | a.sref( 16 | move |mut queued: QueuedIter| -> (Option<WaitingAction>, bool) { 17 | while let Some(q) = queued.next() { 18 | if q.event().is_press() { 19 | let (i, j) = q.event().coord(); 20 | // If any key matches the input, do a tap right away. 21 | if keys.iter().copied().map(u16::from).any(|j2| j2 == j) { 22 | return (Some(WaitingAction::Tap), false); 23 | } 24 | // Otherwise do the PermissiveHold algorithm. 25 | let target = Event::Release(i, j); 26 | if queued.clone().copied().any(|q| q.event() == target) { 27 | return (Some(WaitingAction::Hold), false); 28 | } 29 | } 30 | } 31 | (None, false) 32 | }, 33 | ) 34 | } 35 | 36 | pub(crate) fn custom_tap_hold_except( 37 | keys: &[OsCode], 38 | a: &Allocations, 39 | ) -> &'static (dyn Fn(QueuedIter) -> (Option<WaitingAction>, bool) + Send + Sync) { 40 | let keys = a.sref_vec(Vec::from_iter(keys.iter().copied())); 41 | a.sref( 42 | move |mut queued: QueuedIter| -> (Option<WaitingAction>, bool) { 43 | for q in queued.by_ref() { 44 | if q.event().is_press() { 45 | let (_i, j) = q.event().coord(); 46 | // If any key matches the input, do a tap. 47 | if keys.iter().copied().map(u16::from).any(|j2| j2 == j) { 48 | return (Some(WaitingAction::Tap), false); 49 | } 50 | // Otherwise continue with default behavior 51 | return (None, false); 52 | } 53 | } 54 | // Otherwise skip timeout 55 | (None, true) 56 | }, 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /parser/src/cfg/error.rs: -------------------------------------------------------------------------------- 1 | use miette::{Diagnostic, NamedSource, SourceSpan}; 2 | use thiserror::Error; 3 | 4 | use super::{sexpr::Span, *}; 5 | 6 | pub type MResult<T> = miette::Result<T>; 7 | pub type Result<T> = std::result::Result<T, ParseError>; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct ParseError { 11 | pub msg: String, 12 | pub span: Option<Span>, 13 | } 14 | 15 | impl ParseError { 16 | pub fn new(span: Span, err_msg: impl AsRef<str>) -> Self { 17 | Self { 18 | msg: err_msg.as_ref().to_string(), 19 | span: Some(span), 20 | } 21 | } 22 | 23 | pub fn new_without_span(err_msg: impl AsRef<str>) -> Self { 24 | Self { 25 | msg: err_msg.as_ref().to_string(), 26 | span: None, 27 | } 28 | } 29 | 30 | pub fn from_expr(expr: &sexpr::SExpr, err_msg: impl AsRef<str>) -> Self { 31 | Self::new(expr.span(), err_msg) 32 | } 33 | 34 | pub fn from_spanned<T>(spanned: &Spanned<T>, err_msg: impl AsRef<str>) -> Self { 35 | Self::new(spanned.span.clone(), err_msg) 36 | } 37 | } 38 | 39 | impl From<anyhow::Error> for ParseError { 40 | fn from(value: anyhow::Error) -> Self { 41 | Self::new_without_span(value.to_string()) 42 | } 43 | } 44 | 45 | impl From<ParseError> for miette::Error { 46 | fn from(val: ParseError) -> Self { 47 | let diagnostic = CfgError { 48 | err_span: val 49 | .span 50 | .as_ref() 51 | .map(|s| SourceSpan::new(s.start().into(), (s.end() - s.start()).into())), 52 | help_msg: help(val.msg), 53 | file_name: val.span.as_ref().map(|s| s.file_name()), 54 | file_content: val.span.as_ref().map(|s| s.file_content()), 55 | }; 56 | 57 | let report: miette::Error = diagnostic.into(); 58 | 59 | if let Some(span) = val.span { 60 | report.with_source_code(NamedSource::new(span.file_name(), span.file_content())) 61 | } else { 62 | report 63 | } 64 | } 65 | } 66 | 67 | #[derive(Error, Debug, Diagnostic, Clone)] 68 | #[error("Error in configuration")] 69 | #[diagnostic()] 70 | struct CfgError { 71 | // Snippets and highlights can be included in the diagnostic! 72 | #[label("Error here")] 73 | err_span: Option<SourceSpan>, 74 | #[help] 75 | help_msg: String, 76 | file_name: Option<String>, 77 | file_content: Option<String>, 78 | } 79 | 80 | pub(super) fn help(err_msg: impl AsRef<str>) -> String { 81 | format!( 82 | r"{} 83 | 84 | For more info, see the configuration guide: 85 | https://github.com/jtroo/kanata/blob/main/docs/config.adoc", 86 | err_msg.as_ref(), 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /parser/src/cfg/is_a_button.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn is_a_button(osc: u16) -> bool { 2 | if cfg!(target_os = "windows") { 3 | matches!(osc, 1..=6 | 256..) 4 | } else { 5 | osc >= 256 6 | } 7 | } 8 | 9 | #[test] 10 | fn mouse_inputs_most_care_about_are_considered_buttons() { 11 | use crate::keys::{OsCode, OsCode::*}; 12 | const MOUSE_INPUTS: &[OsCode] = &[ 13 | MouseWheelUp, 14 | MouseWheelDown, 15 | MouseWheelLeft, 16 | MouseWheelRight, 17 | BTN_LEFT, 18 | BTN_RIGHT, 19 | BTN_MIDDLE, 20 | BTN_SIDE, 21 | BTN_EXTRA, 22 | BTN_FORWARD, 23 | BTN_BACK, 24 | ]; 25 | for input in MOUSE_INPUTS.iter().copied() { 26 | println!("{input}"); 27 | assert!(is_a_button(input.into())); 28 | } 29 | } 30 | 31 | #[test] 32 | fn standard_keys_are_not_considered_buttons() { 33 | use crate::keys::{OsCode, OsCode::*}; 34 | const KEY_INPUTS: &[OsCode] = &[ 35 | KEY_0, 36 | KEY_1, 37 | KEY_2, 38 | KEY_3, 39 | KEY_4, 40 | KEY_5, 41 | KEY_6, 42 | KEY_7, 43 | KEY_8, 44 | KEY_9, 45 | KEY_A, 46 | KEY_B, 47 | KEY_C, 48 | KEY_D, 49 | KEY_E, 50 | KEY_F, 51 | KEY_G, 52 | KEY_H, 53 | KEY_I, 54 | KEY_J, 55 | KEY_K, 56 | KEY_L, 57 | KEY_M, 58 | KEY_N, 59 | KEY_O, 60 | KEY_P, 61 | KEY_Q, 62 | KEY_R, 63 | KEY_S, 64 | KEY_T, 65 | KEY_U, 66 | KEY_V, 67 | KEY_W, 68 | KEY_X, 69 | KEY_Y, 70 | KEY_Z, 71 | KEY_SEMICOLON, 72 | KEY_SLASH, 73 | KEY_GRAVE, 74 | KEY_LEFTBRACE, 75 | KEY_BACKSLASH, 76 | KEY_RIGHTBRACE, 77 | KEY_APOSTROPHE, 78 | KEY_MINUS, 79 | KEY_DOT, 80 | KEY_EQUAL, 81 | KEY_BACKSPACE, 82 | KEY_ESC, 83 | KEY_TAB, 84 | KEY_ENTER, 85 | KEY_LEFTCTRL, 86 | KEY_LEFTSHIFT, 87 | KEY_COMMA, 88 | KEY_RIGHTSHIFT, 89 | KEY_KPASTERISK, 90 | KEY_LEFTALT, 91 | KEY_SPACE, 92 | KEY_CAPSLOCK, 93 | KEY_F1, 94 | KEY_F2, 95 | KEY_F3, 96 | KEY_F4, 97 | KEY_F5, 98 | KEY_F6, 99 | KEY_F7, 100 | KEY_F8, 101 | KEY_F9, 102 | KEY_F10, 103 | KEY_F11, 104 | KEY_F12, 105 | KEY_NUMLOCK, 106 | KEY_SCROLLLOCK, 107 | KEY_KP0, 108 | KEY_KP1, 109 | KEY_KP2, 110 | KEY_KP3, 111 | KEY_KP4, 112 | KEY_KP5, 113 | KEY_KP6, 114 | KEY_KP7, 115 | KEY_KP8, 116 | KEY_KP9, 117 | KEY_KPMINUS, 118 | KEY_KPPLUS, 119 | KEY_KPDOT, 120 | KEY_KPENTER, 121 | KEY_RIGHTCTRL, 122 | KEY_KPSLASH, 123 | KEY_RIGHTALT, 124 | KEY_HOME, 125 | KEY_UP, 126 | KEY_PAGEUP, 127 | KEY_LEFT, 128 | KEY_RIGHT, 129 | KEY_END, 130 | KEY_DOWN, 131 | KEY_PAGEDOWN, 132 | KEY_INSERT, 133 | KEY_DELETE, 134 | KEY_MUTE, 135 | KEY_VOLUMEDOWN, 136 | KEY_VOLUMEUP, 137 | KEY_PAUSE, 138 | KEY_LEFTMETA, 139 | KEY_RIGHTMETA, 140 | KEY_COMPOSE, 141 | KEY_BACK, 142 | KEY_FORWARD, 143 | KEY_NEXTSONG, 144 | KEY_PLAYPAUSE, 145 | KEY_PREVIOUSSONG, 146 | KEY_STOP, 147 | KEY_HOMEPAGE, 148 | KEY_MAIL, 149 | KEY_MEDIA, 150 | KEY_REFRESH, 151 | KEY_F13, 152 | KEY_F14, 153 | KEY_F15, 154 | KEY_F16, 155 | KEY_F17, 156 | KEY_F18, 157 | KEY_F19, 158 | KEY_F20, 159 | KEY_F21, 160 | KEY_F22, 161 | KEY_F23, 162 | KEY_F24, 163 | KEY_HANGEUL, 164 | KEY_HANJA, 165 | KEY_252, 166 | KEY_102ND, 167 | KEY_PLAY, 168 | KEY_PRINT, 169 | KEY_SEARCH, 170 | KEY_RO, 171 | KEY_HENKAN, 172 | KEY_MUHENKAN, 173 | ]; 174 | for input in KEY_INPUTS.iter().copied() { 175 | assert!(!is_a_button(input.into())); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /parser/src/cfg/layer_opts.rs: -------------------------------------------------------------------------------- 1 | use crate::cfg::*; 2 | use crate::*; 3 | 4 | pub(crate) const DEFLAYER_ICON: [&str; 3] = ["icon", "🖻", "🖼"]; 5 | pub(crate) type LayerIcons = HashMap<String, Option<String>>; 6 | 7 | pub fn parse_layer_opts(list: &[SExpr]) -> Result<HashMap<String, String>> { 8 | let mut layer_opts: HashMap<String, String> = HashMap::default(); 9 | let mut opts = list.chunks_exact(2); 10 | for kv in opts.by_ref() { 11 | let key_expr = &kv[0]; 12 | let val_expr = &kv[1]; 13 | // Read k-v pairs from the configuration 14 | // todo: add hashmap for future options, currently only parse icons 15 | let opt_key = key_expr.atom(None) 16 | .ok_or_else(|| anyhow_expr!(key_expr, "No lists are allowed in {DEFLAYER} options")) 17 | .and_then(|opt_key| { 18 | if DEFLAYER_ICON.contains(&opt_key) { 19 | if layer_opts.contains_key(DEFLAYER_ICON[0]) { 20 | // separate dupe check since multi-keys are stored 21 | // with one "canonical" repr, so '🖻' → 'icon' 22 | // and this info will be lost after the loop 23 | bail_expr!( 24 | key_expr, 25 | "Duplicate option found in {DEFLAYER}: {opt_key}, one of {DEFLAYER_ICON:?} already exists" 26 | ); 27 | } 28 | Ok(DEFLAYER_ICON[0]) 29 | } else { 30 | bail_expr!(key_expr, "Invalid option in {DEFLAYER}: {opt_key}, expected one of {DEFLAYER_ICON:?}") 31 | } 32 | })?; 33 | if layer_opts.contains_key(opt_key) { 34 | bail_expr!(key_expr, "Duplicate option found in {DEFLAYER}: {opt_key}"); 35 | } 36 | let opt_val = val_expr.atom(None).ok_or_else(|| { 37 | anyhow_expr!( 38 | val_expr, 39 | "No lists are allowed in {DEFLAYER}'s option values" 40 | ) 41 | })?; 42 | layer_opts.insert(opt_key.to_owned(), opt_val.to_owned()); 43 | } 44 | let rem = opts.remainder(); 45 | if !rem.is_empty() { 46 | bail_expr!(&rem[0], "This option is missing a value."); 47 | } 48 | Ok(layer_opts) 49 | } 50 | -------------------------------------------------------------------------------- /parser/src/cfg/permutations.rs: -------------------------------------------------------------------------------- 1 | //! Implements Heap's algorithm. 2 | 3 | /* 4 | From Wikipedia: 5 | 6 | procedure generate(k: integer, A : array of any): 7 | if k = 1 then 8 | output(A) 9 | else 10 | // Generate permutations with k-th unaltered 11 | // Initially k = length(A) 12 | generate(k - 1, A) 13 | 14 | // Generate permutations for k-th swapped with each k-1 initial 15 | for i := 0; i < k-1; i += 1 do 16 | // Swap choice dependent on parity of k (even or odd) 17 | if k is even then 18 | swap(A[i], A[k-1]) // zero-indexed, the k-th is at k-1 19 | else 20 | swap(A[0], A[k-1]) 21 | end if 22 | generate(k - 1, A) 23 | end for 24 | end if 25 | */ 26 | 27 | /// Heap's algorithm 28 | pub fn gen_permutations<T: Clone + Default>(a: &[T]) -> Vec<Vec<T>> { 29 | let mut a2 = vec![Default::default(); a.len()]; 30 | a2.clone_from_slice(a); 31 | let mut outs = vec![]; 32 | heaps_alg(a.len(), &mut a2, &mut outs); 33 | outs 34 | } 35 | 36 | fn heaps_alg<T: Clone>(k: usize, a: &mut [T], outs: &mut Vec<Vec<T>>) { 37 | if k == 1 { 38 | outs.push(a.to_vec()); 39 | } else { 40 | heaps_alg(k - 1, a, outs); 41 | for i in 0..k - 1 { 42 | if (k % 2) == 0 { 43 | a.swap(i, k - 1); 44 | } else { 45 | a.swap(0, k - 1); 46 | } 47 | heaps_alg(k - 1, a, outs); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /parser/src/cfg/str_ext.rs: -------------------------------------------------------------------------------- 1 | pub trait TrimAtomQuotes { 2 | fn trim_atom_quotes(&self) -> &str; 3 | } 4 | 5 | impl TrimAtomQuotes for str { 6 | fn trim_atom_quotes(&self) -> &str { 7 | match self.strip_prefix("r#\"") { 8 | Some(a) => a.strip_suffix("\"#").unwrap_or(a), 9 | None => self 10 | .strip_prefix('"') 11 | .unwrap_or(self) 12 | .strip_suffix('"') 13 | .unwrap_or(self), 14 | } 15 | } 16 | } 17 | 18 | impl TrimAtomQuotes for String { 19 | fn trim_atom_quotes(&self) -> &str { 20 | match self.as_str().strip_prefix("r#\"") { 21 | Some(a) => a.strip_suffix("\"#").unwrap_or(a), 22 | None => self 23 | .strip_prefix('"') 24 | .unwrap_or(self) 25 | .strip_suffix('"') 26 | .unwrap_or(self), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /parser/src/cfg/tests/ambiguous.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn parse_double_dollar_var() { 5 | let source = r#" 6 | (defsrc) 7 | (deflayer base) 8 | (defvar $num 100 9 | $num 99 10 | num not-a-number-or-key) 11 | (defalias test 12 | (movemouse-accel-up $num $$num $num $$num)) 13 | "#; 14 | parse_cfg(source) 15 | .map_err(|e| eprintln!("{:?}", miette::Error::from(e))) 16 | .expect("parses"); 17 | } 18 | 19 | #[test] 20 | fn parse_double_at_alias() { 21 | let source = r#" 22 | (defsrc) 23 | (deflayer base) 24 | ;; alias cannot be used in macro, @alias can 25 | (defalias @alias 0 26 | alias (tap-hold 9 9 a b) 27 | test (macro @@alias)) 28 | "#; 29 | parse_cfg(source) 30 | .map_err(|e| eprintln!("{:?}", miette::Error::from(e))) 31 | .expect("parses"); 32 | } 33 | -------------------------------------------------------------------------------- /parser/src/cfg/tests/defcfg.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn disallow_same_key_in_defsrc_unmapped_except() { 5 | let source = " 6 | (defcfg process-unmapped-keys (all-except bspc)) 7 | (defsrc bspc) 8 | (deflayermap (name) 0 0) 9 | "; 10 | parse_cfg(source) 11 | .map(|_| ()) 12 | //.map_err(|e| eprintln!("{:?}", miette::Error::from(e))) 13 | .expect_err("fails"); 14 | } 15 | 16 | #[test] 17 | fn unmapped_except_keys_cannot_have_dupes() { 18 | let source = " 19 | (defcfg process-unmapped-keys (all-except bspc bspc)) 20 | (defsrc) 21 | (deflayermap (name) 0 0) 22 | "; 23 | parse_cfg(source) 24 | .map(|_| ()) 25 | //.map_err(|e| eprintln!("{:?}", miette::Error::from(e))) 26 | .expect_err("fails"); 27 | } 28 | 29 | #[test] 30 | fn unmapped_except_keys_must_be_known() { 31 | let source = " 32 | (defcfg process-unmapped-keys (all-except notakey)) 33 | (defsrc) 34 | (deflayermap (name) 0 0) 35 | "; 36 | parse_cfg(source) 37 | .map(|_| ()) 38 | //.map_err(|e| eprintln!("{:?}", miette::Error::from(e))) 39 | .expect_err("fails"); 40 | } 41 | 42 | #[test] 43 | fn unmapped_except_keys_respects_deflocalkeys() { 44 | let source = " 45 | (deflocalkeys-win lkey90 555) 46 | (deflocalkeys-winiov2 lkey90 555) 47 | (deflocalkeys-wintercept lkey90 555) 48 | (deflocalkeys-linux lkey90 555) 49 | (deflocalkeys-macos lkey90 555) 50 | (defcfg process-unmapped-keys (all-except lkey90)) 51 | (defsrc) 52 | (deflayermap (name) 0 0) 53 | "; 54 | let cfg = parse_cfg(source) 55 | .map_err(|e| eprintln!("{:?}", miette::Error::from(e))) 56 | .expect("passes"); 57 | assert!(!cfg.mapped_keys.contains(&OsCode::from(555u16))); 58 | assert!(cfg.mapped_keys.contains(&OsCode::KEY_ENTER)); 59 | for osc in 0..KEYS_IN_ROW as u16 { 60 | if let Some(osc) = OsCode::from_u16(osc) { 61 | match KeyCode::from(osc) { 62 | KeyCode::No | KeyCode::K555 => { 63 | assert!(!cfg.mapped_keys.contains(&osc)); 64 | } 65 | _ => { 66 | assert!(cfg.mapped_keys.contains(&osc)); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | #[test] 74 | fn unmapped_except_keys_is_removed_from_mapping() { 75 | let source = " 76 | (defcfg process-unmapped-keys (all-except 1 2 3)) 77 | (defsrc) 78 | (deflayermap (name) 0 0) 79 | "; 80 | let cfg = parse_cfg(source) 81 | .map_err(|e| eprintln!("{:?}", miette::Error::from(e))) 82 | .expect("passes"); 83 | assert!(cfg.mapped_keys.contains(&OsCode::KEY_A)); 84 | assert!(cfg.mapped_keys.contains(&OsCode::KEY_0)); 85 | assert!(!cfg.mapped_keys.contains(&OsCode::KEY_1)); 86 | assert!(!cfg.mapped_keys.contains(&OsCode::KEY_2)); 87 | assert!(!cfg.mapped_keys.contains(&OsCode::KEY_3)); 88 | assert!(cfg.mapped_keys.contains(&OsCode::KEY_4)); 89 | for osc in 0..KEYS_IN_ROW as u16 { 90 | if let Some(osc) = OsCode::from_u16(osc) { 91 | match KeyCode::from(osc) { 92 | KeyCode::No | KeyCode::Kb1 | KeyCode::Kb2 | KeyCode::Kb3 => { 93 | assert!(!cfg.mapped_keys.contains(&osc)); 94 | } 95 | _ => { 96 | assert!(cfg.mapped_keys.contains(&osc)); 97 | } 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /parser/src/cfg/tests/device_detect.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "linux")] 2 | mod linux { 3 | use super::super::*; 4 | 5 | #[test] 6 | fn linux_device_parses_properly() { 7 | let source = r#" 8 | (defcfg linux-device-detect-mode any) 9 | (defsrc) (deflayer base)"#; 10 | let icfg = parse_cfg(source) 11 | .map_err(|e| log::info!("{:?}", miette::Error::from(e))) 12 | .expect("no error"); 13 | assert_eq!( 14 | icfg.options.linux_opts.linux_device_detect_mode, 15 | Some(DeviceDetectMode::Any) 16 | ); 17 | 18 | let source = r#" 19 | (defcfg linux-device-detect-mode keyboard-only) 20 | (defsrc) (deflayer base)"#; 21 | let icfg = parse_cfg(source) 22 | .map_err(|e| log::info!("{:?}", miette::Error::from(e))) 23 | .expect("no error"); 24 | assert_eq!( 25 | icfg.options.linux_opts.linux_device_detect_mode, 26 | Some(DeviceDetectMode::KeyboardOnly) 27 | ); 28 | 29 | let source = r#" 30 | (defcfg linux-device-detect-mode keyboard-mice) 31 | (defsrc) (deflayer base)"#; 32 | let icfg = parse_cfg(source) 33 | .map_err(|e| log::info!("{:?}", miette::Error::from(e))) 34 | .expect("no error"); 35 | assert_eq!( 36 | icfg.options.linux_opts.linux_device_detect_mode, 37 | Some(DeviceDetectMode::KeyboardMice) 38 | ); 39 | 40 | let source = r#"(defsrc mmid) (deflayer base 1)"#; 41 | let icfg = parse_cfg(source) 42 | .map_err(|e| log::info!("{:?}", miette::Error::from(e))) 43 | .expect("no error"); 44 | assert_eq!( 45 | icfg.options.linux_opts.linux_device_detect_mode, 46 | Some(DeviceDetectMode::Any) 47 | ); 48 | 49 | let source = r#"(defsrc a) (deflayer base b)"#; 50 | let icfg = parse_cfg(source) 51 | .map_err(|e| log::info!("{:?}", miette::Error::from(e))) 52 | .expect("no error"); 53 | assert_eq!( 54 | icfg.options.linux_opts.linux_device_detect_mode, 55 | Some(DeviceDetectMode::KeyboardMice) 56 | ); 57 | 58 | let source = r#" 59 | (defcfg linux-device-detect-mode not an opt) 60 | (defsrc) (deflayer base)"#; 61 | parse_cfg(source) 62 | .map(|_| ()) 63 | .map_err(|e| log::info!("{:?}", miette::Error::from(e))) 64 | .expect_err("error should happen"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /parser/src/cfg/tests/environment.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | fn parse_cfg_env(cfg: &str, env_vars: Vec<(String, String)>) -> Result<IntermediateCfg> { 4 | let _lk = lock(&CFG_PARSE_LOCK); 5 | let mut s = ParserState::default(); 6 | parse_cfg_raw_string( 7 | cfg, 8 | &mut s, 9 | &PathBuf::from("test"), 10 | &mut FileContentProvider { 11 | get_file_content_fn: &mut |_| unimplemented!(), 12 | }, 13 | DEF_LOCAL_KEYS, 14 | Ok(env_vars), 15 | ) 16 | } 17 | 18 | #[test] 19 | fn parse_env() { 20 | parse_cfg_env( 21 | r#" 22 | (environment (hello "") (defsrc a)) 23 | (environment (goodbye "") (deflayer 1 (layer-switch 2))) 24 | (environment (farewell val) (deflayer 2 (layer-switch 1))) 25 | ;; below would conflict if environment did not cancel 26 | (environment (hello yea) (defsrc)) 27 | (environment (goodbye yea) (deflayer 1)) 28 | (environment (farewell notval) (deflayer 2)) 29 | "#, 30 | vec![ 31 | ("goodbye".into(), "".into()), 32 | ("farewell".into(), "val".into()), 33 | ], 34 | ) 35 | .map_err(|e| eprintln!("{:?}", miette::Error::from(e))) 36 | .unwrap(); 37 | } 38 | -------------------------------------------------------------------------------- /parser/src/cfg/tests/macros.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn unsupported_action_in_macro_triggers_error() { 5 | let source = r#" 6 | (defsrc) 7 | (deflayer base) 8 | (defalias a (macro (multi a b c))) "#; 9 | parse_cfg(source) 10 | .map(|_| ()) 11 | .map_err(|e| log::info!("{:?}", miette::Error::from(e))) 12 | .expect_err("errors"); 13 | } 14 | 15 | #[test] 16 | fn incorrectly_configured_supported_action_in_macro_triggers_useful_error() { 17 | let source = r#" 18 | (defsrc) 19 | (deflayer base) 20 | (defalias a (macro (on-press press-vkey does-not-exist))) "#; 21 | parse_cfg(source) 22 | .map(|_| ()) 23 | .map_err(|e| { 24 | let e = miette::Error::from(e); 25 | let msg = format!("{e:?}"); 26 | log::info!("{msg}"); 27 | assert!(msg.contains("unknown virtual key name: does-not-exist")); 28 | }) 29 | .expect_err("errors"); 30 | } 31 | -------------------------------------------------------------------------------- /parser/src/layers.rs: -------------------------------------------------------------------------------- 1 | use kanata_keyberon::key_code::KeyCode; 2 | use kanata_keyberon::layout::*; 3 | 4 | use crate::cfg::alloc::*; 5 | use crate::cfg::KanataAction; 6 | use crate::custom_action::*; 7 | use crate::keys::OsCode; 8 | 9 | use std::sync::Arc; 10 | 11 | // OsCode::KEY_MAX is the biggest OsCode 12 | pub const KEYS_IN_ROW: usize = OsCode::KEY_MAX as usize; 13 | pub const LAYER_ROWS: usize = 2; 14 | pub const DEFAULT_ACTION: KanataAction = KanataAction::KeyCode(KeyCode::ErrorUndefined); 15 | 16 | pub type IntermediateLayers = Box<[[Row; LAYER_ROWS]]>; 17 | 18 | pub type KLayers = 19 | Layers<'static, KEYS_IN_ROW, LAYER_ROWS, &'static &'static [&'static CustomAction]>; 20 | 21 | pub struct KanataLayers { 22 | pub(crate) layers: 23 | Layers<'static, KEYS_IN_ROW, LAYER_ROWS, &'static &'static [&'static CustomAction]>, 24 | _allocations: Arc<Allocations>, 25 | } 26 | 27 | impl std::fmt::Debug for KanataLayers { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | f.debug_struct("KanataLayers").finish() 30 | } 31 | } 32 | 33 | pub type Row = [kanata_keyberon::action::Action<'static, &'static &'static [&'static CustomAction]>; 34 | KEYS_IN_ROW]; 35 | 36 | pub fn new_layers(layers: usize) -> IntermediateLayers { 37 | let actual_num_layers = layers; 38 | // Note: why construct it like this? 39 | // Because don't want to construct KanataLayers on the stack. 40 | // The stack will overflow because of lack of placement new. 41 | let mut layers = Vec::with_capacity(actual_num_layers); 42 | for _ in 0..actual_num_layers { 43 | layers.push([[DEFAULT_ACTION; KEYS_IN_ROW], [DEFAULT_ACTION; KEYS_IN_ROW]]); 44 | } 45 | layers.into_boxed_slice() 46 | } 47 | 48 | impl KanataLayers { 49 | /// # Safety 50 | /// 51 | /// The allocations must hold all of the &'static pointers found in layers. 52 | pub(crate) unsafe fn new(layers: KLayers, allocations: Arc<Allocations>) -> Self { 53 | Self { 54 | layers, 55 | _allocations: allocations, 56 | } 57 | } 58 | 59 | pub(crate) fn get(&self) -> (KLayers, Arc<Allocations>) { 60 | (self.layers, self._allocations.clone()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A parser for configuration language of [kanata](https://github.com/jtroo/kanata), a keyboard remapper. 2 | 3 | pub mod cfg; 4 | pub mod custom_action; 5 | pub mod keys; 6 | pub mod layers; 7 | pub mod lsp_hints; 8 | pub mod sequences; 9 | pub mod subset; 10 | pub mod trie; 11 | -------------------------------------------------------------------------------- /parser/src/lsp_hints.rs: -------------------------------------------------------------------------------- 1 | pub use inner::*; 2 | 3 | #[cfg(not(feature = "lsp"))] 4 | mod inner { 5 | #[derive(Debug, Default)] 6 | pub struct LspHints {} 7 | } 8 | 9 | #[cfg(feature = "lsp")] 10 | mod inner { 11 | use crate::cfg::sexpr::{Span, Spanned}; 12 | type HashMap<K, V> = rustc_hash::FxHashMap<K, V>; 13 | 14 | #[derive(Debug, Default)] 15 | pub struct LspHints { 16 | pub inactive_code: Vec<InactiveCode>, 17 | pub definition_locations: DefinitionLocations, 18 | pub reference_locations: ReferenceLocations, 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct InactiveCode { 23 | pub span: Span, 24 | pub reason: String, 25 | } 26 | 27 | #[derive(Debug, Default, Clone)] 28 | pub struct DefinitionLocations { 29 | pub alias: HashMap<String, Span>, 30 | pub variable: HashMap<String, Span>, 31 | pub virtual_key: HashMap<String, Span>, 32 | pub layer: HashMap<String, Span>, 33 | pub template: HashMap<String, Span>, 34 | } 35 | 36 | #[derive(Debug, Default, Clone)] 37 | pub struct ReferenceLocations { 38 | pub alias: ReferencesMap, 39 | pub variable: ReferencesMap, 40 | pub virtual_key: ReferencesMap, 41 | pub layer: ReferencesMap, 42 | pub template: ReferencesMap, 43 | pub include: ReferencesMap, 44 | } 45 | 46 | #[derive(Debug, Default, Clone)] 47 | pub struct ReferencesMap(pub HashMap<String, Vec<Span>>); 48 | 49 | #[allow(unused)] 50 | impl ReferencesMap { 51 | pub(crate) fn push_from_atom(&mut self, atom: &Spanned<String>) { 52 | match self.0.get_mut(&atom.t) { 53 | Some(refs) => refs.push(atom.span.clone()), 54 | None => { 55 | self.0.insert(atom.t.clone(), vec![atom.span.clone()]); 56 | } 57 | }; 58 | } 59 | 60 | pub(crate) fn push(&mut self, name: &str, span: Span) { 61 | match self.0.get_mut(name) { 62 | Some(refs) => refs.push(span), 63 | None => { 64 | self.0.insert(name.to_owned(), vec![span]); 65 | } 66 | }; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /parser/src/sequences.rs: -------------------------------------------------------------------------------- 1 | use kanata_keyberon::key_code::KeyCode; 2 | 3 | pub const MASK_KEYCODES: u16 = 0x03FF; 4 | pub const MASK_MODDED: u16 = 0xFC00; 5 | pub const KEY_OVERLAP: KeyCode = KeyCode::ErrorRollOver; 6 | pub const KEY_OVERLAP_MARKER: u16 = 0x0400; 7 | 8 | pub fn mod_mask_for_keycode(kc: KeyCode) -> u16 { 9 | use KeyCode::*; 10 | match kc { 11 | LShift | RShift => 0x8000, 12 | LCtrl | RCtrl => 0x4000, 13 | LAlt => 0x2000, 14 | RAlt => 0x1000, 15 | LGui | RGui => 0x0800, 16 | // This is not real... this is a marker to help signify that key presses should be 17 | // overlapping. The way this will look in the chord sequence is as such: 18 | // 19 | // [ (0x0400 | X), (0x0400 | Y), (0x0400) ] 20 | ErrorRollOver => KEY_OVERLAP_MARKER, 21 | _ => 0, 22 | } 23 | } 24 | 25 | #[test] 26 | fn keys_fit_within_mask() { 27 | use crate::keys::OsCode; 28 | assert!(MASK_KEYCODES >= u16::from(OsCode::KEY_MAX)); 29 | } 30 | -------------------------------------------------------------------------------- /parser/src/trie.rs: -------------------------------------------------------------------------------- 1 | //! Wrapper around a trie type for (hopefully) easier swapping of libraries if desired. 2 | 3 | use bytemuck::cast_slice; 4 | use patricia_tree::map::PatriciaMap; 5 | 6 | pub type TrieKeyElement = u16; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct Trie<T> { 10 | inner: patricia_tree::map::PatriciaMap<T>, 11 | } 12 | 13 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 14 | pub enum GetOrDescendentExistsResult<T> { 15 | NotInTrie, 16 | InTrie, 17 | HasValue(T), 18 | } 19 | 20 | use GetOrDescendentExistsResult::*; 21 | 22 | impl<T> Default for Trie<T> { 23 | fn default() -> Self { 24 | Self::new() 25 | } 26 | } 27 | 28 | fn key_len(k: impl AsRef<[u16]>) -> usize { 29 | debug_assert!(std::mem::size_of::<TrieKeyElement>() == 2 * std::mem::size_of::<u8>()); 30 | k.as_ref().len() * 2 31 | } 32 | 33 | impl<T> Trie<T> { 34 | pub fn new() -> Self { 35 | Self { 36 | inner: PatriciaMap::new(), 37 | } 38 | } 39 | 40 | pub fn ancestor_exists(&self, key: impl AsRef<[u16]>) -> bool { 41 | self.inner 42 | .get_longest_common_prefix(cast_slice(key.as_ref())) 43 | .is_some() 44 | } 45 | 46 | pub fn descendant_exists(&self, key: impl AsRef<[u16]>) -> bool { 47 | // Length of the [u8] interpretation of the [u16] key is doubled. 48 | self.inner 49 | .longest_common_prefix_len(cast_slice(key.as_ref())) 50 | == key_len(key) 51 | } 52 | 53 | pub fn insert(&mut self, key: impl AsRef<[u16]>, val: T) { 54 | self.inner.insert(cast_slice(key.as_ref()), val); 55 | } 56 | 57 | pub fn get_or_descendant_exists(&self, key: impl AsRef<[u16]>) -> GetOrDescendentExistsResult<T> 58 | where 59 | T: Clone, 60 | { 61 | let mut descendants = self.inner.iter_prefix(cast_slice(key.as_ref())); 62 | match descendants.next() { 63 | None => NotInTrie, 64 | Some(descendant) => { 65 | if descendant.0.len() == key_len(key.as_ref()) { 66 | HasValue(descendant.1.clone()) 67 | } else { 68 | InTrie 69 | } 70 | } 71 | } 72 | } 73 | 74 | pub fn is_empty(&self) -> bool { 75 | self.inner.is_empty() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /parser/test_cfgs/all_keys_in_defsrc.kbd: -------------------------------------------------------------------------------- 1 | (defcfg) 2 | 3 | (defsrc 4 | grv 5 | 1 6 | 2 7 | 3 8 | 4 9 | 5 10 | 6 11 | 7 12 | 8 13 | 9 14 | 0 15 | min 16 | eql 17 | bspc 18 | tab 19 | q 20 | w 21 | e 22 | r 23 | t 24 | y 25 | u 26 | i 27 | o 28 | p 29 | { 30 | } 31 | \ 32 | caps 33 | a 34 | s 35 | d 36 | f 37 | g 38 | h 39 | j 40 | k 41 | l 42 | scln 43 | ' 44 | ret 45 | lshift 46 | z 47 | x 48 | c 49 | v 50 | b 51 | n 52 | m 53 | comm 54 | . 55 | / 56 | kp= 57 | kp0 58 | kp1 59 | kp2 60 | kp3 61 | kp4 62 | kp5 63 | kp6 64 | kp7 65 | kp8 66 | kp9 67 | kprt 68 | kp/ 69 | kp+ 70 | kp* 71 | kp- 72 | kp. 73 | 102d 74 | scrlck 75 | pause 76 | wkup 77 | esc 78 | rshift 79 | lctrl 80 | lalt 81 | spc 82 | ralt 83 | comp 84 | lmeta 85 | rmeta 86 | rctrl 87 | del 88 | ins 89 | bck 90 | fwd 91 | pgup 92 | pgdn 93 | up 94 | down 95 | lft 96 | rght 97 | home 98 | end 99 | nlck 100 | mute 101 | volu 102 | voldwn 103 | brup 104 | brdown 105 | blup 106 | bldn 107 | next 108 | pp 109 | prev 110 | f1 111 | f2 112 | f3 113 | f4 114 | f5 115 | f6 116 | f7 117 | f8 118 | f9 119 | f10 120 | f11 121 | f12 122 | f13 123 | f14 124 | f15 125 | f16 126 | f17 127 | f18 128 | f19 129 | f20 130 | f21 131 | f22 132 | f23 133 | f24 134 | ) 135 | 136 | (deflayer base 137 | grv 138 | 1 139 | 2 140 | 3 141 | 4 142 | 5 143 | 6 144 | 7 145 | 8 146 | 9 147 | 0 148 | min 149 | eql 150 | bspc 151 | tab 152 | q 153 | w 154 | e 155 | r 156 | t 157 | y 158 | u 159 | i 160 | o 161 | p 162 | { 163 | } 164 | \ 165 | caps 166 | a 167 | s 168 | d 169 | f 170 | g 171 | h 172 | j 173 | k 174 | l 175 | scln 176 | ' 177 | ret 178 | lshift 179 | z 180 | x 181 | c 182 | v 183 | b 184 | n 185 | m 186 | comm 187 | . 188 | / 189 | kp= 190 | kp0 191 | kp1 192 | kp2 193 | kp3 194 | kp4 195 | kp5 196 | kp6 197 | kp7 198 | kp8 199 | kp9 200 | kprt 201 | kp/ 202 | kp+ 203 | kp* 204 | kp- 205 | kp. 206 | 102d 207 | scrlck 208 | pause 209 | wkup 210 | esc 211 | rshift 212 | lctrl 213 | lalt 214 | spc 215 | ralt 216 | comp 217 | lmeta 218 | rmeta 219 | rctrl 220 | del 221 | ins 222 | bck 223 | fwd 224 | pgup 225 | pgdn 226 | up 227 | down 228 | lft 229 | rght 230 | home 231 | end 232 | nlck 233 | mute 234 | volu 235 | voldwn 236 | brup 237 | brdown 238 | blup 239 | bldn 240 | next 241 | pp 242 | prev 243 | f1 244 | f2 245 | f3 246 | f4 247 | f5 248 | f6 249 | f7 250 | f8 251 | f9 252 | f10 253 | f11 254 | f12 255 | f13 256 | f14 257 | f15 258 | f16 259 | f17 260 | f18 261 | f19 262 | f20 263 | f21 264 | f22 265 | f23 266 | f24 267 | ) 268 | -------------------------------------------------------------------------------- /parser/test_cfgs/ancestor_seq.kbd: -------------------------------------------------------------------------------- 1 | (defcfg) 2 | 3 | (defsrc a b c) 4 | 5 | (deflayer base _ _ _) 6 | 7 | (deffakekeys a a) 8 | 9 | (defseq a (a b c)) 10 | (defseq a (a b)) 11 | -------------------------------------------------------------------------------- /parser/test_cfgs/bad_multi.kbd: -------------------------------------------------------------------------------- 1 | (defcfg) 2 | (defsrc 1) 3 | (deflayer base (multi (tap-hold 1 1 a b) (tap-dance 1 (a b c)))) 4 | -------------------------------------------------------------------------------- /parser/test_cfgs/descendant_seq.kbd: -------------------------------------------------------------------------------- 1 | (defcfg) 2 | 3 | (defsrc a b c) 4 | 5 | (deflayer base _ _ _) 6 | 7 | (deffakekeys a a) 8 | 9 | (defseq a (a b)) 10 | (defseq a (a b c)) 11 | -------------------------------------------------------------------------------- /parser/test_cfgs/icon_bad_dupe.kbd: -------------------------------------------------------------------------------- 1 | ;; This config file is invalid and should be rejected 2 | (defcfg) 3 | (defsrc 1) 4 | (deflayer (base icon base.png 🖻 n.ico ) 1) 5 | -------------------------------------------------------------------------------- /parser/test_cfgs/icon_good.kbd: -------------------------------------------------------------------------------- 1 | (defcfg) 2 | (defsrc 1) 3 | (deflayer (base icon base.png ) 1) 4 | (deflayer (1emoji 🖻 1symbols.png ) 1) 5 | (deflayer (2icon-quote 🖻 "2Nav Num.png" ) 1) 6 | (deflayer (3emoji_alt 🖼 3trans.parent ) 1) 7 | (deflayermap (4layermap 🖼 3trans.parent ) 0 0) 8 | -------------------------------------------------------------------------------- /parser/test_cfgs/include-bad.kbd: -------------------------------------------------------------------------------- 1 | (defsrc a) 2 | (include included-bad.kbd) 3 | -------------------------------------------------------------------------------- /parser/test_cfgs/include-bad2.kbd: -------------------------------------------------------------------------------- 1 | (defsrc a) 2 | (include included-bad2.kbd) 3 | (defalias no-action-uh-oh) 4 | -------------------------------------------------------------------------------- /parser/test_cfgs/include-good-optional-absent.kbd: -------------------------------------------------------------------------------- 1 | (defsrc a) 2 | (include included-non-existing-file.kbd) 3 | (include included-good.kbd) 4 | -------------------------------------------------------------------------------- /parser/test_cfgs/include-good.kbd: -------------------------------------------------------------------------------- 1 | (defsrc a) 2 | (include included-good.kbd) 3 | -------------------------------------------------------------------------------- /parser/test_cfgs/included-bad.kbd: -------------------------------------------------------------------------------- 1 | (deflayer not-enough-elements) 2 | -------------------------------------------------------------------------------- /parser/test_cfgs/included-bad2.kbd: -------------------------------------------------------------------------------- 1 | (deflayer base a) 2 | -------------------------------------------------------------------------------- /parser/test_cfgs/included-good.kbd: -------------------------------------------------------------------------------- 1 | (deflayer base a) 2 | -------------------------------------------------------------------------------- /parser/test_cfgs/macro-chord-dont-panic.kbd: -------------------------------------------------------------------------------- 1 | (defsrc a) 2 | (deflayer test (macro @ch|bas)) 3 | -------------------------------------------------------------------------------- /parser/test_cfgs/multiline_comment.kbd: -------------------------------------------------------------------------------- 1 | (defcfg) 2 | 3 | #| 4 | 5 | Top level multi-line comment 6 | Hello world 7 | 8 | |# 9 | 10 | (defsrc #||# a #| |# b #| |# c #| |#) 11 | 12 | (deflayer 13 | base _ 14 | 15 | #| -------------------------------------------------------------------------- 16 | Quick reference: https://github.com/jtroo/kanata/blob/main/cfg_samples/kanata.kbd 17 | List of keycodes: https://github.com/kmonad/kmonad/blob/master/src/KMonad/Keyboard/Keycode.hs 18 | -------------------------------------------------------------------------- |# 19 | _ 20 | #| inner multi-line comment block 21 | 1 22 | 2 23 | 3|# 24 | #| |# 25 | #| |# 26 | #||# 27 | #||##||##| |##| |# 28 | _ 29 | ) 30 | 31 | #| 32 | #| || # #| 33 | |# 34 | 35 | -------------------------------------------------------------------------------- /parser/test_cfgs/nested_tap_hold.kbd: -------------------------------------------------------------------------------- 1 | (defcfg 2 | linux-dev /dev/input/by-path/platform-i8042-serio-0-event-kbd 3 | ) 4 | 5 | (defsrc 6 | a 7 | ) 8 | 9 | ;; Note: this config file is invalid and should be rejected 10 | (deflayer test 11 | (tap-hold 200 200 (tap-hold 200 200 a b) c) 12 | ) 13 | -------------------------------------------------------------------------------- /parser/test_cfgs/test.zch: -------------------------------------------------------------------------------- 1 | dy day 2 | dy 1 Monday 3 | abc Alphabet 4 | r df recipient 5 | w a Washington 6 | -------------------------------------------------------------------------------- /parser/test_cfgs/testzch.kbd: -------------------------------------------------------------------------------- 1 | (defsrc) 2 | (deflayer base) 3 | (defzippy test.zch) 4 | -------------------------------------------------------------------------------- /parser/test_cfgs/unknown_defcfg_opt.kbd: -------------------------------------------------------------------------------- 1 | (defcfg 2 | this-should-error yes 3 | ) 4 | 5 | (defsrc) 6 | (deflayer base) 7 | -------------------------------------------------------------------------------- /parser/test_cfgs/utf8bom-included.kbd: -------------------------------------------------------------------------------- 1 | #| 2 | 3 | BOM should have been added via: 4 | 5 | $f = Get-Content .\utf8bom-included.kbd 6 | $f | Out-File -Encoding UTF8 .\utf8bom-included.kbd 7 | 8 | |# 9 | 10 | (deflayermap (layer-included) 11 | y (unicode 🚀) 12 | ) 13 | -------------------------------------------------------------------------------- /parser/test_cfgs/utf8bom.kbd: -------------------------------------------------------------------------------- 1 | #| 2 | 3 | BOM should have been added via: 4 | 5 | $f = Get-Content .\utf8bom.kbd 6 | $f | Out-File -Encoding UTF8 .\utf8bom.kbd 7 | 8 | |# 9 | 10 | (defsrc) 11 | (deflayermap (layer-name) 12 | x (unicode 🙂) 13 | ) 14 | 15 | (include utf8bom-included.kbd) 16 | -------------------------------------------------------------------------------- /simulated_input/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kanata-sim" 3 | version = "0.1.0" 4 | authors = ["jtroo <j.andreitabs@gmail.com>"] 5 | description = "Simulated input using kanata" 6 | keywords = ["kanata", "input", "simulated"] 7 | homepage = "https://github.com/jtroo/kanata" 8 | repository = "https://github.com/jtroo/kanata" 9 | readme = "README.md" 10 | license = "LGPL-3.0" 11 | edition = "2021" 12 | 13 | [[bin]] 14 | name = "kanata_simulated_input" 15 | path = "src/sim.rs" 16 | 17 | [dependencies] 18 | anyhow = "1" 19 | clap = { version = "4", features = [ "std", "derive", "help", "suggestions" ], default-features = false } 20 | dirs = "5.0.1" 21 | log = { version = "0.4.8", default-features = false } 22 | simplelog = "0.12.0" 23 | time = "0.3.36" 24 | 25 | kanata = { path = ".." , default-features = false } 26 | 27 | [features] 28 | default = ["simulated_output", "tcp_server"] 29 | simulated_output = ["kanata/simulated_output"] 30 | simulated_input = ["kanata/simulated_input"] 31 | passthru_ahk = ["simulated_input","simulated_output"] 32 | tcp_server = ["kanata/tcp_server"] 33 | -------------------------------------------------------------------------------- /simulated_input/README.md: -------------------------------------------------------------------------------- 1 | # Kanata simulated input 2 | 3 | A CLI tool that lets you run simulated kanata input. 4 | 5 | Use the `-c` flag to specify a kanata configuration file 6 | and the `-s` flag to specify an input simulation file. 7 | You can pass the `--help` flag for more details. 8 | 9 | The input file format is described in the 10 | [guide](https://github.com/jtroo/kanata/blob/main/docs/config.adoc#test-your-config). 11 | 12 | -------------------------------------------------------------------------------- /simulated_passthru/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simulated_passthru" 3 | version = "0.0.1" 4 | edition = "2021" 5 | 6 | [lib] 7 | name = "kanata_passthru" 8 | path = "src/lib_passthru.rs" 9 | crate-type = ['lib','cdylib'] 10 | 11 | [dependencies] 12 | anyhow = "1" 13 | log = { version = "0.4.8", default-features = false } 14 | parking_lot = "0.12" 15 | regex = "1.10.3" 16 | 17 | kanata = {path=".." , default-features=false} 18 | 19 | lazy_static = "1.4.0" 20 | 21 | [target.'cfg(target_os = "windows")'.dependencies] 22 | encode_unicode = "0.3.6" 23 | winapi = { version = "0.3.9", features = [ 24 | "wincon", 25 | "timeapi", 26 | "mmsystem", 27 | ] } 28 | native-windows-gui = { version = "1.0.12", default-features = false } 29 | kanata-interception = { version = "0.3.0", optional = true } 30 | win_dbg_logger = "0.1.0" 31 | widestring = "1.1.0" 32 | 33 | [features] 34 | default = ["simulated_output","tcp_server"] 35 | tcp_server = ["kanata/tcp_server"] 36 | simulated_output = ["kanata/simulated_output"] 37 | simulated_input = ["kanata/simulated_input"] 38 | passthru_ahk = ["simulated_input","simulated_output","kanata/passthru_ahk"] 39 | perf_logging = [] 40 | -------------------------------------------------------------------------------- /simulated_passthru/ReadMe.md: -------------------------------------------------------------------------------- 1 | # Kanata passthru (simulated input and output) 2 | 3 | A Windows dynamic library (DLL) that lets you run simulated kanata input and get simulated kanata output. 4 | 5 | See a simplified [example](./../docs/simulated_passthru_ahk/) of using AutoHotkey to redirect custom inputhook's key events to kanata and get them remapped using kanata config, e.g., to get better modtap functionality 6 | -------------------------------------------------------------------------------- /simulated_passthru/src/key_in.rs: -------------------------------------------------------------------------------- 1 | use kanata_state_machine::oskbd::*; 2 | use log::*; 3 | 4 | use winapi::ctypes::*; 5 | use winapi::shared::minwindef::*; 6 | 7 | use crate::oskbd::HOOK_CB; 8 | 9 | /// Exported function: receives key input and uses event_loop's input event handler 10 | /// callback (which will in turn communicate via the internal kanata's channels to 11 | /// keyberon state machine etc.) 12 | #[no_mangle] 13 | pub extern "win64" fn input_ev_listener(vk: c_uint, sc: c_uint, up: c_int) -> LRESULT { 14 | #[cfg(feature = "perf_logging")] 15 | let start = std::time::Instant::now(); 16 | let key_event = InputEvent::from_vk_sc(vk, sc, up); //{code:KEY_0,value:Press} 17 | let mut h_cbl = HOOK_CB.lock(); // to access the closure we move its box out of the mutex 18 | // and put it back after it returned 19 | if let Some(mut fnhook) = h_cbl.take() { 20 | // move our opt+boxed closure, replacing it with None, can't just .unwrap since Copy 21 | // trait not implemented for dyn fnMut 22 | let handled = fnhook(key_event); // box(closure)() = closure() 23 | *h_cbl = Some(fnhook); // put our closure back 24 | if handled { 25 | // now try to get the out key events that another thread should've sent via 26 | #[cfg(feature = "perf_logging")] 27 | debug!( 28 | " 🕐{}μs →→→✓ {key_event} from {vk} sc={sc} up={up}", 29 | (start.elapsed()).as_micros() 30 | ); 31 | #[cfg(not(feature = "perf_logging"))] 32 | debug!(" →→→✓ {key_event} from {vk} sc={sc} up={up}"); 33 | 1 34 | } else { 35 | 0 36 | } 37 | } else { 38 | error!("fnHook processing key events isn't available yet {key_event} from {vk} sc={sc} up={up}"); 39 | 0 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /simulated_passthru/src/key_out.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use kanata_state_machine::oskbd::*; 4 | use log::*; 5 | 6 | use winapi::ctypes::*; 7 | use winapi::shared::minwindef::*; 8 | 9 | use std::cell::Cell; 10 | 11 | type CbOutEvFn = dyn Fn(i64, i64, i64) -> i64 + 'static; 12 | thread_local! {static CBOUTEV_WRAP:Cell<Option<Box<CbOutEvFn>>> = Cell::default();} 13 | // Stores the hook callback for the current thread 14 | 15 | /// - Get the address of AutoHotkey's callback function that accepts simulated output 16 | /// events (and sends them to the OS) 17 | /// - `cbKanataOut(vk,sc,up) {return 1}` All args are i64 (AHK doesn't support u64) 18 | /// - Store it in a static thread-local Cell (AHK is single-threaded, so we can only 19 | /// use this callback from the main thread). KbdOut will use a channel to send a 20 | /// message key event that will use call the fn from this Cell 21 | /// address: pointer-sized integer, equivalent to Int64 on ahk64 (c_longlong=i64). 22 | /// Will be `as`-cast to a raw pointer before `transmute`ing to a function pointer to avoid 23 | /// an integer-to-pointer `transmute`, which can be problematic. Transmuting between raw pointers 24 | /// and function pointers (i.e., two pointer types) is fine. 25 | /// AHK uses x64 calling convention: TODO: is this the same as win64? extern "C" also seems to work? 26 | #[cfg(feature = "passthru_ahk")] 27 | pub fn set_cb_out_ev(cb_addr: c_longlong) -> Result<()> { 28 | trace!("got func address {}", cb_addr); 29 | let ptr_fn = cb_addr as *const (); 30 | let cb_out_ev = 31 | unsafe { std::mem::transmute::<*const (), fn(vk: i64, sc: i64, up: i64) -> i64>(ptr_fn) }; 32 | CBOUTEV_WRAP.with(|state| { 33 | assert!( 34 | state.take().is_none(), 35 | "Only 1 callback can be registered per thread" 36 | ); 37 | state.set(Some(Box::new(cb_out_ev))); 38 | }); 39 | Ok(()) 40 | } 41 | #[cfg(not(feature = "passthru_ahk"))] 42 | fn set_cb_out_ev(cb_addr: c_longlong) -> Result<()> { 43 | debug!("✗✗✗✗ unimplemented!"); 44 | unimplemented!(); 45 | Ok(()) 46 | } 47 | 48 | pub fn send_out_ev(in_ev: InputEvent) -> Result<()> { 49 | // ext callback accepts vk:i64,sc:i64,up:i64 50 | #[cfg(feature = "perf_logging")] 51 | let start = std::time::Instant::now(); 52 | let key_event = KeyEvent::try_from(in_ev); 53 | debug!("@send_out_ev key_event={key_event:?}"); 54 | let vk: i64 = in_ev.code.into(); 55 | let sc: i64 = 0; 56 | let up: i64 = in_ev.up.into(); 57 | 58 | let mut handled = 0i64; 59 | CBOUTEV_WRAP.with(|state| { 60 | if let Some(hook) = state.take() { 61 | handled = hook(vk, sc, up); 62 | state.set(Some(hook)); 63 | } 64 | }); 65 | #[cfg(feature = "perf_logging")] 66 | debug!( 67 | "🕐{}μs ←←←{} fnHookCC {key_event:?} {vk} {sc} {up}", 68 | (start.elapsed()).as_micros(), 69 | if handled == 1 { "✓" } else { "✗" } 70 | ); 71 | #[cfg(not(feature = "perf_logging"))] 72 | debug!( 73 | "←←←{} fnHookCC {key_event:?} {vk} {sc} {up}", 74 | if handled == 1 { "✓" } else { "✗" } 75 | ); 76 | Ok(()) 77 | } 78 | 79 | use crate::RX_KEY_EV_OUT; 80 | use std::sync::mpsc::TryRecvError; // thread_local Cell<Option<Receiver<InputEvent>>> 81 | // Stores receiver for key data to be sent out for 82 | // the current thread 83 | /// Exported function: checks if processing thread has sent key output and sends it 84 | /// back to an external callback 85 | #[no_mangle] 86 | pub extern "win64" fn output_ev_check() -> LRESULT { 87 | let mut res: isize = 0; 88 | RX_KEY_EV_OUT.with(|state| { 89 | if let Some(rx) = state.take() { 90 | match rx.try_recv() { 91 | Ok(in_ev) => { 92 | debug!("✓ rx_kout@key_out(dll) ‘{in_ev}’"); 93 | if send_out_ev(in_ev).is_ok() { 94 | res = 0; 95 | } else { 96 | res = -1; 97 | }; 98 | } 99 | Err(TryRecvError::Empty) => { 100 | debug!("✗ rx_kout@key_out(dll) no data yet"); 101 | res = -2 102 | } 103 | Err(TryRecvError::Disconnected) => { 104 | debug!("✗ rx_kout@key_out(dll) Disconnected"); 105 | res = -3 106 | } 107 | } 108 | state.set(Some(rx)); 109 | } else { 110 | debug!("✗ RX_KEY_EV_OUT@key_out(dll) empty"); 111 | state.set(None); 112 | res = -4 113 | } 114 | }); 115 | res 116 | } 117 | -------------------------------------------------------------------------------- /simulated_passthru/src/log_win.rs: -------------------------------------------------------------------------------- 1 | //! A logger that prints to OutputDebugString (Windows only) 2 | use log::{Level, Metadata, Record}; 3 | 4 | /// Implements `log::Log`, so can be used as a logging provider to 5 | /// forward log messages to the Windows `OutputDebugString` API 6 | pub struct WinDebugLogger; 7 | 8 | /// Static instance of `WinDebugLogger`, can be directly registered using `log::set_logger`<br> 9 | /// ``` 10 | /// use kanata_passthru::log_win; 11 | /// let _ = log_win::init(); // Init 12 | /// log::set_max_level(log::LevelFilter::Debug); 13 | /// use log::debug; // Use 14 | /// debug!("Debug log"); 15 | /// ``` 16 | pub static WINDBG_LOGGER: WinDebugLogger = WinDebugLogger; 17 | 18 | /// Convert logging levels to shorter and more visible icons 19 | pub fn iconify(lvl: log::Level) -> char { 20 | match lvl { 21 | Level::Error => '❗', 22 | Level::Warn => '⚠', 23 | Level::Info => 'ⓘ', 24 | Level::Debug => 'ⓓ', 25 | Level::Trace => 'ⓣ', 26 | } 27 | } 28 | 29 | use std::sync::OnceLock; 30 | pub fn is_thread_state() -> &'static bool { 31 | set_thread_state(false) 32 | } 33 | pub fn set_thread_state(is: bool) -> &'static bool { 34 | // accessor function to avoid get_or_init on every call (lazycell 35 | // allows doing that without an extra function) 36 | static CELL: OnceLock<bool> = OnceLock::new(); 37 | CELL.get_or_init(|| is) 38 | } 39 | 40 | use lazy_static::lazy_static; 41 | use regex::Regex; 42 | lazy_static! { // shorten source file name, no src/ no .rs ext 43 | static ref RE_EXT:Regex = Regex::new(r"\..*quot; ).unwrap(); 44 | static ref RE_SRC:Regex = Regex::new(r"src[\\/]").unwrap(); 45 | } 46 | fn clean_name(path: Option<&str>) -> String { 47 | if let Some(p) = path { 48 | RE_SRC.replace(&RE_EXT.replace(p, ""), "").to_string() 49 | } else { 50 | "?".to_string() 51 | } 52 | } 53 | 54 | #[cfg(target_os = "windows")] 55 | use winapi::um::processthreadsapi::GetCurrentThreadId; 56 | impl log::Log for WinDebugLogger { 57 | #[cfg(windows)] 58 | fn enabled(&self, _metadata: &Metadata) -> bool { 59 | true 60 | } 61 | #[cfg(not(windows))] 62 | fn enabled(&self, metadata: &Metadata) -> bool { 63 | false 64 | } 65 | fn log(&self, record: &Record) { 66 | #[cfg(not(target_os = "windows"))] 67 | let thread_id = ""; 68 | #[cfg(target_os = "windows")] 69 | let thread_id = if *is_thread_state() { 70 | format!("¦{}¦", unsafe { GetCurrentThreadId() }) 71 | } else { 72 | "".to_string() 73 | }; 74 | if self.enabled(record.metadata()) { 75 | let s = format!( 76 | "{}{}{}:{} {}", 77 | thread_id, 78 | iconify(record.level()), 79 | clean_name(record.file()), 80 | record.line().unwrap_or(0), 81 | record.args() 82 | ); 83 | dbg_win(&s); 84 | } 85 | } 86 | fn flush(&self) {} 87 | } 88 | 89 | pub fn dbg_win(s: &str) { 90 | //! Calls the `OutputDebugString` API to log a string (on Windows only)<br> 91 | //! See [`OutputDebugStringW`](https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-outputdebugstringw). 92 | #[cfg(windows)] 93 | { 94 | let len = s.encode_utf16().count() + 1; 95 | let mut s_utf16: Vec<u16> = Vec::with_capacity(len + 1); 96 | s_utf16.extend(s.encode_utf16()); 97 | s_utf16.push(0); 98 | unsafe { 99 | OutputDebugStringW(&s_utf16[0]); 100 | } 101 | } 102 | } 103 | 104 | #[cfg(windows)] 105 | extern "stdcall" { 106 | fn OutputDebugStringW(chars: *const u16); 107 | } 108 | 109 | pub fn init() { 110 | //! Set `WinDebugLogger` as the active logger<br> 111 | //! Doesn't panic on failure as it creates other problems for FFI etc. 112 | match log::set_logger(&WINDBG_LOGGER) { 113 | Ok(()) => {} 114 | Err(_) => { 115 | dbg_win("Warning: ✗ Failed to register WinDebugLogger\n"); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/gui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod win; 2 | pub use win::*; 3 | pub mod win_dbg_logger; 4 | pub mod win_nwg_ext; 5 | pub use win_dbg_logger as log_win; 6 | pub use win_dbg_logger::WINDBG_LOGGER; 7 | pub use win_nwg_ext::*; 8 | 9 | use crate::*; 10 | use parking_lot::Mutex; 11 | use std::sync::mpsc::Sender as ASender; 12 | use std::sync::{Arc, OnceLock}; 13 | pub static CFG: OnceLock<Arc<Mutex<Kanata>>> = OnceLock::new(); 14 | pub static GUI_TX: OnceLock<native_windows_gui::NoticeSender> = OnceLock::new(); 15 | pub static GUI_CFG_TX: OnceLock<native_windows_gui::NoticeSender> = OnceLock::new(); 16 | pub static GUI_ERR_TX: OnceLock<native_windows_gui::NoticeSender> = OnceLock::new(); 17 | pub static GUI_ERR_MSG_TX: OnceLock<ASender<(String, String)>> = OnceLock::new(); 18 | pub static GUI_EXIT_TX: OnceLock<native_windows_gui::NoticeSender> = OnceLock::new(); 19 | -------------------------------------------------------------------------------- /src/gui/win_dbg_logger/win_dbg_logger.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "win_dbg_logger" 3 | version = "0.1.0" 4 | authors = ["Arlie Davis <ardavis@microsoft.com>"] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/sivadeilra/win_dbg_logger" 8 | description = "A logger for use with Windows debuggers." 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | log = "0.4.*" 14 | winapi = {version="0.3.9", features=["processthreadsapi",]} 15 | regex = {version="1.10.4"} 16 | simplelog = {version="0.12.0", optional=true} 17 | 18 | [features] 19 | simple_shared = ["simplelog"] 20 | -------------------------------------------------------------------------------- /src/gui/win_nwg_ext/license-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © `2024` `Niccolò Betto` 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | -------------------------------------------------------------------------------- /src/gui/win_nwg_ext/license-nwg-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gabriel Dube 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/kanata.exe.manifest.rc: -------------------------------------------------------------------------------- 1 | #define RT_MANIFEST 24 2 | 1 RT_MANIFEST "./target/kanata.exe.manifest" 3 | iconMain ICON "../assets/kanata.ico" 4 | imgMain IMAGE "../assets/kanata.ico" 5 | imgReload IMAGE "../assets/reload_32px.png" 6 | -------------------------------------------------------------------------------- /src/kanata/caps_word.rs: -------------------------------------------------------------------------------- 1 | use kanata_keyberon::key_code::KeyCode; 2 | use rustc_hash::FxHashSet as HashSet; 3 | 4 | use kanata_parser::custom_action::CapsWordCfg; 5 | 6 | #[derive(Debug)] 7 | pub struct CapsWordState { 8 | /// Keys that will trigger an `lsft` key to be added to the active keys if present in the 9 | /// currently active keys. 10 | pub keys_to_capitalize: HashSet<KeyCode>, 11 | /// An extra list of keys that should **not** terminate the caps_word state, in addition to 12 | /// keys_to_capitalize, but which don't trigger a capitalization. 13 | pub keys_nonterminal: HashSet<KeyCode>, 14 | /// The configured timeout for caps_word. 15 | pub timeout: u16, 16 | /// The number of ticks remaining for caps_word, after which its state should be cleared. The 17 | /// number of ticks gets reset back to `timeout` when `maybe_add_lsft` is called. The reason 18 | /// for having this timeout at all is in case somebody was in the middle of typing a word, had 19 | /// to go do something, and forgot that caps_word was active. Having this timeout means that 20 | /// shift won't be active for their next keypress. 21 | pub timeout_ticks: u16, 22 | } 23 | 24 | #[derive(PartialEq, Eq, Debug, Clone, Copy)] 25 | pub enum CapsWordNextState { 26 | Active, 27 | End, 28 | } 29 | 30 | use CapsWordNextState::*; 31 | 32 | impl CapsWordState { 33 | pub(crate) fn new(cfg: &CapsWordCfg) -> Self { 34 | Self { 35 | keys_to_capitalize: cfg.keys_to_capitalize.iter().copied().collect(), 36 | keys_nonterminal: cfg.keys_nonterminal.iter().copied().collect(), 37 | timeout: cfg.timeout, 38 | timeout_ticks: cfg.timeout, 39 | } 40 | } 41 | 42 | pub(crate) fn maybe_add_lsft(&mut self, active_keys: &mut Vec<KeyCode>) -> CapsWordNextState { 43 | if self.timeout_ticks == 0 { 44 | return End; 45 | } 46 | for kc in active_keys.iter() { 47 | if !self.keys_to_capitalize.contains(kc) && !self.keys_nonterminal.contains(kc) { 48 | return End; 49 | } 50 | } 51 | if active_keys 52 | .last() 53 | .map(|kc| self.keys_to_capitalize.contains(kc)) 54 | .unwrap_or(false) 55 | { 56 | active_keys.insert(0, KeyCode::LShift); 57 | } 58 | if !active_keys.is_empty() { 59 | self.timeout_ticks = self.timeout; 60 | } 61 | self.timeout_ticks = self.timeout_ticks.saturating_sub(1); 62 | Active 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/kanata/cfg_forced.rs: -------------------------------------------------------------------------------- 1 | //! Options in the configuration file that are overidden/forced to some value other than what's in 2 | //! the configuration file, with the primary example being CLI arguments. 3 | 4 | use std::sync::OnceLock; 5 | 6 | static LOG_LAYER_CHANGES: OnceLock<bool> = OnceLock::new(); 7 | 8 | /// Force the log_layer_changes configuration to some value. 9 | /// This can only be called up to once. Panics if called a second time. 10 | pub fn force_log_layer_changes(v: bool) { 11 | LOG_LAYER_CHANGES 12 | .set(v) 13 | .expect("force cfg fns can only be called once"); 14 | } 15 | 16 | /// Get the forced log_layer_changes configuration if it was set. 17 | pub fn get_forced_log_layer_changes() -> Option<bool> { 18 | LOG_LAYER_CHANGES.get().copied() 19 | } 20 | -------------------------------------------------------------------------------- /src/kanata/macos.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use anyhow::{anyhow, bail, Result}; 3 | use log::info; 4 | use parking_lot::Mutex; 5 | use std::convert::TryFrom; 6 | use std::sync::mpsc::SyncSender as Sender; 7 | use std::sync::Arc; 8 | 9 | pub(crate) static PRESSED_KEYS: Lazy<Mutex<HashSet<OsCode>>> = 10 | Lazy::new(|| Mutex::new(HashSet::default())); 11 | 12 | impl Kanata { 13 | /// Enter an infinite loop that listens for OS key events and sends them to the processing thread. 14 | pub fn event_loop(kanata: Arc<Mutex<Self>>, tx: Sender<KeyEvent>) -> Result<()> { 15 | info!("entering the event loop"); 16 | 17 | let k = kanata.lock(); 18 | let allow_hardware_repeat = k.allow_hardware_repeat; 19 | let mut kb = match KbdIn::new(k.include_names.clone(), k.exclude_names.clone()) { 20 | Ok(kbd_in) => kbd_in, 21 | Err(e) => bail!("failed to open keyboard device(s): {}", e), 22 | }; 23 | drop(k); 24 | 25 | loop { 26 | let event = kb.read().map_err(|e| anyhow!("failed read: {}", e))?; 27 | 28 | let mut key_event = match KeyEvent::try_from(event) { 29 | Ok(ev) => ev, 30 | _ => { 31 | // Pass-through unrecognized keys 32 | log::debug!("{event:?} is unrecognized!"); 33 | let mut kanata = kanata.lock(); 34 | kanata 35 | .kbd_out 36 | .write(event) 37 | .map_err(|e| anyhow!("failed write: {}", e))?; 38 | continue; 39 | } 40 | }; 41 | 42 | check_for_exit(&key_event); 43 | 44 | if key_event.value == KeyValue::Repeat && !allow_hardware_repeat { 45 | continue; 46 | } 47 | 48 | if !MAPPED_KEYS.lock().contains(&key_event.code) { 49 | log::debug!("{key_event:?} is not mapped"); 50 | let mut kanata = kanata.lock(); 51 | kanata 52 | .kbd_out 53 | .write(event) 54 | .map_err(|e| anyhow!("failed write: {}", e))?; 55 | continue; 56 | } 57 | 58 | log::debug!("sending {key_event:?} to processing loop"); 59 | 60 | match key_event.value { 61 | KeyValue::Release => { 62 | PRESSED_KEYS.lock().remove(&key_event.code); 63 | } 64 | KeyValue::Press => { 65 | let mut pressed_keys = PRESSED_KEYS.lock(); 66 | if pressed_keys.contains(&key_event.code) { 67 | key_event.value = KeyValue::Repeat; 68 | } else { 69 | pressed_keys.insert(key_event.code); 70 | } 71 | } 72 | _ => {} 73 | } 74 | tx.try_send(key_event)?; 75 | } 76 | } 77 | 78 | pub fn check_release_non_physical_shift(&mut self) -> Result<()> { 79 | Ok(()) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/kanata/millisecond_counting.rs: -------------------------------------------------------------------------------- 1 | pub struct MillisecondCountResult { 2 | pub last_tick: instant::Instant, 3 | pub ms_elapsed: u128, 4 | pub ms_remainder_in_ns: u128, 5 | } 6 | 7 | pub fn count_ms_elapsed( 8 | last_tick: instant::Instant, 9 | now: instant::Instant, 10 | prev_ms_remainder_in_ns: u128, 11 | ) -> MillisecondCountResult { 12 | const NS_IN_MS: u128 = 1_000_000; 13 | let ns_elapsed = now.duration_since(last_tick).as_nanos(); 14 | let ns_elapsed_with_rem = ns_elapsed + prev_ms_remainder_in_ns; 15 | let ms_elapsed = ns_elapsed_with_rem / NS_IN_MS; 16 | let ms_remainder_in_ns = ns_elapsed_with_rem % NS_IN_MS; 17 | 18 | let last_tick = match ms_elapsed { 19 | 0 => last_tick, 20 | _ => now, 21 | }; 22 | MillisecondCountResult { 23 | last_tick, 24 | ms_elapsed, 25 | ms_remainder_in_ns, 26 | } 27 | } 28 | 29 | #[test] 30 | fn ms_counts_0_elapsed_correctly() { 31 | use std::time::Duration; 32 | let last_tick = instant::Instant::now(); 33 | let now = last_tick + Duration::from_nanos(999999); 34 | let result = count_ms_elapsed(last_tick, now, 0); 35 | assert_eq!(0, result.ms_elapsed); 36 | assert_eq!(last_tick, result.last_tick); 37 | assert_eq!(999999, result.ms_remainder_in_ns); 38 | } 39 | 40 | #[test] 41 | fn ms_counts_1_elapsed_correctly() { 42 | use std::time::Duration; 43 | let last_tick = instant::Instant::now(); 44 | let now = last_tick + Duration::from_nanos(1234567); 45 | let result = count_ms_elapsed(last_tick, now, 0); 46 | assert_eq!(1, result.ms_elapsed); 47 | assert_eq!(now, result.last_tick); 48 | assert_eq!(234567, result.ms_remainder_in_ns); 49 | } 50 | 51 | #[test] 52 | fn ms_counts_1_then_2_elapsed_correctly() { 53 | use std::time::Duration; 54 | let last_tick = instant::Instant::now(); 55 | let now = last_tick + Duration::from_micros(1750); 56 | let result = count_ms_elapsed(last_tick, now, 0); 57 | assert_eq!(1, result.ms_elapsed); 58 | assert_eq!(now, result.last_tick); 59 | assert_eq!(750000, result.ms_remainder_in_ns); 60 | let last_tick = result.last_tick; 61 | let now = last_tick + Duration::from_micros(1750); 62 | let result = count_ms_elapsed(last_tick, now, result.ms_remainder_in_ns); 63 | assert_eq!(2, result.ms_elapsed); 64 | assert_eq!(now, result.last_tick); 65 | assert_eq!(500000, result.ms_remainder_in_ns); 66 | } 67 | -------------------------------------------------------------------------------- /src/kanata/unknown.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub static PRESSED_KEYS: Lazy<Mutex<HashSet<OsCode>>> = 4 | Lazy::new(|| Mutex::new(HashSet::default())); 5 | 6 | impl Kanata { 7 | pub fn check_release_non_physical_shift(&mut self) -> Result<()> { 8 | // Silence warning 9 | check_for_exit(&KeyEvent::new(OsCode::KEY_UNKNOWN, KeyValue::Release)); 10 | Ok(()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Error, Result}; 2 | use std::net::SocketAddr; 3 | use std::path::PathBuf; 4 | use std::str::FromStr; 5 | 6 | #[cfg(all(target_os = "windows", feature = "gui"))] 7 | pub mod gui; 8 | pub mod kanata; 9 | pub mod oskbd; 10 | pub mod tcp_server; 11 | #[cfg(test)] 12 | pub mod tests; 13 | 14 | pub use kanata::*; 15 | pub use tcp_server::TcpServer; 16 | 17 | type CfgPath = PathBuf; 18 | 19 | pub struct ValidatedArgs { 20 | pub paths: Vec<CfgPath>, 21 | #[cfg(feature = "tcp_server")] 22 | pub tcp_server_address: Option<SocketAddrWrapper>, 23 | #[cfg(target_os = "linux")] 24 | pub symlink_path: Option<String>, 25 | pub nodelay: bool, 26 | } 27 | 28 | pub fn default_cfg() -> Vec<PathBuf> { 29 | let mut cfgs = Vec::new(); 30 | 31 | let default = PathBuf::from("kanata.kbd"); 32 | if default.is_file() { 33 | cfgs.push(default); 34 | } 35 | 36 | if let Some(config_dir) = dirs::config_dir() { 37 | let fallback = config_dir.join("kanata").join("kanata.kbd"); 38 | if fallback.is_file() { 39 | cfgs.push(fallback); 40 | } 41 | } 42 | 43 | cfgs 44 | } 45 | 46 | #[derive(Debug, Clone)] 47 | pub struct SocketAddrWrapper(SocketAddr); 48 | 49 | impl FromStr for SocketAddrWrapper { 50 | type Err = Error; 51 | 52 | fn from_str(s: &str) -> Result<Self, Self::Err> { 53 | let mut address = s.to_string(); 54 | if let Ok(port) = s.parse::<u16>() { 55 | address = format!("127.0.0.1:{port}"); 56 | } 57 | address 58 | .parse::<SocketAddr>() 59 | .map(SocketAddrWrapper) 60 | .map_err(|e| anyhow!("Please specify either a port number, e.g. 8081 or an address, e.g. 127.0.0.1:8081.\n{e}")) 61 | } 62 | } 63 | 64 | impl SocketAddrWrapper { 65 | pub fn into_inner(self) -> SocketAddr { 66 | self.0 67 | } 68 | pub fn get_ref(&self) -> &SocketAddr { 69 | &self.0 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main_lib/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(target_os = "windows", feature = "gui"))] 2 | pub(crate) mod win_gui; 3 | -------------------------------------------------------------------------------- /src/oskbd/mod.rs: -------------------------------------------------------------------------------- 1 | //! Platform specific code for low level keyboard read/write. 2 | 3 | #[cfg(target_os = "linux")] 4 | mod linux; 5 | #[cfg(target_os = "linux")] 6 | pub use linux::*; 7 | 8 | #[cfg(target_os = "windows")] 9 | mod windows; 10 | #[cfg(target_os = "windows")] 11 | pub use windows::*; 12 | 13 | #[cfg(target_os = "macos")] 14 | mod macos; 15 | #[cfg(target_os = "macos")] 16 | pub use macos::*; 17 | 18 | #[cfg(any( 19 | all( 20 | not(feature = "simulated_input"), 21 | feature = "simulated_output", 22 | not(feature = "passthru_ahk") 23 | ), 24 | all( 25 | feature = "simulated_input", 26 | not(feature = "simulated_output"), 27 | not(feature = "passthru_ahk") 28 | ) 29 | ))] 30 | mod simulated; // has KbdOut 31 | #[cfg(any( 32 | all( 33 | not(feature = "simulated_input"), 34 | feature = "simulated_output", 35 | not(feature = "passthru_ahk") 36 | ), 37 | all( 38 | feature = "simulated_input", 39 | not(feature = "simulated_output"), 40 | not(feature = "passthru_ahk") 41 | ) 42 | ))] 43 | pub use simulated::*; 44 | #[cfg(any( 45 | all(feature = "simulated_input", feature = "simulated_output"), 46 | all( 47 | feature = "simulated_input", 48 | feature = "simulated_output", 49 | feature = "passthru_ahk" 50 | ), 51 | ))] 52 | mod sim_passthru; // has KbdOut 53 | #[cfg(any( 54 | all(feature = "simulated_input", feature = "simulated_output"), 55 | all( 56 | feature = "simulated_input", 57 | feature = "simulated_output", 58 | feature = "passthru_ahk" 59 | ), 60 | ))] 61 | pub use sim_passthru::*; 62 | 63 | pub const HI_RES_SCROLL_UNITS_IN_LO_RES: u16 = 120; 64 | 65 | // ------------------ KeyValue -------------------- 66 | 67 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 68 | pub enum KeyValue { 69 | Release = 0, 70 | Press = 1, 71 | Repeat = 2, 72 | Tap, 73 | WakeUp, 74 | } 75 | 76 | impl From<i32> for KeyValue { 77 | fn from(item: i32) -> Self { 78 | match item { 79 | 0 => Self::Release, 80 | 1 => Self::Press, 81 | 2 => Self::Repeat, 82 | _ => unreachable!(), 83 | } 84 | } 85 | } 86 | 87 | impl From<bool> for KeyValue { 88 | fn from(up: bool) -> Self { 89 | match up { 90 | true => Self::Release, 91 | false => Self::Press, 92 | } 93 | } 94 | } 95 | 96 | impl From<KeyValue> for bool { 97 | fn from(val: KeyValue) -> Self { 98 | matches!(val, KeyValue::Release) 99 | } 100 | } 101 | 102 | use kanata_parser::keys::OsCode; 103 | 104 | #[derive(Clone, Copy)] 105 | pub struct KeyEvent { 106 | pub code: OsCode, 107 | pub value: KeyValue, 108 | } 109 | 110 | #[allow(dead_code, unused)] 111 | impl KeyEvent { 112 | pub fn new(code: OsCode, value: KeyValue) -> Self { 113 | Self { code, value } 114 | } 115 | } 116 | 117 | use core::fmt; 118 | impl fmt::Display for KeyEvent { 119 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 120 | use kanata_keyberon::key_code::KeyCode; 121 | let direction = match self.value { 122 | KeyValue::Press => "↓", 123 | KeyValue::Release => "↑", 124 | KeyValue::Repeat => "⟳", 125 | KeyValue::Tap => "↕", 126 | KeyValue::WakeUp => "!", 127 | }; 128 | let key_name = KeyCode::from(self.code); 129 | write!(f, "{direction}{key_name:?}") 130 | } 131 | } 132 | 133 | impl fmt::Debug for KeyEvent { 134 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 135 | f.debug_struct("KeyEvent") 136 | .field( 137 | "code", 138 | &format_args!("{:?} ({})", self.code, self.code.as_u16()), 139 | ) 140 | .field("value", &self.value) 141 | .finish() 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/oskbd/windows/exthook_os.rs: -------------------------------------------------------------------------------- 1 | //! A function listener for keyboard input events replacing Windows keyboard hook API 2 | 3 | use core::fmt; 4 | use once_cell::sync::Lazy; 5 | use parking_lot::Mutex; 6 | 7 | use winapi::ctypes::*; 8 | use winapi::um::winuser::*; 9 | 10 | use crate::oskbd::{KeyEvent, KeyValue}; 11 | use kanata_keyberon::key_code::KeyCode; 12 | 13 | use kanata_parser::keys::*; 14 | 15 | pub const LLHOOK_IDLE_TIME_SECS_CLEAR_INPUTS: u64 = 60; 16 | 17 | type HookFn = dyn FnMut(InputEvent) -> bool + Send + Sync + 'static; 18 | 19 | pub static HOOK_CB: Lazy<Mutex<Option<Box<HookFn>>>> = Lazy::new(|| Mutex::new(None)); // store thread-safe hook callback with a mutex (can be called from an external process) 20 | 21 | pub struct KeyboardHook {} // reusing hook type for our listener 22 | impl KeyboardHook { 23 | /// Sets input callback (panics if already registered) 24 | pub fn set_input_cb( 25 | callback: impl FnMut(InputEvent) -> bool + Send + Sync + 'static, 26 | ) -> KeyboardHook { 27 | let mut cb_opt = HOOK_CB.lock(); 28 | assert!( 29 | cb_opt.take().is_none(), 30 | "Only 1 external listener is allowed!" 31 | ); 32 | *cb_opt = Some(Box::new(callback)); 33 | KeyboardHook {} 34 | } 35 | } 36 | #[cfg(not(feature = "passthru_ahk"))] // unused KeyboardHook will be dropped, breaking our hook, disable it 37 | impl Drop for KeyboardHook { 38 | fn drop(&mut self) { 39 | let mut cb_opt = HOOK_CB.lock(); 40 | cb_opt.take(); 41 | } 42 | } 43 | 44 | #[derive(Debug, Clone, Copy)] 45 | pub struct InputEvent { 46 | // Key event received by the low level keyboard hook. 47 | pub code: u32, 48 | pub up: bool, /*Key was released*/ 49 | } 50 | impl fmt::Display for InputEvent { 51 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 52 | let direction = if self.up { "↑" } else { "↓" }; 53 | let key_name = KeyCode::from(OsCode::from(self.code)); 54 | write!(f, "{}{:?}", direction, key_name) 55 | } 56 | } 57 | impl InputEvent { 58 | pub fn from_vk_sc(vk: c_uint, sc: c_uint, up: c_int) -> Self { 59 | let code = if vk == (VK_RETURN as u32) { 60 | // todo: do a proper check for numpad enter, maybe 0x11c isn't universal 61 | match sc { 62 | 0x11C => u32::from(VK_KPENTER_FAKE), 63 | _ => VK_RETURN as u32, 64 | } 65 | } else { 66 | vk 67 | }; 68 | Self { 69 | code, 70 | up: (up != 0), 71 | } 72 | } 73 | pub fn from_oscode(code: OsCode, val: KeyValue) -> Self { 74 | Self { 75 | code: code.into(), 76 | up: val.into(), 77 | } 78 | } 79 | } 80 | impl TryFrom<InputEvent> for KeyEvent { 81 | type Error = (); 82 | fn try_from(item: InputEvent) -> Result<Self, Self::Error> { 83 | Ok(Self { 84 | code: OsCode::from_u16(item.code as u16).ok_or(())?, 85 | value: match item.up { 86 | true => KeyValue::Release, 87 | false => KeyValue::Press, 88 | }, 89 | }) 90 | } 91 | } 92 | impl From<KeyEvent> for InputEvent { 93 | fn from(item: KeyEvent) -> Self { 94 | Self { 95 | code: item.code.into(), 96 | up: item.value.into(), 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/tests/sim_tests/block_keys_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn block_does_not_block_buttons() { 5 | let result = simulate( 6 | "(defcfg process-unmapped-keys yes 7 | block-unmapped-keys yes) 8 | (defsrc) 9 | (deflayer base)", 10 | "d:mlft d:mrgt d:mmid d:mbck d:mfwd t:10 d:f1 11 | u:mlft u:mrgt u:mmid u:mbck u:mfwd t:10 u:f1", 12 | ); 13 | assert_eq!( 14 | "out🖰:↓Left\nt:1ms\nout🖰:↓Right\nt:1ms\nout🖰:↓Mid\nt:1ms\nout🖰:↓Backward\n\ 15 | t:1ms\nout🖰:↓Forward\nt:7ms\nout🖰:↑Left\nt:1ms\nout🖰:↑Right\nt:1ms\nout🖰:↑Mid\n\ 16 | t:1ms\nout🖰:↑Backward\nt:1ms\nout🖰:↑Forward", 17 | result 18 | ); 19 | } 20 | 21 | #[test] 22 | fn block_does_not_block_wheel() { 23 | let result = simulate( 24 | "(defcfg process-unmapped-keys yes 25 | block-unmapped-keys yes) 26 | (defsrc) 27 | (deflayer base)", 28 | "d:mwu d:mwd d:mwl d:mwr t:10 d:f1 29 | u:mwu u:mwd u:mwl u:mwr t:10 u:f1", 30 | ); 31 | assert_eq!( 32 | "scroll:Up,120\nt:1ms\nscroll:Down,120\nt:1ms\nscroll:Left,120\nt:1ms\nscroll:Right,120", 33 | result 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/tests/sim_tests/capsword_sim_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | const CFG: &str = r##" 4 | (defcfg) 5 | (defsrc 7 8 9 0) 6 | (deflayer base 7 | (caps-word 1000) 8 | (caps-word-custom 200 (a) (b)) 9 | (caps-word-toggle 1000) 10 | (caps-word-custom-toggle 200 (a) (b)) 11 | ) 12 | "##; 13 | 14 | #[test] 15 | fn caps_word_behaves_correctly() { 16 | let result = simulate( 17 | CFG, 18 | "d:7 u:7 d:a u:a d:1 u:1 d:a u:a d:spc u:spc d:a u:a t:1000", 19 | ) 20 | .no_time(); 21 | assert_eq!( 22 | "out:↓LShift out:↓A out:↑LShift out:↑A \ 23 | out:↓Kb1 out:↑Kb1 out:↓LShift out:↓A out:↑LShift out:↑A \ 24 | out:↓Space out:↑Space out:↓A out:↑A", 25 | result 26 | ); 27 | } 28 | 29 | #[test] 30 | fn caps_word_custom_behaves_correctly() { 31 | let result = simulate( 32 | CFG, 33 | "d:8 u:8 d:a u:a d:b u:b d:a u:a d:1 u:1 d:a u:a t:1000", 34 | ) 35 | .no_time(); 36 | assert_eq!( 37 | "out:↓LShift out:↓A out:↑LShift out:↑A \ 38 | out:↓B out:↑B out:↓LShift out:↓A out:↑LShift out:↑A \ 39 | out:↓Kb1 out:↑Kb1 out:↓A out:↑A", 40 | result 41 | ); 42 | } 43 | 44 | #[test] 45 | fn caps_word_times_out() { 46 | let result = simulate(CFG, "d:7 u:7 d:a u:a t:500 d:a u:a t:1001 d:a u:a t:10").no_time(); 47 | assert_eq!( 48 | "out:↓LShift out:↓A out:↑LShift out:↑A \ 49 | out:↓LShift out:↓A out:↑LShift out:↑A \ 50 | out:↓A out:↑A", 51 | result 52 | ); 53 | } 54 | 55 | #[test] 56 | fn caps_word_custom_times_out() { 57 | let result = simulate(CFG, "d:8 u:8 d:a u:a t:100 d:a u:a t:201 d:a u:a t:10").no_time(); 58 | assert_eq!( 59 | "out:↓LShift out:↓A out:↑LShift out:↑A \ 60 | out:↓LShift out:↓A out:↑LShift out:↑A \ 61 | out:↓A out:↑A", 62 | result 63 | ); 64 | } 65 | 66 | #[test] 67 | fn caps_word_does_not_toggle() { 68 | let result = simulate(CFG, "d:7 u:7 d:a u:a t:100 d:7 u:7 t:100 d:a u:a t:10").no_time(); 69 | assert_eq!( 70 | "out:↓LShift out:↓A out:↑LShift out:↑A \ 71 | out:↓LShift out:↓A out:↑LShift out:↑A", 72 | result 73 | ); 74 | } 75 | 76 | #[test] 77 | fn caps_word_custom_does_not_toggle() { 78 | let result = simulate(CFG, "d:8 u:8 d:a u:a t:100 d:8 u:8 t:100 d:a u:a t:10").no_time(); 79 | assert_eq!( 80 | "out:↓LShift out:↓A out:↑LShift out:↑A \ 81 | out:↓LShift out:↓A out:↑LShift out:↑A", 82 | result 83 | ); 84 | } 85 | 86 | #[test] 87 | fn caps_word_toggle_does_toggle() { 88 | let result = simulate(CFG, "d:9 u:9 d:a u:a t:100 d:9 u:9 t:100 d:a u:a t:10").no_time(); 89 | assert_eq!( 90 | "out:↓LShift out:↓A out:↑LShift out:↑A \ 91 | out:↓A out:↑A", 92 | result 93 | ); 94 | } 95 | 96 | #[test] 97 | fn caps_word_custom_toggle_does_toggle() { 98 | let result = simulate(CFG, "d:0 u:0 d:a u:a t:100 d:0 u:0 t:100 d:a u:a t:10").no_time(); 99 | assert_eq!( 100 | "out:↓LShift out:↓A out:↑LShift out:↑A \ 101 | out:↓A out:↑A", 102 | result 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/tests/sim_tests/delay_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | #[ignore] // timing-based: fails intermittently 5 | fn on_press_delay() { 6 | let start = std::time::Instant::now(); 7 | let result = simulate( 8 | "(defsrc) (deflayermap (base) a (on-press-delay 10))", 9 | "d:a t:50 u:a t:50", 10 | ); 11 | assert_eq!("", result); 12 | let end = std::time::Instant::now(); 13 | let duration = end - start; 14 | assert!(duration > std::time::Duration::from_millis(9)); 15 | assert!(duration < std::time::Duration::from_millis(19)); 16 | } 17 | 18 | #[test] 19 | #[ignore] // timing-based: fails intermittently 20 | fn on_release_delay() { 21 | let start = std::time::Instant::now(); 22 | let result = simulate( 23 | "(defsrc) (deflayermap (base) a (on-release-delay 10))", 24 | "d:a t:50 u:a t:50", 25 | ); 26 | assert_eq!("", result); 27 | let end = std::time::Instant::now(); 28 | let duration = end - start; 29 | assert!(duration > std::time::Duration::from_millis(9)); 30 | assert!(duration < std::time::Duration::from_millis(19)); 31 | } 32 | 33 | #[test] 34 | #[ignore] // timing-based: fails intermittently 35 | fn no_delay() { 36 | let start = std::time::Instant::now(); 37 | let result = simulate("(defsrc) (deflayermap (base) a XX)", "d:a t:50 u:a t:50"); 38 | assert_eq!("", result); 39 | let end = std::time::Instant::now(); 40 | let duration = end - start; 41 | assert!(duration < std::time::Duration::from_millis(10)); 42 | } 43 | -------------------------------------------------------------------------------- /src/tests/sim_tests/layer_sim_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn transparent_base() { 5 | let result = simulate( 6 | "(defcfg process-unmapped-keys yes concurrent-tap-hold yes) \ 7 | (defsrc a) \ 8 | (deflayer base _)", 9 | "d:a t:50 u:a t:50", 10 | ); 11 | assert_eq!("out:↓A\nt:50ms\nout:↑A", result); 12 | } 13 | 14 | #[test] 15 | fn delegate_base() { 16 | let result = simulate( 17 | "(defcfg process-unmapped-keys yes \ 18 | delegate-to-first-layer yes) 19 | (defsrc a b) \ 20 | (deflayer base c (layer-switch 2)) \ 21 | (deflayer 2 _ _)", 22 | "d:b t:50 u:b t:50 d:a t:50 u:a t:50", 23 | ); 24 | assert_eq!("t:100ms\nout:↓C\nt:50ms\nout:↑C", result); 25 | } 26 | 27 | #[test] 28 | fn delegate_base_but_base_is_transparent() { 29 | let result = simulate( 30 | "(defcfg process-unmapped-keys yes \ 31 | delegate-to-first-layer yes) 32 | (defsrc a b) \ 33 | (deflayer base _ (layer-switch 2)) \ 34 | (deflayer 2 _ _)", 35 | "d:b t:50 u:b t:50 d:a t:50 u:a t:50", 36 | ); 37 | assert_eq!("t:100ms\nout:↓A\nt:50ms\nout:↑A", result); 38 | } 39 | 40 | #[test] 41 | fn layer_switching() { 42 | let result = simulate( 43 | "(defcfg process-unmapped-keys yes 44 | delegate-to-first-layer yes) 45 | (defsrc a b c d) 46 | (deflayer base x y z (layer-switch 2)) 47 | (deflayer 2 e f _ (layer-switch 3)) 48 | (deflayer 3 g _ _ (layer-switch 4)) 49 | (deflayer 4 _ _ _ XX) 50 | ", 51 | "d:c t:20 u:c t:20 d:d t:20 u:d t:20 52 | d:b t:20 u:b t:20 53 | d:c t:20 u:c t:20 54 | d:d t:20 u:d t:20 55 | d:a t:20 u:a t:20 56 | d:b t:20 u:b t:20 57 | d:d t:20 u:d t:20 58 | d:a t:20 u:a t:20", 59 | ); 60 | assert_eq!( 61 | "out:↓Z\nt:20ms\nout:↑Z\nt:60ms\nout:↓F\nt:20ms\nout:↑F\nt:20ms\nout:↓Z\nt:20ms\nout:↑Z\nt:60ms\nout:↓G\nt:20ms\nout:↑G\nt:20ms\nout:↓Y\nt:20ms\nout:↑Y\nt:60ms\nout:↓X\nt:20ms\nout:↑X", 62 | result 63 | ); 64 | } 65 | 66 | #[test] 67 | fn layer_holding() { 68 | let result = simulate( 69 | "(defcfg process-unmapped-keys yes 70 | delegate-to-first-layer no) 71 | (defsrc a b c d e f) 72 | (deflayer base x y z (layer-while-held 2) XX XX) 73 | (deflayer 2 e f _ XX (layer-while-held 3) XX) 74 | (deflayer 3 g _ _ XX XX (layer-while-held 4)) 75 | (deflayer 4 _ _ _ XX XX XX) 76 | ", 77 | "d:c t:20 u:c t:20 78 | d:d t:20 79 | d:a t:20 u:a t:20 80 | d:b t:20 u:b t:20 81 | d:c t:20 u:c t:20 82 | d:e t:20 83 | d:a t:20 u:a t:20 84 | d:b t:20 u:b t:20 85 | d:c t:20 u:c t:20 86 | d:f t:20 87 | d:a t:20 u:a t:20 88 | d:b t:20 u:b t:20 89 | d:c t:20 u:c t:20", 90 | ); 91 | assert_eq!( 92 | "out:↓Z\nt:20ms\nout:↑Z\nt:40ms\nout:↓E\nt:20ms\nout:↑E\nt:20ms\nout:↓F\nt:20ms\nout:↑F\nt:20ms\nout:↓Z\nt:20ms\nout:↑Z\nt:40ms\nout:↓G\nt:20ms\nout:↑G\nt:20ms\nout:↓F\nt:20ms\nout:↑F\nt:20ms\nout:↓Z\nt:20ms\nout:↑Z\nt:40ms\nout:↓G\nt:20ms\nout:↑G\nt:20ms\nout:↓F\nt:20ms\nout:↑F\nt:20ms\nout:↓Z\nt:20ms\nout:↑Z", 93 | result 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/tests/sim_tests/mod.rs: -------------------------------------------------------------------------------- 1 | //! Contains tests that use simulated inputs. 2 | //! 3 | //! One way to write tests is to write the configuration, write the simulated input, and then let 4 | //! the test fail by comparing the output to an empty string. Run the test then inspect the failure 5 | //! and see if the real output looks sensible according to what is expected. 6 | 7 | use crate::tests::*; 8 | use crate::{ 9 | oskbd::{KeyEvent, KeyValue}, 10 | str_to_oscode, Kanata, 11 | }; 12 | 13 | use rustc_hash::FxHashMap; 14 | 15 | mod block_keys_tests; 16 | mod capsword_sim_tests; 17 | mod chord_sim_tests; 18 | mod delay_tests; 19 | mod layer_sim_tests; 20 | mod macro_sim_tests; 21 | mod oneshot_tests; 22 | mod override_tests; 23 | mod release_sim_tests; 24 | mod repeat_sim_tests; 25 | mod seq_sim_tests; 26 | mod switch_sim_tests; 27 | mod tap_hold_tests; 28 | mod template_sim_tests; 29 | mod timing_tests; 30 | mod unicode_sim_tests; 31 | mod unmod_sim_tests; 32 | mod use_defsrc_sim_tests; 33 | mod vkey_sim_tests; 34 | mod zippychord_sim_tests; 35 | 36 | fn simulate<S: AsRef<str>>(cfg: S, sim: S) -> String { 37 | simulate_with_file_content(cfg, sim, Default::default()) 38 | } 39 | 40 | fn simulate_with_file_content<S: AsRef<str>>( 41 | cfg: S, 42 | sim: S, 43 | file_content: FxHashMap<String, String>, 44 | ) -> String { 45 | init_log(); 46 | let _lk = match CFG_PARSE_LOCK.lock() { 47 | Ok(guard) => guard, 48 | Err(poisoned) => poisoned.into_inner(), 49 | }; 50 | let mut k = Kanata::new_from_str(cfg.as_ref(), file_content).expect("failed to parse cfg"); 51 | for pair in sim.as_ref().split_whitespace() { 52 | match pair.split_once(':') { 53 | Some((kind, val)) => match kind { 54 | "t" => { 55 | let tick = str::parse::<u128>(val).expect("valid num for tick"); 56 | k.tick_ms(tick, &None).unwrap(); 57 | } 58 | "d" => { 59 | let key_code = str_to_oscode(val).expect("valid keycode"); 60 | k.handle_input_event(&KeyEvent { 61 | code: key_code, 62 | value: KeyValue::Press, 63 | }) 64 | .expect("input handles fine"); 65 | } 66 | "u" => { 67 | let key_code = str_to_oscode(val).expect("valid keycode"); 68 | k.handle_input_event(&KeyEvent { 69 | code: key_code, 70 | value: KeyValue::Release, 71 | }) 72 | .expect("input handles fine"); 73 | } 74 | "r" => { 75 | let key_code = str_to_oscode(val).expect("valid keycode"); 76 | k.handle_input_event(&KeyEvent { 77 | code: key_code, 78 | value: KeyValue::Repeat, 79 | }) 80 | .expect("input handles fine"); 81 | } 82 | _ => panic!("invalid item {pair}"), 83 | }, 84 | None => panic!("invalid item {pair}"), 85 | } 86 | } 87 | drop(_lk); 88 | k.kbd_out.outputs.events.join("\n") 89 | } 90 | 91 | #[allow(unused)] 92 | trait SimTransform { 93 | /// Changes newlines to spaces. 94 | fn to_spaces(self) -> Self; 95 | /// Removes out:↑_ items from the string. Also transforms newlines to spaces. 96 | fn no_releases(self) -> Self; 97 | /// Removes t:_ms items from the string. Also transforms newlines to spaces. 98 | fn no_time(self) -> Self; 99 | /// Replaces out:↓_ with dn:_ and out:↑_ with up:_. Also transforms newlines to spaces. 100 | fn to_ascii(self) -> Self; 101 | } 102 | 103 | impl SimTransform for String { 104 | fn to_spaces(self) -> Self { 105 | self.replace('\n', " ") 106 | } 107 | 108 | fn no_time(self) -> Self { 109 | self.split_ascii_whitespace() 110 | .filter(|s| !s.starts_with("t:")) 111 | .collect::<Vec<_>>() 112 | .join(" ") 113 | } 114 | 115 | fn no_releases(self) -> Self { 116 | self.split_ascii_whitespace() 117 | .filter(|s| !s.starts_with("out:↑") && !s.starts_with("up:")) 118 | .collect::<Vec<_>>() 119 | .join(" ") 120 | } 121 | 122 | fn to_ascii(self) -> Self { 123 | self.split_ascii_whitespace() 124 | .map(|s| s.replace("out:↑", "up:").replace("out:↓", "dn:")) 125 | .collect::<Vec<_>>() 126 | .join(" ") 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/tests/sim_tests/oneshot_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn oneshot_pause() { 5 | let result = simulate( 6 | " 7 | (defsrc a lmet rmet) 8 | (deflayer base 9 | 1 @lme @rme) 10 | (deflayer numbers 11 | 2 @lme @rme) 12 | (deflayer navigation 13 | (one-shot 2000 lalt) @lme @rme) 14 | (deflayer symbols 15 | 4 @lme @rme) 16 | 17 | (defvirtualkeys 18 | callum (switch 19 | ((and nop1 nop2)) (layer-while-held numbers) break 20 | (nop1) (layer-while-held navigation) break 21 | (nop2) (layer-while-held symbols) break) 22 | activate-callum (multi 23 | (one-shot-pause-processing 5) 24 | (switch 25 | ((or nop1 nop2)) 26 | (multi (on-press release-vkey callum) 27 | (on-press press-vkey callum)) 28 | break 29 | () (on-press release-vkey callum) break))) 30 | 31 | (defalias 32 | lme (multi nop1 33 | (on-press tap-vkey activate-callum) 34 | (on-release tap-vkey activate-callum)) 35 | rme (multi nop2 36 | (on-press tap-vkey activate-callum) 37 | (on-release tap-vkey activate-callum))) 38 | ", 39 | "d:lmet t:10 d:a u:a t:10 u:lmet t:10 d:a u:a t:10", 40 | ) 41 | .to_ascii(); 42 | assert_eq!("t:10ms dn:LAlt t:20ms dn:Kb1 t:5ms up:LAlt up:Kb1", result); 43 | } 44 | -------------------------------------------------------------------------------- /src/tests/sim_tests/override_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn override_with_unmod() { 5 | let result = simulate( 6 | " 7 | (defoverrides 8 | (a) (b) 9 | (b) (a) 10 | ) 11 | 12 | (defalias 13 | b (unshift b) 14 | a (unshift a) 15 | ) 16 | (defsrc a b) 17 | (deflayer base @a @b) 18 | ", 19 | "d:lsft t:50 d:a t:50 u:a t:50 d:b t:50 u:b t:50", 20 | ) 21 | .to_ascii() 22 | .no_time(); 23 | assert_eq!( 24 | "dn:LShift up:LShift dn:B up:B dn:LShift up:LShift dn:A up:A dn:LShift", 25 | result 26 | ); 27 | } 28 | 29 | #[test] 30 | fn override_release_mod_change_key() { 31 | let cfg = " 32 | (defsrc) 33 | (deflayer base) 34 | (defoverrides 35 | (lsft a) (lsft 9) 36 | (lsft 1) (lctl 2)) 37 | "; 38 | let result = simulate(cfg, "d:lsft t:10 d:a t:10 u:lsft t:10 u:a t:10").to_ascii(); 39 | assert_eq!("dn:LShift t:10ms dn:Kb9 t:10ms up:LShift up:Kb9", result); 40 | let result = simulate(cfg, "d:lsft t:10 d:a t:10 u:a t:10 u:lsft t:10").to_ascii(); 41 | assert_eq!( 42 | "dn:LShift t:10ms dn:Kb9 t:10ms up:Kb9 t:10ms up:LShift", 43 | result 44 | ); 45 | let result = simulate(cfg, "d:lsft t:10 d:a t:10 d:c t:10").to_ascii(); 46 | assert_eq!("dn:LShift t:10ms dn:Kb9 t:10ms up:Kb9 dn:C", result); 47 | let result = simulate(cfg, "d:lsft t:10 d:1 t:10 d:c t:10").to_ascii(); 48 | assert_eq!( 49 | "dn:LShift t:10ms up:LShift dn:LCtrl dn:Kb2 t:10ms up:LCtrl up:Kb2 dn:LShift dn:C", 50 | result 51 | ); 52 | } 53 | 54 | #[test] 55 | fn override_eagerly_releases() { 56 | let result = simulate( 57 | " 58 | (defcfg override-release-on-activation yes) 59 | (defsrc) 60 | (deflayer base) 61 | (defoverrides (lsft a) (lsft 9)) 62 | ", 63 | "d:lsft t:10 d:a t:10 u:lsft t:10 u:a t:10", 64 | ) 65 | .to_ascii(); 66 | assert_eq!( 67 | "dn:LShift t:10ms dn:Kb9 t:1ms up:Kb9 t:9ms up:LShift", 68 | result 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/tests/sim_tests/release_sim_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn release_standard() { 5 | let result = simulate( 6 | " 7 | (defsrc a) 8 | (deflayer base (multi lalt a)) 9 | ", 10 | " 11 | d:a t:10 u:a t:10 12 | ", 13 | ) 14 | .to_ascii(); 15 | assert_eq!("dn:LAlt dn:A t:10ms up:LAlt up:A", result); 16 | } 17 | 18 | #[test] 19 | fn release_reversed() { 20 | let result = simulate( 21 | " 22 | (defsrc a) 23 | (deflayer base (multi lalt a reverse-release-order)) 24 | ", 25 | " 26 | d:a t:10 u:a t:10 27 | ", 28 | ) 29 | .to_ascii(); 30 | assert_eq!("dn:LAlt dn:A t:10ms up:A up:LAlt", result); 31 | } 32 | -------------------------------------------------------------------------------- /src/tests/sim_tests/repeat_sim_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn repeat_standard() { 5 | let result = simulate( 6 | " 7 | (defsrc a) 8 | (deflayer base b) 9 | ", 10 | " 11 | d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a 12 | ", 13 | ); 14 | assert_eq!( 15 | "out:↓B\nt:10ms\nout:↓B\nt:10ms\nout:↓B\nt:10ms\nout:↑B", 16 | result 17 | ); 18 | } 19 | 20 | #[test] 21 | fn repeat_layer_while_held() { 22 | let result = simulate( 23 | " 24 | (defsrc a b) 25 | (deflayer base a (layer-while-held held)) 26 | (deflayer held c b) 27 | ", 28 | " 29 | d:b t:10 r:b t:10 d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a 30 | ", 31 | ); 32 | assert_eq!( 33 | "t:20ms\nout:↓C\nt:10ms\nout:↓C\nt:10ms\nout:↓C\nt:10ms\nout:↑C", 34 | result 35 | ); 36 | } 37 | 38 | #[test] 39 | fn repeat_layer_switch() { 40 | let result = simulate( 41 | " 42 | (defsrc a b) 43 | (deflayer base a (layer-switch swtc)) 44 | (deflayer swtc d b) 45 | ", 46 | " 47 | d:b t:10 r:b t:10 d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a 48 | ", 49 | ); 50 | assert_eq!( 51 | "t:20ms\nout:↓D\nt:10ms\nout:↓D\nt:10ms\nout:↓D\nt:10ms\nout:↑D", 52 | result 53 | ); 54 | } 55 | 56 | #[test] 57 | fn repeat_layer_held_trans() { 58 | let result = simulate( 59 | " 60 | (defsrc a b) 61 | (deflayer base e (layer-while-held held)) 62 | (deflayer held _ b) 63 | ", 64 | " 65 | d:b t:10 r:b t:10 d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a 66 | ", 67 | ); 68 | assert_eq!( 69 | "t:20ms\nout:↓E\nt:10ms\nout:↓E\nt:10ms\nout:↓E\nt:10ms\nout:↑E", 70 | result 71 | ); 72 | } 73 | 74 | #[test] 75 | fn repeat_many_layer_held_trans() { 76 | let result = simulate( 77 | " 78 | (defsrc a b c d e) 79 | (deflayer base e (layer-while-held held1) _ _ _) 80 | (deflayer held1 f b (layer-while-held held2) _ _) 81 | (deflayer held2 _ _ _ (layer-while-held held3) _) 82 | (deflayer held3 _ _ _ _ (layer-while-held held4)) 83 | (deflayer held4 _ _ _ _ _) 84 | ", 85 | " 86 | d:b t:10 r:b t:10 87 | d:c t:10 r:c t:10 88 | d:d t:10 r:d t:10 89 | d:e t:10 r:e t:10 90 | d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a 91 | ", 92 | ); 93 | assert_eq!( 94 | "t:80ms\nout:↓F\nt:10ms\nout:↓F\nt:10ms\nout:↓F\nt:10ms\nout:↑F", 95 | result 96 | ); 97 | } 98 | 99 | #[test] 100 | fn repeat_base_layer_trans() { 101 | let result = simulate( 102 | " 103 | (defsrc a) 104 | (deflayer base _) 105 | ", 106 | " 107 | d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a 108 | ", 109 | ); 110 | assert_eq!( 111 | "out:↓A\nt:10ms\nout:↓A\nt:10ms\nout:↓A\nt:10ms\nout:↑A", 112 | result 113 | ); 114 | } 115 | 116 | #[test] 117 | fn repeat_delegate_to_base_layer_trans() { 118 | let result = simulate( 119 | " 120 | (defcfg delegate-to-first-layer yes) 121 | (defsrc a c b) 122 | (deflayer base e _ (layer-switch swtc)) 123 | (deflayer swtc _ _ _) 124 | ", 125 | " 126 | d:b t:10 r:b t:10 127 | d:a t:10 r:a t:10 r:a t:10 u:a t:10 r:a 128 | d:c t:10 r:c t:10 r:c t:10 u:c t:10 r:c 129 | ", 130 | ); 131 | assert_eq!( 132 | "t:20ms\nout:↓E\nt:10ms\nout:↓E\nt:10ms\nout:↓E\nt:10ms\nout:↑E\n\ 133 | t:10ms\nout:↓C\nt:10ms\nout:↓C\nt:10ms\nout:↓C\nt:10ms\nout:↑C", 134 | result 135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /src/tests/sim_tests/switch_sim_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn sim_switch_layer() { 5 | let result = simulate( 6 | " 7 | (defcfg) 8 | (defsrc a b) 9 | (defalias b (switch 10 | ((layer base)) x break 11 | ((layer other)) y break)) 12 | (deflayer base (layer-while-held other) @b) 13 | (deflayer other XX @b) 14 | ", 15 | "d:b u:b t:10 d:a d:b u:b u:a t:10", 16 | ) 17 | .no_time(); 18 | assert_eq!("out:↓X out:↑X out:↓Y out:↑Y", result); 19 | } 20 | 21 | #[test] 22 | fn sim_switch_base_layer() { 23 | let result = simulate( 24 | " 25 | (defcfg) 26 | (defsrc a b c) 27 | (defalias b (switch 28 | ((base-layer base)) x break 29 | ((base-layer other)) y break)) 30 | (deflayer base (layer-switch other) @b c) 31 | (deflayer other XX @b (layer-while-held base)) 32 | ", 33 | "d:b u:b t:10 d:a d:b u:b u:a t:10 d:c t:10 d:b t:10 u:c u:b t:10", 34 | ) 35 | .no_time(); 36 | assert_eq!("out:↓X out:↑X out:↓Y out:↑Y out:↓Y out:↑Y", result); 37 | } 38 | -------------------------------------------------------------------------------- /src/tests/sim_tests/tap_hold_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn nested_template() { 5 | let result = simulate( 6 | " 7 | (defcfg concurrent-tap-hold yes ) 8 | (defsrc a j ) 9 | (deflayer base @a @j) 10 | (defalias 11 | a (tap-hold 200 1000 a lctl) 12 | j (tap-hold 200 500 j lsft)) 13 | ", 14 | "d:a t:100 d:j t:10 u:j t:1100 u:a t:50", 15 | ) 16 | .to_ascii(); 17 | assert_eq!( 18 | "t:999ms dn:LCtrl t:2ms dn:J t:6ms up:J t:203ms up:LCtrl", 19 | result 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/tests/sim_tests/template_sim_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn nested_template() { 5 | let result = simulate( 6 | " 7 | (deftemplate one (v1) 8 | a b c $v1 9 | ) 10 | (deftemplate two (v2) 11 | (t! one $v2) 12 | e f g 13 | ) 14 | (defsrc (t! two d)) 15 | (deflayer base (t! two x)) 16 | ", 17 | "d:a t:10 u:a t:10 d:d t:10 u:d t:10 d:g t:10 u:g t:10", 18 | ) 19 | .no_time(); 20 | assert_eq!("out:↓A out:↑A out:↓X out:↑X out:↓G out:↑G", result); 21 | } 22 | -------------------------------------------------------------------------------- /src/tests/sim_tests/timing_tests.rs: -------------------------------------------------------------------------------- 1 | use std::thread::sleep; 2 | use std::time::Duration; 3 | 4 | use crate::Kanata; 5 | 6 | use instant::Instant; 7 | 8 | #[test] 9 | fn one_second_is_roughly_1000_counted_ticks() { 10 | let mut k = Kanata::new_from_str("(defsrc)(deflayer base)", Default::default()) 11 | .expect("failed to parse cfg"); 12 | 13 | let mut accumulated_ticks = 0; 14 | 15 | let start = Instant::now(); 16 | while start.elapsed() < Duration::from_secs(1) { 17 | sleep(Duration::from_millis(1)); 18 | accumulated_ticks += k.get_ms_elapsed(); 19 | } 20 | 21 | let actually_elapsed_ms = start.elapsed().as_millis(); 22 | 23 | // Allow fudge of 1% 24 | // In practice this is within 1ms purely due to the remainder. 25 | eprintln!("ticks:{accumulated_ticks}, actual elapsed:{actually_elapsed_ms}"); 26 | assert!(accumulated_ticks < (actually_elapsed_ms + 10)); 27 | assert!(accumulated_ticks > (actually_elapsed_ms - 10)); 28 | } 29 | -------------------------------------------------------------------------------- /src/tests/sim_tests/unicode_sim_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn unicode() { 5 | let result = simulate( 6 | r##" 7 | (defcfg) 8 | (defsrc 6 7 8 9 0 f1) 9 | (deflayer base 10 | (unicode r#"("#) 11 | (unicode r#")"#) 12 | (unicode r#"""#) 13 | (unicode "(") 14 | (unicode ")") 15 | (tap-dance 200 (f1(unicode 😀)f2(unicode 🙂))) 16 | ) 17 | "##, 18 | "d:6 d:7 d:8 d:9 d:0 t:100", 19 | ) 20 | .no_time(); 21 | assert_eq!(r#"outU:( outU:) outU:" outU:( outU:)"#, result); 22 | } 23 | 24 | #[test] 25 | #[cfg(target_os = "macos")] 26 | fn macos_unicode_handling() { 27 | let result = simulate( 28 | r##" 29 | (defcfg) 30 | (defsrc a) 31 | (deflayer base 32 | (unicode "🎉") ;; Test with an emoji that uses multi-unit UTF-16 33 | ) 34 | "##, 35 | "d:a t:100", 36 | ) 37 | .no_time(); 38 | assert_eq!("outU:🎉", result); 39 | } 40 | -------------------------------------------------------------------------------- /src/tests/sim_tests/unmod_sim_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn unmod_keys_functionality_works() { 5 | let result = simulate( 6 | " 7 | (defcfg) 8 | (defsrc f1 1 2 3 4 5 6 7 8 9 0) 9 | (deflayer base 10 | (multi lctl rctl lsft rsft lmet rmet lalt ralt) 11 | (unmod a) 12 | (unmod (lctl) b) 13 | (unmod (rctl) c) 14 | (unmod (lsft) d) 15 | (unmod (rsft) e) 16 | (unmod (lmet) f) 17 | (unmod (rmet) g) 18 | (unmod (lalt) h) 19 | (unmod (ralt) i) 20 | (unmod (lctl lsft lmet lalt) j) 21 | ) 22 | ", 23 | "d:f1 t:5 d:1 u:1 t:5 d:2 u:2 t:5 d:3 u:3 t:5 d:4 u:4 t:5 d:5 u:5 t:5 d:6 u:6 t:5 24 | d:7 u:7 t:5 d:8 u:8 t:5 d:9 u:9 t:5 d:0 u:0 t:5", 25 | ) 26 | .no_time() 27 | .to_ascii(); 28 | assert_eq!( 29 | "dn:LCtrl dn:RCtrl dn:LShift dn:RShift dn:LGui dn:RGui dn:LAlt dn:RAlt \ 30 | up:LCtrl up:RCtrl up:LShift up:RShift up:LGui up:RGui up:LAlt up:RAlt dn:A up:A \ 31 | dn:LCtrl dn:RCtrl dn:LShift dn:RShift dn:LGui dn:RGui dn:LAlt dn:RAlt \ 32 | up:LCtrl dn:B up:B dn:LCtrl \ 33 | up:RCtrl dn:C up:C dn:RCtrl \ 34 | up:LShift dn:D up:D dn:LShift \ 35 | up:RShift dn:E up:E dn:RShift \ 36 | up:LGui dn:F up:F dn:LGui \ 37 | up:RGui dn:G up:G dn:RGui \ 38 | up:LAlt dn:H up:H dn:LAlt \ 39 | up:RAlt dn:I up:I dn:RAlt \ 40 | up:LCtrl up:LShift up:LGui up:LAlt dn:J up:J dn:LCtrl dn:LShift dn:LGui dn:LAlt", 41 | result 42 | ); 43 | } 44 | 45 | #[test] 46 | #[should_panic] 47 | fn unmod_keys_mod_list_cannot_be_empty() { 48 | simulate( 49 | " 50 | (defcfg) 51 | (defsrc a) 52 | (deflayer base (unmod () a)) 53 | ", 54 | "", 55 | ); 56 | } 57 | 58 | #[test] 59 | #[should_panic] 60 | fn unmod_keys_mod_list_cannot_have_nonmod_key() { 61 | simulate( 62 | " 63 | (defcfg) 64 | (defsrc a) 65 | (deflayer base (unmod (lmet c) a)) 66 | ", 67 | "", 68 | ); 69 | } 70 | 71 | #[test] 72 | #[should_panic] 73 | fn unmod_keys_mod_list_cannot_have_empty_keys_after_mod_list() { 74 | simulate( 75 | " 76 | (defcfg) 77 | (defsrc a) 78 | (deflayer base (unmod (lmet))) 79 | ", 80 | "", 81 | ); 82 | } 83 | 84 | #[test] 85 | #[should_panic] 86 | fn unmod_keys_mod_list_cannot_have_empty_keys() { 87 | simulate( 88 | " 89 | (defcfg) 90 | (defsrc a) 91 | (deflayer base (unmod)) 92 | ", 93 | "", 94 | ); 95 | } 96 | 97 | #[test] 98 | #[should_panic] 99 | fn unmod_keys_mod_list_cannot_have_invalid_keys() { 100 | simulate( 101 | " 102 | (defcfg) 103 | (defsrc a) 104 | (deflayer base (unmod invalid-key)) 105 | ", 106 | "", 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /src/tests/sim_tests/use_defsrc_sim_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn use_defsrc_deflayer() { 5 | let result = simulate( 6 | r##" 7 | (defcfg) 8 | (defsrc a b c d) 9 | (deflayer base 10 | 1 2 3 (layer-while-held other) 11 | ) 12 | (deflayer other 13 | 4 5 (layer-while-held src) XX 14 | ) 15 | (deflayer src 16 | use-defsrc use-defsrc XX XX 17 | ) 18 | "##, 19 | "d:d d:c d:b d:a t:100", 20 | ) 21 | .to_ascii(); 22 | assert_eq!("t:2ms dn:B t:1ms dn:A", result); 23 | } 24 | 25 | #[test] 26 | fn use_defsrc_deflayermap() { 27 | const CFG: &str = " 28 | (defcfg process-unmapped-keys yes) 29 | (defsrc a b c d) 30 | (deflayer base 31 | 1 32 | (layer-while-held othermap1) 33 | (layer-while-held othermap2) 34 | (layer-while-held othermap3) 35 | ) 36 | (deflayermap (othermap1) 37 | a 5 38 | ___ use-defsrc 39 | ) 40 | (deflayermap (othermap2) 41 | a 6 42 | __ use-defsrc 43 | _ x 44 | ) 45 | (deflayermap (othermap3) 46 | a 7 47 | _ use-defsrc 48 | __ x 49 | ) 50 | "; 51 | let result = simulate(CFG, "d:b d:a d:c d:e t:10").to_ascii(); 52 | assert_eq!("t:1ms dn:Kb5 t:1ms dn:C t:1ms dn:E", result); 53 | let result = simulate(CFG, "d:c d:a d:c d:e t:10").to_ascii(); 54 | assert_eq!("t:1ms dn:Kb6 t:1ms dn:X t:1ms dn:E", result); 55 | let result = simulate(CFG, "d:d d:a d:c d:e t:10").to_ascii(); 56 | assert_eq!("t:1ms dn:Kb7 t:1ms dn:C t:1ms dn:X", result); 57 | } 58 | -------------------------------------------------------------------------------- /src/tests/sim_tests/vkey_sim_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | const CFG: &str = r" 4 | (defsrc a b c) 5 | (defvirtualkeys lmet lmet) 6 | (defalias hm (hold-for-duration 50 lmet)) 7 | (deflayer base 8 | (multi @hm (macro-repeat 40 @hm)) 9 | (multi 1 @hm) 10 | (release-key lmet) 11 | ) 12 | "; 13 | 14 | #[test] 15 | fn hold_for_duration() { 16 | let result = simulate(CFG, "d:a t:200 u:a t:60").to_ascii(); 17 | assert_eq!("t:1ms dn:LGui t:258ms up:LGui", result); 18 | let result = simulate(CFG, "d:a u:a t:25 d:c u:c t:25").to_ascii(); 19 | assert_eq!("t:2ms dn:LGui t:23ms up:LGui", result); 20 | let result = simulate(CFG, "d:a u:a t:25 d:b u:b t:25 d:b u:b t:60").to_ascii(); 21 | assert_eq!( 22 | "t:2ms dn:LGui t:23ms dn:Kb1 t:1ms up:Kb1 t:24ms dn:Kb1 t:1ms up:Kb1 t:49ms up:LGui", 23 | result 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /tcp_protocol/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kanata-tcp-protocol" 3 | version = "0.190.0" 4 | edition = "2021" 5 | description = "TCP protocol for kanata. This does not follow semver." 6 | license = "LGPL-3.0-only" 7 | 8 | [dependencies] 9 | serde = { version = "1", features = ["alloc", "derive"], default-features = false } 10 | serde_derive = "1.0" 11 | serde_json = { version = "1", features = ["alloc"], default-features = false } 12 | -------------------------------------------------------------------------------- /tcp_protocol/src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::str::FromStr; 3 | 4 | #[derive(Debug, Serialize, Deserialize)] 5 | pub enum ServerMessage { 6 | LayerChange { new: String }, 7 | LayerNames { names: Vec<String> }, 8 | CurrentLayerInfo { name: String, cfg_text: String }, 9 | ConfigFileReload { new: String }, 10 | CurrentLayerName { name: String }, 11 | MessagePush { message: serde_json::Value }, 12 | Error { msg: String }, 13 | } 14 | 15 | impl ServerMessage { 16 | pub fn as_bytes(&self) -> Vec<u8> { 17 | let mut msg = serde_json::to_vec(self).expect("ServerMessage should serialize"); 18 | msg.push(b'\n'); 19 | msg 20 | } 21 | } 22 | 23 | #[derive(Debug, Serialize, Deserialize)] 24 | pub enum ClientMessage { 25 | ChangeLayer { 26 | new: String, 27 | }, 28 | RequestLayerNames {}, 29 | RequestCurrentLayerInfo {}, 30 | RequestCurrentLayerName {}, 31 | ActOnFakeKey { 32 | name: String, 33 | action: FakeKeyActionMessage, 34 | }, 35 | SetMouse { 36 | x: u16, 37 | y: u16, 38 | }, 39 | } 40 | 41 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] 42 | pub enum FakeKeyActionMessage { 43 | Press, 44 | Release, 45 | Tap, 46 | Toggle, 47 | } 48 | 49 | impl FromStr for ClientMessage { 50 | type Err = serde_json::Error; 51 | 52 | fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 53 | serde_json::from_str(s) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /wasm/.gitignore: -------------------------------------------------------------------------------- 1 | # wasm-pack output 2 | pkg/ 3 | 4 | # do not commit lockfile; not important for wasm project 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["."] 3 | 4 | [package] 5 | name = "kanata-wasm" 6 | version = "0.1.0" 7 | edition = "2021" 8 | 9 | [lib] 10 | crate-type = [ "cdylib", "rlib" ] 11 | 12 | [dependencies] 13 | wasm-bindgen = "0.2.95" 14 | kanata = { path = ".." , default-features = false, features = [ "simulated_output", "wasm", "zippychord" ] } 15 | anyhow = "1.0.81" 16 | log = "0.4.21" 17 | console_error_panic_hook = "0.1.7" 18 | rustc-hash = "1.1.0" 19 | -------------------------------------------------------------------------------- /wasm/README.md: -------------------------------------------------------------------------------- 1 | # Kanata WASM 2 | 3 | Code to expose kanata functionality over WASM. 4 | 5 | Prerequisites: 6 | 7 | ``` 8 | cargo install wasm-pack 9 | ``` 10 | 11 | You can run the command below to generate files for use in the browser: 12 | 13 | ``` 14 | wasm-pack build --target web 15 | ``` 16 | 17 | This will output files into `pkg/` which can be used for a website. 18 | This has yet not been tested with targets other than web (e.g. node). 19 | 20 | An example project using this code is the 21 | [online kanata simulator](https://github.com/jtroo/jtroo.github.io). 22 | -------------------------------------------------------------------------------- /windows_key_tester/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "windows_key_tester" 3 | version = "0.3.0" 4 | authors = ["jtroo <j.andreitabs@gmail.com>"] 5 | description = "Windows keycode tester" 6 | keywords = [] 7 | categories = ["command-line-utilities"] 8 | homepage = "https://github.com/jtroo/kanata" 9 | repository = "https://github.com/jtroo/kanata" 10 | readme = "README.md" 11 | license = "LGPL-3.0" 12 | edition = "2021" 13 | 14 | [target.'cfg(target_os = "windows")'.dependencies] 15 | clap = { version = "4", features = [ "std", "derive", "help", "suggestions" ], default-features = false } 16 | log = "0.4.8" 17 | simplelog = "0.12.0" 18 | anyhow = "1" 19 | winapi = { version = "0.3.9", features = [ 20 | "wincon", 21 | "timeapi", 22 | "mmsystem", 23 | ] } 24 | native-windows-gui = { version = "1.0.12", default-features = false } 25 | kanata-interception = { version = "0.3.0", optional = true } 26 | kanata = { path = "..", optional = true } 27 | 28 | [features] 29 | interception_driver = [ "kanata-interception" ] 30 | winiov2 = [ "kanata" ] 31 | -------------------------------------------------------------------------------- /windows_key_tester/README.md: -------------------------------------------------------------------------------- 1 | # Windows key tester 2 | 3 | This directory contains the code for a Windows key tester. This can be used to 4 | help test keyboard->keycode mappings in Windows that may not yet be listed in 5 | kanata. For Linux, use the existing [evtest](https://www.systutorials.com/docs/linux/man/1-evtest/). 6 | -------------------------------------------------------------------------------- /windows_key_tester/src/main.rs: -------------------------------------------------------------------------------- 1 | //! This program is intended to be similar to `evtest` but for Windows. It will read keyboard 2 | //! events, print out the event info, then forward it the keyboard event as-is to the rest of the 3 | //! operating system handling. 4 | 5 | #[cfg(target_os = "windows")] 6 | mod windows; 7 | #[cfg(target_os = "windows")] 8 | use windows::*; 9 | 10 | #[cfg(target_os = "windows")] 11 | fn main() { 12 | let ret = main_impl(); 13 | if let Err(ref e) = ret { 14 | log::error!("main got error {}", e); 15 | } 16 | eprintln!("\nPress any key to exit"); 17 | let _ = std::io::stdin().read_line(&mut String::new()); 18 | } 19 | 20 | #[cfg(not(target_os = "windows"))] 21 | fn main() { 22 | print!("Hello world! Wrong OS. Doing nothing."); 23 | } 24 | -------------------------------------------------------------------------------- /windows_key_tester/src/windows.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use simplelog::*; 3 | 4 | use clap::Parser; 5 | #[cfg(not(feature = "interception_driver"))] 6 | mod llhook; 7 | #[cfg(not(feature = "interception_driver"))] 8 | use llhook::*; 9 | 10 | #[cfg(feature = "interception_driver")] 11 | mod interception; 12 | #[cfg(feature = "interception_driver")] 13 | use interception::*; 14 | 15 | #[derive(Parser, Debug)] 16 | #[clap(author, version, about, long_about = None)] 17 | struct Args { 18 | /// Enable debug logging 19 | #[clap(short, long)] 20 | debug: bool, 21 | 22 | /// Enable trace logging (implies --debug as well) 23 | #[clap(short, long)] 24 | trace: bool, 25 | } 26 | 27 | #[cfg(target_os = "windows")] 28 | /// Parse CLI arguments and initialize logging. 29 | fn cli_init() { 30 | let args = Args::parse(); 31 | 32 | let log_lvl = match (args.debug, args.trace) { 33 | (_, true) => LevelFilter::Trace, 34 | (true, false) => LevelFilter::Debug, 35 | (false, false) => LevelFilter::Info, 36 | }; 37 | 38 | let mut log_cfg = ConfigBuilder::new(); 39 | if let Err(e) = log_cfg.set_time_offset_to_local() { 40 | eprintln!("WARNING: could not set log TZ to local: {e:?}"); 41 | }; 42 | CombinedLogger::init(vec![TermLogger::new( 43 | log_lvl, 44 | log_cfg.build(), 45 | TerminalMode::Mixed, 46 | ColorChoice::AlwaysAnsi, 47 | )]) 48 | .expect("logger can init"); 49 | log::info!("windows_key_tester v{} starting", env!("CARGO_PKG_VERSION")); 50 | } 51 | 52 | pub(crate) fn main_impl() -> Result<()> { 53 | cli_init(); 54 | log::info!("Sleeping for 2s. Please release all keys and don't press additional ones."); 55 | std::thread::sleep(std::time::Duration::from_secs(2)); 56 | start()?; 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /windows_key_tester/src/windows/llhook.rs: -------------------------------------------------------------------------------- 1 | //! Safe abstraction over the low-level windows keyboard hook API. 2 | 3 | // This file is taken from kbremap with modifications. 4 | // https://github.com/timokroeger/kbremap 5 | 6 | use std::ptr; 7 | 8 | use anyhow::Result; 9 | use winapi::ctypes::*; 10 | use winapi::shared::minwindef::*; 11 | use winapi::shared::windef::*; 12 | use winapi::um::winuser::*; 13 | 14 | /// Wrapper for the low-level keyboard hook API. 15 | /// Automatically unregisters the hook when dropped. 16 | pub struct KeyboardHook { 17 | handle: HHOOK, 18 | } 19 | 20 | impl KeyboardHook { 21 | /// Sets the low-level keyboard hook for this thread. 22 | /// 23 | /// Panics when a hook is already registered from the same thread. 24 | #[must_use = "The hook will immediatelly be unregistered and not work."] 25 | pub fn attach_hook() -> KeyboardHook { 26 | KeyboardHook { 27 | handle: unsafe { 28 | SetWindowsHookExW(WH_KEYBOARD_LL, Some(hook_proc), ptr::null_mut(), 0) 29 | .as_mut() 30 | .expect("install low-level keyboard hook successfully") 31 | }, 32 | } 33 | } 34 | } 35 | 36 | impl Drop for KeyboardHook { 37 | fn drop(&mut self) { 38 | unsafe { UnhookWindowsHookEx(self.handle) }; 39 | } 40 | } 41 | 42 | /// Key event received by the low level keyboard hook. 43 | #[allow(dead_code)] 44 | #[derive(Debug, Clone, Copy)] 45 | pub struct InputEvent { 46 | pub code: u32, 47 | /// Key was released 48 | pub up: bool, 49 | } 50 | 51 | impl InputEvent { 52 | #[cfg(not(feature = "winiov2"))] 53 | fn from_hook_lparam(lparam: &KBDLLHOOKSTRUCT) -> Self { 54 | Self { 55 | code: lparam.vkCode, 56 | up: lparam.flags & LLKHF_UP != 0, 57 | } 58 | } 59 | 60 | #[cfg(feature = "winiov2")] 61 | fn from_hook_lparam(lparam: &KBDLLHOOKSTRUCT) -> Self { 62 | let extended = if lparam.flags & 0x1 == 0x1 { 0xE000 } else { 0 }; 63 | let code = kanata_state_machine::oskbd::u16_to_osc((lparam.scanCode as u16) | extended) 64 | .map(Into::into) 65 | .unwrap_or(lparam.vkCode); 66 | Self { 67 | code, 68 | up: lparam.flags & LLKHF_UP != 0, 69 | } 70 | } 71 | } 72 | 73 | /// The actual WinAPI compatible callback. 74 | unsafe extern "system" fn hook_proc(code: c_int, wparam: WPARAM, lparam: LPARAM) -> LRESULT { 75 | let hook_lparam = &*(lparam as *const KBDLLHOOKSTRUCT); 76 | let is_injected = hook_lparam.flags & LLKHF_INJECTED != 0; 77 | let key_event = InputEvent::from_hook_lparam(hook_lparam); 78 | log::info!("{code}, {wparam:?}, {is_injected}, {key_event:?}"); 79 | CallNextHookEx(ptr::null_mut(), code, wparam, lparam) 80 | } 81 | 82 | pub fn start() -> Result<()> { 83 | // Display debug and panic output when launched from a terminal. 84 | unsafe { 85 | use winapi::um::wincon::*; 86 | if AttachConsole(ATTACH_PARENT_PROCESS) != 0 { 87 | panic!("Could not attach to console"); 88 | } 89 | }; 90 | native_windows_gui::init()?; 91 | // This callback should return `false` if the input event is **not** handled by the 92 | // callback and `true` if the input event **is** handled by the callback. Returning false 93 | // informs the callback caller that the input event should be handed back to the OS for 94 | // normal processing. 95 | let _kbhook = KeyboardHook::attach_hook(); 96 | log::info!("hook attached, you can type now"); 97 | // The event loop is also required for the low-level keyboard hook to work. 98 | native_windows_gui::dispatch_thread_events(); 99 | Ok(()) 100 | } 101 | --------------------------------------------------------------------------------